feat: support firefox and other gecko apps

This commit is contained in:
Felitendo committed 2026-10-01 09:33:22 +02:00
1 parent 9b6c64bd97
commit 95d602e1c4
23 files changed
+472 -59

No files matched your search

+4 -1
View File
@@ -7,7 +7,7 @@
<h3 align="center">Middle-click autoscroll in every app that supports it.</h3>
<p align="center">
Browsers, Electron apps like Discord and Spotify, Steam and anything else built on Chromium.
Firefox, Chromium browsers, Electron apps like Discord and Spotify, Steam and anything else built on Chromium.
</p>
<h5 align="center">
@@ -126,6 +126,9 @@ Adding it to every app by hand is a pain, and updates can undo it. This tool doe
**Browsers** get a spelling of the flag that shows no warning bar. Brave and Helium also get their
own name for the feature. A browser ignores names it does not know.
**Firefox** has autoscroll too, but as a setting, not a flag. It goes into the `user.js` of each
profile. The same works for LibreWolf, Zen, Floorp, Waterfox and Thunderbird.
**Steam** builds its own command line and repairs any file that looks changed. So the flag goes into
the script that starts its web helper, and the file keeps the size and date Steam expects. Steam
finds nothing to repair, and autoscroll works in the store and the library. After a Steam update,
+33 -4
View File
@@ -34,10 +34,13 @@ it). A name a browser does not know is ignored. 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.
Firefox and everything else built on Gecko has autoscroll as well, behind a
setting rather than a flag. See *FIREFOX*.
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
edited by hand. Its applications list turns single applications on or off,
Steam and Spotify included. Its settings hold the rest: the desktop's
Steam, Spotify and Firefox included. Its settings hold the rest: the desktop's
middle-click paste, the watcher, and more Chromium arguments for every
application.
@@ -69,8 +72,7 @@ is refused.
last run was.
*list*
Every Chromium-based application that was found, and how each one is
handled.
Every application that was found, and how each one is handled.
*-h*, *--help*
Show a summary of the commands.
@@ -191,6 +193,29 @@ where the flag goes, and the launcher shows up in the applications list like
any other application. Installed as a plain package, a Flatpak or a snap, it is
an ordinary desktop entry and needs nothing special.
# FIREFOX
Firefox has autoscroll of its own, switched off on Linux. There is no flag for
it, only a setting: *general.autoScroll*. It goes into _user.js_ in every
profile, as a marked block, and Firefox reads that file at every start. The
same goes for everything built on Gecko: LibreWolf, Zen, Floorp, Waterfox and
Thunderbird.
Where the profiles are is read from the application's own _application.ini_,
the way Firefox works it out: _~/.mozilla/firefox_, or
_~/.config/mozilla/firefox_ for a new install of Firefox 147 or later. A Flatpak
or a snap has the same inside its own folder. Every profile in _profiles.ini_
is covered. The watcher looks at that file too, so a new profile is covered
from its second start on.
A profile where the setting is already on, in Firefox's own settings or in
_user.js_, is left alone and not recorded.
Firefox copies the setting into _prefs.js_, so *disable* takes it out of both
files. A Firefox that is still open would write it back when it quits. So for
that profile _user.js_ says *false* instead, and the next *disable* or
*enable* clears it up.
# MIDDLE-CLICK PASTE
Inside a Chromium application the argument settles this on its own: with
@@ -269,8 +294,12 @@ _~/.config/kwinrc_
written when the desktop's middle-click paste is turned off, and put
back by *disable*.
_user.js_ in every Firefox profile
Gets a block between *// >>> middleclick-autoscroll* and
*// <<< middleclick-autoscroll*. Nothing else in the file is touched.
_~/.cache/middleclick-autoscroll/detect_
Which programs were found to be Chromium, keyed by size and modification
Which programs were found to be Chromium or Gecko, keyed by size and modification
time. Its first line says which version of the program wrote it, and a
file from an older one is ignored rather than trusted. Otherwise an entry
for a file that has not changed would never be looked at again.
+5 -2
View File
@@ -39,6 +39,9 @@ msgstr "soubor s příznaky"
msgid "launcher"
msgstr "spouštěč"
msgid "profile"
msgstr "profil"
msgid "cannot tell"
msgstr "nelze určit"
@@ -131,8 +134,8 @@ msgstr "Nastavení se nepodařilo uložit."
msgid "Looking at the installed applications..."
msgstr "Procházím nainstalované aplikace..."
msgid "No Chromium-based applications found."
msgstr "Nenalezeny žádné aplikace založené na Chromiu."
msgid "No supported applications found."
msgstr "Nenalezeny žádné podporované aplikace."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr "Co je označeno „nelze určit“, zůstane vypnuté, dokud to tu nezapnete."
+5 -2
View File
@@ -38,6 +38,9 @@ msgstr "Flag-Datei"
msgid "launcher"
msgstr "Startereintrag"
msgid "profile"
msgstr "Profil"
msgid "cannot tell"
msgstr "nicht erkennbar"
@@ -131,8 +134,8 @@ msgstr "Einstellung konnte nicht gespeichert werden."
msgid "Looking at the installed applications..."
msgstr "Installierte Anwendungen werden durchgesehen …"
msgid "No Chromium-based applications found."
msgstr "Keine Chromium-basierten Anwendungen gefunden."
msgid "No supported applications found."
msgstr "Keine unterstützten Anwendungen gefunden."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr "Was „nicht erkennbar“ ist, bleibt aus, bis es hier eingeschaltet wird."
+5 -2
View File
@@ -39,6 +39,9 @@ msgstr "archivo de flags"
msgid "launcher"
msgstr "lanzador"
msgid "profile"
msgstr "perfil"
msgid "cannot tell"
msgstr "no se sabe"
@@ -132,8 +135,8 @@ msgstr "No se pudo guardar el ajuste."
msgid "Looking at the installed applications..."
msgstr "Revisando las aplicaciones instaladas..."
msgid "No Chromium-based applications found."
msgstr "No se encontraron aplicaciones basadas en Chromium."
msgid "No supported applications found."
msgstr "No se encontraron aplicaciones compatibles."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr "Lo marcado como «no se sabe» sigue desactivado hasta activarlo aquí."
+5 -2
View File
@@ -39,6 +39,9 @@ msgstr "fichier de flags"
msgid "launcher"
msgstr "lanceur"
msgid "profile"
msgstr "profil"
msgid "cannot tell"
msgstr "indéterminé"
@@ -132,8 +135,8 @@ msgstr "Impossible d'enregistrer le paramètre."
msgid "Looking at the installed applications..."
msgstr "Examen des applications installées..."
msgid "No Chromium-based applications found."
msgstr "Aucune application basée sur Chromium trouvée."
msgid "No supported applications found."
msgstr "Aucune application prise en charge trouvée."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr ""
+5 -2
View File
@@ -39,6 +39,9 @@ msgstr "file di flag"
msgid "launcher"
msgstr "lanciatore"
msgid "profile"
msgstr "profilo"
msgid "cannot tell"
msgstr "incerto"
@@ -132,8 +135,8 @@ msgstr "Impossibile salvare l'impostazione."
msgid "Looking at the installed applications..."
msgstr "Esame delle applicazioni installate..."
msgid "No Chromium-based applications found."
msgstr "Nessuna applicazione basata su Chromium trovata."
msgid "No supported applications found."
msgstr "Nessuna applicazione supportata trovata."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr ""
+4 -1
View File
@@ -38,6 +38,9 @@ msgstr ""
msgid "launcher"
msgstr ""
msgid "profile"
msgstr ""
msgid "cannot tell"
msgstr ""
@@ -130,7 +133,7 @@ msgstr ""
msgid "Looking at the installed applications..."
msgstr ""
msgid "No Chromium-based applications found."
msgid "No supported applications found."
msgstr ""
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
+5 -2
View File
@@ -39,6 +39,9 @@ msgstr "flagbestand"
msgid "launcher"
msgstr "starter"
msgid "profile"
msgstr "profiel"
msgid "cannot tell"
msgstr "onbekend"
@@ -132,8 +135,8 @@ msgstr "De instelling kon niet worden opgeslagen."
msgid "Looking at the installed applications..."
msgstr "Geïnstalleerde toepassingen worden bekeken..."
msgid "No Chromium-based applications found."
msgstr "Geen op Chromium gebaseerde toepassingen gevonden."
msgid "No supported applications found."
msgstr "Geen ondersteunde toepassingen gevonden."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr ""
+5 -2
View File
@@ -39,6 +39,9 @@ msgstr "plik flag"
msgid "launcher"
msgstr "skrót"
msgid "profile"
msgstr "profil"
msgid "cannot tell"
msgstr "nie wiadomo"
@@ -131,8 +134,8 @@ msgstr "Nie udało się zapisać ustawienia."
msgid "Looking at the installed applications..."
msgstr "Przeglądanie zainstalowanych aplikacji..."
msgid "No Chromium-based applications found."
msgstr "Nie znaleziono aplikacji opartych na Chromium."
msgid "No supported applications found."
msgstr "Nie znaleziono obsługiwanych aplikacji."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr ""
+5 -2
View File
@@ -39,6 +39,9 @@ msgstr "arquivo de flags"
msgid "launcher"
msgstr "lançador"
msgid "profile"
msgstr "perfil"
msgid "cannot tell"
msgstr "indeterminado"
@@ -132,8 +135,8 @@ msgstr "Não foi possível salvar a configuração."
msgid "Looking at the installed applications..."
msgstr "Verificando os aplicativos instalados..."
msgid "No Chromium-based applications found."
msgstr "Nenhum aplicativo baseado em Chromium encontrado."
msgid "No supported applications found."
msgstr "Nenhum aplicativo compatível encontrado."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr ""
+5 -2
View File
@@ -39,6 +39,9 @@ msgstr "файл флагов"
msgid "launcher"
msgstr "ярлык"
msgid "profile"
msgstr "профиль"
msgid "cannot tell"
msgstr "неизвестно"
@@ -132,8 +135,8 @@ msgstr "Не удалось сохранить настройку."
msgid "Looking at the installed applications..."
msgstr "Просмотр установленных приложений..."
msgid "No Chromium-based applications found."
msgstr "Приложения на основе Chromium не найдены."
msgid "No supported applications found."
msgstr "Поддерживаемые приложения не найдены."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr ""
+5 -2
View File
@@ -39,6 +39,9 @@ msgstr "bayrak dosyası"
msgid "launcher"
msgstr "başlatıcı"
msgid "profile"
msgstr "profil"
msgid "cannot tell"
msgstr "belirsiz"
@@ -132,8 +135,8 @@ msgstr "Ayar kaydedilemedi."
msgid "Looking at the installed applications..."
msgstr "Kurulu uygulamalara bakılıyor..."
msgid "No Chromium-based applications found."
msgstr "Chromium tabanlı uygulama bulunamadı."
msgid "No supported applications found."
msgstr "Desteklenen uygulama bulunamadı."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr "\"belirsiz\" olarak işaretlenenler burada açılana kadar kapalı kalır."
+5 -2
View File
@@ -39,6 +39,9 @@ msgstr "файл прапорців"
msgid "launcher"
msgstr "ярлик"
msgid "profile"
msgstr "профіль"
msgid "cannot tell"
msgstr "невідомо"
@@ -131,8 +134,8 @@ msgstr "Не вдалося зберегти налаштування."
msgid "Looking at the installed applications..."
msgstr "Перегляд встановлених застосунків..."
msgid "No Chromium-based applications found."
msgstr "Застосунків на основі Chromium не знайдено."
msgid "No supported applications found."
msgstr "Підтримуваних застосунків не знайдено."
msgid "Anything marked \"cannot tell\" stays off until it is turned on here."
msgstr ""
+1 -1
View File
@@ -1,5 +1,5 @@
[Unit]
Description=Watch for newly installed Chromium-based applications
Description=Watch for newly installed applications
Documentation=man:middleclick-autoscroll(1)
[Path]
+1 -1
View File
@@ -1,5 +1,5 @@
[Unit]
Description=Apply middle-click autoscroll to Chromium-based applications
Description=Apply middle-click autoscroll
Documentation=man:middleclick-autoscroll(1)
[Service]
+45 -13
View File
@@ -16,6 +16,7 @@ MCA_ROUTES=()
# desktop - no flag file; shadow or edit the desktop entry
# steam - Steam's own two-part treatment
# spotify - the flag goes into spotify-launcher's configuration file
# gecko - a setting in each of the application's profiles
# unknown - cannot tell what engine this is (an AppImage), so nothing is done
# off - detected, but switched off
#
@@ -27,7 +28,7 @@ MCA_ROUTE=''
mca_route() {
local kind="$1" id="$2" prog="$3" packaging="${4:-native}"
if [[ $kind == steam || $kind == spotify ]]; then
if [[ $kind == steam || $kind == spotify || $kind == gecko ]]; then
if mca_config_list_has Skip "$id"; then
MCA_ROUTE=off
else
@@ -65,6 +66,7 @@ mca_apply() {
MCA_CHANGES=0
MCA_ROUTES=()
MCA_N_ON=0; MCA_N_OFF=0; MCA_N_UNKNOWN=0
MCA_GECKO_SEEN=(); MCA_GECKO_WATCH=()
mca_config_load
@@ -116,6 +118,9 @@ mca_apply() {
(( spotify_done )) || mca_spotify_apply
spotify_done=1
;;
gecko)
mca_gecko_apply "${MCA_PROFILES[i]}" "$packaging" "$prog"
;;
esac
done
@@ -178,7 +183,8 @@ MCA_UNIT_SERVICE="middleclick-autoscroll.service"
# The desktop folder is watched like every other directory an entry can turn up
# in, but its name is translated and the unit that ships with the package
# cannot know it. So that one path is written here, into a drop-in, from the
# name this system actually uses.
# name this system actually uses. The same goes for where Firefox and its forks
# keep their profiles, which depends on what is installed.
MCA_UNIT_DROPIN="${MCA_XDG_CONFIG}/systemd/user/${MCA_UNIT_PATH}.d"
mca_watch_available() {
@@ -196,23 +202,47 @@ mca_watch_enabled() {
# same reason: the watcher runs after every desktop entry that appears anywhere on
# the system, and a daemon-reload each time would be absurd.
_mca_watch_dropin_write() {
local dir file="$MCA_UNIT_DROPIN/desktop.conf" content
dir="$(mca_desktop_folder)"
[[ -d $dir ]] || { _mca_watch_dropin_remove; return $?; }
local dir path content='' changed=1
# A per cent sign starts a specifier in a unit file and has to be doubled
# to mean itself. Rare in a folder name, fatal when it happens.
content="[Path]"$'\n'"PathModified=${dir//%/%%}"$'\n'
dir="$(mca_desktop_folder)"
[[ -d $dir ]] && content="[Path]"$'\n'"PathModified=${dir//%/%%}"$'\n'
_mca_watch_conf desktop.conf "$content" && changed=0
mca_write_if_changed "$file" "$content"
# Filled by the apply that ran just before. A new profile is added there.
content=''
for path in "${MCA_GECKO_WATCH[@]}"; do
content+="PathChanged=${path//%/%%}"$'\n'
done
[[ -n $content ]] && content="[Path]"$'\n'"$content"
_mca_watch_conf profiles.conf "$content" && changed=0
return "$changed"
}
# _mca_watch_conf <name> <content>
# Writes one drop-in, or removes it when the content is empty. Returns 0 when
# it changed.
_mca_watch_conf() {
local file="$MCA_UNIT_DROPIN/$1"
if [[ -n $2 ]]; then
mca_write_if_changed "$file" "$2"
return
fi
[[ -e $file ]] || return 1
rm -f -- "$file"
rmdir "$MCA_UNIT_DROPIN" 2>/dev/null || true
return 0
}
_mca_watch_dropin_remove() {
[[ -e "$MCA_UNIT_DROPIN/desktop.conf" ]] || return 1
rm -f -- "$MCA_UNIT_DROPIN/desktop.conf"
rmdir "$MCA_UNIT_DROPIN" 2>/dev/null || true
return 0
local changed=1
_mca_watch_conf desktop.conf '' && changed=0
_mca_watch_conf profiles.conf '' && changed=0
return "$changed"
}
mca_watch_enable() {
@@ -226,7 +256,8 @@ mca_watch_enable() {
# mca_watch_refresh
# Keeps a running watcher in step with a desktop folder that has been renamed,
# which is what a change of session language does to it.
# which is what a change of session language does to it, and with the Firefox
# profiles there are now.
mca_watch_refresh() {
mca_watch_available || return 0
_mca_watch_dropin_write || return 0
@@ -295,6 +326,7 @@ mca_route_label() {
desktop) mca_msg "launcher" ;;
steam) mca_msg "Steam" ;;
spotify) printf 'spotify-launcher' ;;
gecko) mca_msg "profile" ;;
unknown) mca_msg "cannot tell" ;;
*) mca_msg "off" ;;
esac
+3 -1
View File
@@ -283,11 +283,13 @@ mca_state_write() {
# inplace <edited file> <basename of its backup copy>
# flags <flag file> created | appended | merged
# kwin <kwinrc> what EnablePrimarySelection said before
# gecko <user.js in a profile> created | block
#
# The kinds differ in how they are undone: a backup is restored wholesale, a
# flag file only loses the one line that was added to it, and kwinrc gets one
# key written back. It is a file KDE writes to itself, and putting a whole
# copy of it back would take everything else settled since with it.
# copy of it back would take everything else settled since with it. A user.js
# loses its block, and prefs.js the line Firefox copied out of it.
mca_ledger_add() {
local kind="$1" path="$2" detail="${3:-}"
+53 -8
View File
@@ -343,7 +343,9 @@ _mca_stat_batch() {
# taught the hint scan about Discord's launcher, and Discord stayed out of the
# list anyway, because /usr/bin/discord had not changed and its old "no" was
# still in here.
MCA_CACHE_FORMAT="# middleclick-autoscroll detect 2 $MCA_VERSION"
#
# 3 adds the gecko verdict.
MCA_CACHE_FORMAT="# middleclick-autoscroll detect 3 $MCA_VERSION"
_mca_cache_load() {
local path stamp verdict first=1
@@ -862,6 +864,8 @@ _mca_appimage_verdict() {
# no - something else
# unknown - an image whose payload could not be read, so neither answer has
# been earned and the applications screen offers it as a choice
# gecko:<rel> - Firefox or another Gecko application, which keeps its
# profiles in <rel>. See gecko.sh.
#
# Assigns rather than returns three states through an exit code, and is the one
# place the memo and the on-disk cache are consulted.
@@ -900,8 +904,11 @@ mca_detect_verdict() {
real="$(readlink -f -- "$prog" 2>/dev/null)" || real="$prog"
MCA_DETECT_UNSURE=0
MCA_DETECT_GECKO=''
if _mca_detect_uncached "$real"; then
MCA_VERDICT=yes
elif [[ -n $MCA_DETECT_GECKO ]]; then
MCA_VERDICT="gecko:$MCA_DETECT_GECKO"
elif (( MCA_DETECT_UNSURE )); then
MCA_VERDICT=unknown
else
@@ -930,6 +937,10 @@ mca_is_chromium() {
# was nothing there. Only mca_detect_verdict reads it, straight after the call.
MCA_DETECT_UNSURE=0
# Set by _mca_detect_uncached to where the profiles are, when the program turned
# out to be Gecko rather than Chromium.
MCA_DETECT_GECKO=''
_mca_detect_uncached() {
local real="$1" depth="${2:-0}" dir target
@@ -943,6 +954,7 @@ _mca_detect_uncached() {
if target="$(_mca_script_target "$real")" && [[ -n $target ]]; then
_mca_detect_uncached "$(readlink -f -- "$target" 2>/dev/null || printf '%s' "$target")" \
$(( depth + 1 )) && return 0
[[ -n $MCA_DETECT_GECKO ]] && return 1
fi
grep -qE "$MCA_SCRIPT_HINTS" -- "$real" 2>/dev/null && return 0
return 1
@@ -954,6 +966,14 @@ _mca_detect_uncached() {
_mca_has_markers "$dir" && return 0
[[ ${dir##*/} == bin ]] && _mca_has_markers "${dir%/*}" && return 0
# Not Chromium but Gecko, which has autoscroll of its own.
if _mca_gecko_rel "$dir" \
|| { [[ ${dir##*/} == bin ]] && _mca_gecko_rel "${dir%/*}"; }
then
MCA_DETECT_GECKO="$MCA_GECKO_REL"
return 1
fi
# An AppImage keeps all of that inside a filesystem appended to itself, so
# there is nothing next to the binary to find. But the names of everything
# in there can be read, and that settles it either way.
@@ -989,8 +1009,9 @@ MCA_IDS=() # desktop file id, without the .desktop suffix
MCA_FILES=() # the desktop entry that is in effect for that id
MCA_NAMES=() # display name
MCA_PROGS=() # resolved program, or a Flatpak app id or a snap name
MCA_KINDS=() # app | browser | steam | spotify | unknown | no
MCA_KINDS=() # app | browser | steam | spotify | gecko | unknown | no
MCA_PACKAGING=() # native | flatpak | snap
MCA_PROFILES=() # where a Gecko application keeps its profiles, see gecko.sh
# A scan reads every desktop entry on the system, so the menu does it once and
# then redraws from what it found. Applying rescans on its own, so nothing else
@@ -1003,12 +1024,12 @@ mca_scan_once() {
}
mca_scan() {
local dir file id name exec_line prog kind packaging i
local dir file id name exec_line prog kind packaging profiles i
local -A seen=()
local -a c_ids=() c_files=() c_names=() c_progs=() c_browser=() c_stat=()
MCA_IDS=(); MCA_FILES=(); MCA_NAMES=(); MCA_PROGS=(); MCA_KINDS=()
MCA_PACKAGING=()
MCA_PACKAGING=(); MCA_PROFILES=()
# Pass one: read the entries and work out what each of them starts. No
# detection yet: that needs a stat per program, and those are collected so
@@ -1073,6 +1094,7 @@ mca_scan() {
prog="${c_progs[i]}"
kind=no
packaging=native
profiles=''
if [[ $prog == flatpak:* ]]; then
packaging=flatpak
@@ -1080,6 +1102,8 @@ mca_scan() {
kind=steam
elif mca_flatpak_is_chromium "${prog#flatpak:}"; then
(( c_browser[i] )) && kind=browser || kind=app
elif mca_flatpak_gecko "${prog#flatpak:}"; then
kind=gecko profiles="$MCA_GECKO_REL"
fi
prog="${prog#flatpak:}"
elif [[ $prog == snap:* ]]; then
@@ -1089,6 +1113,8 @@ mca_scan() {
kind=steam
elif mca_snap_is_chromium "$prog"; then
(( c_browser[i] )) && kind=browser || kind=app
elif mca_snap_gecko "$prog"; then
kind=gecko profiles="$MCA_GECKO_REL"
fi
elif mca_prog_is_steam "$prog"; then
# Steam is Chromium inside, but nothing about it can be changed
@@ -1103,6 +1129,7 @@ mca_scan() {
case "$MCA_VERDICT" in
yes) (( c_browser[i] )) && kind=browser || kind=app ;;
unknown) kind=unknown ;;
gecko:*) kind=gecko profiles="${MCA_VERDICT#gecko:}" ;;
esac
fi
@@ -1114,6 +1141,7 @@ mca_scan() {
MCA_PROGS+=("$prog")
MCA_KINDS+=("$kind")
MCA_PACKAGING+=("$packaging")
MCA_PROFILES+=("$profiles")
done
mca_cache_flush
@@ -1211,16 +1239,14 @@ mca_snap_is_chromium() {
# Flatpak keeps every application in its own tree, so the marker check works the
# same way once that tree has been located.
mca_flatpak_is_chromium() {
local id="$1" loc
mca_have flatpak || return 1
local id="$1"
if [[ -n ${MCA_DETECT_MEMO[flatpak:$id]+set} ]]; then
[[ ${MCA_DETECT_MEMO[flatpak:$id]} == yes ]]
return $?
fi
loc="$(flatpak info --show-location "$id" 2>/dev/null)"
if [[ -n $loc ]] && _mca_find_markers "$loc/files" 4; then
if mca_flatpak_location "$id" && _mca_find_markers "$MCA_FLATPAK_AT/files" 4; then
MCA_DETECT_MEMO[flatpak:$id]=yes
return 0
fi
@@ -1229,6 +1255,25 @@ mca_flatpak_is_chromium() {
return 1
}
# mca_flatpak_location <app id>
# Where Flatpak keeps the application, in MCA_FLATPAK_AT. Assigned rather than
# printed so the memo survives: both the Chromium and the Gecko check ask.
declare -A MCA_FLATPAK_LOC=()
MCA_FLATPAK_AT=''
mca_flatpak_location() {
local id="$1"
if [[ -z ${MCA_FLATPAK_LOC[$id]+set} ]]; then
MCA_FLATPAK_LOC[$id]=''
mca_have flatpak \
&& MCA_FLATPAK_LOC[$id]="$(flatpak info --show-location "$id" 2>/dev/null)"
fi
MCA_FLATPAK_AT="${MCA_FLATPAK_LOC[$id]}"
[[ -n $MCA_FLATPAK_AT ]]
}
# mca_kind_wanted <kind> <id>
# Whether this entry should be patched. Skip beats everything, an explicit
# include beats detection, and detection beats nothing.
+254
View File
@@ -0,0 +1,254 @@
# shellcheck shell=bash
#
# Firefox, and everything else built on Gecko: LibreWolf, Zen, Floorp,
# Waterfox, Thunderbird.
#
# Gecko has autoscroll of its own, off on Linux. There is no command line flag
# for it, only a setting: general.autoScroll. Settings live in each profile, so
# the line goes into the profile's user.js, which Gecko reads at every start.
#
# Gecko copies what user.js sets into prefs.js. So undoing takes the line out
# of prefs.js as well. A browser that is still open would write it back when it
# quits, so for that profile user.js says false instead, and the record stays
# until the next undo can finish the job.
MCA_GECKO_BEGIN='// >>> middleclick-autoscroll'
MCA_GECKO_END='// <<< middleclick-autoscroll'
MCA_GECKO_ON='user_pref("general.autoScroll", true);'
MCA_GECKO_OFF='user_pref("general.autoScroll", false);'
# The profile folders one apply has done already, and the profiles.ini files
# the watcher should look at. Both are filled by mca_gecko_apply.
declare -A MCA_GECKO_SEEN=()
MCA_GECKO_WATCH=()
# Set by mca_gecko_revert when a profile is still open and has to be undone
# again later.
MCA_GECKO_PENDING=0
# _mca_gecko_rel <directory>
# Whether a Gecko application is installed there. If so, where it keeps its
# profiles, relative to the home or config folder, in MCA_GECKO_REL. Read from
# application.ini the way Gecko does it: [App] Profile= when it is set, else
# Vendor/Name, in lowercase.
MCA_GECKO_REL=''
_mca_gecko_rel() {
local dir="$1" line group='' vendor='' name='' profile='' rel=''
MCA_GECKO_REL=''
[[ -e $dir/libxul.so && -r $dir/application.ini ]] || return 1
while IFS= read -r line || [[ -n $line ]]; do
line="${line%$'\r'}"
case "$line" in
'['*) group="$line" ;;
Vendor=*) [[ $group == '[App]' ]] && vendor="${line#*=}" ;;
Name=*) [[ $group == '[App]' ]] && name="${line#*=}" ;;
Profile=*) [[ $group == '[App]' ]] && profile="${line#*=}" ;;
esac
done < "$dir/application.ini"
if [[ -n $profile ]]; then
rel="${profile//\\//}"
while [[ $rel == /* ]]; do rel="${rel#/}"; done
rel="${rel#.}"
elif [[ -n $name ]]; then
rel="${vendor:+$vendor/}$name"
fi
# Gecko lowercases in ASCII. A Turkish locale turns I into a dotless ı.
rel="${rel,,}"
rel="${rel//ı/i}"
[[ -n $rel && /$rel/ != */../* ]] || return 1
MCA_GECKO_REL="$rel"
}
# _mca_gecko_tree <root> <depth>
# The same for a Flatpak or a snap, where libxul.so is somewhere in the tree.
_mca_gecko_tree() {
local lib
MCA_GECKO_REL=''
[[ -d $1 ]] || return 1
lib="$(find "$1" -maxdepth "$2" -name libxul.so -print -quit 2>/dev/null)"
[[ -n $lib ]] && _mca_gecko_rel "${lib%/*}"
}
# mca_flatpak_gecko <app id> / mca_snap_gecko <snap name>
# Whether it is Gecko, with the folder in MCA_GECKO_REL. Memoized like the
# Chromium check, and an empty answer means no.
declare -A MCA_GECKO_MEMO=()
mca_flatpak_gecko() {
local key="flatpak:$1"
if [[ -z ${MCA_GECKO_MEMO[$key]+set} ]]; then
MCA_GECKO_REL=''
mca_flatpak_location "$1" && _mca_gecko_tree "$MCA_FLATPAK_AT/files" 4
MCA_GECKO_MEMO[$key]="$MCA_GECKO_REL"
fi
MCA_GECKO_REL="${MCA_GECKO_MEMO[$key]}"
[[ -n $MCA_GECKO_REL ]]
}
mca_snap_gecko() {
local key="snap:$1" d
if [[ -z ${MCA_GECKO_MEMO[$key]+set} ]]; then
MCA_GECKO_REL=''
for d in "${MCA_SNAP_DIRS[@]}"; do
_mca_gecko_tree "$d/$1/current" 5 && break
done
MCA_GECKO_MEMO[$key]="$MCA_GECKO_REL"
fi
MCA_GECKO_REL="${MCA_GECKO_MEMO[$key]}"
[[ -n $MCA_GECKO_REL ]]
}
# mca_gecko_roots <rel> <packaging> <name>
# Every folder that can hold the application's profiles.ini. An older install
# uses ~/.<rel>. A new one (Firefox 147 and later) uses $XDG_CONFIG_HOME/<rel>,
# or ~/<rel> for a fork with a profile name of its own. A Flatpak or a snap has
# the same inside its own folder.
mca_gecko_roots() {
local rel="$1" packaging="$2" name="$3" d
case "$packaging" in
flatpak)
printf '%s\n' "$HOME/.var/app/$name/.$rel" "$HOME/.var/app/$name/config/$rel"
;;
snap)
for d in common current; do
printf '%s\n' "$HOME/snap/$name/$d/.$rel" "$HOME/snap/$name/$d/.config/$rel"
done
;;
*)
printf '%s\n' "$HOME/.$rel" "$MCA_XDG_CONFIG/$rel"
[[ $rel == */* ]] || printf '%s\n' "$HOME/$rel"
;;
esac
}
# mca_gecko_profiles <root>
# Every profile folder that profiles.ini there lists.
mca_gecko_profiles() {
local root="$1" line path='' relative=1 inprofile=0
[[ -r $root/profiles.ini ]] || return 0
while IFS= read -r line || [[ -n $line ]]; do
line="${line%$'\r'}"
case "$line" in
'['*)
_mca_gecko_profile_emit
[[ $line == '[Profile'* ]] && inprofile=1 || inprofile=0
path='' relative=1
;;
Path=*) path="${line#Path=}" ;;
IsRelative=*) relative="${line#IsRelative=}" ;;
esac
done < "$root/profiles.ini"
_mca_gecko_profile_emit
}
_mca_gecko_profile_emit() {
(( inprofile )) && [[ -n $path ]] || return 0
[[ $relative == 1 ]] && path="$root/$path"
[[ -d $path ]] && printf '%s\n' "$path"
return 0
}
# _mca_gecko_write <user.js> <line>
# Puts the line into user.js as a marked block, replacing an older block.
# Returns like mca_write_if_changed.
_mca_gecko_write() {
local file="$1" line="$2" content=''
[[ -f $file ]] && content="$(_mca_flags_drop_block "$file" "$MCA_GECKO_BEGIN" "$MCA_GECKO_END")"
content="${content:+$content$'\n'}$MCA_GECKO_BEGIN"$'\n'"$line"$'\n'"$MCA_GECKO_END"
mca_write_if_changed "$file" "$content"$'\n'
}
# mca_gecko_apply <rel> <packaging> <name>
# Turns autoscroll on in every profile of the application.
mca_gecko_apply() {
local root profile
while IFS= read -r root; do
[[ -n ${MCA_GECKO_SEEN[$root]+set} ]] && continue
MCA_GECKO_SEEN[$root]=1
# A new profile is written into profiles.ini, so the watcher can pick
# it up.
MCA_GECKO_WATCH+=("$root/profiles.ini")
while IFS= read -r profile; do
_mca_gecko_profile_apply "$profile"
done < <(mca_gecko_profiles "$root")
done < <(mca_gecko_roots "$@")
}
_mca_gecko_profile_apply() {
local dir="$1" file="$1/user.js" detail=created
# A user.js that links somewhere else is part of a setup of its own.
[[ -L $file ]] && return 0
[[ -f $file ]] && detail=block
if ! grep -qxF -- "$MCA_GECKO_BEGIN" "$file" 2>/dev/null; then
# Set by the user already, in user.js or in the browser's own
# settings. Their choice: nothing is written and nothing recorded.
grep -qE '^[[:space:]]*user_pref\([[:space:]]*"general\.autoScroll"' \
-- "$file" 2>/dev/null && return 0
grep -qxF -- "$MCA_GECKO_ON" "$dir/prefs.js" 2>/dev/null && return 0
fi
_mca_gecko_write "$file" "$MCA_GECKO_ON"
case $? in
0) MCA_CHANGES=$(( MCA_CHANGES + 1 )) ;;
2) return 0 ;;
esac
# A file this program created stays one it created.
mca_ledger_has "$file" && detail="$(mca_ledger_detail "$file")"
mca_ledger_add gecko "$file" "$detail"
}
# mca_gecko_revert <user.js> <created|block>
# Returns 0 when something changed. Sets MCA_GECKO_PENDING when the profile is
# open and the record has to stay.
mca_gecko_revert() {
local file="$1" detail="$2" dir="${1%/*}" prefs content tmp rc=0
MCA_GECKO_PENDING=0
[[ -f $file && ! -L $file ]] || return 1
grep -qxF -- "$MCA_GECKO_BEGIN" "$file" 2>/dev/null || return 1
# The lock link only exists while the profile is open.
if [[ -L $dir/lock ]]; then
MCA_GECKO_PENDING=1
_mca_gecko_write "$file" "$MCA_GECKO_OFF"
return
fi
content="$(_mca_flags_drop_block "$file" "$MCA_GECKO_BEGIN" "$MCA_GECKO_END")"
if [[ $detail == created && -z ${content//[[:space:]]/} ]]; then
rm -f -- "$file"
else
printf '%s\n' "$content" > "$file"
fi
prefs="$dir/prefs.js"
if [[ -f $prefs && ! -L $prefs ]] && grep -qxF -- "$MCA_GECKO_ON" "$prefs"; then
tmp="$(mktemp "$prefs.XXXXXX")" || return 0
chmod --reference="$prefs" -- "$tmp" 2>/dev/null || chmod 600 -- "$tmp"
# grep says 1 when no line is left, which is fine here.
grep -vxF -- "$MCA_GECKO_ON" "$prefs" > "$tmp" || rc=$?
if (( rc <= 1 )); then mv -f -- "$tmp" "$prefs"; else rm -f -- "$tmp"; fi
fi
return 0
}
+5 -2
View File
@@ -419,13 +419,14 @@ mca_ui_apps() {
# The per-row labels are resolved once, here. Looking them up inside the
# drawing loop is a fork per row per keypress, and that is enough to make
# the arrow keys feel like the screen is reloading.
local l_off l_cannot l_on l_steam l_flagfile l_launcher
local l_off l_cannot l_on l_steam l_flagfile l_launcher l_profile
mca_msg_into "$locale" "off"; l_off="$MCA_MSG_RESULT"
mca_msg_into "$locale" "cannot tell"; l_cannot="$MCA_MSG_RESULT"
mca_msg_into "$locale" "on"; l_on="$MCA_MSG_RESULT"
mca_msg_into "$locale" "Steam"; l_steam="$MCA_MSG_RESULT"
mca_msg_into "$locale" "flag file"; l_flagfile="$MCA_MSG_RESULT"
mca_msg_into "$locale" "launcher"; l_launcher="$MCA_MSG_RESULT"
mca_msg_into "$locale" "profile"; l_profile="$MCA_MSG_RESULT"
local s_off="${MCA_C_DIM}${l_off}${MCA_C_RESET}"
local s_cannot="${MCA_C_DIM}${l_cannot}${MCA_C_RESET}"
@@ -434,6 +435,7 @@ mca_ui_apps() {
local s_flags="${s_on} ${MCA_C_DIM}(${l_flagfile})${MCA_C_RESET}"
local s_desktop="${s_on} ${MCA_C_DIM}(${l_launcher})${MCA_C_RESET}"
local s_spotify="${s_on} ${MCA_C_DIM}(spotify-launcher)${MCA_C_RESET}"
local s_gecko="${s_on} ${MCA_C_DIM}(${l_profile})${MCA_C_RESET}"
local clearseq
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
@@ -444,7 +446,7 @@ mca_ui_apps() {
count=${#MCA_IDS[@]}
if (( count == 0 )); then
MCA_UI_MSG="$(mca_msg "No Chromium-based applications found.")"
MCA_UI_MSG="$(mca_msg "No supported applications found.")"
return 1
fi
@@ -526,6 +528,7 @@ mca_ui_apps() {
steam) shown="$s_steam" ;;
flags) shown="$s_flags" ;;
spotify) shown="$s_spotify" ;;
gecko) shown="$s_gecko" ;;
*) shown="$s_desktop" ;;
esac
pad=$(( 34 - ${#labels[idx]} ))
+10 -1
View File
@@ -260,8 +260,9 @@ _mca_flags_write() {
return 0
}
# _mca_flags_drop_block <file> [begin marker] [end marker]
_mca_flags_drop_block() {
awk -v b="$MCA_MARK_BEGIN" -v e="$MCA_MARK_END" '
awk -v b="${2:-$MCA_MARK_BEGIN}" -v e="${3:-$MCA_MARK_END}" '
$0 == b { skip = 1; next }
$0 == e { skip = 0; next }
!skip
@@ -632,6 +633,7 @@ mca_prune_orphans() {
mca_revert_all() {
local kind path detail backup
local -a keep=()
[[ -f $MCA_LEDGER ]] || return 0
@@ -674,10 +676,17 @@ mca_revert_all() {
mca_kde_paste_revert "$path" "$detail" \
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
;;
gecko)
mca_gecko_revert "$path" "$detail" \
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
# Still open, so not done yet. See gecko.sh.
(( MCA_GECKO_PENDING )) && keep+=("$kind"$'\t'"$path"$'\t'"$detail")
;;
esac
done < <(tac "$MCA_LEDGER" 2>/dev/null)
rm -f -- "$MCA_LEDGER"
(( ${#keep[@]} )) && printf '%s\n' "${keep[@]}" > "$MCA_LEDGER"
rmdir "$MCA_BACKUPDIR" 2>/dev/null || true
return 0
}
+4 -4
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env bash
#
# middleclick-autoscroll: middle-click autoscroll for Chromium-based
# applications on Linux
# applications and Firefox on Linux
#
# Blink has had Windows-style autoscroll for years, behind a runtime flag that
# is not turned on for Linux because middle click is taken by primary-selection
# paste there. Turning it on is a matter of one command line argument. The
# work is finding every application that needs it, putting the argument
# somewhere it survives an update, and doing the same for the two programs that
# do not take arguments at all.
# do not take arguments at all. Firefox has the same feature behind a setting.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later
@@ -17,7 +17,7 @@ set -uo pipefail
MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}"
for _mod in common config detect patch steam kde apply menu; do
for _mod in common config detect patch steam gecko kde apply menu; do
# shellcheck source=/dev/null
if ! source "$MCA_LIBDIR/$_mod.sh"; then
printf 'middleclick-autoscroll: cannot load %s/%s.sh\n' "$MCA_LIBDIR" "$_mod" >&2
@@ -144,7 +144,7 @@ mca_do_list() {
mca_count_routes
if (( ${#MCA_IDS[@]} == 0 )); then
mca_note "$(mca_msg "No Chromium-based applications found.")"
mca_note "$(mca_msg "No supported applications found.")"
return 0
fi