"It just opens files" - xdg-open under the hood
How the logic behind the freedesktop file opener works
Table of Contents
What is xdg-open ?
xdg-open, part of the xdg-utils package is a freedesktop.org tool that aims to give you (and everyone else) a desktop independent interface for opening a file or URL, taking care of all the environments quirks and settings so the application or script that "just wants to open a file" doesn't have to.
file:// URI with xdg-open
This article is based on version 1.1.3, the version that is currently in development is called 1.1.3+.
Questions
Before starting out I was trying to find answers to the following questions:
- How does
xdg-openknow my favourite applications? - Which ways are there to configure it?
- Is there an interface to extending it?
This is why this article is about high level logic rather than little tricks.
First look at the file
To analyse any tool one should try to get it's source code.
A quick command -v xdg-open reveals the binary to be under /usr/bin/xdg-open for me, file /usr/bin/xdg-open tells me that it looks like a POSIX shell script
and a shellcheck /usr/bin/xdg-open command reveals that file was not quite right about the POSIX part.
But it is a shellscript that anyone can just open in their favourite text file viewer/editor.
The file contains the following:
- UI code to make it easier to use
- Helpers for querying
.desktopfiles - Lots of checks for a lot of things
- cli parser code
- Heuristics for detecting desktop environments
- Workarounds for tools that seem to misbehave
- Wrappers for various desktop specific openers
- Heuristics spaghetti with lots of "xdg" and "generic" in them (I assume those are the really interesting parts)
- One giant switch that decides what to do
In case you don't have a local copy of a recent xdg-open: Its source lives in the freedesktop gitlab in xdg-open.in and xdg-utils-common.in which is reused across the entire xdg-urils package. This article will link to the 1.1.3 tag instead of the latest version.
What does it do?
xdg-open really only does one thing, open whatever one throws at it and therefore has a pretty linear flow.
It only has options to query help and version information, I'll ignore those code parts.
Note: Those options are checked for near the start of the script so a xdg-open --help doesn't even have to load all the logic.
Note: There is an undocumented XDG_UTILS_DEBUG_LEVEL environment variable that makes xdg-open more chatty when set to 1.
- get one URL or filename from the cli arguments
-
try to find out which desktop is currently in use using
detectDE. - fall back to a
genericdesktop if detection wasn't successful. - remove itself from the
BROWSERenvironment variable - based on the detected desktop call the appropriate helper for opening the file.
Desktops
The "desktops" xdg-open is aware of are (in order of them appearing):
kde- Both modern day and very old KDE desktops.
deepin- The Deepin Desktop Environment
gnome3- The GNOME desktop in versions 3 (and 4)
cinnamon-
Cinnamon, the Linux Mint Desktop, it is treated as if it was
gnome3. gnome- The GNOME desktop in version 2
mate- The MATE Desktop Environment that continues as a modern version of GNOME 2, but gets treated differently.
xfce- The Xfce Desktop Environment.
lxde- The Lightweight X Desktop Environment.
lxqt- The Lightweight Qt Desktop Environment, a qt remake of Lxde, but not related enough to share code here.
enlightenment- The Enlightenment Desktop.
Some of them aren't even desktops:
cygwin- We are running under Windows in a Cygwin environment.
wsl- We are running in a WSL environment. Note that this is not yet present in version
1.1.3. darwin- We are running on MacOS.
flatpak- We are in a flatpak container.
generic- This is a desktop that doesn't require any special treatment 🥳
Good news: many of those seem to map to the open_generic function, but for some desktops assumptions are made.
Update note regarding Deepin: Previously this article listed deepin as dde, which is icorrect. The XDG-utils use the string deepin.
Detecting the desktop
Desktop detection is done by the detectDE function using multiple heuristics in order of decreasing accuracy. The result is written to the DE global script variable.
The following tests are executed in order given that all previous tests didn't match:
- Test if the
XDG_CURRENT_DESKTOPenvironment variable contains a known value. - Test for desktop specific clues
KDE_FULL_SESSIONis setGNOME_DESKTOP_SESSION_IDis setMATE_DESKTOP_SESSION_IDis set- The
org.gnome.SessionManagername is owned by some application on the d-bus - The
_DT_SAVE_MODExprop on the root window contains the stringxfce4 xprop -roothasxfce_desktop_windowat the start of a line in its output- The
DESKTOPenvironment variable begins withEnlightenment LXQT_SESSION_CONFIGis set
- Test if the
DESKTOP_SESSIONenvironment variable contains a known value. - Test if
unameoutputs eitherCYGWIN,DarwinorLinux- In case of Linux, if
/proc/versioncontains the stringmicrosoftand the commandpowershell.exeis present, assumewsl. (This was added very recently and is not in version1.1.3)
- In case of Linux, if
The following tests always run:
- When the desktop is a
gnome, test if thegnome-default-applications-propertiescommand is present, if not, assumegnome3 - Test if
$XDG_RUNTIME_DIR/flatpak-infois present and if yes assumeflatpak(Why isn't this the first check?)
Note: Because this is implemented in xdg-utils-common.in this also applies to all of the other xdg-utils tools.
Desktop specific openers
Since xdg-open tries to offload the work when the desktop environment has its own mechanism for opening files lets cover those first.
Desktops that "just call an opener"
| Desktop | Opener | Fall back to open_generic |
|---|---|---|
| deepin | dde-open | yes |
| enlightenment | enlightenment |
yes |
| lxqt | always | |
| cygwin | cygstart | no |
| darwin | open | no |
| wsl | powershell.exe start | no |
flatpak
When in a flatpak xdg-open invokes the Desktop portals OpenURI function using gdbus.
Note: for the 1.1.3+ version xdg-open uses the org.freedesktop.portal.OpenURI.OpenFile method for files and file URIs. It also added a timeout of 5 seconds.
GNOME, MATE and Xfce
GNOME 2, GNOME 3, MATE and Xfce are related in that they all share a similar cascade of openers (falling back to the next one, if the previous one isn't present) is used.
exo-openwhen on xfcegio opengvfs-opengnome-openwhen ongnome(GNOME 2)mate-openwhen onmateopen_generic
Note: for GNOME 3 the only options really are gio open and gvfs-open before falling back to the generic opener.
KDE
For KDE, if the KDE_SESSION_VERSION environment variable is set kde-open is used for KDE 4 and kde-open5 for for KDE 5. Otherwise kfmclient exec (with some logic to fix the exit code) is used for even older KDE versions.
KDE 6 will use kde-open again, but that isn't in version 1.1.3.
Lxde
For Lxde pcmanfm is used if present, but only for filepaths and file:// URIs. Otherwise falling back to open_generic.
Generic Opener
The generic opener is invoked by the calling the open_generic function with the filepath or URI as the only argument.
This is yet another heuristics spaghetti function that tries to delegate work to other tools that may already be configured.
Opening files
In case the passed argument looks like a filepath or file:// URI, xdg-open tries some additional openers.
If the file doesn't exist the script will exit with an appropriate error message.
If an X or Wayland display is present (checked using has_display) the files mime-type is determined using xdg-mime query filetype and the function open_generic_xdg_mime is attempted for opening.
If no display is present or open_generic_xdg_mime returned, opening with run-mailcap is attempted if present. run-mailcap apparently is the Debian solution to the problem from before freedesktop standardised file opening.
If we have a display and mimeopen from the File-MimeInfo perl package is present attempt to open using that.
If all of those miss then the open_generic function continues with treating the first argument given to it as if it was an URI.
Opening URIs
For URIs the following happens until one of the steps finds a way to open the URI.
If there is a display (X or Wayland, checked by has_display) try to derive a URI scheme and open it using open_generic_xdg_mime with a mime-type of x-scheme-handler/<scheme>.
This allows registering custom URI handlers using the already existing mechanism for mime-types.
If the BROWSER environment variable (a colon : separated command list) is set try all browser commands in that variable (if %s is present insert the URI in the desired position using printf) until one of them succeeds. (implemented in open_envvar)
If unset try a set of sane defaults depending on whether a display is present or not.
Note: The BROWSER variable was cleaned up at the start of xdg-open.
At the time of writing the list of browser commands was …
… when a display is present:
- x-www-browser
- firefox
- iceweasel
- seamonkey
- mozilla
- epiphany
- konqueror
- chromium
- chromium-browser
- google-chrome
… for the terminal (always attempted):
- www-browser
- links2
- elinks
- links
- lynx
- w3m
open_generic_xdg_mime
This function takes a filepath (or URI) and a mime-type as input.
It uses xdg-mime query default to get the name of the .desktop file, then searches for that in all $XDG_DATA_DIRS/applications/, when found parses the Exec and other needed fields, runs the resulting command and makes the script exit.
If it didn't find a matching file it returns.
Summary
xdg-open tries to open a file or URI.
If it recognises the environment and knows a opener that is specific to that environment it will use that.
If there is no specific opener it will fall back to a cascade of generic openers, preferring the freedesktop way, falling back to legacy openers. The freedesktop mime-type to application mapping uses xdg-mime.
For URIs it will attempt to open the URI as if it was a file with the mime-type x-scheme-handler/<scheme>.
The last configurable fallback is the BROWSER environment variable which is a colon : separated list of commands of which all will be tried until the first one succeeds. If %s is present in the command the URI or filepath will be inserted in its place instead of being appended.
If the BROWSER environment variable is empty, it falls back to a list of hard coded default applications.
Extending
- For fully custom behaviour wrapping
xdg-openstill seems to be the best choice. - Defaults can be customised by setting a handler in the
BROWSERenvironment variable. xdg-openlistens to whatxdg-mimesays on which mime-type a file is and which application to use for opening.
Expectations
That was quite a bit more desktop-specific stuff in there than expected, while the generic part was supisingly simple at the high level logic view.
One thing that was unexpected was that there is no obvious connection to xdg-settings i.e. for the default web browser, but maybe that is hidden in xdg-mime.
I'll try to follow up with articles on both tools. Make sure you subscribe to my atom feed 😉. Bye!