"It just opens files" - xdg-open under the hood
How the logic behind the freedesktop file opener works
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.
This article is based on version
1.1.3, the version that is currently in development is called
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.
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
- 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
- get one URL or filename from the cli arguments
try to find out which desktop is currently in use using
- fall back to a
genericdesktop if detection wasn't successful.
- remove itself from the
- based on the detected desktop call the appropriate helper for opening the file.
xdg-open is aware of are (in order of them appearing):
- Both modern day and very old KDE desktops.
- The Deepin Desktop Environment
- The GNOME desktop in versions 3 (and 4)
Cinnamon, the Linux Mint Desktop, it is treated as if it was
- The GNOME desktop in version 2
- The MATE Desktop Environment that continues as a modern version of GNOME 2, but gets treated differently.
- The Xfce Desktop Environment.
- The Lightweight X Desktop Environment.
- The Lightweight Qt Desktop Environment, a qt remake of Lxde, but not related enough to share code here.
- The Enlightenment Desktop.
Some of them aren't even desktops:
- We are running under Windows in a Cygwin environment.
- We are running in a WSL environment. Note that this is not yet present in version
- We are running on MacOS.
- We are in a flatpak container.
- 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.
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
org.gnome.SessionManagername is owned by some application on the d-bus
_DT_SAVE_MODExprop on the root window contains the string
xfce_desktop_windowat the start of a line in its output
DESKTOPenvironment variable begins with
- Test if the
DESKTOP_SESSIONenvironment variable contains a known value.
- Test if
- In case of Linux, if
/proc/versioncontains the string
microsoftand the command
powershell.exeis present, assume
wsl. (This was added very recently and is not in version
- In case of Linux, if
The following tests always run:
- When the desktop is a
gnome, test if the
gnome-default-applications-propertiescommand is present, if not, assume
- Test if
$XDG_RUNTIME_DIR/flatpak-infois present and if yes assume
flatpak(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
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
When in a flatpak
xdg-open invokes the
OpenURI function using
Note: for the
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 xfce
Note: for GNOME 3 the only options really are
gio open and
gvfs-open before falling back to the generic opener.
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
pcmanfm is used if present, but only for filepaths and
file:// URIs. Otherwise falling back to
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.
In case the passed argument looks like a filepath or
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.
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
This allows registering custom URI handlers using the already existing mechanism for mime-types.
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
If unset try a set of sane defaults depending on whether a display is present or not.
BROWSER variable was cleaned up at the start of
At the time of writing the list of browser commands was …
… when a display is present:
… for the terminal (always attempted):
This function takes a filepath (or URI) and a mime-type as input.
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.
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
For URIs it will attempt to open the URI as if it was a file with the mime-type
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.
BROWSER environment variable is empty, it falls back to a list of hard coded default applications.
- For fully custom behaviour wrapping
xdg-openstill seems to be the best choice.
- Defaults can be customised by setting a handler in the
xdg-openlistens to what
xdg-mimesays on which mime-type a file is and which application to use for opening.
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
I'll try to follow up with articles on both tools. Make sure you subscribe to my atom feed 😉. Bye!