How to create desktop shortcuts

Overview
People coming from Windows will be very familiar with the concept of shortcuts. You can right-click any file or folder, select Send To > Desktop (Create Shortcut). To my surprise, there is no equivalent to this in Gnome/Nautilus (v46.4). But we can roughly approximate this feature using Symlinks and Desktop Entries. I've created a Nautilus-Python extension to simplify their creation via a "Create Shortcut" context menu (right-click menu) item. It uses desktop entries for Folder shortcuts, and absolute symlinks for File shortcuts (reasoning is explained below).

Extension Install

  1. Ensure this folder exists: mkdir -p ~/.local/share/nautilus-python/extensions
  2. Create the extension file with your favorite editor. Here's one way: nano ~/.local/share/nautilus-python/extensions/shortcuts-menu.py
  3. Paste in the extension code below
  4. Save (Ctrl+S in nano)
  5. Restart Nautilus: nautilus -q
  6. Enjoy

Extension Code:

import sys
import os
import gi
import urllib

from urllib.parse import unquote
from gi.repository import GObject

if "nemo" in sys.argv[0].lower():
    # Host runtime is nemo-python
    gi.require_version("Nemo", "3.0")
    from gi.repository import Nemo as FileManager
else:
    # Otherwise, just assume it's nautilus-python
    from gi.repository import Nautilus as FileManager

class ShortcutsMenuExtension(GObject.GObject, FileManager.MenuProvider):

    def __init__(self):
        GObject.Object.__init__(self)

    def create_desktop_entry_shortcut(self, menu, fitem, target):
        # Determine name and path of Desktop Entry
        defilename = fitem.get_uri().replace("://",".").replace("/",".").replace("..",".")
        depath = target + defilename + ".desktop"

        # Write the desktop entry. Will overwrite if it already exists.
        with open(depath, "w", encoding="utf-8") as f:
            f.write("[Desktop Entry]\nType=Application\nIcon=folder\nTerminal=false\n")
            f.write("Name=" + fitem.get_name() + "\n")
            f.write("Exec=xdg-open \"" + urllib.parse.unquote(fitem.get_uri()) + "\"\n")

        # Allow execute
        os.system("gio set \"" + depath + "\" metadata::trusted true")
        os.system("chmod a+x \"" + depath + "\"")

    def create_absolute_symlink(self, menu, fitem, target):
        # Determine where to place the symlink
        if target == "desktop":
            slpath = os.environ['HOME'] + "/Desktop/Link to " + fitem.get_name()
        elif target == "here":
            slpath = fitem.get_location().get_parent().get_path() + "/Link to " + fitem.get_name()
        else:
            return ()

        # Create the symlink
        os.system("ln -s \"" + fitem.get_location().get_path() + "\" \"" + slpath + "\"")

    def get_file_items(self, *args):
        # Get all args and choose last for compatibility
            # Nautilus 4.0: (self, List[FileInfo])
            # Nautilus 3.0: (self, Gtk.Widget, List[FileInfo])
        fileitems = args[-1]

        # Ensure only 1 object is selected
        if len(fileitems) != 1:
            return ()
        fitem = fileitems[0]

        # Create main menu and sub menu
        menu = FileManager.MenuItem(name="ShortcutsMenu::Main", label="Create Shortcut")
        submenu = FileManager.Menu()
        menu.set_submenu(submenu)

        # Populate sub menu
        if fitem.is_directory(): # fitem.get_uri_scheme() == "file" for both files and directories
            # Send to Desktop menu item
            mitem = FileManager.MenuItem(name="ShortcutsMenu::DesktopDE", label="Send to Desktop (Shortcut)")
            mitem.connect("activate", self.create_desktop_entry_shortcut, fitem, os.environ['HOME'] + "/Desktop/")
            submenu.append_item(mitem)

            # Send to Main Menu menu item
            mitem = FileManager.MenuItem(name="ShortcutsMenu::MainMenuDE", label="Send to Main Menu (Shortcut)")
            mitem.connect("activate", self.create_desktop_entry_shortcut, fitem, os.environ['HOME'] + "/.local/share/applications/")
            submenu.append_item(mitem)

        else:
            # Send to Desktop menu item
            mitem = FileManager.MenuItem(name="ShortcutsMenu::DesktopLinkAbs", label="Send to Desktop (Absolute Link)")
            mitem.connect("activate", self.create_absolute_symlink, fitem, "desktop")
            submenu.append_item(mitem)

            # Create Here menu item
            mitem = FileManager.MenuItem(name="ShortcutsMenu::HereLinkAbs", label="Create Here (Absolute Link)")
            mitem.connect("activate", self.create_absolute_symlink, fitem, "here")
            submenu.append_item(mitem)

        return (menu,)

Troubleshooting
If you modify the extension, you'll need to restart nautilus twice (nautilus -q) for it to unload and reload the changes. I think this has something to do with it's caching mechanism.

Ensure you have python3-nautilus (legacy: python-nautilus) installed. This was pre-installed on Zorin OS 18 Core for me.
sudo apt install python3-nautilus

Why make this? What's wrong with Create Link?
Zorin OS ships with the "Create Link" context menu item, which will create a symlink in the current directory. While trying to setup Zorin OS for myself and my parents, I ran into a few issues with "Create Link":

  • Symlinks are not shortcuts. The linked path appears as a child of the current directory structure. For example, when navigating into a symlink on a network drive, you cannot browse "up" the directory tree (like you can with a shortcut on Windows).
  • The "Create Link" action doesn't work well for read-only file systems, because the default action creates the link in the current directory (and fails).
  • Using "Create Link" on smb:// shares mounted with GIO fails, and results in a broken link. There are two workarounds for this:
    • Workaround #1: Symlink the absolute local path of the files (which my extension does for File shortcuts). But the symlink will show broken if the share isn't mounted, and GIO won't auto mount it because it uses the absolute path instead of smb://. Also, it's not readily apparent how to find the absolute path, at least for a casual user.
    • Workaround #2: Use fstab or crontab to mount the share to a local directory, instead of relying on GIO. This means navigating all the mount options (like users to allow unprivileged mount operations), creating a credentials file, and juggle it's security permissions. There's a lot for a casual user to mess up. Using GIO to auto mount shares on-demand with stored credentials is an awesome user experience, so I don't want to ignore it.
  • Not every file system supports symlinks. I briefly used fstab to mount my SMB/CIFS share, and soon discovered that "Create Link" still doesn't work because the server doesn't have support for symlinks enabled. Yes, I could try to modify my server to support it, but I can't modify every SMB server I encounter. A better solution is to add the mfsymlinks mount option. I'm not sure if GIO mounts use this option, or could be configured to use it, since I never got that far. But adding mfsymlinks is not something I'd expect a casual user (i.e. my parents) to figure out.

If Symlinks are so problematic, why not use Desktop Entries for everything?
Desktop Entries are the closest thing Gnome has to a shortcut. They can be used to spawn an instance of the FileManager (i.e. Nautilus) at a given point in the file system. Network shares will be auto mounted as required if the FileManager supports GIO uri schemes like smb://, which Zorin OS does. But they have some limitations:

  • A Desktop Entry file will only function on the Desktop or in the Main Menu (/applications)
  • They can be cumbersome to create by hand, and while many tools exist to help with their creation, the number of options can be intimidating and way more than what the casual user needs.
  • An icon must be statically assigned. The Desktop Entry will not auto-magically pick the current icon. This isn't a big deal for directories (they all use the same icon), but determining a file's icon could get complicated.
  • After creating a Desktop Entry, you must trust it in GIO and grant it execute (right-click Allow Launching). My extension automates this.
  • Desktop Entries cannot be renamed by simply right-clicking them. They must be edited in a text editor.

Conclusion
This means Desktop Entries work pretty good to build shortcuts for Directories (on the Desktop or Main Menu), but not for Files. For file shortcuts, I'm sticking with absolute symlinks, as described in Workaround #1 above. This preserves the file's icon, and the inability to "navigate up" the directory tree doesn't matter with a File. However, symlinks to a network share will show broken after a restart, so you may want to force a gio mount to the relevant shares at login (or you could just open a Desktop Entry shortcut to a folder on that share to mount it :wink:).

Extension Shortcomings / Future Improvements
Here's some things I thought about when making this extension, but didn't make it into the first cut:

  • make shortcut (desktop entry) menu available when right-clicking a folder's whitespace, so you can shortcut the root of a network drive
  • display error when unable to create a symlink "Create Here" due to read-only filesystem or folder permissions
  • create context menu item to rename .desktop entries easily
    • Add a " - shortcut" suffix to .desktop entry names?
  • Get desktop entry folder icons to show the arrow modifier/sub-icon that normally displays on symlinks
    • Make the arrow a different color to differentiate .desktop shortcuts from symlinks
  • in the Absolute Link submenu, detect gio mount and add option to automount at login
  • Check if max directory path would be exceeded for .desktop files, or fallback to a hash of the filepath
  • Instead of Main Menu links going to "Other", create a custom "Shortcuts" menu/category for them

This is concerning due to the shell injection being particularly exploitable.
Importing subprocess and using a "check if true" rather than "assuming as true" would be a lot safer:

import subprocess

subprocess.run(["gio", "set", depath, "metadata::trusted", "true"], check=True)
subprocess.run(["chmod", "a+x", depath], check=True)
subprocess.run(["ln", "-s", fitem.get_location().get_path(), slpath], check=True)

Otherwise, any corrupted or even malicious action can be placed to run whenever the end user attempts to create a .desktop file.
Due to criticality, I address this directly.

Moderate feedback is summarized below:

Summary

The Exec=xdg-open <URI> will allow launching a folder as an application.
defilename = fitem.get_uri().replace("://",".").replace("/",".").replace("..",".")
Replacing the backticks can collapse paths into filenames.
Replacing .. with . can cause breakage, as well.
Filenames should be sanitized as a base name, not a URI.
For example:

from pathlib import Path
sanename = Path(unquote(fitem.get_uri())).name
defilename = re.sub(r'[^A-Za-z0-9_.-]', '_', sanename)

The use of args[-1] can fail if more arguments are added to Nautilus.

There is also no error handling, so all failures will fail silently without alerting the endd user.

This os.environ['HOME'] + "/Desktop/" line assumes English use.
If the user is using a different locale, that can fail. The path should determine what language is used for the ~/Desktop folder and use that:

from gi.repository import GLib
desktop = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP)

The ln -s command is great. I use it all the time. But for this extentions intention, that creates an absolute path when you want this extension to apply to many users. Absolute paths should be shunned when deploying something dynamic like an extension, making use of relative paths, instead. A user might move a file, which would break the absolute path.
os.path.relpath(...)

1 Like

Hi, from what I understand in good Spanish, creating a script like this isn't without errors and vulnerabilities. Is there anyone who can enable this without problems for those coming from Windows? This was done with Gdedit, creating a file with a name, comment, exec, icon, terminal, type, and categories, then saving it as .desktop, and then granting permissions with chmod +x. For a file or folder, ln -s is used. Is this valid, or does it also have risks?

Sure, let me send you a PM with a suggestion that you can test and modify.

2 Likes

While true, we try to mitigate as much as we can.

Yes, most users probably won't experience a major problem; but it is a bit of a roulette - you do not want to be the ones that do experience a problem, so a few adjustments will benefit the original script.

Even so, some things are more likely to create problems, especially for a Windows OS user:

Whereas, something like this:

is Unlikely to be a problem soon; it is an API issue and whether it becomes a problem would depend on if Gnome devs added arguments in a later Nautilus version. Very low risk of an issue.

Doing it this way as an end user is totally fine.
It is valid and very low risk.

But if a person is deploying an Extension to perform an action for many different users, we want to be wary of chmod +x or using an absolute path, when a relative path would be better.

1 Like

When the extension is in the software store, I'll consider installing it, as this appears to be a nice addition to the OS. I won't be however coding, creating my own files, manual process.

Also, when and if a real extension is made, put in the software store, or Gnome-Look website, if @Aravisian signs off that its safe, then I will get it. But I'm not doing any of this manual process in this thread. I'm also glad that he raised the alarm on vulnerabilities.

Unless those are patched as well, I won't be taking the risk. But I am interested in an extension that does this. Your right, Windows users are used to send to desktop create shortcut, its wild how different Linux can be at times.


Wow, such great and detailed feedback! I suppose I should get a git repo spun up.

100% agree. I'm experienced with PowerShell, but still learning Python. Thanks for the detailed example of subprocess.run usage.

I originally had Exec=nautilus <URI> but changed it to xdg-open to make it more portable to other File Managers. If there's a better way to open the default FileManager, I'm all for it.

Let me explain this better, because I think you're not picking up on what I'm trying to do here. Imagine you have 2 folders named the same thing, in 2 different locations, and then you try to shortcut both to the desktop. If we name the Desktop Entry file after the folder, then when we shortcut the second folder, it will clobber the first folder. So we need a unique name for the Desktop Entries. If we collapse the full path of a folder (e.g. by replacing slashes with periods) into a filename, then the shortcuts will not clobber each other. The Desktop Entry spec talks about naming them in reverse DNS order (most granular/specific name last), which this conforms to.

See the comments above the line where args[-1] is used. It's that way for compatibility purposes. I pulled that nugget from looking at the Zorin Connect python extension, so I'm not the only one using this workaround. But you're right, if arg order changes again, everything will break again.

Yes, this is a deficiency. I alluded to this lack of error handling in the 2nd item of the "Extension Shortcomings" section. I just need to read up on how to catch errors and spawn message windows in python.

You rock! Thank you for that code snippet.

My primary objective was to get shortcuts on the user's desktop. Normally user's don't share desktops with each other. Second, one of the main reasons I created this extension is because the native "Create Link" action (which uses a relative path) does not function with gio mounts (i.e. smb://192.168...). This is explained more in the original post, section "Why make this? What's wrong with Create Link?", bullet #3. If Nautilus/GIO can fix that problem with relative links, I'm all for removing the part of the extension responsible for creating absolute symlinks.

I'll work on getting a repo up on gitlab, and edit the original post with the repo info once it's up.