"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.
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-open
know 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
.desktop
files - 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
generic
desktop if detection wasn't successful. - remove itself from the
BROWSER
environment 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.
dde
- 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.
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_DESKTOP
environment variable contains a known value. - Test for desktop specific clues
KDE_FULL_SESSION
is setGNOME_DESKTOP_SESSION_ID
is setMATE_DESKTOP_SESSION_ID
is set- The
org.gnome.SessionManager
name is owned by some application on the d-bus - The
_DT_SAVE_MODE
xprop on the root window contains the stringxfce4
xprop -root
hasxfce_desktop_window
at the start of a line in its output- The
DESKTOP
environment variable begins withEnlightenment
LXQT_SESSION_CONFIG
is set
- Test if the
DESKTOP_SESSION
environment variable contains a known value. - Test if
uname
outputs eitherCYGWIN
,Darwin
orLinux
- In case of Linux, if
/proc/version
contains the stringmicrosoft
and the commandpowershell.exe
is 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-properties
command is present, if not, assumegnome3
- Test if
$XDG_RUNTIME_DIR/flatpak-info
is 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 |
---|---|---|
dde | 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-open
when on xfcegio open
gvfs-open
gnome-open
when ongnome
(GNOME 2)mate-open
when onmate
open_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-open
still seems to be the best choice. - Defaults can be customised by setting a handler in the
BROWSER
environment variable. xdg-open
listens to whatxdg-mime
says 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!