From 36498f2acf9790df0d7ca1a1ed8d9fe64dba2869 Mon Sep 17 00:00:00 2001 From: Felitendo Date: Mon, 24 Aug 2026 10:23:47 +0200 Subject: [PATCH] feat: work on distributions other than Arch The README asked for "Arch or an Arch derivative" and the code had two reasons for it. Neither of them was the mechanism, which is why this is mostly a matter of not assuming. The first was the flag file. Arch wraps Electron and Chromium in launchers that read $XDG_CONFIG_HOME/-flags.conf, and that route is the good one - it survives upgrades and applies to a launch from a terminal. Debian, Ubuntu, Fedora and openSUSE keep the equivalent under /etc, where it is the system's file and not the user's, so there is nothing to write and those applications have to go through their desktop entry instead. That already worked, because a launcher is read rather than assumed - but only if the launcher was recognised as Chromium at all, and it was not: APPNAME=chromium LIBDIR=/usr/lib/chromium exec -a "$APPNAME" "$LIBDIR/$APPNAME" $CHROMIUM_FLAGS "$@" is the shape every one of those wrappers has, and following it needs the assignments above resolved and -a understood as renaming the process rather than naming the program. Both are done now, and the wrappers that still cannot be followed are caught by CHROMIUM_FLAGS and CHROME_WRAPPER, which nothing but a Chromium launcher sets. Two applications on the machine this was written on turn out to have been missed for the same reason: Helium, whose wrapper is followed to a payload full of markers, and ONLYOFFICE, which ships libcef.so. Resolving more wrappers made an old inference dangerous. Any launcher that could be followed also had "-flags.conf" invented for it, on the theory that a wrapper builds that name from a variable at runtime. For an application that simply execs its own binary that file is read by nobody: the flag would have gone to ~/.config/DesktopEditors-flags.conf and the desktop entry that would have worked was skipped. The name is now derived only once the target has shown it reads a flag file at all. The second reason was snaps. /snap/bin/ is a symlink to snapd, so following it lands on /usr/bin/snap and says nothing; the payload is in the mounted revision, and that tree takes the same marker check as anything else. They get a settings switch of their own next to Flatpak, and the launcher entry as their only way in. Packaging is now a gate in front of the category rather than a category beside it. A Chromium installed as a snap or a Flatpak was filed as neither an application nor a browser, so turning browsers off did not reach it - which on Ubuntu means the default browser. It is a browser that happens to be packaged as a snap, and both switches apply. The rest is the same not-assuming: Steam is found in Debian's ~/.steam/debian-installation and in the snap's private tree, the Flatpak and snap export directories are scanned even when a session started before they were installed left them out of XDG_DATA_DIRS, /usr/lib/x86_64-linux-gnu counts as a shared directory the way /usr/lib does, the watcher covers snapd's export directory and the NixOS and Guix profiles, LANG is read from /etc/default/locale as well as /etc/locale.conf, and the systemd user unit directory is asked of systemd instead of guessed - while still following a PREFIX that was asked for. --- Makefile | 13 + README.md | 73 +++++- doc/middleclick-autoscroll.1.scd | 41 +++- po/de.po | 3 + po/middleclick-autoscroll.pot | 3 + res/systemd/middleclick-autoscroll.path | 20 +- src/lib/apply.sh | 26 +- src/lib/common.sh | 13 +- src/lib/config.sh | 1 + src/lib/detect.sh | 308 +++++++++++++++++++++--- src/lib/menu.sh | 1 + src/lib/patch.sh | 24 +- src/lib/steam.sh | 10 +- 13 files changed, 455 insertions(+), 81 deletions(-) diff --git a/Makefile b/Makefile index ea04302..bd0ebe5 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,20 @@ DATADIR ?= $(PREFIX)/share LIBDIR ?= $(DATADIR)/middleclick-autoscroll/lib LOCALEDIR ?= $(DATADIR)/locale MANDIR ?= $(DATADIR)/man + +# Where systemd looks for user units. For a normal install into /usr this is +# asked of systemd itself, because the answer is not the same everywhere - a +# distribution that still keeps /lib separate from /usr/lib says so here - and +# only guessed at when there is no systemd installed to ask. +# +# A build with a prefix of its own keeps the units under that prefix instead. +# systemd searches $(PREFIX)/lib/systemd/user as well, and a file outside the +# prefix it was asked for is not this build's to place. +ifeq ($(PREFIX),/usr) +USERUNITDIR ?= $(shell pkg-config --variable=systemduserunitdir systemd 2>/dev/null || echo /usr/lib/systemd/user) +else USERUNITDIR ?= $(PREFIX)/lib/systemd/user +endif LINGUAS := de MOFILES := $(patsubst %,po/%.mo,$(LINGUAS)) diff --git a/README.md b/README.md index aa7dbe3..75ed7d3 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,14 @@ underneath are identified by what they ship rather than by their name, and each of them is handled. ```bash -paru -S middleclick-autoscroll +paru -S middleclick-autoscroll # Arch and its derivatives middleclick-autoscroll enable ``` +There is no package for anything else yet, so elsewhere it is `make && sudo +make install` from a checkout and then the same one command. See +[Building from source](#building-from-source). + That is the whole setup. Nothing else has to be configured, and no file has to be edited. @@ -31,9 +35,10 @@ back on: Getting that argument into one application is a five-minute job. Getting it into all of them, in a way that survives the next package upgrade, is not: -- Some applications read a flag file, some don't. +- Some applications read a flag file, some don't - and which do depends on the + distribution as much as on the application. - Some ship their own copy of Electron, some use the system one. -- Flatpaks see none of the host's configuration. +- Flatpaks and snaps see none of the host's configuration. - An application that starts itself at login uses a different entry than the one in the menu, and Discord launched at login used to behave differently from Discord launched by hand. @@ -83,7 +88,7 @@ on — with the space bar: ``` **[4] Settings** has the categories — Electron and CEF applications, browsers, -Flatpaks, autostart entries, Steam, Spotify, whether to watch for new +Flatpaks, snaps, autostart entries, Steam, Spotify, whether to watch for new applications, and a field for extra Chromium arguments if you want any. There is a configuration file behind all of this. You are never asked to open @@ -95,8 +100,12 @@ Two routes, picked per application. | | | |---|---| -| **Flag file** | Arch's Electron and Chromium wrappers read extra arguments from `~/.config/-flags.conf`. This is the good one: it is the supported way to pass arguments, it survives package upgrades untouched, and it applies to a launch from a terminal as much as one from the menu. | -| **Desktop entry** | For applications that ship their own binary with no wrapper, and for Flatpaks, a copy of the entry with the argument appended goes into `~/.local/share/applications`, where it shadows the system one. | +| **Flag file** | Where the launcher reads extra arguments from `~/.config/-flags.conf`. This is the good one: it is the supported way to pass arguments, it survives package upgrades untouched, and it applies to a launch from a terminal as much as one from the menu. Arch's Electron and Chromium packages all work this way, and a number of individual vendors' launchers do everywhere else. | +| **Desktop entry** | For applications that ship their own binary with no wrapper, and for everything inside a Flatpak or a snap, a copy of the entry with the argument appended goes into `~/.local/share/applications`, where it shadows the system one. | + +Which of the two an application ends up on is decided by reading its launcher, +never by knowing which distribution this is. Nothing here has a list of +distributions in it any more than it has a list of applications. Entries that already live in `~/.local/share/applications` — AppImages, web app shortcuts — are edited in place and the original is kept. So are the entries in @@ -148,10 +157,15 @@ started once, at the start. ## Applications installed later A systemd user path unit watches every directory a launcher can appear in — -`/usr/share/applications`, the Flatpak exports, `~/.local/share/applications`, -`~/.config/autostart` — plus Steam's helper script. Anything new is handled -within a second of being installed, whether it came from pacman, the AUR, -Flatpak or an AppImage manager. There is no hook to install per package manager. +`/usr/share/applications`, the Flatpak exports, snapd's export directory, the +NixOS and Guix profiles, `~/.local/share/applications`, `~/.config/autostart` — +plus Steam's helper script. Anything new is handled within a second of being +installed, whether it came from pacman, apt, dnf, zypper, the AUR, Flatpak, +snapd or an AppImage manager. There is no hook to install per package manager, +which is the only reason one program can cover all of them. + +Without systemd nothing breaks; new applications are picked up the next time +`middleclick-autoscroll apply` runs instead of on their own. ## What it will not guess @@ -191,11 +205,34 @@ Run this before uninstalling the package. See `man middleclick-autoscroll` for the details. +## Distributions + +Any of them. Nothing here is keyed to a distribution name — what differs is +which of the two routes above an application ends up on, and that is read off +its launcher. + +On Arch and its derivatives most Electron and Chromium packages ship a wrapper +that reads a flag file, so most applications take that route. On Debian, +Ubuntu, Fedora and openSUSE the equivalent file lives under `/etc` and belongs +to the system rather than to you, so there is no flag file to write and those +applications go through their launcher entry instead. Both work. The flag file +is only the nicer of the two, because it applies to a launch from a terminal as +well. + +Snaps are handled the way Flatpaks are: what a snap ships lives in its own +mounted tree, `/snap/bin/` is a shim into snapd and says nothing about +what is behind it, and the launcher entry is the only way in. Each has a switch +of its own in the settings. + +Steam is found wherever the installation actually is — `~/.local/share/Steam` +for Valve's own package and Arch's, `~/.steam/debian-installation` for +Debian's, and inside the private tree for the Flatpak and the snap. + ## Requirements -Arch or an Arch derivative (CachyOS, EndeavourOS, Manjaro), bash, systemd for -the watcher. Nothing outside your home directory is ever written to, and running -it as root is refused. +Bash 4.2 or newer, GNU coreutils, and systemd for the watcher — that is all, +and it is what a desktop Linux install already has. Nothing outside your home +directory is ever written to, and running it as root is refused. ## Building from source @@ -204,6 +241,16 @@ make sudo make install ``` +`make` needs `msgfmt` (gettext) for the translations and `scdoc` for the man +page; both are optional and skipped with a note when missing. `make install` +puts the systemd user units where systemd itself says they go, and honours the +usual `PREFIX` and `DESTDIR`: + +```bash +make PREFIX=/usr/local +sudo make PREFIX=/usr/local install +``` + `make check` runs `bash -n` and, if installed, `shellcheck` over every script. ## License diff --git a/doc/middleclick-autoscroll.1.scd b/doc/middleclick-autoscroll.1.scd index 5c3f93d..de4af83 100644 --- a/doc/middleclick-autoscroll.1.scd +++ b/doc/middleclick-autoscroll.1.scd @@ -18,7 +18,9 @@ on the command line. *middleclick-autoscroll* finds every Chromium-based application on the system and puts that argument somewhere the application will actually read it, then -keeps doing so for anything installed later. +keeps doing so for anything installed later. It works on any distribution: +which of the routes below an application takes is read off its launcher, not +assumed from where the launcher came from. Run without a command it shows an interactive menu. Everything it can be told is reachable from there; the configuration file behind it does not need to be @@ -66,7 +68,7 @@ is refused. Two ways, chosen per application. *Flag file* - Arch's Electron and Chromium wrappers read extra arguments from + Where the launcher reads extra arguments from _$XDG_CONFIG_HOME/-flags.conf_. This is the preferred route: it is the supported way to pass arguments, it survives package upgrades, and it applies to a launch from a terminal as much as one from the menu. An @@ -74,12 +76,22 @@ Two ways, chosen per application. rather than duplicated - Chromium keeps only the last occurrence of that option, so a second one would switch the first one off. + Arch's Electron and Chromium packages all wrap their binaries this way, and + so do a number of individual vendors' launchers elsewhere. Whether a given + launcher does is read off the launcher itself, never assumed from the + distribution: only one that really names such a file takes this route. + *Desktop entry* For applications that ship their own binary with no wrapper, and for - Flatpaks, a copy of the desktop entry with the argument appended is written - to _~/.local/share/applications_, where it shadows the system one. Entries - that already live there - AppImages, web app shortcuts - are edited in - place, with the original kept. + everything inside a Flatpak or a snap, a copy of the desktop entry with the + argument appended is written to _~/.local/share/applications_, where it + shadows the system one. Entries that already live there - AppImages, web + app shortcuts - are edited in place, with the original kept. + + This is the route everything takes on the distributions whose Chromium + wrappers keep their equivalent file under _/etc_, where it is the system's + to write and not the user's: Debian, Ubuntu, Fedora and openSUSE among + them. A generated entry is marked *X-MCA-Generated* and an entry edited in place *X-MCA-Patched*. The two are never confused: the first is deleted when @@ -98,6 +110,11 @@ helper, inside Steam's own installation: ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh +Where that installation is depends on how Steam was installed: +_~/.local/share/Steam_ for Valve's own package and Arch's, +_~/.steam/debian-installation_ for Debian's, and the private tree of the +sandbox for the Flatpak and the snap. All of them are looked at. + Steam compares the installed files against its manifest at every start - by size, not by content - and restores whatever differs, so its launcher entry gets *-noverifyfiles*. So does its entry in _~/.config/autostart_, which Steam @@ -125,8 +142,8 @@ anyway. The official client is CEF rather than Electron. Installed through *spotify-launcher*, it is started by a program that builds its own command line and has a configuration file with a slot for extra arguments; that slot is -where the flag goes. Installed as a plain package, it is an ordinary desktop -entry and needs nothing special. +where the flag goes. Installed as a plain package, a Flatpak or a snap, it is +an ordinary desktop entry and needs nothing special. # WHAT CANNOT BE DETECTED @@ -155,9 +172,15 @@ _~/.cache/middleclick-autoscroll/detect_ *NO_COLOR* Disables colour. +# REQUIREMENTS + +Bash 4.2 or newer and GNU coreutils. The watcher needs a systemd user session; +without one everything else works and applications installed later are picked +up at the next *apply* rather than on their own. + # SEE ALSO -*systemctl*(1), *flatpak*(1) +*systemctl*(1), *flatpak*(1), *snap*(8) # AUTHORS diff --git a/po/de.po b/po/de.po index c8edeb1..4bc0922 100644 --- a/po/de.po +++ b/po/de.po @@ -106,6 +106,9 @@ msgstr "Chromium-basierte Browser" msgid "Flatpak applications" msgstr "Flatpak-Anwendungen" +msgid "Snap applications" +msgstr "Snap-Anwendungen" + msgid "Programs that start themselves at login" msgstr "Programme, die sich beim Anmelden selbst starten" diff --git a/po/middleclick-autoscroll.pot b/po/middleclick-autoscroll.pot index 48ec7d2..45cd735 100644 --- a/po/middleclick-autoscroll.pot +++ b/po/middleclick-autoscroll.pot @@ -106,6 +106,9 @@ msgstr "" msgid "Flatpak applications" msgstr "" +msgid "Snap applications" +msgstr "" + msgid "Programs that start themselves at login" msgstr "" diff --git a/res/systemd/middleclick-autoscroll.path b/res/systemd/middleclick-autoscroll.path index d23f833..d86984a 100644 --- a/res/systemd/middleclick-autoscroll.path +++ b/res/systemd/middleclick-autoscroll.path @@ -3,21 +3,33 @@ Description=Watch for newly installed Chromium-based applications Documentation=man:middleclick-autoscroll(1) [Path] -# Every way an application can turn up: a package, a Flatpak, something +# Every way an application can turn up: a package, a Flatpak, a snap, something # installed by hand. Watching where the launchers land means there is no hook -# to write per package manager - pacman, flatpak and an AppImage manager all -# end up here. +# to write per package manager - pacman, apt, dnf, zypper, flatpak, snapd and +# an AppImage manager all end up here. +# +# A directory that does not exist on this system costs nothing: systemd watches +# the nearest parent that does and picks the path up when it appears. PathModified=/usr/share/applications PathModified=/usr/local/share/applications PathModified=%h/.local/share/applications PathModified=%h/.config/autostart PathModified=/var/lib/flatpak/exports/share/applications PathModified=%h/.local/share/flatpak/exports/share/applications +PathModified=/var/lib/snapd/desktop/applications + +# NixOS and Guix build the search path out of profiles rather than /usr. +PathModified=/run/current-system/sw/share/applications +PathModified=%h/.nix-profile/share/applications +PathModified=%h/.guix-profile/share/applications # Steam replaces the script that starts its interface on every client update, -# which takes the patch with it. +# which takes the patch with it. Where that installation lives depends on how +# Steam was installed. PathChanged=%h/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh +PathChanged=%h/.steam/debian-installation/ubuntu12_64/steamwebhelper_sniper_wrap.sh PathChanged=%h/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh +PathChanged=%h/snap/steam/common/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh Unit=middleclick-autoscroll.service diff --git a/src/lib/apply.sh b/src/lib/apply.sh index f2d06d1..dc09693 100644 --- a/src/lib/apply.sh +++ b/src/lib/apply.sh @@ -10,7 +10,7 @@ MCA_CHANGES=0 MCA_ROUTES=() -# mca_route +# mca_route [packaging] # What happens to one application, left in MCA_ROUTE: # flags - the launcher reads a flag file; write it there # desktop - no flag file; shadow or edit the desktop entry @@ -24,7 +24,7 @@ MCA_ROUTES=() MCA_ROUTE='' mca_route() { - local kind="$1" id="$2" prog="$3" + local kind="$1" id="$2" prog="$3" packaging="${4:-native}" if [[ $kind == steam ]]; then if mca_config_list_has Skip "$id" || [[ $CFG_STEAM != yes ]]; then @@ -35,14 +35,14 @@ mca_route() { return fi - if ! mca_kind_wanted "$kind" "$id"; then + if ! mca_kind_wanted "$kind" "$id" "$packaging"; then [[ $kind == unknown ]] && MCA_ROUTE=unknown || MCA_ROUTE=off return fi - # A Flatpak has its own copy of everything and none of the host's wrappers, - # so the desktop entry is the only way in. - if [[ $kind == flatpak ]]; then + # A Flatpak or a snap carries its own copy of everything and sees none of + # the host's wrappers, so the desktop entry is the only way in. + if [[ $packaging != native ]]; then MCA_ROUTE=desktop return fi @@ -59,7 +59,7 @@ mca_route() { # as it likes to be - it writes only what differs, which is what keeps the # watcher from chasing its own changes. mca_apply() { - local i id file prog kind route steam_done=0 + local i id file prog kind packaging route steam_done=0 MCA_CHANGES=0 MCA_ROUTES=() @@ -73,8 +73,9 @@ mca_apply() { file="${MCA_FILES[i]}" prog="${MCA_PROGS[i]}" kind="${MCA_KINDS[i]}" + packaging="${MCA_PACKAGING[i]}" - mca_route "$kind" "$id" "$prog" + mca_route "$kind" "$id" "$prog" "$packaging" route="$MCA_ROUTE" MCA_ROUTES[i]="$route" @@ -153,9 +154,9 @@ mca_revert() { # The watcher # --------------------------------------------------------------------------- # A systemd user path unit watching every directory a desktop entry can appear -# in. That covers a package installed with pacman, a Flatpak, an AppImage -# registered by hand and a Steam client update, without a hook per package -# manager. +# in. That covers a package from whatever the distribution's package manager +# is, a Flatpak, a snap, an AppImage registered by hand and a Steam client +# update, without a hook per package manager. MCA_UNIT_PATH="middleclick-autoscroll.path" MCA_UNIT_SERVICE="middleclick-autoscroll.service" @@ -201,7 +202,8 @@ mca_count_routes() { MCA_ROUTES=() for i in "${!MCA_IDS[@]}"; do - mca_route "${MCA_KINDS[i]}" "${MCA_IDS[i]}" "${MCA_PROGS[i]}" + mca_route "${MCA_KINDS[i]}" "${MCA_IDS[i]}" "${MCA_PROGS[i]}" \ + "${MCA_PACKAGING[i]}" route="$MCA_ROUTE" MCA_ROUTES[i]="$route" case "$route" in diff --git a/src/lib/common.sh b/src/lib/common.sh index e833c8f..9c2682f 100644 --- a/src/lib/common.sh +++ b/src/lib/common.sh @@ -34,7 +34,7 @@ MCA_CACHEDIR="${MCA_CACHEDIR:-${MCA_XDG_CACHE}/${MCA_NAME}}" # Where generated desktop entries go. A file here shadows the one with the same # name in /usr/share/applications, which is how an application gets extra -# command line arguments without touching anything pacman owns. +# command line arguments without touching a file the package manager owns. MCA_APPDIR="${MCA_XDG_DATA}/applications" # Copies of every file that is edited in place rather than shadowed. @@ -60,8 +60,15 @@ mca_ui_locale() { [[ -z $l ]] && l="${LANG:-}" fi - if [[ -z $l && -r /etc/locale.conf ]]; then - l="$(sed -n 's/^LANG=//p' /etc/locale.conf | tr -d '"' | head -n1)" + # systemd writes /etc/locale.conf and most distributions use it; Debian and + # Ubuntu keep the same LANG= line in /etc/default/locale instead. + if [[ -z $l ]]; then + local f + for f in /etc/locale.conf /etc/default/locale; do + [[ -r $f ]] || continue + l="$(sed -n 's/^LANG=//p' "$f" | tr -d '"' | head -n1)" + [[ -n $l ]] && break + done fi printf '%s\n' "${l:-C}" diff --git a/src/lib/config.sh b/src/lib/config.sh index 19e2182..a67d41d 100644 --- a/src/lib/config.sh +++ b/src/lib/config.sh @@ -139,6 +139,7 @@ mca_config_load() { CFG_APPS=no; mca_config_bool PatchApps yes && CFG_APPS=yes CFG_BROWSERS=no; mca_config_bool PatchBrowsers yes && CFG_BROWSERS=yes CFG_FLATPAK=no; mca_config_bool PatchFlatpak yes && CFG_FLATPAK=yes + CFG_SNAP=no; mca_config_bool PatchSnap yes && CFG_SNAP=yes CFG_AUTOSTART=no; mca_config_bool PatchAutostart yes && CFG_AUTOSTART=yes CFG_STEAM=no; mca_config_bool PatchSteam yes && CFG_STEAM=yes CFG_SPOTIFY=no; mca_config_bool PatchSpotify yes && CFG_SPOTIFY=yes diff --git a/src/lib/detect.sh b/src/lib/detect.sh index db8ab52..8adebd5 100644 --- a/src/lib/detect.sh +++ b/src/lib/detect.sh @@ -27,9 +27,23 @@ MCA_MARKERS=( MCA_SYSTEM_DIRS=( / /bin /lib /lib32 /lib64 /sbin /usr /usr/bin /usr/lib /usr/lib32 /usr/lib64 /usr/libexec /usr/sbin /usr/local /usr/local/bin - /usr/local/lib /opt + /usr/local/lib /usr/local/libexec /opt ) +# The same thing for the layouts that put a machine triplet in the path. +# Debian and Ubuntu keep the shared libraries in /usr/lib/x86_64-linux-gnu +# rather than /usr/lib, so that directory is every bit as shared as /usr/lib is +# elsewhere and a marker sitting in it belongs to nobody in particular. +MCA_SYSTEM_DIR_GLOBS=( + '/usr/lib/*-linux-gnu*' '/usr/lib32/*-linux-gnu*' + '/usr/lib64/*-linux-gnu*' '/usr/local/lib/*-linux-gnu*' +) + +# Where snapd mounts the installed snaps. /snap is the usual place and the one +# the shims point into; distributions that keep /snap free of a top-level +# directory use the second. +MCA_SNAP_DIRS=(/snap /var/lib/snapd/snap) + # Strings in a launcher script that mean it starts a Chromium or Electron # process, for the wrappers whose command line is assembled out of variables and # cannot be followed from the outside. @@ -38,23 +52,51 @@ MCA_SYSTEM_DIRS=( # "chromium" on its own is not one of them: /usr/bin/xdg-open lists every # browser it knows how to start, and that is not a browser. # -# The flag file convention is an Arch packaging habit and says nothing about the -# engine either, so it is not in here. -MCA_SCRIPT_HINTS='ELECTRON_|app\.asar|chrome-sandbox|libcef|enable-blink-features|ozone-platform-hint' +# The flag file convention is one distribution's packaging habit and says +# nothing about the engine either, so it is not in here. +# +# CHROMIUM_FLAGS and CHROME_WRAPPER earn their place: they are the variables +# the Debian, Fedora and openSUSE Chromium wrappers and Google's own Chrome +# wrapper build their command line out of, and nothing else sets them. +MCA_SCRIPT_HINTS='ELECTRON_|app\.asar|chrome-sandbox|libcef|enable-blink-features|ozone-platform-hint|CHROMIUM_FLAGS|CHROME_WRAPPER|CHROME_VERSION_EXTRA' # --------------------------------------------------------------------------- # Desktop entries # --------------------------------------------------------------------------- +# Where Flatpak and snapd put the launchers they export. Both add these to +# XDG_DATA_DIRS themselves, through a file in /etc/profile.d - but only for a +# session that was started after they were installed, and only for a session +# manager that reads it at all. They are appended, after everything XDG names, +# so a directory that is already in the search path keeps its own position and +# the ones that were missing are still scanned. +mca_extra_desktop_dirs() { + printf '%s\n' \ + "$MCA_XDG_DATA/flatpak/exports/share/applications" \ + /var/lib/flatpak/exports/share/applications \ + /var/lib/snapd/desktop/applications +} + # The directories a desktop entry can come from, most specific first - which is # also XDG lookup order, so the first file found for an id is the one that is # actually used. mca_desktop_dirs() { local dirs="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" d - printf '%s\n' "$MCA_APPDIR" - while IFS= read -r -d: d; do - [[ -n $d ]] && printf '%s/applications\n' "${d%/}" - done <<< "${dirs}:" + local -A seen=() + + while IFS= read -r d; do + [[ -n $d ]] || continue + d="${d%/}" + [[ -n ${seen[$d]+set} ]] && continue + seen[$d]=1 + printf '%s\n' "$d" + done < <( + printf '%s\n' "$MCA_APPDIR" + while IFS= read -r -d: d; do + [[ -n $d ]] && printf '%s/applications\n' "${d%/}" + done <<< "${dirs}:" + mca_extra_desktop_dirs + ) } # mca_desktop_get @@ -192,7 +234,7 @@ mca_exec_is_steam_link() { case "${prog##*/}" in steam|steam-runtime) return 0 ;; esac - [[ $prog == flatpak:com.valvesoftware.Steam ]] + [[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]] } # --------------------------------------------------------------------------- @@ -265,6 +307,11 @@ _mca_has_markers() { for s in "${MCA_SYSTEM_DIRS[@]}"; do [[ $dir == "$s" ]] && return 1 done + for s in "${MCA_SYSTEM_DIR_GLOBS[@]}"; do + # Unquoted on purpose - these are patterns, not names. + # shellcheck disable=SC2053 + [[ $dir == $s ]] && return 1 + done for m in "${MCA_MARKERS[@]}"; do [[ -e "$dir/$m" ]] && return 0 @@ -273,23 +320,118 @@ _mca_has_markers() { return 1 } +# The plain assignments the script made before it handed over, for +# _mca_script_subst to read. A variable of its own rather than something passed +# around: every caller of _mca_script_target reads it through a command +# substitution, so each call already works on a copy and there is nothing here +# that two of them could collide over. +declare -A MCA_SCRIPT_VARS=() + +# _mca_script_subst +# The text with $NAME and ${NAME} replaced by what the script assigned to them, +# left in MCA_SUBST. +# +# Fails as soon as something turns up that only a running shell could work out +# - a positional parameter, a name the script never set, a default value. That +# is the point: an unresolvable path has to come out as no path at all, never +# as a wrong one. +MCA_SUBST='' + +_mca_script_subst() { + local text="$1" out='' rest name + + while [[ $text == *'$'* ]]; do + out+="${text%%\$*}" + rest="${text#*\$}" + + if [[ $rest == '{'* ]]; then + [[ $rest == *'}'* ]] || return 1 + name="${rest%%\}*}"; name="${name#\{}" + rest="${rest#*\}}" + else + name="${rest%%[!A-Za-z0-9_]*}" + rest="${rest:${#name}}" + fi + + [[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || return 1 + [[ -n ${MCA_SCRIPT_VARS[$name]+set} ]] || return 1 + + out+="${MCA_SCRIPT_VARS[$name]}" + text="$rest" + done + + MCA_SUBST="$out$text" + return 0 +} + # _mca_script_target