Compare commits

...
6 Commits
Author SHA1 Message Date
Felitendo e1c77f049f chore: 1.3.0
release / Debian package (push) Failing after 13s
release / RPM package (push) Failing after 26s
release / Release and repositories (push) Skipped
release / Install from the RPM repository (push) Skipped
A browser no longer shows the "unsupported command-line flag" bar. It is given
--enable-features=MiddleClickAutoscroll, which asks for the same feature
without being on the list of flags Chromium warns about; everything that embeds
an older Chromium keeps the flag that works there, and shows no bar either way.

Helium gets its own name for the feature alongside it, so autoscroll works
there for the first time.

An installation from 1.2.0 or earlier is taken back and written again on the
next apply.
2026-09-05 00:44:52 +02:00
Felitendo fa624380b1 fix: keep the unsupported-flag warning off browsers
--enable-blink-features is on the list of flags Chromium warns about, so every
browser the flag was applied to put a yellow "unsupported command-line flag"
bar above each page.

Browsers are given --enable-features=MiddleClickAutoscroll instead. It asks for
the same thing - Blink generates a feature of the same name for each of its
runtime flags - and is not on that list. That spelling only works from Chromium
124 onwards, so everything else keeps the flag that works everywhere: an
application embedding an older Chromium, Steam's CEF among them, has no such
bar to show anyway.

Helium knows the feature under a name of its own and ignores the Chromium one,
so browsers are asked for HeliumMiddleClickAutoscroll as well; autoscroll never
worked there before. A name a browser does not know is ignored, which is what
makes one list safe for all of them.

An installation set up by an earlier version is taken back and written again
once, because a file that is already marked as patched would otherwise be left
alone with the old flag in it.
2026-09-05 00:44:47 +02:00
Felitendo d8900ef80f docs: show the TUI in the readme 2026-09-04 13:02:13 +02:00
Felitendo 46e63e9faa docs: rewrite the readme in plain words 2026-09-04 12:59:50 +02:00
Felitendo af82ffec78 chore: 1.2.0
release / Debian package (push) Failing after 14s
release / Release and repositories (push) Skipped
release / Install from the RPM repository (push) Skipped
release / RPM package (push) Failing after 30s
Steam's autoscroll stops depending on -noverifyfiles: the patch is written at
the size and timestamp Steam recorded, so the client's own file check finds
nothing to repair and the flag survives whichever way Steam was started. An
installation patched by 1.1.0 is redone at the original size on the next apply.

Also picks up the launch paths that were never covered - shortcuts on the
desktop, Steam as a Flatpak, steam-native and steam-jupiter.
2026-09-01 13:24:20 +02:00
Felitendo b7e0ff6fd1 fix: make the Steam patch survive Steam's own file check
Steam's autoscroll worked, except when it didn't. Every report of that came
down to the same thing: Steam started in a way that carried no -noverifyfiles,
noticed the patched web helper script, put its own copy back, and spent the
session without the flag. There is no way to make every possible way of
starting Steam carry an argument - a game launcher, a shortcut, a terminal,
another program calling `steam steam://` - so the patch stops depending on it.

The check is on size and timestamp, not on content, so the patch is now written
to look untouched: the bytes the flag costs come back out of the script's own
comments, and the timestamp of the untouched copy is restored afterwards. The
file Steam finds is exactly as long and exactly as old as the one it wrote. A
client that verifies its files finds nothing to repair. -noverifyfiles stays as
the fallback for a script with no comments left to pay for the flag.

Two things fall out of that. A patch that keeps the size no longer has to wait
for Steam to close, because there is no size mismatch for the client to chase;
only the growing one still defers. And an installation patched by the earlier
version is quietly redone at the original size on the next apply, so the fix
arrives without anyone having to know about it.

Then the launch paths that were never covered:

- Shortcuts on the desktop itself. Nothing in the XDG search path looks at
  that folder, so nothing had ever seen them - and Steam writes one there for
  every game somebody asks for a shortcut to. Its name is translated, so it is
  read from user-dirs.dirs and handed to the watcher in a drop-in.
- Steam as a Flatpak was never recognised as Steam, so its entry got nothing
  while its script got patched - the worst of both. The switch goes after the
  application id there, where flatpak passes it on rather than reading it.
- steam-native and steam-jupiter, which are ordinary Steam starts under
  another name, and Flatpak and snap entries in ~/.config/autostart.

Two things found on the way: a client update left the undo copy holding the
script from before the update, so undoing would have put an old version back;
and a shortcut edited in place and later deleted would have been recreated by
`disable`.
2026-09-01 13:21:57 +02:00
11 changed files with 692 additions and 195 deletions

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
# Overridable so a packager can pass the version it is actually building # Overridable so a packager can pass the version it is actually building
# (`make VERSION=$pkgver`). The literal below is the fallback for builds # (`make VERSION=$pkgver`). The literal below is the fallback for builds
# straight from a checkout, and is what a release tag has to carry. # straight from a checkout, and is what a release tag has to carry.
VERSION ?= 1.1.0 VERSION ?= 1.3.0
PREFIX ?= /usr PREFIX ?= /usr
DESTDIR ?= DESTDIR ?=
+86 -35
View File
@@ -1,21 +1,27 @@
# middleclick-autoscroll # middleclick-autoscroll
Middle-click autoscroll for Linux. Hold the middle mouse button, move the Middle-click autoscroll is a CLI for Linux that enables autoscroll in every application that supports it.
mouse, the page scrolls — like on Windows. Works with browsers, Electron apps, Works with browsers, Electron apps like Discord and Spotify, Steam, and anything else that runs on Chromium under the hood.
Flatpaks, snaps, Steam, and anything else that runs on Chromium under the hood.
No app list to maintain. The program looks at what's actually installed, figures There is no app list. My tool just looks at what uses chromium under the hood and applies the necessary steps to get it working.
out what's Chromium-based, and handles it.
## Installing ## How to install
**Arch, CachyOS, EndeavourOS, Manjaro** **Arch**
```bash ```bash
paru -S middleclick-autoscroll yay -S middleclick-autoscroll
``` ```
**Debian, Ubuntu, Linux Mint, Pop!_OS** **Fedora**
```bash
sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \
https://felitendo.github.io/middleclick-autoscroll/middleclick-autoscroll.repo
sudo dnf install middleclick-autoscroll
```
**Debian**
```bash ```bash
sudo install -d -m 0755 /etc/apt/keyrings sudo install -d -m 0755 /etc/apt/keyrings
@@ -26,14 +32,6 @@ echo "deb [signed-by=/etc/apt/keyrings/middleclick-autoscroll.gpg] https://felit
sudo apt update && sudo apt install middleclick-autoscroll sudo apt update && sudo apt install middleclick-autoscroll
``` ```
**Fedora, RHEL, CentOS Stream**
```bash
sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \
https://felitendo.github.io/middleclick-autoscroll/middleclick-autoscroll.repo
sudo dnf install middleclick-autoscroll
```
**openSUSE** **openSUSE**
```bash ```bash
@@ -43,39 +41,94 @@ sudo zypper addrepo --gpgcheck --refresh \
sudo zypper install middleclick-autoscroll sudo zypper install middleclick-autoscroll
``` ```
Then run `middleclick-autoscroll enable`. That's it — nothing else to configure. ## How to use
Updates come through your package manager like anything else. Without a package, Just run `middleclick-autoscroll`. This will open the configuration TUI that looks like this:
build [from source](#building-from-source) or grab a `.deb`/`.rpm` from the
[releases page](https://github.com/Felitendo/middleclick-autoscroll/releases).
Run `middleclick-autoscroll disable` before removing the package — it undoes ```
everything. Middle-Click Autoscroll
Autoscroll ON
Applications covered 11 of 13
Not identified 1 - see the applications list
Steam ON
New applications ON
Last applied 2 minutes ago
Applications pick this up the next time they are started.
[1] Turn autoscroll on or off
[2] Re-apply everything
[3] Applications
[4] Settings
[q] Quit
>
```
`[1]` is all you need for the normal case. `[3]` lists every app that was found
and what is being done with it, so you can leave a single one out or switch on
an AppImage that couldn't be identified:
```
Applications
▸ Steam on (Steam)
Chromium on (launcher)
Discord on (flag file)
Obsidian on (launcher)
Spotify on (launcher)
Slack off
Cursor cannot tell
Anything not identified is left alone until it is turned on here.
Up/Down select - Space turns one on or off - q goes back
```
`[4]` has the same switches per category - browsers, Electron and CEF apps,
Flatpaks, snaps, autostart entries, Steam, Spotify - plus a field for extra
Chromium arguments.
## How it works ## How it works
Blink (the engine in Chromium, Electron, and CEF) already has autoscroll, but Blink (the engine in Chromium, Electron, and CEF) already has autoscroll, but
it's off on Linux because middle click does primary-selection paste there. One it's off on Linux because middle click does primary-selection paste there. This
flag turns it on: flag turns it on:
```
--enable-blink-features=MiddleClickAutoscroll --enable-blink-features=MiddleClickAutoscroll
```
Doing that for one app is a five-minute job. Doing it for *all* of them — across But doing that for every app is kinda bothersome and it also might break with updates.
flag files, desktop entries, Flatpaks, snaps, autostart entries, Steam — so it That's why I created this small tool to automate that.
survives upgrades, is not. That's what this does.
New apps are picked up within a second by a systemd path unit that watches the Browsers get the same thing asked for differently:
--enable-features=MiddleClickAutoscroll
Both mean the same feature - Blink generates a matching feature name for each of
its runtime flags - but the first one is on Chromium's list of flags worth
warning about, so a browser started with it shows a yellow "unsupported
command-line flag" bar over every page. The second one is not on that list, so
there's no bar. It only works from Chromium 124 on, which is why apps that
embed something older (Steam's CEF, older Electron) keep the first one - they
have no such bar to begin with. Helium, which carries the feature under its own
name, is asked for `HeliumMiddleClickAutoscroll` alongside it.
New apps are picked up by a systemd path unit that watches the
relevant directories. Without systemd, `middleclick-autoscroll apply` does the relevant directories. Without systemd, `middleclick-autoscroll apply` does the
same thing manually. same thing manually.
## Steam ## Steam
Steam's web UI supports autoscroll but has no way to pass extra arguments to its Steam's web UI supports autoscroll but has no way to pass extra arguments to its
helper. The program patches the helper launch script and adds `-noverifyfiles` helper, so the program patches the script that starts it. Steam checks its own
to Steam's launcher so the patch sticks. That means Steam won't auto-repair files at every start and repairs whatever looks changed, so the patch is written
damaged files on its own — you can turn this off separately in the settings. to look unchanged.
As a fallback, for the case where that isn't possible, `-noverifyfiles` goes on
everything that starts Steam: its launcher, its autostart entry, and the
shortcuts it writes for single games. That one means Steam won't auto-repair
damaged files on its own just so you're aware of that.
## Commands ## Commands
@@ -89,8 +142,6 @@ damaged files on its own — you can turn this off separately in the settings.
| `… status` | What's covered | | `… status` | What's covered |
| `… list` | All apps and how they're handled | | `… list` | All apps and how they're handled |
See `man middleclick-autoscroll` for more.
## Building from source ## Building from source
```bash ```bash
+51 -23
View File
@@ -18,7 +18,18 @@ on the command line.
*middleclick-autoscroll* finds every Chromium-based application on the system *middleclick-autoscroll* finds every Chromium-based application on the system
and puts that argument somewhere the application will actually read it, then and puts that argument somewhere the application will actually read it, then
keeps doing so for anything installed later. It works on any distribution: keeps doing so for anything installed later.
A browser is given *--enable-features=MiddleClickAutoscroll* instead. It asks
for the same feature - Blink generates a feature of that name for each of its
runtime flags - but it is not on the list of flags Chromium warns about, so the
browser does not put a bar reading "You are using an unsupported command-line
flag" above every page. That spelling only works from Chromium 124 onwards,
which is why everything else keeps the first one: an application that embeds an
older Chromium, such as Steam's CEF, has no such bar to show anyway. Helium
knows the feature under a name of its own and is asked for
*HeliumMiddleClickAutoscroll* as well; 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 which of the routes below an application takes is read off its launcher, not
assumed from where the launcher came from. assumed from where the launcher came from.
@@ -71,10 +82,10 @@ Two ways, chosen per application.
Where the launcher reads extra arguments from Where the launcher reads extra arguments from
_$XDG_CONFIG_HOME/<name>-flags.conf_. This is the preferred route: it is _$XDG_CONFIG_HOME/<name>-flags.conf_. This is the preferred route: it is
the supported way to pass arguments, it survives package upgrades, and it the supported way to pass arguments, it survives package upgrades, and it
applies to a launch from a terminal as much as one from the menu. An applies to a launch from a terminal as much as one from the menu. A
*--enable-blink-features* line that is already in the file is extended feature list that is already in the file is extended rather than
rather than duplicated - Chromium keeps only the last occurrence of that duplicated - Chromium keeps only the last occurrence of such an option, so
option, so a second one would switch the first one off. a second one would switch the first one off.
Arch's Electron and Chromium packages all wrap their binaries this way, and Arch's Electron and Chromium packages all wrap their binaries this way, and
so do a number of individual vendors' launchers elsewhere. Whether a given so do a number of individual vendors' launchers elsewhere. Whether a given
@@ -102,6 +113,13 @@ Programs that start themselves at login write their own entry into
_~/.config/autostart_ pointing straight at their binary, bypassing the menu _~/.config/autostart_ pointing straight at their binary, bypassing the menu
entry entirely. Those are patched in place as well. entry entirely. Those are patched in place as well.
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
otherwise be invisible - and Steam puts one there for every game somebody asks
for a shortcut to. The folder's name is translated, and the name in use is read
from _~/.config/user-dirs.dirs_ rather than guessed; the watcher is told about
it in a drop-in written when autoscroll is turned on.
# STEAM # STEAM
Steam's interface is CEF and supports the feature, but Steam builds the command Steam's interface is CEF and supports the feature, but Steam builds the command
@@ -116,26 +134,36 @@ _~/.steam/debian-installation_ for Debian's, and the private tree of the
sandbox for the Flatpak and the snap. All of them are looked at. sandbox for the Flatpak and the snap. All of them are looked at.
Steam compares the installed files against its manifest at every start - by Steam compares the installed files against its manifest at every start - by
size, not by content - and restores whatever differs, so its launcher entry size and timestamp rather than by content - and restores whatever differs. So
gets *-noverifyfiles*. So does its entry in _~/.config/autostart_, which Steam the patch is written to look untouched: the bytes the argument costs are taken
writes as soon as it is set to run at login: that entry bypasses the menu one back out of the script's own comments and the timestamp is put back afterwards,
entirely, and without the switch a Steam started at login spends the session in leaving a file exactly as long and exactly as old as Steam left it. A client
an update dialog. The trade-off is that Steam no longer repairs a damaged that checks its files finds nothing to repair, and the argument survives
installation on its own; that is why Steam is a switch of its own in the however Steam was started - from the menu, from a game shortcut, from a
settings. launcher like Heroic or Lutris, from a terminal.
The shortcuts Steam writes for single games carry the switch too. A game is not That is what has to work, because there is no way to make every possible way of
an application this program has anything to offer and none of them is listed starting Steam carry an argument. *-noverifyfiles* is the second line rather
under *Applications*, but starting one with Steam closed is a Steam start like than the first: it covers the case where the script has no comments left to pay
any other, and without the switch it costs the interface its autoscroll for the for the argument and the patch has to grow the file. It goes on Steam's
rest of the session. launcher entry, on its entry in _~/.config/autostart_, which Steam writes as
soon as it is set to run at login, and on the shortcuts Steam writes for single
games, on the desktop and in the menu alike. A game is not an application this
program has anything to offer and none of them is listed under *Applications*,
but starting one is a Steam start like any other. The trade-off of the switch
is that Steam no longer repairs a damaged installation on its own; that is why
Steam is a switch of its own in the settings.
Starting Steam some other way - from a terminal, from a script - leaves the A patch that does change the size is still held back while the client is
switch out, and Steam puts its own copy of the script back for that session. running: Steam puts its own copy back, the two would only undo each other, and
The patch returns at the next apply with Steam closed. It is deliberately not the helper is started once, at the start, so patching again would not help that
repeated while the client is running: the two would only undo each other, and session anyway. It goes in at the next apply with Steam closed. A patch that
the helper is started once, at the start, so it would not help that session keeps the size has nothing to wait for and goes in either way.
anyway.
A client update brings a new version of the script. The watcher notices and
patches it again, and the copy kept for undoing is replaced with the new
version at the same time, so undoing puts back the script Steam last shipped
rather than the one from before the update.
# SPOTIFY # SPOTIFY
+6
View File
@@ -23,12 +23,18 @@ PathModified=/run/current-system/sw/share/applications
PathModified=%h/.nix-profile/share/applications PathModified=%h/.nix-profile/share/applications
PathModified=%h/.guix-profile/share/applications PathModified=%h/.guix-profile/share/applications
# The desktop folder is watched too - Steam writes a shortcut there for every
# game somebody asks for one, and starting a game is a Steam start - but its
# name is translated and cannot be written down here. `enable` adds it in a
# drop-in, from the name this system uses.
# Steam replaces the script that starts its interface on every client update, # 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 # which takes the patch with it. Where that installation lives depends on how
# Steam was installed. # Steam was installed.
PathChanged=%h/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh PathChanged=%h/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
PathChanged=%h/.steam/debian-installation/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/.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/snap/steam/common/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
Unit=middleclick-autoscroll.service Unit=middleclick-autoscroll.service
+69 -4
View File
@@ -66,6 +66,15 @@ mca_apply() {
MCA_N_ON=0; MCA_N_OFF=0; MCA_N_UNKNOWN=0 MCA_N_ON=0; MCA_N_OFF=0; MCA_N_UNKNOWN=0
mca_config_load mca_config_load
# An installation that was set up by a version whose answer was a different
# flag still has that flag where it put it, and nothing below would touch it
# again: a file that is already marked as patched is left alone. Taking
# everything back once, here, is what carries such an installation over.
if [[ "$(mca_state_read flag_scheme 1)" != "$MCA_FLAG_SCHEME" ]]; then
mca_revert_all
fi
mca_scan mca_scan
for i in "${!MCA_IDS[@]}"; do for i in "${!MCA_IDS[@]}"; do
@@ -87,13 +96,14 @@ mca_apply() {
case "$route" in case "$route" in
flags) flags)
mca_flags_apply "$prog" || mca_desktop_apply "$id" "$file" mca_flags_apply "$prog" "$kind" \
|| mca_desktop_apply "$id" "$file" "$(mca_flags "$kind")"
;; ;;
desktop) desktop)
mca_desktop_apply "$id" "$file" mca_desktop_apply "$id" "$file" "$(mca_flags "$kind")"
;; ;;
steam) steam)
mca_steam_desktop_apply "$id" "$file" mca_steam_desktop_apply "$id" "$file" "$packaging"
if (( ! steam_done )); then if (( ! steam_done )); then
mca_steam_apply mca_steam_apply
steam_done=1 steam_done=1
@@ -115,7 +125,8 @@ mca_apply() {
# autoscroll for the rest of the session. # autoscroll for the rest of the session.
if [[ $CFG_STEAM == yes ]]; then if [[ $CFG_STEAM == yes ]]; then
for i in "${!MCA_STEAM_LINKS[@]}"; do for i in "${!MCA_STEAM_LINKS[@]}"; do
mca_steam_desktop_apply "${MCA_STEAM_LINKS[i]}" "${MCA_STEAM_LINK_FILES[i]}" mca_steam_desktop_apply "${MCA_STEAM_LINKS[i]}" \
"${MCA_STEAM_LINK_FILES[i]}" "${MCA_STEAM_LINK_PACK[i]}"
done done
fi fi
@@ -125,6 +136,14 @@ mca_apply() {
if [[ $CFG_AUTOSTART == yes || $CFG_STEAM == yes ]]; then if [[ $CFG_AUTOSTART == yes || $CFG_STEAM == yes ]]; then
mca_autostart_apply mca_autostart_apply
fi fi
# Shortcuts on the desktop itself. Nothing above has seen them - the XDG
# search path does not go there - and a game started from one starts Steam
# without its switch, which is the difference between autoscroll working
# and autoscroll working most of the time. Each entry is gated on its own,
# so there is nothing to check out here.
mca_shortcuts_apply
[[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply [[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply
mca_prune_orphans mca_prune_orphans
@@ -134,6 +153,7 @@ mca_apply() {
fi fi
mca_state_write last_apply "$(date +%s)" mca_state_write last_apply "$(date +%s)"
mca_state_write flag_scheme "$MCA_FLAG_SCHEME"
return 0 return 0
} }
@@ -161,6 +181,12 @@ mca_revert() {
MCA_UNIT_PATH="middleclick-autoscroll.path" MCA_UNIT_PATH="middleclick-autoscroll.path"
MCA_UNIT_SERVICE="middleclick-autoscroll.service" 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.
MCA_UNIT_DROPIN="${MCA_XDG_CONFIG}/systemd/user/${MCA_UNIT_PATH}.d"
mca_watch_available() { mca_watch_available() {
mca_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} || -S "/run/user/$(id -u)/bus" ]] mca_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} || -S "/run/user/$(id -u)/bus" ]]
} }
@@ -170,17 +196,56 @@ mca_watch_enabled() {
systemctl --user is-enabled --quiet "$MCA_UNIT_PATH" 2>/dev/null systemctl --user is-enabled --quiet "$MCA_UNIT_PATH" 2>/dev/null
} }
# _mca_watch_dropin_write
# Returns 0 when the drop-in changed and systemd has to be told, 1 when it was
# already right - the same contract as mca_write_if_changed, and for the 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 $?; }
# 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'
mca_write_if_changed "$file" "$content"
}
_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
}
mca_watch_enable() { mca_watch_enable() {
mca_watch_available || return 1 mca_watch_available || return 1
_mca_watch_dropin_write
systemctl --user daemon-reload > /dev/null 2>&1 || true
systemctl --user enable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || return 1 systemctl --user enable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || return 1
systemctl --user enable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true systemctl --user enable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
return 0 return 0
} }
# 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.
mca_watch_refresh() {
mca_watch_available || return 0
_mca_watch_dropin_write || return 0
systemctl --user daemon-reload > /dev/null 2>&1 || true
systemctl --user restart "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
return 0
}
mca_watch_disable() { mca_watch_disable() {
mca_watch_available || return 0 mca_watch_available || return 0
systemctl --user disable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || true systemctl --user disable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
systemctl --user disable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true systemctl --user disable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
_mca_watch_dropin_remove && systemctl --user daemon-reload > /dev/null 2>&1
return 0 return 0
} }
+56 -2
View File
@@ -13,11 +13,42 @@ MCA_PRETTY="Middle-Click Autoscroll"
# The whole point of the package. Blink implements Windows-style autoscroll # The whole point of the package. Blink implements Windows-style autoscroll
# behind a runtime flag that Chromium does not turn on for Linux, because # behind a runtime flag that Chromium does not turn on for Linux, because
# middle click is taken by primary-selection paste there. # middle click is taken by primary-selection paste there.
#
# There are two ways to ask for that flag, and which one is right depends on
# what is being started:
#
# --enable-blink-features=MiddleClickAutoscroll is the one that works
# everywhere, and the only one that works in the Chromium versions embedded
# applications ship - Steam's CEF, an Electron a few years old, anything
# before Chromium 124. A browser started with it puts a yellow bar above the
# page saying an unsupported command line flag is in use, because Chromium
# keeps a list of flags worth warning about and this is on it.
#
# --enable-features=MiddleClickAutoscroll is not on that list, so a browser
# started with it says nothing. Blink generates a feature of the same name
# for every one of its runtime flags, which is what makes the two the same
# request - but only since Chromium 124. Before that the name means nothing
# and is ignored without a word.
#
# So a browser, which is the only thing that shows the bar and the one thing
# that keeps itself up to date, is given the quiet one; everything else keeps
# the one that always works.
MCA_FLAG="--enable-blink-features=MiddleClickAutoscroll" MCA_FLAG="--enable-blink-features=MiddleClickAutoscroll"
# The Blink feature name on its own, for merging into an --enable-blink-features # Helium ships the feature under its own name and does not answer to the
# list that an app (or the user) already carries. # Chromium one, so browsers are asked for both. A name a browser does not know
# is ignored, which is what makes one list safe to hand to all of them.
MCA_BROWSER_FLAG="--enable-features=MiddleClickAutoscroll,HeliumMiddleClickAutoscroll"
# The feature names on their own, for merging into a list that an application
# (or the user) already carries, and for taking them back out again.
MCA_FEATURE="MiddleClickAutoscroll" MCA_FEATURE="MiddleClickAutoscroll"
MCA_BROWSER_FEATURES="MiddleClickAutoscroll,HeliumMiddleClickAutoscroll"
# Bumped when the flags above change. An installation that was set up by a
# version with a different answer is taken back and done again once, because a
# file that is already patched is otherwise left alone. See mca_apply.
MCA_FLAG_SCHEME=2
MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}" MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}"
MCA_LOCALEDIR="${MCA_LOCALEDIR:-@LOCALEDIR@}" MCA_LOCALEDIR="${MCA_LOCALEDIR:-@LOCALEDIR@}"
@@ -44,6 +75,29 @@ MCA_BACKUPDIR="${MCA_STATEDIR}/backup"
# else. See mca_ledger_add. # else. See mca_ledger_add.
MCA_LEDGER="${MCA_STATEDIR}/ledger" MCA_LEDGER="${MCA_STATEDIR}/ledger"
# The folder the desktop itself shows. Nothing in the XDG search path looks at
# it, so an entry that lives only there is invisible to a scan - and a game
# shortcut dragged onto the desktop is exactly that.
#
# Its name is translated: Schreibtisch, Bureau, Escritorio. The name in use is
# in the file xdg-user-dirs writes, so it is read rather than guessed, and
# ~/Desktop is only the fallback for a system that has no such file.
mca_desktop_folder() {
local file="$MCA_XDG_CONFIG/user-dirs.dirs" line dir=''
if [[ -r $file ]]; then
while IFS= read -r line; do
[[ $line == XDG_DESKTOP_DIR=* ]] || continue
dir="${line#XDG_DESKTOP_DIR=}"
dir="${dir%\"}"; dir="${dir#\"}"
done < "$file"
dir="${dir/#\$HOME/$HOME}"
fi
[[ $dir == /* ]] || dir="$HOME/Desktop"
printf '%s\n' "${dir%/}"
}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Translations # Translations
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+9 -1
View File
@@ -154,10 +154,18 @@ mca_config_load() {
CFG_SKIP="$(mca_config_get Skip '')" CFG_SKIP="$(mca_config_get Skip '')"
} }
# mca_flags [kind]
# The full argument string that gets injected. Kept in one place so the flag # The full argument string that gets injected. Kept in one place so the flag
# file writer and the desktop entry writer cannot drift apart. # file writer and the desktop entry writer cannot drift apart.
#
# A browser gets the flag that leaves it without a warning bar; everything else
# gets the one that works in every Chromium there is. See MCA_FLAG.
mca_flags() { mca_flags() {
printf '%s' "$MCA_FLAG" if [[ ${1:-app} == browser ]]; then
printf '%s' "$MCA_BROWSER_FLAG"
else
printf '%s' "$MCA_FLAG"
fi
[[ -n ${CFG_EXTRA_FLAGS:-} ]] && printf ' %s' "$CFG_EXTRA_FLAGS" [[ -n ${CFG_EXTRA_FLAGS:-} ]] && printf ' %s' "$CFG_EXTRA_FLAGS"
printf '\n' printf '\n'
} }
+32 -17
View File
@@ -99,17 +99,6 @@ mca_desktop_dirs() {
) )
} }
# mca_desktop_get <file> <key>
# A single value from the [Desktop Entry] group. Desktop Action groups repeat
# the same keys, and reading past the first group would pick up the wrong one.
mca_desktop_get() {
local file="$1" key="$2"
awk -v key="$key" '
/^[[:space:]]*\[/ { inentry = ($0 ~ /^[[:space:]]*\[Desktop Entry\][[:space:]]*$/); next }
inentry && index($0, key "=") == 1 { print substr($0, length(key) + 2); exit }
' "$file" 2>/dev/null
}
# _mca_desktop_read <file> # _mca_desktop_read <file>
# Every key the scan needs, in one pass and without a single fork. There are a # Every key the scan needs, in one pass and without a single fork. There are a
# couple of hundred desktop entries on an ordinary system, and doing this with # couple of hundred desktop entries on an ordinary system, and doing this with
@@ -231,8 +220,22 @@ mca_exec_is_steam_link() {
local line="$1" prog="$2" local line="$1" prog="$2"
[[ $line == *steam://* ]] || return 1 [[ $line == *steam://* ]] || return 1
mca_prog_is_steam "$prog"
}
# mca_prog_is_steam <program>
# Whether running this program starts the Steam client. Every packaging is in
# here and every name Valve and the distributions give the launcher, because
# the answer decides whether an entry gets Steam's own switch - and an entry
# that starts Steam without it undoes the web helper patch on the way up.
#
# The program is what a scan leaves behind: an absolute path for a native
# install, flatpak:<id> or snap:<name> for the other two.
mca_prog_is_steam() {
local prog="$1"
case "${prog##*/}" in case "${prog##*/}" in
steam|steam-runtime) return 0 ;; steam|steam-runtime|steam-native|steam-jupiter) return 0 ;;
esac esac
[[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]] [[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]]
} }
@@ -580,6 +583,8 @@ MCA_PACKAGING=() # native | flatpak | snap
# kept at all: the Steam module gives them Steam's own switch. # kept at all: the Steam module gives them Steam's own switch.
MCA_STEAM_LINKS=() # desktop file id MCA_STEAM_LINKS=() # desktop file id
MCA_STEAM_LINK_FILES=() # the entry that is in effect for it MCA_STEAM_LINK_FILES=() # the entry that is in effect for it
MCA_STEAM_LINK_PACK=() # native | flatpak | snap, which decides where the
# switch goes on the command line
# A scan reads every desktop entry on the system, so the menu does it once and # 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 # then redraws from what it found. Applying rescans on its own, so nothing else
@@ -598,7 +603,7 @@ mca_scan() {
MCA_IDS=(); MCA_FILES=(); MCA_NAMES=(); MCA_PROGS=(); MCA_KINDS=() MCA_IDS=(); MCA_FILES=(); MCA_NAMES=(); MCA_PROGS=(); MCA_KINDS=()
MCA_PACKAGING=() MCA_PACKAGING=()
MCA_STEAM_LINKS=(); MCA_STEAM_LINK_FILES=() MCA_STEAM_LINKS=(); MCA_STEAM_LINK_FILES=(); MCA_STEAM_LINK_PACK=()
# Pass one: read the entries and work out what each of them starts. No # 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 # detection yet - that needs a stat per program, and those are collected so
@@ -641,6 +646,13 @@ mca_scan() {
if mca_exec_is_steam_link "$exec_line" "$prog"; then if mca_exec_is_steam_link "$exec_line" "$prog"; then
MCA_STEAM_LINKS+=("$id") MCA_STEAM_LINKS+=("$id")
MCA_STEAM_LINK_FILES+=("$file") MCA_STEAM_LINK_FILES+=("$file")
if [[ $prog == flatpak:* ]]; then
MCA_STEAM_LINK_PACK+=(flatpak)
elif [[ $prog == snap:* ]]; then
MCA_STEAM_LINK_PACK+=(snap)
else
MCA_STEAM_LINK_PACK+=(native)
fi
continue continue
fi fi
@@ -664,10 +676,13 @@ mca_scan() {
packaging=native packaging=native
if [[ $prog == flatpak:* ]]; then if [[ $prog == flatpak:* ]]; then
prog="${prog#flatpak:}"
packaging=flatpak packaging=flatpak
mca_flatpak_is_chromium "$prog" \ if mca_prog_is_steam "$prog"; then
&& { (( c_browser[i] )) && kind=browser || kind=app; } kind=steam
elif mca_flatpak_is_chromium "${prog#flatpak:}"; then
(( c_browser[i] )) && kind=browser || kind=app
fi
prog="${prog#flatpak:}"
elif [[ $prog == snap:* ]]; then elif [[ $prog == snap:* ]]; then
prog="${prog#snap:}" prog="${prog#snap:}"
packaging=snap packaging=snap
@@ -676,7 +691,7 @@ mca_scan() {
elif mca_snap_is_chromium "$prog"; then elif mca_snap_is_chromium "$prog"; then
(( c_browser[i] )) && kind=browser || kind=app (( c_browser[i] )) && kind=browser || kind=app
fi fi
elif [[ ${prog##*/} == steam || ${prog##*/} == steam-runtime ]]; then elif mca_prog_is_steam "$prog"; then
# Steam is Chromium inside, but nothing about it can be changed # Steam is Chromium inside, but nothing about it can be changed
# from a command line argument; it has its own module. # from a command line argument; it has its own module.
kind=steam kind=steam
+201 -72
View File
@@ -29,6 +29,40 @@
MCA_MARK_BEGIN='# >>> middleclick-autoscroll' MCA_MARK_BEGIN='# >>> middleclick-autoscroll'
MCA_MARK_END='# <<< middleclick-autoscroll' MCA_MARK_END='# <<< middleclick-autoscroll'
# ---------------------------------------------------------------------------
# Feature lists
# ---------------------------------------------------------------------------
# Both --enable-features and --enable-blink-features take a comma separated
# list, and Chromium keeps the last one it is given and drops the rest. A
# second one of the same kind would therefore silently switch off whatever the
# first one turned on, so an option that is already there is extended instead.
# _mca_list_merge <list> <wanted>
# The two lists as one, in the order they came and without repeating anything.
_mca_list_merge() {
local out="$1" item
for item in ${2//,/ }; do
[[ ",$out," == *",$item,"* ]] && continue
out="${out:+$out,}$item"
done
printf '%s' "$out"
}
# _mca_list_strip <list> <unwanted>
# The list without any of the named entries.
_mca_list_strip() {
local out='' item
for item in ${1//,/ }; do
[[ ",$2," == *",$item,"* ]] && continue
out="${out:+$out,}$item"
done
printf '%s' "$out"
}
# The two options, as a regular expression branch, for the places that have to
# recognise either of them.
MCA_FEATURE_OPTS='--enable-blink-features|--enable-features'
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Command lines # Command lines
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -70,28 +104,32 @@ _mca_exec_after_program() {
# Prints nothing and returns 1 when there was nothing to change, so callers can # Prints nothing and returns 1 when there was nothing to change, so callers can
# tell a real edit from a no-op without diffing. # tell a real edit from a no-op without diffing.
# #
# An --enable-blink-features that is already there is extended rather than # A feature list that is already on the line is extended rather than repeated.
# repeated: Chromium keeps the last occurrence of the option and drops the # See _mca_list_merge.
# rest, so a second one would silently disable whatever the first turned on.
mca_exec_inject() { mca_exec_inject() {
local line="$1" flagstr="${2:-}" where="${3:-before-fields}" local line="$1" flagstr="${2:-}" where="${3:-before-fields}"
local flag value existing at changed=0 local flag opt value existing merged re at changed=0
local -a flags local -a flags
[[ -n $flagstr ]] || flagstr="$(mca_flags)" [[ -n $flagstr ]] || flagstr="$(mca_flags)"
read -r -a flags <<< "$flagstr" read -r -a flags <<< "$flagstr"
for flag in "${flags[@]}"; do for flag in "${flags[@]}"; do
if [[ $flag == --enable-blink-features=* ]]; then case "$flag" in
value="${flag#--enable-blink-features=}" --enable-blink-features=*|--enable-features=*)
if [[ $line =~ --enable-blink-features=([A-Za-z0-9,_-]*) ]]; then opt="${flag%%=*}"
existing="${BASH_REMATCH[1]}" value="${flag#*=}"
[[ ",$existing," == *",$value,"* ]] && continue re="(^|[[:space:]])$opt=([^[:space:]]*)"
line="${line/--enable-blink-features=$existing/--enable-blink-features=${existing:+$existing,}$value}" if [[ $line =~ $re ]]; then
changed=1 existing="${BASH_REMATCH[2]}"
continue merged="$(_mca_list_merge "$existing" "$value")"
fi [[ $merged == "$existing" ]] && continue
fi line="${line/$opt=$existing/$opt=$merged}"
changed=1
continue
fi
;;
esac
[[ " $line " == *" $flag "* ]] && continue [[ " $line " == *" $flag "* ]] && continue
@@ -117,12 +155,12 @@ mca_exec_inject() {
# Flag files # Flag files
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# mca_flags_apply <program> # mca_flags_apply <program> [kind]
# Writes the flags into the file the program's launcher reads. Returns 0 when # Writes the flags into the file the program's launcher reads. Returns 0 when
# the program has such a file (whether or not anything needed changing), 1 when # the program has such a file (whether or not anything needed changing), 1 when
# it has none and the desktop entry route has to be used instead. # it has none and the desktop entry route has to be used instead.
mca_flags_apply() { mca_flags_apply() {
local prog="$1" name target='' existing_target='' local prog="$1" kind="${2:-app}" name target='' existing_target=''
local -a candidates=() local -a candidates=()
mapfile -t candidates < <(mca_flags_candidates "$prog" | awk '!seen[$0]++') mapfile -t candidates < <(mca_flags_candidates "$prog" | awk '!seen[$0]++')
@@ -133,7 +171,7 @@ mca_flags_apply() {
for name in "${candidates[@]}"; do for name in "${candidates[@]}"; do
if [[ -f "$MCA_XDG_CONFIG/$name" ]]; then if [[ -f "$MCA_XDG_CONFIG/$name" ]]; then
existing_target="$name" existing_target="$name"
_mca_flags_write "$MCA_XDG_CONFIG/$name" _mca_flags_write "$MCA_XDG_CONFIG/$name" "$kind"
fi fi
done done
[[ -n $existing_target ]] && return 0 [[ -n $existing_target ]] && return 0
@@ -143,19 +181,21 @@ mca_flags_apply() {
# creating that one also covers every other application using the same # creating that one also covers every other application using the same
# shared Electron. # shared Electron.
target="${candidates[-1]}" target="${candidates[-1]}"
_mca_flags_write "$MCA_XDG_CONFIG/$target" _mca_flags_write "$MCA_XDG_CONFIG/$target" "$kind"
return 0 return 0
} }
# _mca_flags_write <file> # _mca_flags_write <file> [kind]
# Adds the flags as a marked block, or merges into an --enable-blink-features # Adds the flags as a marked block, or merges into a feature list that is
# line that is already in the file. # already in the file.
_mca_flags_write() { _mca_flags_write() {
local file="$1" content='' line flag value existing merged=0 block='' local file="$1" kind="${2:-app}"
local content='' line flag opt want existing merged=0 block=''
local -a flags local -a flags
local -A merged_opts=()
local created=0 local created=0
read -r -a flags <<< "$(mca_flags)" read -r -a flags <<< "$(mca_flags "$kind")"
[[ -f $file ]] || created=1 [[ -f $file ]] || created=1
if (( ! created )); then if (( ! created )); then
@@ -166,16 +206,23 @@ _mca_flags_write() {
fi fi
fi fi
# Merge into a blink-features line the user (or a distribution) put there. # Merge into a feature list the user (or a distribution) put there, rather
if [[ $content == *--enable-blink-features=* ]]; then # than write a second line of the same option further down.
if [[ $content == *--enable-* ]]; then
local out='' local out=''
while IFS= read -r line; do while IFS= read -r line; do
if [[ $line =~ ^[[:space:]]*--enable-blink-features=([A-Za-z0-9,_-]*)[[:space:]]*$ ]]; then if [[ $line =~ ^[[:space:]]*($MCA_FEATURE_OPTS)=([^[:space:]]*)[[:space:]]*$ ]]; then
existing="${BASH_REMATCH[1]}" opt="${BASH_REMATCH[1]}"
if [[ ",$existing," != *",$MCA_FEATURE,"* ]]; then existing="${BASH_REMATCH[2]}"
line="--enable-blink-features=${existing:+$existing,}$MCA_FEATURE" want=''
for flag in "${flags[@]}"; do
[[ $flag == "$opt="* ]] && want="${flag#*=}"
done
if [[ -n $want ]]; then
line="$opt=$(_mca_list_merge "$existing" "$want")"
merged_opts["$opt"]=1
merged=1
fi fi
merged=1
fi fi
out+="$line"$'\n' out+="$line"$'\n'
done <<< "$content" done <<< "$content"
@@ -183,7 +230,7 @@ _mca_flags_write() {
fi fi
for flag in "${flags[@]}"; do for flag in "${flags[@]}"; do
(( merged )) && [[ $flag == --enable-blink-features=* ]] && continue [[ -n ${merged_opts["${flag%%=*}"]:-} ]] && continue
block+="$flag"$'\n' block+="$flag"$'\n'
done done
@@ -218,7 +265,7 @@ _mca_flags_drop_block() {
# mca_flags_revert <file> <detail> # mca_flags_revert <file> <detail>
mca_flags_revert() { mca_flags_revert() {
local file="$1" detail="$2" content line out='' existing new local file="$1" detail="$2" content line out='' opt existing new
[[ -f $file ]] || return 0 [[ -f $file ]] || return 0
@@ -238,12 +285,13 @@ mca_flags_revert() {
if [[ $detail == merged* ]]; then if [[ $detail == merged* ]]; then
while IFS= read -r line; do while IFS= read -r line; do
if [[ $line =~ ^[[:space:]]*--enable-blink-features=([A-Za-z0-9,_-]*)[[:space:]]*$ ]]; then if [[ $line =~ ^[[:space:]]*($MCA_FEATURE_OPTS)=([^[:space:]]*)[[:space:]]*$ ]]; then
existing="${BASH_REMATCH[1]}" opt="${BASH_REMATCH[1]}"
new="${existing//$MCA_FEATURE/}" existing="${BASH_REMATCH[2]}"
new="${new//,,/,}"; new="${new#,}"; new="${new%,}" # Either name, whichever version of this program put it there.
new="$(_mca_list_strip "$existing" "$MCA_BROWSER_FEATURES")"
[[ -z $new ]] && continue [[ -z $new ]] && continue
line="--enable-blink-features=$new" line="$opt=$new"
fi fi
out+="$line"$'\n' out+="$line"$'\n'
done <<< "$content" done <<< "$content"
@@ -349,51 +397,119 @@ mca_desktop_apply() {
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Autostart entries # Entries outside the search path
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Applications that start themselves at login write their own entry into # Two directories hold desktop entries that no part of the XDG search path
# ~/.config/autostart, pointing straight at their binary. Those bypass the # looks at, so nothing the scan does can reach them:
# desktop entry in the menu completely, which is why Discord launched at login
# used to behave differently from Discord launched from the menu.
# #
# Steam is here too, even though it is not a Chromium process itself: its entry # ~/.config/autostart, where applications that start themselves at login
# needs -noverifyfiles exactly like the menu one, and it is the entry most # write their own entry, pointing straight at their binary. Those bypass the
# likely to exist, because Steam writes it as soon as "run at startup" is # entry in the menu completely, which is why Discord launched at login used
# ticked. Without it, a Steam started at login restores the patched web helper # to behave differently from Discord launched from the menu.
# script and the watcher patches it back, over and over. #
# The desktop folder, where a shortcut somebody dragged out of the menu -
# or asked Steam for - lives and nowhere else.
#
# Steam is in both, even though it is not a Chromium process itself: its
# entries need -noverifyfiles exactly like the menu one. The autostart entry is
# the one most likely to exist, because Steam writes it as soon as "run at
# startup" is ticked; without it a Steam started at login restores the patched
# web helper script and the watcher patches it back, over and over. The desktop
# ones are the gap behind "autoscroll works, except sometimes": starting a game
# from the desktop is a Steam start like any other, and a Steam start without
# the switch costs the interface its autoscroll for the rest of the session.
#
# Neither can be shadowed from anywhere, so both are edited where they stand,
# with the original kept.
# _mca_entry_patch_inplace <file> <gate: autostart|apps>
# One desktop entry that lives outside the XDG search path, edited where it is
# because there is nowhere to shadow it from.
#
# Anything that starts Steam gets Steam's own switch; a Chromium application
# gets the flag. What decides whether a Chromium application here is in scope
# differs by where the entry came from, which is what the gate says: an
# autostart entry follows the autostart setting, a shortcut follows the same
# rules as the application it is a shortcut to.
_mca_entry_patch_inplace() {
local file="$1" gate="$2"
local id prog packaging=native content backup kind
grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null && return 0
_mca_desktop_read "$file"
[[ -n $DE_EXEC ]] || return 0
[[ $DE_HIDDEN == true ]] && return 0
mca_exec_program "$DE_EXEC" || return 0
prog="$MCA_PROG"
# A Flatpak or a snap entry names the wrapper rather than the application.
# The scan resolves those into an id; outside the scan the same has to
# happen here, or every Flatpak looks like the flatpak program itself.
if [[ ${prog##*/} == flatpak ]]; then
mca_exec_flatpak_id "$DE_EXEC" || return 0
prog="flatpak:$MCA_PROG"
packaging=flatpak
elif mca_snap_name "$prog"; then
prog="snap:$MCA_PROG"
packaging=snap
fi
if mca_prog_is_steam "$prog"; then
[[ $CFG_STEAM == yes ]] || return 0
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \
"$MCA_STEAM_LAUNCH_FLAG" "$(mca_steam_flag_position "$packaging")")"
else
case "$packaging" in
flatpak) mca_flatpak_is_chromium "${prog#flatpak:}" || return 0 ;;
snap) mca_snap_is_chromium "${prog#snap:}" || return 0 ;;
*) mca_is_chromium "$prog" || return 0 ;;
esac
mca_desktop_is_browser && kind=browser || kind=app
if [[ $gate == autostart ]]; then
[[ $CFG_AUTOSTART == yes ]] || return 0
else
id="${file##*/}"; id="${id%.desktop}"
mca_kind_wanted "$kind" "$id" "$packaging" || return 0
fi
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \
"$(mca_flags "$kind")")"
fi
[[ -n $content ]] || return 0
[[ "$content" == "$(< "$file")" ]] && return 0
backup="$(mca_backup "$file")" || return 0
if mca_write_if_changed "$file" "$content"$'\n'; then
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
fi
mca_ledger_add inplace "$file" "$backup"
}
mca_autostart_apply() { mca_autostart_apply() {
local dir="$MCA_XDG_CONFIG/autostart" file exec_line prog content backup local dir="$MCA_XDG_CONFIG/autostart" file
[[ -d $dir ]] || return 0 [[ -d $dir ]] || return 0
for file in "$dir"/*.desktop; do for file in "$dir"/*.desktop; do
[[ -f $file ]] || continue [[ -f $file ]] || continue
grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null && continue _mca_entry_patch_inplace "$file" autostart
done
}
exec_line="$(mca_desktop_get "$file" Exec)" mca_shortcuts_apply() {
[[ -n $exec_line ]] || continue local dir file
dir="$(mca_desktop_folder)"
mca_exec_program "$exec_line" || continue [[ -d $dir ]] || return 0
prog="$MCA_PROG"
if [[ ${prog##*/} == steam || ${prog##*/} == steam-runtime ]]; then for file in "$dir"/*.desktop; do
[[ $CFG_STEAM == yes ]] || continue [[ -f $file ]] || continue
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \ _mca_entry_patch_inplace "$file" apps
"$MCA_STEAM_LAUNCH_FLAG" after-program)"
else
[[ $CFG_AUTOSTART == yes ]] || continue
mca_is_chromium "$prog" || continue
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE")"
fi
[[ -n $content ]] || continue
[[ "$content" == "$(< "$file")" ]] && continue
backup="$(mca_backup "$file")" || continue
if mca_write_if_changed "$file" "$content"$'\n'; then
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
fi
mca_ledger_add inplace "$file" "$backup"
done done
} }
@@ -446,13 +562,15 @@ mca_spotify_apply() {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# mca_prune_orphans # mca_prune_orphans
# Removes generated entries whose application is gone. # Forgets changes there is no longer anything to undo.
# #
# Uninstalling something deletes its entry from /usr/share/applications, but the # Uninstalling something deletes its entry from /usr/share/applications, but the
# copy shadowing it is in the user's home and pacman knows nothing about it. It # copy shadowing it is in the user's home and pacman knows nothing about it. It
# would sit in the menu forever, offering to start a program that is no longer # would sit in the menu forever, offering to start a program that is no longer
# installed - and the watcher would not notice, because a plain apply only ever # installed - and the watcher would not notice, because a plain apply only ever
# looks at what is there now. # looks at what is there now. A file that was edited where it stood and has
# since been deleted is the same problem from the other side: its record would
# have `disable` put the file back.
mca_prune_orphans() { mca_prune_orphans() {
local kind path source line local kind path source line
local -a lines=() local -a lines=()
@@ -462,6 +580,17 @@ mca_prune_orphans() {
for line in "${lines[@]}"; do for line in "${lines[@]}"; do
IFS=$'\t' read -r kind path source <<< "$line" IFS=$'\t' read -r kind path source <<< "$line"
# An entry that was edited where it stood and has since been deleted -
# a game shortcut dragged to the wastebasket, an autostart entry the
# application removed. There is nothing left to put back, and keeping
# the record would have `disable` recreate a file the user threw away.
if [[ $kind == inplace && ! -e $path ]]; then
[[ -n $source ]] && rm -f -- "$MCA_BACKUPDIR/$source"
mca_ledger_forget "$path"
continue
fi
[[ $kind == shadow && -n $source ]] || continue [[ $kind == shadow && -n $source ]] || continue
[[ -e $source ]] && continue [[ -e $source ]] && continue
+175 -38
View File
@@ -10,18 +10,27 @@
# ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh # ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
# #
# Steam compares the installed files against its manifest at every start - by # Steam compares the installed files against its manifest at every start - by
# size, not by content - and restores whatever differs, which is why the # size and timestamp rather than by content - and restores whatever differs.
# launcher gets -noverifyfiles as well. The trade-off is real and belongs to # So the patch is written to look untouched: the bytes the flag costs are taken
# the user, so this is a switch of its own rather than part of the general # back out of the script's comments and the timestamp is put back afterwards,
# application handling: with verification off, Steam no longer repairs a # which leaves a file that is byte-for-byte the size Steam recorded and as old
# damaged installation by itself. # as Steam left it. A client that verifies its files finds nothing to repair
# and the flag survives however Steam was started - from the menu, from a game
# shortcut, from a launcher like Heroic, from a terminal.
# #
# Every way of starting Steam has to carry that switch, or the two programs # That is the part that has to work, because there is no way to make every
# spend the session undoing each other: Steam restores the script, the watcher # possible way of starting Steam carry an argument. -noverifyfiles on the
# patches it again, Steam restores it again, and the client never gets past its # launcher entries is the second line rather than the first: it covers the case
# update dialog. So the autostart entry gets it as well as the menu one (see # where the script has no comments left to pay for the flag and the patch has
# mca_autostart_apply), and the patch below stays out of the way of a client # to grow the file. The trade-off is real and belongs to the user, so it is a
# that is already running. # switch of its own rather than part of the general application handling: with
# verification off, Steam no longer repairs a damaged installation by itself.
#
# A patch that does change the size still has to stay out of the way of a
# client that is already running, or the two programs spend the session undoing
# each other: Steam restores the script, the watcher patches it again, Steam
# restores it again, and the client never gets past its update dialog. A patch
# that keeps the size is invisible to that check and goes in either way.
# #
# The file comes back on every client update, and the watcher re-applies the # The file comes back on every client update, and the watcher re-applies the
# patch when that happens. # patch when that happens.
@@ -145,12 +154,112 @@ _mca_steam_exec_line() {
' "$1" 2>/dev/null ' "$1" 2>/dev/null
} }
# _mca_steam_build <script> <flags> <reclaim: 0|1>
# The patched script on stdout: the flags appended to the line that starts the
# web helper, and - when asked for - the same number of bytes taken back out of
# the script's comments, so that the file Steam finds is the length Steam wrote
# down. Everything else - setting up the container runtime, deciding whether
# the sandbox can be used - is left alone.
#
# Comments are eaten from the bottom up, so the header that says what the
# script is for is the last thing to lose anything, and the '#' itself always
# stays: a line that loses it stops being a comment and starts being a command.
# Fails when the comments are too short to pay for the flags.
_mca_steam_build() {
local script="$1" flags="$2" reclaim="$3" lineno need=0
lineno="$(_mca_steam_exec_line "$script")"
[[ -n $lineno ]] || return 1
if (( reclaim )); then
need="$(printf '%s' " $flags" | wc -c)"
need="${need//[^0-9]/}"
fi
# LC_ALL=C so awk counts bytes: a comment with an accent in it is shorter
# in characters than it is on disk, and it is the disk that Steam measures.
LC_ALL=C awk -v n="$lineno" -v f=" $flags" -v need="$need" '
{ lines[NR] = (NR == n ? $0 f : $0) }
END {
for (i = NR; i >= 2 && need > 0; i--) {
if (i == n) continue
p = index(lines[i], "#")
if (p == 0) continue
# Only a line that is nothing but a comment; a trailing one
# sits behind a command that has to stay intact.
if (substr(lines[i], 1, p - 1) ~ /[^ \t]/) continue
can = length(lines[i]) - p
if (can <= 0) continue
if (can > need) can = need
lines[i] = substr(lines[i], 1, length(lines[i]) - can)
need -= can
}
if (need > 0) exit 1
for (i = 1; i <= NR; i++) print lines[i]
}
' "$script"
}
# _mca_steam_same_size <script> <content>
# Whether writing that content leaves the file at the size it has now. Measured
# rather than worked out: a script without a final newline gains one on the way
# through awk, and one byte is all it takes for Steam to notice.
_mca_steam_same_size() {
local script="$1" content="$2"
(( $(wc -c < "$script") == $(printf '%s\n' "$content" | wc -c) ))
}
# _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
# script is unpatched, so the copy that replaces it is Steam's own.
_mca_steam_refresh_backup() {
local script="$1" name
name="$(mca_backup_name "$script")"
[[ -e "$MCA_BACKUPDIR/$name" ]] || return 0
cmp -s -- "$script" "$MCA_BACKUPDIR/$name" && return 0
cp -p -- "$script" "$MCA_BACKUPDIR/$name" 2>/dev/null || true
}
# _mca_steam_reneutralise <script> <flags>
# An earlier version of this program patched by appending and left the script
# longer than Steam expects, which is a patch that only survives because
# -noverifyfiles is there to stop Steam looking. If the same flags can be made
# to fit, they are: the file goes back to the length and the timestamp Steam
# wrote down and stops depending on that switch.
#
# The new copy is built from the untouched original that was kept when the
# script was first patched, never from the patched file - patching a patched
# file is how a flag ends up in there twice.
_mca_steam_reneutralise() {
local script="$1" flags="$2" name copy out
name="$(mca_backup_name "$script")"
copy="$MCA_BACKUPDIR/$name"
[[ -f $copy ]] || return 1
# Already the length Steam expects: there is nothing to gain.
(( $(wc -c < "$script") == $(wc -c < "$copy") )) && return 1
out="$(_mca_steam_build "$copy" "$flags" 1)" || return 1
_mca_steam_same_size "$copy" "$out" || return 1
if mca_write_if_changed "$script" "$out"$'\n'; then
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
fi
chmod +x -- "$script" 2>/dev/null || true
touch -r "$copy" -- "$script" 2>/dev/null || true
mca_ledger_add steam "$script" "$name"
return 0
}
# mca_steam_apply # mca_steam_apply
# Appends the flags to the line that starts the web helper. Everything else in # Puts the flags into every Steam installation that has the script, preferring
# the script - setting up the container runtime, deciding whether the sandbox # the patch that keeps the file's size and falling back to the one that does
# can be used - is left alone. # not. Returns 1 when there is no Steam to patch at all.
mca_steam_apply() { mca_steam_apply() {
local root script backup flags lineno found=0 out local root script backup flags found=0 out neutral
flags="$(mca_flags)" flags="$(mca_flags)"
@@ -158,34 +267,51 @@ mca_steam_apply() {
script="$(mca_steam_script "$root")" || continue script="$(mca_steam_script "$root")" || continue
found=1 found=1
mca_steam_script_patched "$script" && continue if mca_steam_script_patched "$script"; then
_mca_steam_reneutralise "$script" "$flags" || true
continue
fi
# Not patched now, but patched before: Steam has just put its own copy _mca_steam_refresh_backup "$script"
# back. Patching it again while the client watches is what turns one
# size mismatch into an endless update dialog, and it would not help neutral=1
# this session anyway - the helper is started once, at the start. The if ! { out="$(_mca_steam_build "$script" "$flags" 1)" \
# patch waits for the next apply with Steam closed. && _mca_steam_same_size "$script" "$out"; }
if mca_steam_deferred "$script"; then then
neutral=0
out="$(_mca_steam_build "$script" "$flags" 0)" || {
# A client update changed how the helper is started. Leaving
# the file alone is the only safe answer: this script is what
# starts Steam's entire interface.
mca_note "$(mca_msg "Steam starts its interface in a way this version does not recognise; leaving it alone.")"
continue
}
fi
# The patch has to grow the file, and it is not patched now but was
# before: Steam has just put its own copy back. Patching it again
# while the client watches is what turns one size mismatch into an
# endless update dialog, and it would not help this session anyway -
# the helper is started once, at the start. The patch waits for the
# next apply with Steam closed. A patch that keeps the size has
# nothing to wait for.
if (( ! neutral )) && mca_steam_deferred "$script"; then
mca_note "$(mca_msg "Steam is running and has put its own file back; the change waits until Steam is closed.")" mca_note "$(mca_msg "Steam is running and has put its own file back; the change waits until Steam is closed.")"
continue continue
fi fi
lineno="$(_mca_steam_exec_line "$script")"
if [[ -z $lineno ]]; then
# A client update changed how the helper is started. Leaving the
# file alone is the only safe answer: this script is what starts
# Steam's entire interface.
mca_note "$(mca_msg "Steam starts its interface in a way this version does not recognise; leaving it alone.")"
continue
fi
out="$(awk -v n="$lineno" -v f=" $flags" 'NR == n { $0 = $0 f } { print }' "$script")" || continue
backup="$(mca_backup "$script")" || continue backup="$(mca_backup "$script")" || continue
if mca_write_if_changed "$script" "$out"$'\n'; then if mca_write_if_changed "$script" "$out"$'\n'; then
MCA_CHANGES=$(( MCA_CHANGES + 1 )) MCA_CHANGES=$(( MCA_CHANGES + 1 ))
fi fi
chmod +x -- "$script" 2>/dev/null || true 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_ledger_add steam "$script" "$backup" mca_ledger_add steam "$script" "$backup"
done < <(mca_steam_roots) done < <(mca_steam_roots)
@@ -214,11 +340,22 @@ mca_steam_revert() {
return 0 return 0
} }
# mca_steam_desktop_apply <id> <source entry> # mca_steam_flag_position <packaging>
# Where Steam's own switch goes on a command line. Straight after the program
# for a native or snap entry, because Steam reads its options before the
# steam:// argument that the right-click actions pass. For a Flatpak the first
# token is flatpak itself, which would take the switch for one of its own and
# refuse to start, so there it goes at the end of the arguments - after the
# application id, where flatpak hands everything on to Steam.
mca_steam_flag_position() {
[[ ${1:-native} == flatpak ]] && { printf 'before-fields\n'; return; }
printf 'after-program\n'
}
# mca_steam_desktop_apply <id> <source entry> [packaging]
# Steam restores its own files at every start unless it is told not to verify # Steam restores its own files at every start unless it is told not to verify
# them, so the launcher entry carries that switch. It goes directly after the # them, so the launcher entry carries that switch.
# program because Steam reads its options before the steam:// argument that the
# right-click actions pass.
mca_steam_desktop_apply() { mca_steam_desktop_apply() {
mca_desktop_apply "$1" "$2" "$MCA_STEAM_LAUNCH_FLAG" after-program mca_desktop_apply "$1" "$2" "$MCA_STEAM_LAUNCH_FLAG" \
"$(mca_steam_flag_position "${3:-native}")"
} }
+6 -2
View File
@@ -119,8 +119,12 @@ mca_do_apply() {
mca_ok "$(mca_msg "Already applied to %d applications." "$MCA_N_ON")" mca_ok "$(mca_msg "Already applied to %d applications." "$MCA_N_ON")"
fi fi
if [[ $CFG_WATCH == yes ]] && mca_watch_available && ! mca_watch_enabled; then if [[ $CFG_WATCH == yes ]] && mca_watch_available; then
mca_watch_enable || true if mca_watch_enabled; then
mca_watch_refresh
else
mca_watch_enable || true
fi
elif [[ $CFG_WATCH != yes ]] && mca_watch_enabled; then elif [[ $CFG_WATCH != yes ]] && mca_watch_enabled; then
mca_watch_disable mca_watch_disable
fi fi