Compare commits

..
13 Commits
Author SHA1 Message Date
Felitendo de6cf98dcc chore: 1.7.0
check / syntax and shellcheck (push) Successful in 1m10s
release / Debian package (push) Failing after 14s
release / RPM package (push) Failing after 24s
release / Release and repositories (push) Skipped
release / Install from the RPM repository (push) Skipped
2026-10-01 09:36:40 +02:00
Felitendo 95d602e1c4 feat: support firefox and other gecko apps 2026-10-01 09:33:22 +02:00
Felitendo 9b6c64bd97 chore: add a more info field to the app issue template 2026-10-01 09:12:45 +02:00
Felitendo 8a7e67b1f9 chore: ask for the app in the issue title 2026-10-01 09:06:20 +02:00
Felitendo c0fca75cbc chore: add issue template for apps that do not work 2026-10-01 09:00:19 +02:00
Felitendo 15a7cb9cf9 chore: 1.6.1
release / Debian package (push) Failing after 14s
release / RPM package (push) Failing after 21s
release / Release and repositories (push) Skipped
release / Install from the RPM repository (push) Skipped
2026-09-29 08:16:25 +02:00
Felitendo 0234e81811 fix: patch symlinked entries instead of writing through them 2026-09-29 08:15:38 +02:00
Felitendo 6f62c7f21c fix: patch the steamrt3 Steam client 2026-09-29 08:15:33 +02:00
Felitendo 3c000b8a6e docs: show new ui as screenshots in the readme 2026-09-28 15:08:16 +02:00
Felitendo 0f9b041b20 chore: show feature requests before bug reports 2026-09-26 21:19:26 +02:00
Felitendo 98f6ac9c48 chore: commit different topics separately 2026-09-25 09:30:38 +02:00
Felitendo 9861d24c2a chore: switch to ko-fi 2026-09-24 13:21:21 +02:00
Felitendo f74fc3e8ca chore: add issue templates 2026-09-24 12:39:52 +02:00
34 changed files with 881 additions and 123 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
buy_me_a_coffee: felitendo
ko_fi: felitendo
@@ -0,0 +1,35 @@
name: 💡 Feature request
description: An idea to make middleclick-autoscroll better.
labels:
- enhancement
body:
- type: checkboxes
id: checks
attributes:
label: Before you start
options:
- label: I searched the issues and this is not requested yet.
required: true
- type: textarea
id: problem
attributes:
label: What problem would this solve?
description: What are you trying to do, and what gets in the way?
validations:
required: true
- type: textarea
id: idea
attributes:
label: What would you like?
description: How it could work. A rough idea is fine.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Other ways you thought of
- type: textarea
id: more
attributes:
label: Anything else?
description: Mockups, screenshots, or other tools that do it well.
+85
View File
@@ -0,0 +1,85 @@
name: 🐛 Bug report
description: Something does not work as it should.
labels:
- bug
body:
- type: markdown
attributes:
value: Thanks for taking the time! The more you fill in, the faster it can be fixed.
- type: checkboxes
id: checks
attributes:
label: Before you start
options:
- label: I searched the issues and this is not reported yet.
required: true
- label: I use the latest version.
required: true
- type: textarea
id: what
attributes:
label: What happened?
description: What did you do, and what went wrong?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
placeholder: |
1.
2.
3.
- type: textarea
id: expected
attributes:
label: What did you expect?
validations:
required: true
- type: input
id: app
attributes:
label: App
description: 'Which app, and how it is installed: native package, Flatpak, Snap or AppImage.'
placeholder: Discord, Flatpak
- type: input
id: version
attributes:
label: Version
description: The output of `middleclick-autoscroll --version`.
placeholder: middleclick-autoscroll 1.6.0
validations:
required: true
- type: dropdown
id: install
attributes:
label: How did you install it?
options:
- AUR (Arch, CachyOS, EndeavourOS, Manjaro)
- Fedora package
- Bazzite
- Debian or Ubuntu package
- openSUSE package
- Built from source
- Other
validations:
required: true
- type: input
id: system
attributes:
label: System
description: Distribution, desktop and session (Wayland or X11).
placeholder: CachyOS, KDE Plasma 6.4, Wayland
validations:
required: true
- type: textarea
id: logs
attributes:
label: Status and logs
description: The output of `middleclick-autoscroll status` and `middleclick-autoscroll list`.
render: shell
- type: textarea
id: more
attributes:
label: Anything else?
description: Screenshots, videos or anything else that could help.
+23
View File
@@ -0,0 +1,23 @@
name: 🧩 App does not work
description: Autoscroll does not work in an app. Just tell us which one.
title: 'App: '
labels:
- app
body:
- type: markdown
attributes:
value: Just add the app's name to the title. The rest is optional.
- type: dropdown
id: install
attributes:
label: How is it installed?
description: Skip it if you are not sure.
options:
- Native package
- Flatpak
- Snap
- AppImage
- type: textarea
id: more
attributes:
label: More info
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+1 -1
View File
@@ -125,7 +125,7 @@ $entry
If $name is useful to you, you can buy me a coffee. It keeps these tools going. Found a bug or have an idea? Tell me in the [issues](https://github.com/$repo/issues).
<a href="https://buymeacoffee.com/felitendo"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="48"></a>
<a href="https://ko-fi.com/felitendo"><img src="https://storage.ko-fi.com/cdn/kofi5.png?v=6" alt="Buy me a coffee on Ko-fi" height="48"></a>
----
+31
View File
@@ -0,0 +1,31 @@
name: issues
on:
issues:
types: [labeled]
permissions:
issues: write
jobs:
# The "fixed" and "not chromium" labels answer and close the issue.
close:
name: close on label
if: >-
github.event.issue.state == 'open'
&& (github.event.label.name == 'fixed' || github.event.label.name == 'not chromium')
runs-on: ubuntu-latest
steps:
- env:
GH_TOKEN: ${{ github.token }}
LABEL: ${{ github.event.label.name }}
NUMBER: ${{ github.event.issue.number }}
run: |
if [ "$LABEL" = fixed ]; then
reason=completed
comment='Fixed, thanks for the report! The fix comes with the next release.'
else
reason='not planned'
comment='Thanks for the report! This app is not built on Chromium, Electron or CEF. So it has no autoscroll built in that this tool could turn on.'
fi
gh issue close "$NUMBER" --repo "$GITHUB_REPOSITORY" --reason "$reason" --comment "$comment"
+19
View File
@@ -2,6 +2,25 @@
What each release brings, newest first. The [GitHub releases](https://github.com/LoonixTools/middleclick-autoscroll/releases) add every commit that went into it. Versions before `v1.1.0` came without notes.
## v1.7.0
_2026-10-01_
Welcome to middleclick-autoscroll `v1.7.0`! Firefox gets autoscroll now, and so do LibreWolf, Zen, Floorp, Waterfox and Thunderbird. It starts working after your next login, or right away with **Re-apply everything**. Then restart Firefox.
### Highlights
- Autoscroll in Firefox
- LibreWolf, Zen, Floorp, Waterfox and Thunderbird too
- Flatpak and snap included
- New Firefox profiles get it on their own
## v1.6.1
_2026-09-29_
A small patch. Autoscroll now works in Steam with **Use experimental SteamRT3 Steam Client** turned on. And apps that start at login through a link, like Mullvad VPN, get autoscroll now instead of an error.
## v1.6.0
_2026-09-24_
+8 -1
View File
@@ -15,7 +15,8 @@
- Body only when something genuinely cannot be inferred from the diff.
- Never add `Co-Authored-By`, "Generated with" or any other AI attribution to commits or PR descriptions.
- Do not commit or push before I have checked the changes locally and said they are fine. Then
commit the result once and push. No commits for attempts that did not work. This also goes for
commit and push. Several attempts at the same thing make one commit, and attempts that did not
work make none. Different things done in one session get a commit each. This also goes for
releases and tags.
## Releases
@@ -67,3 +68,9 @@ when you press Enter." The list of commits follows on its own.
- Commands and settings they type go in backticks, buttons and labels in bold.
- To change an old release: edit its entry, commit, then
`gh release edit <tag> --title <tag> --notes "$(.github/release-notes.sh <tag>)"`.
## README
- New UI (a window, a screen, a menu, a setting, a notification) gets a screenshot in the README,
next to the text about it. Like for releases: a real screenshot of it running, in English.
- When a screen changes, take its screenshot again. The README never shows an old one.
+1 -1
View File
@@ -8,7 +8,7 @@
# Overridable so a packager can pass the version it is actually building
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
# straight from a checkout, and is what a release tag has to carry.
VERSION ?= 1.6.0
VERSION ?= 1.7.0
PREFIX ?= /usr
DESTDIR ?=
+5 -2
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">
@@ -17,7 +17,7 @@
</h5>
<p align="center">
<a href="https://buymeacoffee.com/felitendo"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="48"></a>
<a href="https://ko-fi.com/felitendo"><img src="https://storage.ko-fi.com/cdn/kofi5.png?v=6" alt="Buy me a coffee on Ko-fi" height="48"></a>
</p>
## Install
@@ -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,
+52 -10
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.
@@ -116,7 +118,9 @@ Two ways, chosen per application.
Programs that start themselves at login write their own entry into
_~/.config/autostart_ pointing straight at their binary, bypassing the menu
entry entirely. Those are patched in place as well, unless the application is
turned off in the applications list.
turned off in the applications list. An entry that is a symlink, often to an
application's entry in _/usr/share/applications_, is replaced by a patched
file rather than written through, and undoing puts the link back.
Shortcuts on the desktop itself are patched in place too. Nothing in the XDG
search path looks at that folder, so a shortcut that lives only there would
@@ -133,6 +137,12 @@ helper, inside Steam's own installation:
~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
The steamrt3 client, a beta for now, has a script of its own:
~/.local/share/Steam/steamrt64/steamwebhelper.sh
Both are patched where both are there, so switching clients keeps autoscroll.
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
@@ -154,11 +164,16 @@ it was given arguments for, and that one carries no arguments of anybody's. One
wrong length costs the whole client package downloaded, extracted and installed
again, and a client that quits at the end of it instead of coming up.
So where the comments cannot pay for the argument, or a client update has
changed how the helper is started, nothing is written and the status screen
says Steam is not patched. Nothing goes on Steam's launcher entry, its
autostart entry or the shortcuts it writes for single games either, and Steam
goes on repairing its own installation.
The steamrt3 script has no comments to pay with. So it is copied aside as
_steamwebhelper.sh.orig_, and a stub of the same size takes its place. The stub
runs the copy with the argument added. Steam only checks the files it knows,
and the copy is not one of them.
So where neither fits, or a client update has changed how the helper is
started, nothing is written and the status screen says Steam is not patched.
Nothing goes on Steam's launcher entry, its autostart entry or the shortcuts it
writes for single games either, and Steam goes on repairing its own
installation.
An installation patched by an earlier version, which appended where the
comments were short, is written again to fit at the next apply, or taken back
@@ -178,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
@@ -256,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 ""
+7 -2
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]
@@ -30,12 +30,17 @@ PathModified=%h/.guix-profile/share/applications
# Steam replaces the script that starts its interface on every client update,
# which takes the patch with it. Where that installation lives depends on how
# Steam was installed.
# Steam was installed. The steamrt3 client has a script of its own.
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/.var/app/com.valvesoftware.Steam/data/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
PathChanged=%h/snap/steam/common/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
PathChanged=%h/.local/share/Steam/steamrt64/steamwebhelper.sh
PathChanged=%h/.steam/debian-installation/steamrt64/steamwebhelper.sh
PathChanged=%h/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamrt64/steamwebhelper.sh
PathChanged=%h/.var/app/com.valvesoftware.Steam/data/Steam/steamrt64/steamwebhelper.sh
PathChanged=%h/snap/steam/common/.local/share/Steam/steamrt64/steamwebhelper.sh
Unit=middleclick-autoscroll.service
+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
+10 -2
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:-}"
@@ -343,12 +345,18 @@ mca_backup_name() {
# copy was stored under. Existing backups are never overwritten: the first copy
# is the pristine one, and a second apply must not replace it with an already
# patched version.
#
# A symlink is kept as a symlink, so undo can put the link back. It is taken
# again every time: this program never writes a link, so one is always pristine.
mca_backup() {
local file="$1" name
name="$(mca_backup_name "$file")"
mkdir -p "$MCA_BACKUPDIR" 2>/dev/null || return 1
if [[ ! -e "$MCA_BACKUPDIR/$name" ]]; then
if [[ -L $file ]]; then
rm -f -- "$MCA_BACKUPDIR/$name"
cp -P -p -- "$file" "$MCA_BACKUPDIR/$name" 2>/dev/null || return 1
elif [[ ! -e "$MCA_BACKUPDIR/$name" && ! -L "$MCA_BACKUPDIR/$name" ]]; then
cp -p -- "$file" "$MCA_BACKUPDIR/$name" 2>/dev/null || return 1
fi
printf '%s\n' "$name"
+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]} ))
+43 -7
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
@@ -375,7 +376,7 @@ mca_desktop_apply() {
[[ "$content" == "$(< "$src")" ]] && return 0
backup="$(mca_backup "$src")" || return 1
if mca_write_if_changed "$src" "$content"$'\n'; then
if _mca_entry_write "$src" "$content"$'\n'; then
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
fi
mca_ledger_add inplace "$src" "$backup"
@@ -418,6 +419,30 @@ mca_desktop_apply() {
# Neither can be shadowed from anywhere, so both are edited where they stand,
# with the original kept.
# _mca_entry_write <file> <content>
# Writes an entry edited in place, like mca_write_if_changed. A symlink is
# replaced by the edited file instead of written through: it usually points at
# a package's entry in /usr, which is not ours to change. Undo puts the link
# back from the backup.
_mca_entry_write() {
local file="$1" content="$2" tmp
[[ -L $file ]] || { mca_write_if_changed "$file" "$content"; return; }
tmp="$(mktemp "$file.XXXXXX")" || return 2
chmod --reference="$file" -- "$tmp" 2>/dev/null || chmod 644 -- "$tmp"
# A link into /usr/share/applications is trusted for where it points. A
# copy on the desktop is only trusted by KDE when it can be run.
chmod u+x -- "$tmp" 2>/dev/null || true
if printf '%s' "$content" > "$tmp" && mv -f -- "$tmp" "$file"; then
return 0
fi
rm -f -- "$tmp"
return 2
}
# _mca_entry_patch_inplace <file>
# One desktop entry that lives outside the XDG search path, edited where it is
# because there is nowhere to shadow it from. It follows the applications list
@@ -482,7 +507,7 @@ _mca_entry_patch_inplace() {
[[ "$content" == "$(< "$file")" ]] && return 0
backup="$(mca_backup "$file")" || return 0
if mca_write_if_changed "$file" "$content"$'\n'; then
if _mca_entry_write "$file" "$content"$'\n'; then
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
fi
mca_ledger_add inplace "$file" "$backup"
@@ -607,7 +632,8 @@ mca_prune_orphans() {
# ---------------------------------------------------------------------------
mca_revert_all() {
local kind path detail
local kind path detail backup
local -a keep=()
[[ -f $MCA_LEDGER ]] || return 0
@@ -623,12 +649,15 @@ mca_revert_all() {
fi
;;
inplace)
if [[ -n $detail && -f "$MCA_BACKUPDIR/$detail" ]]; then
backup="$MCA_BACKUPDIR/$detail"
if [[ -n $detail ]] && [[ -f $backup || -L $backup ]]; then
if [[ -e $path ]] || [[ -d "$(dirname -- "$path")" ]]; then
cp -p -- "$MCA_BACKUPDIR/$detail" "$path" 2>/dev/null \
# A link goes back as a link, not written through.
[[ -L $backup ]] && rm -f -- "$path"
cp -P -p -- "$backup" "$path" 2>/dev/null \
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
fi
rm -f -- "$MCA_BACKUPDIR/$detail"
rm -f -- "$backup"
elif [[ -f $path ]]; then
# No backup: the file did not exist before we wrote it.
rm -f -- "$path"
@@ -647,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
}
+138 -45
View File
@@ -9,6 +9,10 @@
#
# ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
#
# The steamrt3 client, a beta for now, has a script of its own:
#
# ~/.local/share/Steam/steamrt64/steamwebhelper.sh
#
# Steam compares the installed files against its manifest at every start (by
# size and timestamp, not by content) and restores whatever differs.
# So the patch is written to look untouched: the bytes the flag costs are taken
@@ -28,6 +32,10 @@
# all: no autoscroll in the interface is a smaller thing than a client that
# reinstalls itself.
#
# The steamrt3 script has no comments to give back. So it is copied aside, as
# it is, and replaced by a stub of the same size that runs the copy with the
# flag. See _mca_steam_stub.
#
# The file comes back on every client update, and the watcher re-applies the
# patch when that happens.
@@ -60,24 +68,28 @@ mca_steam_roots() {
done
}
# The script to patch inside a Steam installation. Newer clients start the
# helper through a wrapper inside the container runtime; older ones exec it
# from steamwebhelper.sh directly, and that one forwards its arguments too.
mca_steam_script() {
local root="$1" f
# The scripts to patch inside a Steam installation, one per client. The default
# client starts the helper through a wrapper inside the container runtime;
# older ones exec it from steamwebhelper.sh directly, and that one forwards its
# arguments too. The steamrt3 client has its own. Both clients are patched
# where both are there, so switching between them keeps autoscroll.
mca_steam_scripts() {
local root="$1" f found=1
for f in \
"$root/ubuntu12_64/steamwebhelper_sniper_wrap.sh" \
"$root/ubuntu12_64/steamwebhelper.sh"
do
[[ -f $f && -w $f ]] && { printf '%s\n' "$f"; return 0; }
[[ -f $f && -w $f ]] && { printf '%s\n' "$f"; found=0; break; }
done
return 1
f="$root/steamrt64/steamwebhelper.sh"
[[ -f $f && -w $f ]] && { printf '%s\n' "$f"; found=0; }
return "$found"
}
mca_steam_installed() {
local root
while IFS= read -r root; do
mca_steam_script "$root" > /dev/null && return 0
mca_steam_scripts "$root" > /dev/null && return 0
done < <(mca_steam_roots)
return 1
}
@@ -87,13 +99,17 @@ mca_steam_script_patched() {
grep -q -- "$MCA_FEATURE" "$1" 2>/dev/null
}
# Whether every script there is patched. One that is not is a client that
# starts without autoscroll.
mca_steam_patched() {
local root script
local root script found=0
while IFS= read -r root; do
script="$(mca_steam_script "$root")" || continue
mca_steam_script_patched "$script" && return 0
while IFS= read -r script; do
mca_steam_script_patched "$script" || return 1
found=1
done < <(mca_steam_scripts "$root")
done < <(mca_steam_roots)
return 1
(( found ))
}
# _mca_steam_exec_line <script>
@@ -163,6 +179,81 @@ _mca_steam_same_size() {
(( $(wc -c < "$script") == $(printf '%s\n' "$content" | wc -c) ))
}
# Where a stub keeps Steam's own script, and the line that runs it from there.
MCA_STEAM_ORIG=.orig
MCA_STEAM_STUB="exec \"\$0$MCA_STEAM_ORIG\" \"\$@\""
# _mca_steam_stub <script> <flags>
# The stub on stdout, for a script with no comments to pay for the flags. It
# is exactly as long as the script and runs Steam's own script, kept next to
# it, with the flags added. That script hands its arguments on to the helper.
# Steam only checks the files in its manifest, and the copy is not one of them.
# Fails when the script does not hand its arguments on, has no #! line, or is
# too short to hold the stub.
_mca_steam_stub() {
local script="$1" flags="$2" head body pad
[[ -n "$(_mca_steam_exec_line "$script")" ]] || return 1
head="$(head -n 1 -- "$script")"
[[ $head == '#!'* ]] || return 1
body="$MCA_STEAM_STUB $flags"
pad=$(( $(wc -c < "$script") - $(printf '%s\n%s\n' "$head" "$body" | wc -c) ))
(( pad >= 0 )) || return 1
# The bytes left over go into a comment line.
printf '%s\n' "$head"
if (( pad == 1 )); then
printf '\n'
elif (( pad > 1 )); then
printf '#%*s\n' $(( pad - 2 )) ''
fi
printf '%s' "$body"
}
# _mca_steam_is_stub <script>
_mca_steam_is_stub() {
grep -qF -- "$MCA_STEAM_STUB" "$1" 2>/dev/null
}
# _mca_steam_text <script> <flags>
# The patched script on stdout: from the comments if they can pay for the
# flags, as a stub if not. Fails when neither keeps the size.
_mca_steam_text() {
local script="$1" flags="$2" out
if ! { out="$(_mca_steam_build "$script" "$flags")" \
&& _mca_steam_same_size "$script" "$out"; }
then
out="$(_mca_steam_stub "$script" "$flags")" \
&& _mca_steam_same_size "$script" "$out" || return 1
fi
printf '%s' "$out"
}
# _mca_steam_write <script> <backup> <content>
# Writes the patched script. <backup> is the untouched copy kept for undoing.
_mca_steam_write() {
local script="$1" backup="$2" content="$3"
# A stub needs Steam's script next to it before it can start anything.
if [[ $content == *"$MCA_STEAM_STUB"* ]]; then
cp -p -- "$backup" "$script$MCA_STEAM_ORIG" 2>/dev/null || return 1
chmod +x -- "$script$MCA_STEAM_ORIG" 2>/dev/null || true
fi
if mca_write_if_changed "$script" "$content"$'\n'; then
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
fi
chmod +x -- "$script" 2>/dev/null || true
# Steam looks at when the file was last written as well as at how big
# it is, and the copy taken before the edit still carries the original
# timestamp. Putting it back costs nothing and removes the other half
# of what the client would notice.
touch -r "$backup" -- "$script" 2>/dev/null || true
}
# _mca_steam_refresh_backup <script>
# A client update brings a new version of the script, and the copy kept from
# before it is no longer what putting it back means. Only ever done while the
@@ -195,15 +286,18 @@ _mca_steam_settle() {
# Nothing to compare against, so nothing can be said about the length.
[[ -f $copy ]] || return 0
# Already the length Steam expects: the patch is a good one.
(( $(wc -c < "$script") == $(wc -c < "$copy") )) && return 0
if out="$(_mca_steam_build "$copy" "$flags")" && _mca_steam_same_size "$copy" "$out"; then
if mca_write_if_changed "$script" "$out"$'\n'; then
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
# Already the length Steam expects: the patch is a good one. Unless it is
# a stub that lost Steam's script next to it, and would start nothing.
if (( $(wc -c < "$script") == $(wc -c < "$copy") )); then
if _mca_steam_is_stub "$script" && [[ ! -f $script$MCA_STEAM_ORIG ]]; then
cp -p -- "$copy" "$script$MCA_STEAM_ORIG" 2>/dev/null || true
fi
chmod +x -- "$script" 2>/dev/null || true
touch -r "$copy" -- "$script" 2>/dev/null || true
return 0
fi
if out="$(_mca_steam_text "$copy" "$flags")" \
&& _mca_steam_write "$script" "$copy" "$out"
then
mca_ledger_add steam "$script" "$name"
return 0
fi
@@ -224,7 +318,7 @@ mca_steam_apply() {
flags="$(mca_flags)"
while IFS= read -r root; do
script="$(mca_steam_script "$root")" || continue
while IFS= read -r script; do
found=1
if mca_steam_script_patched "$script"; then
@@ -234,31 +328,21 @@ mca_steam_apply() {
_mca_steam_refresh_backup "$script"
# Either the comments can pay for the flag or nothing is written.
# A longer file is one Steam reinstalls itself over; a client update
# that changed how the helper is started leaves no line to patch at
# all. Both end here, and this script is what starts Steam's entire
# interface, so leaving it alone is the only safe answer to either.
if ! { out="$(_mca_steam_build "$script" "$flags")" \
&& _mca_steam_same_size "$script" "$out"; }
then
# Either the file keeps its length or nothing is written. A
# longer file is one Steam reinstalls itself over; a client
# update that changed how the helper is started leaves no line
# to patch at all. Both end here, and this script is what starts
# Steam's entire interface, so leaving it alone is the only safe
# answer to either.
if ! out="$(_mca_steam_text "$script" "$flags")"; then
mca_note "$(mca_msg "Steam starts its interface in a way this version cannot change without Steam noticing; leaving it alone.")"
continue
fi
backup="$(mca_backup "$script")" || continue
if mca_write_if_changed "$script" "$out"$'\n'; then
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
fi
chmod +x -- "$script" 2>/dev/null || true
# Steam looks at when the file was last written as well as at how big
# it is, and the copy taken before the edit still carries the original
# timestamp. Putting it back costs nothing and removes the other half
# of what the client would notice.
touch -r "$MCA_BACKUPDIR/$backup" -- "$script" 2>/dev/null || true
_mca_steam_write "$script" "$MCA_BACKUPDIR/$backup" "$out" || continue
mca_ledger_add steam "$script" "$backup"
done < <(mca_steam_scripts "$root")
done < <(mca_steam_roots)
(( found ))
@@ -266,7 +350,9 @@ mca_steam_apply() {
# mca_steam_revert <script> <backup name>
mca_steam_revert() {
local script="$1" backup="$2"
local script="$1" backup="$2" orig="$1$MCA_STEAM_ORIG" stub=1 tmp
_mca_steam_is_stub "$script" || stub=0
if [[ -n $backup && -f "$MCA_BACKUPDIR/$backup" ]]; then
if [[ -e $script ]]; then
@@ -274,14 +360,21 @@ mca_steam_revert() {
chmod +x -- "$script" 2>/dev/null || true
fi
rm -f -- "$MCA_BACKUPDIR/$backup"
return 0
fi
elif (( stub )) && [[ -f $orig ]]; then
# No backup, but a stub keeps Steam's own script right next to it.
cp -p -- "$orig" "$script" 2>/dev/null || return 1
else
# No backup to fall back on, so take the flags back out of the exec line.
[[ -f $script ]] || return 1
local tmp
tmp="$(mktemp "${script}.XXXXXX")" || return 1
sed "s| $MCA_FLAG||g" "$script" > "$tmp" && mv -f "$tmp" "$script" || { rm -f "$tmp"; return 1; }
chmod +x -- "$script" 2>/dev/null || true
return 0
fi
# The copy goes once the stub that ran it is gone.
if (( stub )) && ! _mca_steam_is_stub "$script"; then
rm -f -- "$orig"
fi
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