diff --git a/README.md b/README.md index d3e2e7e..d35e6e4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

Middle-click autoscroll in every app that supports it.

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

@@ -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, diff --git a/doc/middleclick-autoscroll.1.scd b/doc/middleclick-autoscroll.1.scd index e0a860d..a4e5df3 100644 --- a/doc/middleclick-autoscroll.1.scd +++ b/doc/middleclick-autoscroll.1.scd @@ -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. diff --git a/po/cs.po b/po/cs.po index 38d8bba..239d256 100644 --- a/po/cs.po +++ b/po/cs.po @@ -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." diff --git a/po/de.po b/po/de.po index 5521ea6..a15140e 100644 --- a/po/de.po +++ b/po/de.po @@ -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." diff --git a/po/es.po b/po/es.po index 5d244df..1c4843d 100644 --- a/po/es.po +++ b/po/es.po @@ -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í." diff --git a/po/fr.po b/po/fr.po index 6f0a9ff..296bc66 100644 --- a/po/fr.po +++ b/po/fr.po @@ -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 "" diff --git a/po/it.po b/po/it.po index 01b53fd..9ca8e4e 100644 --- a/po/it.po +++ b/po/it.po @@ -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 "" diff --git a/po/middleclick-autoscroll.pot b/po/middleclick-autoscroll.pot index 18a6f0a..8ba59fe 100644 --- a/po/middleclick-autoscroll.pot +++ b/po/middleclick-autoscroll.pot @@ -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." diff --git a/po/nl.po b/po/nl.po index 33879fe..d6eb3e6 100644 --- a/po/nl.po +++ b/po/nl.po @@ -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 "" diff --git a/po/pl.po b/po/pl.po index 6aff2a5..521823d 100644 --- a/po/pl.po +++ b/po/pl.po @@ -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 "" diff --git a/po/pt_BR.po b/po/pt_BR.po index 188711d..3266716 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -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 "" diff --git a/po/ru.po b/po/ru.po index 0e19661..e55cb53 100644 --- a/po/ru.po +++ b/po/ru.po @@ -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 "" diff --git a/po/tr.po b/po/tr.po index bb5dd38..20f0522 100644 --- a/po/tr.po +++ b/po/tr.po @@ -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." diff --git a/po/uk.po b/po/uk.po index cbac719..72c39b8 100644 --- a/po/uk.po +++ b/po/uk.po @@ -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 "" diff --git a/res/systemd/middleclick-autoscroll.path b/res/systemd/middleclick-autoscroll.path index 8977d3a..c7f51fd 100644 --- a/res/systemd/middleclick-autoscroll.path +++ b/res/systemd/middleclick-autoscroll.path @@ -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] diff --git a/res/systemd/middleclick-autoscroll.service b/res/systemd/middleclick-autoscroll.service index 8d9c6e1..25a4fba 100644 --- a/res/systemd/middleclick-autoscroll.service +++ b/res/systemd/middleclick-autoscroll.service @@ -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] diff --git a/src/lib/apply.sh b/src/lib/apply.sh index ded3570..8c01476 100644 --- a/src/lib/apply.sh +++ b/src/lib/apply.sh @@ -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 +# 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 diff --git a/src/lib/common.sh b/src/lib/common.sh index db9aa5a..9053a7e 100644 --- a/src/lib/common.sh +++ b/src/lib/common.sh @@ -283,11 +283,13 @@ mca_state_write() { # inplace # flags created | appended | merged # kwin what EnablePrimarySelection said before +# gecko 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:-}" diff --git a/src/lib/detect.sh b/src/lib/detect.sh index ab0493a..ceaae91 100644 --- a/src/lib/detect.sh +++ b/src/lib/detect.sh @@ -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: - Firefox or another Gecko application, which keeps its +# profiles in . 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 +# 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 # Whether this entry should be patched. Skip beats everything, an explicit # include beats detection, and detection beats nothing. diff --git a/src/lib/gecko.sh b/src/lib/gecko.sh new file mode 100644 index 0000000..2df8f3e --- /dev/null +++ b/src/lib/gecko.sh @@ -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 +# 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 +# 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 / mca_snap_gecko +# 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 +# Every folder that can hold the application's profiles.ini. An older install +# uses ~/.. A new one (Firefox 147 and later) uses $XDG_CONFIG_HOME/, +# or ~/ 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 +# 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 +# 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 +# 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 +# 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 +} diff --git a/src/lib/menu.sh b/src/lib/menu.sh index 55a34e5..16c3f91 100644 --- a/src/lib/menu.sh +++ b/src/lib/menu.sh @@ -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]} )) diff --git a/src/lib/patch.sh b/src/lib/patch.sh index ca583d9..c149c3a 100644 --- a/src/lib/patch.sh +++ b/src/lib/patch.sh @@ -260,8 +260,9 @@ _mca_flags_write() { return 0 } +# _mca_flags_drop_block [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 } diff --git a/src/middleclick-autoscroll b/src/middleclick-autoscroll index 2f89c83..e4dac52 100644 --- a/src/middleclick-autoscroll +++ b/src/middleclick-autoscroll @@ -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