"It just opens files" - xdg-open under the hood

Date: — Topics: , — Lang: — by Slatian

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.

Examples for opening a file, a URI and a file:// URI with xdg-open
xdg-open example.txt
xdg-open https://slatecave.net
xdg-open file:///usr/bin/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:

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:

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.

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:

  1. Test if the XDG_CURRENT_DESKTOP environment variable contains a known value.
  2. Test for desktop specific clues
    • KDE_FULL_SESSION is set
    • GNOME_DESKTOP_SESSION_ID is set
    • MATE_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 string xfce4
    • xprop -root has xfce_desktop_window at the start of a line in its output
    • The DESKTOP environment variable begins with Enlightenment
    • LXQT_SESSION_CONFIG is set
  3. Test if the DESKTOP_SESSION environment variable contains a known value.
  4. Test if uname outputs either CYGWIN, Darwin or Linux
    • In case of Linux, if /proc/version contains the string microsoft and the command powershell.exe is present, assume wsl. (This was added very recently and is not in version 1.1.3)

The following tests always run:

  1. When the desktop is a gnome, test if the gnome-default-applications-properties command is present, if not, assume gnome3
  2. Test if $XDG_RUNTIME_DIR/flatpak-info is 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

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_open 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.

gdbus call --session \
    --dest org.freedesktop.portal.Desktop \
    --object-path /org/freedesktop/portal/desktop \
    --method org.freedesktop.portal.OpenURI.OpenURI \
    "" "$1" {}

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.

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:

… for the terminal (always attempted):

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

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!