Compare commits

..
34 Commits
Author SHA1 Message Date
Felitendo e3278ecf27 chore: 1.5.3
release / Debian package (push) Failing after 15s
release / RPM package (push) Failing after 34s
release / Release and repositories (push) Skipped
release / Install from the RPM repository (push) Skipped
2026-09-24 08:25:50 +02:00
Felitendo 2401855b70 fix: keep the flag in front of a bare double dash
Discord's entry is `discord --url -- %u`. The flag went in before the
field code, which put it after the double dash, and that is where
Chromium stops reading switches and starts treating everything as an
address. The feature never came on.
2026-09-24 08:24:15 +02:00
Felitendo da291cefa5 fix: recognise Discord's bootstrap launcher
Discord no longer ships its application. /usr/bin/discord is a few lines
of shell that run updater_bootstrap, which downloads Electron into the
user's config directory on first start and execs it from there. So there
is no binary next to the launcher to find markers around, and the path it
hands over to is built out of $HOME and the version it just downloaded,
which the script reader cannot resolve. Discord came out as "not
Chromium" and was dropped from the list entirely.

The name of the bootstrap it ships is the one thing left that says
Discord, so that is the hint now.
2026-09-24 08:24:15 +02:00
Felitendo e2e05748f7 docs: explain steam and the browser flag names 2026-09-24 08:22:09 +02:00
Felitendo c20ea98de4 chore: 1.5.2
release / RPM package (push) Failing after 23s
release / Release and repositories (push) Skipped
release / Install from the RPM repository (push) Skipped
release / Debian package (push) Failing after 13s
2026-09-24 02:01:16 +02:00
Felitendo d0d977ff56 docs: cut the readme down, fold the details 2026-09-24 02:00:33 +02:00
Felitendo cf48fc47df fix: say 1 minute ago, not 1 minutes ago 2026-09-24 02:00:33 +02:00
Felitendo 335d4cc9ec docs: show the menu as real screenshots 2026-09-24 02:00:33 +02:00
Felitendo c7222d22bd docs: add a changelog and the release notes flow 2026-09-24 02:00:33 +02:00
Felitendo 4fffba005d ci: take the release notes from the changelog 2026-09-24 01:13:05 +02:00
Felitendo b93d0f5f1e docs: add commit rules to claude.md 2026-09-24 01:13:05 +02:00
Felitendo d39e669743 docs: coffee button and bug report link 2026-09-24 01:13:05 +02:00
Felitendo f9826edc24 chore: switch funding to buy me a coffee 2026-09-24 01:13:05 +02:00
Felitendo 5e645acbd0 docs: add icon and centered readme header 2026-09-21 14:06:54 +02:00
Felitendo 1042639a08 docs: add claude.md and avoid dashes 2026-09-21 14:00:02 +02:00
Felitendo ab8c6477cb chore: move to LoonixTools 2026-09-21 13:07:19 +02:00
Felitendo 0fbe8bdb39 chore: 1.5.1
release / Debian package (push) Failing after 13s
release / RPM package (push) Failing after 22s
release / Release and repositories (push) Skipped
release / Install from the RPM repository (push) Skipped
2026-09-15 19:12:54 +02:00
Felitendo a992efb0cb fix: pass Brave's autoscroll feature name to browsers 2026-09-15 19:01:01 +02:00
Felitendo b97ccf0379 chore: toned down readme 2026-09-15 08:44:43 +02:00
Felitendo 7a446364dc feat: identify AppImages by reading their squashfs name table
Every AppImage came out as "cannot tell" because the payload is a
compressed filesystem and none of the Chromium markers is on disk to be
found. The names are reachable, though: squashfs keeps the name of
everything it holds in one table of a few kilobytes, and a name is all
this question needs. That table is now read and inflated in place -
nothing is unpacked, and the image is never run, which a scan started by
the path unit has no business doing anyway.

So a Tauri AppImage such as Modrinth is recognised as WebKitGTK and
dropped from the list instead of being offered as something that could be
switched on - there is no autoscroll in WebKit to ask for - and an
Electron one is covered like any other application.

Left as "cannot tell": lzo and lz4, which squashfs stores as bare blocks
no tool will read without their own framing, and the original ISO9660
layout.

Alongside:

- The detect cache carries a format line. Its entries are keyed on size
  and mtime, so an AppImage that has not changed would otherwise keep
  answering the way an older version decided it did, forever.

- A marker list for whole-tree searches, which is what a Flatpak, a snap
  and an AppImage need. icudtl.dat, snapshot_blob.bin and resources.pak
  are out of it: Flutter ships an icudtl.dat in data/, and next to a
  binary those names are evidence while four directories down they are
  not. The Flatpak and snap searches now share that list.
2026-09-14 14:06:23 +02:00
Felitendo 449a20ce9b chore: added funding 2026-09-08 14:33:51 +02:00
Felitendo ae4e8b9294 chore: 1.5.0
release / Debian package (push) Failing after 14s
release / RPM package (push) Failing after 22s
release / Release and repositories (push) Skipped
release / Install from the RPM repository (push) Skipped
KDE's own middle-click paste is turned off along with everything else. Inside a
Chromium application the flag already settles it - with autoscroll on, Blink
stops pasting the primary selection - but everywhere else on the desktop middle
click goes on pasting, which is the thing this program exists to get away from.

KWin has a switch for it on a Wayland session, where the paste is a protocol
the compositor either offers or does not, and that is what gets written. It is
true from the next login on. What the key said before is recorded, so disable
puts it back, and a key that already said no is left alone and never switched
back on. X11 has no such switch and nothing is written there.

Off under Settings for anyone who wants the paste kept.
2026-09-08 14:01:06 +02:00
Felitendo b4fa0edf28 docs: reword the install steps 2026-09-08 14:01:06 +02:00
Felitendo e09b145f3a fix: install the user units where systemd will find them
A build with a prefix of its own put them in $(PREFIX)/lib/systemd/user, which
is right for /usr/local and wrong for the one prefix an install without root
can use: systemd searches $XDG_DATA_HOME/systemd/user and never
~/.local/lib/systemd/user. An install into ~/.local came out with a working
program whose watcher could not be enabled at all.

They go under share now, which both a system prefix and a home one are
searched for. Nothing changes for the packages - the deb and the rpm pass
USERUNITDIR in themselves.

That prefix is what an atomic distribution leaves a user, so the readme says
how to install on Bazzite: layer the rpm and reboot, or put the whole thing in
$HOME, which is where everything it writes ends up anyway.
2026-09-08 13:38:39 +02:00
Felitendo 004094ef78 feat: turn off KDE's middle-click paste
Inside a Chromium application the flag settles this on its own - with
autoscroll on, Blink stops pasting the primary selection. Everywhere else
on the desktop middle click goes on pasting, which is the thing this whole
program exists to get away from.

KWin has a switch for it, on Wayland, where the paste is a protocol the
compositor either offers or does not: EnablePrimarySelection in kwinrc.
Written with kwriteconfig, the way System Settings would, because kwinrc
has a cascade behind it. What the key said before goes in the ledger, so
disable puts it back - and a key that already said false is left alone and
not recorded, so a paste the user switched off themselves is never
switched back on.

X11 has no such switch and nothing is written there.
2026-09-08 13:30:43 +02:00
Felitendo 0c4e7645d0 docs: rework the readme 2026-09-08 13:30:43 +02:00
Felitendo c777e8280d chore: 1.4.0
release / Debian package (push) Failing after 14s
release / RPM package (push) Failing after 23s
release / Release and repositories (push) Skipped
release / Install from the RPM repository (push) Skipped
Steam's helper script is only patched when the patch keeps the file at the
length Steam wrote down. Where the comments cannot pay for the argument,
nothing is written at all: a file of the wrong length costs the whole client
package downloaded and installed again, and a client that quits at the end of
it instead of coming up.

With no such patch left to prop up, -noverifyfiles is gone from everything that
carried it - Steam's launcher entry, its autostart entry, the shortcut on the
desktop and the ones Steam writes for single games. Steam repairs its own
installation again, and nothing of this program's is on its command line. A
patch an earlier version appended is rebuilt to fit at the next apply, or taken
back if it cannot be.

A script somebody puts in front of the client to add a switch of its own is
recognised as a way of starting Steam, rather than as an application to hand
the flag to.

A desktop entry edited in place that the ledger has lost is written again from
the copy kept beside it. Without that, an entry from an older version kept an
older answer for good - a browser patched that way stayed on the flag that
shows the "unsupported command-line flag" bar while every other browser moved
off it in 1.3.0.
2026-09-06 14:00:43 +02:00
Felitendo 2ea64d0603 fix: write the current answer into an entry the ledger forgot
A desktop entry that was edited in place is left alone once it carries the
marker, and the only thing that ever writes it again is the undo a change of
flags goes through - which reads the ledger. So an entry the ledger has lost is
one nothing looks at any more: it keeps whatever an older version put on its
command line, and an update that moves browsers to a different flag moves every
browser but that one.

The copy taken before the edit is named after the path, not recorded in the
ledger, and is still there. A marked entry with no ledger line is now put back
from that copy and written again from scratch, which is the same thing the undo
would have done, and it lands in the ledger on the way out.

Only entries edited in place need this. A generated entry is built from the
source entry every time and is right by construction, and a flag file has its
block rewritten whenever the contents differ.
2026-09-06 13:58:11 +02:00
Felitendo dcdf2c1df3 fix: never leave Steam a file it would reinstall itself over
The patch that keeps the script's length was the good path and appending was
the fallback, with -noverifyfiles on everything that starts Steam to cover for
it. That cover does not exist.

Steam checks its files at the shutdown it runs itself, not only at a start
somebody handed arguments to, and that run carries no -noverifyfiles whatever
the session was started with. One wrong length there costs the whole client
package downloaded, extracted and installed again - and the client quits at the
end of it instead of coming up, which is the "Steam just closed itself" that
this was seen as:

    BVerifyInstalledFiles: steamwebhelper.sh is 130 bytes, expected 84
    Update wird heruntergeladen ... Paket wird extrahiert ...
    Aktualisierung abgeschlossen, Steam wird geladen ... Shutdown

So the flag now either fits in the space the comments give back or it is not
written at all, and the status screen says Steam is not patched. No autoscroll
in the interface is a far smaller thing than a client that reinstalls itself.

With no growing patch left to prop up, -noverifyfiles has nothing to do and is
gone with everything that carried it: Steam's launcher entry, its autostart
entry, the shortcuts on the desktop and the ones it writes for single games.
Steam repairs its own installation again. An installation from an older version
is taken back once through the flag scheme, and a patch that version appended
is rebuilt to fit at the next apply - or taken back, if the comments cannot pay
for it.
2026-09-05 01:07:15 +02:00
Felitendo c1841ec169 fix: recognise a wrapper script in front of Steam as Steam
A script somebody puts in front of the client to add a switch of its own is
still a way of starting Steam, but nothing recognised it as one: the check is
by name, and the script is not called steam.

Such a script tends to name the flag it exists to add, and that flag is one of
the markers the hint scan reads as "this is Chromium". The entry was therefore
handled as an application and had the flag appended to every Exec line of it,
where Steam ignores it, instead of being handed to the Steam module - so the
interface it was pointing at never got autoscroll, and the launcher entry never
got Steam's own switch either.

The handover a launcher script makes is now followed one step, and a script
that hands over to Steam counts as Steam. One step is all there is: the
client's own launcher is recognised by name already.
2026-09-05 00:51:42 +02:00
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
31 changed files with 1673 additions and 578 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
buy_me_a_coffee: felitendo
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
#
# Builds the GitHub release notes for a tag, the way big projects such as
# Immich lay them out: the hand-written entry from CHANGELOG.md (a welcome,
# the highlights), a support section, then every commit since the last
# release, sorted by kind, and a link to the full changelog.
#
# .github/release-notes.sh v1.2.0 the notes
# .github/release-notes.sh --title v1.2.0 the title (checks the entry exists)
#
# Commit authors come from the GitHub API through gh. Without it the list
# goes without them.
set -euo pipefail
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
title=0
if [[ ${1:-} == --title ]]; then
title=1
shift
fi
tag="${1:?usage: release-notes.sh [--title] vX.Y.Z}"
if ! grep -qx "## $tag" CHANGELOG.md; then
echo "CHANGELOG.md has no entry for $tag. Add \"## $tag\" first." >&2
exit 1
fi
if (( title )); then
printf '%s\n' "$tag"
exit 0
fi
repo="${GITHUB_REPOSITORY:-$(git remote get-url origin | sed -E 's#^.*github\.com[:/]##; s#\.git$##')}"
name="${repo#*/}"
# The entry: up to the next one, without the date line (GitHub shows the
# date), one heading level up, outside code blocks.
entry="$(awk -v h="## $tag" '
$0 == h { on = 1; next }
on && /^## / { exit }
!on { next }
/^```/ { code = !code }
!code && /^_[0-9]{4}-[0-9]{2}-[0-9]{2}_$/ { next }
!code && /^###/ { sub(/^#/, "") }
{ print }
' CHANGELOG.md | sed -e '/./,$!d')"
# The commits: from the last release, or from the start. A tag that does not
# exist yet is a preview of what HEAD would become.
if git rev-parse -q --verify "refs/tags/$tag" > /dev/null; then
target="$tag"
prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' "$tag^" 2>/dev/null || true)"
else
target="$(git rev-parse HEAD)"
prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' HEAD 2>/dev/null || true)"
fi
declare -A login=()
if command -v gh > /dev/null; then
if [[ -n $prev ]]; then
api=(api "repos/$repo/compare/$prev...$target" --jq '.commits[] | [.sha, (.author.login // "")] | @tsv')
else
api=(api --paginate "repos/$repo/commits?sha=$target&per_page=100" --jq '.[] | [.sha, (.author.login // "")] | @tsv')
fi
while IFS=$'\t' read -r sha who; do
[[ -n $who ]] && login[$sha]="$who"
done < <(gh "${api[@]}" 2>/dev/null || true)
fi
declare -A list=()
count_maint=0
while IFS=$'\t' read -r sha subject body; do
# Version bumps say nothing a reader needs.
[[ $subject =~ ^(chore:\ )?($name\ )?v?[0-9]+\.[0-9]+\.[0-9]+$ ]] && continue
case "$subject" in
*!:*) kind=breaking ;;
feat:* | feat\(*) kind=feat ;;
fix:* | fix\(*) kind=fix ;;
perf:* | perf\(*) kind=enh ;;
docs:* | docs\(*) kind=docs ;;
chore* | ci:* | ci\(* | build* | refactor* | test* | style*) kind=maint ;;
# Older commits without a prefix, sorted by what they say.
*README* | *readme* | *Readme*) kind=docs ;;
Add\ * | Put\ * | Introduce\ *) kind=feat ;;
Fix\ * | Repair\ * | Recover\ * | Stop\ *) kind=fix ;;
*\ *) kind=enh ;;
*) kind=maint ;;
esac
[[ $body == *"BREAKING CHANGE"* ]] && kind=breaking
[[ $subject == "Initial commit" ]] && kind=maint
line="* $subject"
[[ -n ${login[$sha]:-} ]] && line+=" by @${login[$sha]}"
line+=" in https://github.com/$repo/commit/$sha"
list[$kind]+="$line"$'\n'
[[ $kind == maint ]] && count_maint=$((count_maint + 1))
done < <(git log --reverse --no-merges --format='%H%x09%s%x09%b%x1e' "${prev:+$prev..}$target" | tr '\n\036' ' \n' | sed 's/^ //')
changes=''
for pair in "breaking:🚨 Breaking Changes" "feat:🚀 Features" "enh:🌟 Enhancements" "fix:🐛 Bug fixes" "docs:📚 Documentation"; do
kind="${pair%%:*}"
[[ -n ${list[$kind]:-} ]] || continue
changes+="### ${pair#*:}"$'\n'"${list[$kind]}"
done
if [[ -n ${list[maint]:-} ]]; then
changes+=$'\n'"<details><summary>🧰 Maintenance ($count_maint)</summary>"$'\n\n'"${list[maint]}"$'\n'"</details>"$'\n'
fi
if [[ -n $prev ]]; then
full="**Full Changelog**: https://github.com/$repo/compare/$prev...$tag"
else
full="**Full Changelog**: https://github.com/$repo/commits/$tag"
fi
# A release with highlights is a big one: it gets a heading and the support
# section, as in Immich. A patch is a sentence or two and the list.
if grep -q '^## Highlights$' <<< "$entry"; then
cat <<EOF
# 🚀 $name $tag
$entry
## ☕ Support $name
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>
----
EOF
else
printf '%s\n\n' "$entry"
fi
printf "## What's Changed\n%s\n\n%s\n" "$changes" "$full"
+11 -3
View File
@@ -33,6 +33,10 @@ jobs:
- name: Check the tag against the Makefile - name: Check the tag against the Makefile
run: packaging/check-version.sh "${{ github.ref_name }}" run: packaging/check-version.sh "${{ github.ref_name }}"
- name: Check CHANGELOG.md has this release
if: startsWith(github.ref, 'refs/tags/v')
run: .github/release-notes.sh --title "${{ github.ref_name }}"
- run: packaging/build-deb.sh - run: packaging/build-deb.sh
- name: Look inside what was built - name: Look inside what was built
@@ -106,6 +110,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
with:
# All tags, for the link to the changes since the last release.
fetch-depth: 0
- uses: actions/download-artifact@v8 - uses: actions/download-artifact@v8
with: with:
@@ -117,9 +124,10 @@ jobs:
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
.github/release-notes.sh "${{ github.ref_name }}" > notes.md
gh release create "${{ github.ref_name }}" \ gh release create "${{ github.ref_name }}" \
--title "${{ github.ref_name }}" \ --title "$(.github/release-notes.sh --title "${{ github.ref_name }}")" \
--generate-notes \ --notes-file notes.md \
incoming/*.deb incoming/*.rpm \ incoming/*.deb incoming/*.rpm \
|| gh release upload "${{ github.ref_name }}" \ || gh release upload "${{ github.ref_name }}" \
incoming/*.deb incoming/*.rpm --clobber incoming/*.deb incoming/*.rpm --clobber
@@ -143,7 +151,7 @@ jobs:
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
else else
echo "present=no" >> "$GITHUB_OUTPUT" echo "present=no" >> "$GITHUB_OUTPUT"
echo "::warning::No GPG_PRIVATE_KEY secret - the apt and dnf repositories were not updated. The packages are on the release." echo "::warning::No GPG_PRIVATE_KEY secret, so the apt and dnf repositories were not updated. The packages are on the release."
exit 0 exit 0
fi fi
echo "present=yes" >> "$GITHUB_OUTPUT" echo "present=yes" >> "$GITHUB_OUTPUT"
+80
View File
@@ -0,0 +1,80 @@
# Changelog
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.5.3
_2026-09-24_
A small patch. Discord changed the way it installs itself and slipped out of the list, so autoscroll never reached it. It is back.
## v1.5.2
_2026-09-24_
A small patch. The menu now says "1 minute ago" instead of "1 minutes ago".
## v1.5.1
_2026-09-15_
A small patch. Autoscroll now works in Brave, which has its own name for it. AppImages are recognised too: an Electron AppImage gets autoscroll like any other app, and one that cannot autoscroll at all, like Modrinth, is left out of the list.
## v1.5.0
_2026-09-08_
Welcome to middleclick-autoscroll `v1.5.0`! A middle click now scrolls everywhere instead of pasting text.
<p align="center">
<img width="620" alt="The middleclick-autoscroll menu in Konsole, with middle-click paste off" src="https://raw.githubusercontent.com/LoonixTools/middleclick-autoscroll/22bd4f2a3dd89c4cb90fb497c69762b5df7dc6e7/res/screenshots/menu.png">
</p>
### Highlights
- Middle-click paste is off
## v1.4.0
_2026-09-06_
Welcome to middleclick-autoscroll `v1.4.0`! This one is all about Steam.
### Highlights
- Steam stops reinstalling itself
- Scripts that start Steam
## v1.3.0
_2026-09-04_
Welcome to middleclick-autoscroll `v1.3.0`! Browsers look clean again, and Helium joins the list.
### Highlights
- No more warning bar in browsers
- Autoscroll in Helium
## v1.2.0
_2026-09-01_
Welcome to middleclick-autoscroll `v1.2.0`! Steam keeps its autoscroll now, however you start it.
### Highlights
- Steam keeps autoscroll
- More ways to start Steam
## v1.1.0
_2026-08-24_
Welcome to middleclick-autoscroll `v1.1.0`! It now works far beyond Arch, with packages that keep themselves up to date.
### Highlights
- Debian, Ubuntu, Fedora and openSUSE
- Packages that update with your system
- Steam fixes
+66
View File
@@ -0,0 +1,66 @@
# CLAUDE.md
## Style
- Keep everything short: replies, explanations, comments, docs.
- Use simple English: short sentences, common words.
- Avoid em dashes (—). Do not just swap them for "-" either. Rewrite the sentence instead, for
example with a comma, a colon, brackets or two sentences.
## Commits
- English only.
- Conventional Commits prefix: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `ci:`, `build:`.
- Subject as short as possible. Imperative, lowercase, no trailing period.
- 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.
## Releases
Releases look like the ones of big projects such as Immich. `.github/release-notes.sh <tag>` builds
the notes: the entry from `CHANGELOG.md` (welcome and highlights), a support section, every commit
since the last release sorted by its prefix (`feat`, `fix`, `docs`, ...) with author and link, and
the full changelog link. So commit subjects end up in public: keep them clear.
1. Read `git log <last tag>..HEAD` and pick the version: only fixes → patch, something new → minor,
something that breaks or needs the user to act → major.
2. Bump the version and add the entry at the top of `CHANGELOG.md`, in one commit (`chore: 1.4.0`).
3. Push, then push the tag: `git tag v1.4.0 && git push origin main v1.4.0`. The `release` workflow
builds the packages and creates the release. It stops before building when the entry is missing.
4. PKGBUILDS picks up the new release for the AUR on its own.
A minor or major release (has `### Highlights`, gets a heading and the support section):
```markdown
## v1.4.0
_2026-09-24_
Welcome to middleclick-autoscroll `v1.4.0`! One or two sentences on what this release is about.
<p align="center">
<img width="480" alt="What the picture shows" src="https://raw.githubusercontent.com/LoonixTools/middleclick-autoscroll/v1.4.0/<path>">
</p>
### 🚨 Breaking changes
- Only if there are any: what changed, and what the user has to do.
### Highlights
- First highlight, a few words
- Second highlight
```
A picture under the welcome is optional. To show the menu or another screen of the program, use a
real screenshot of it running in Konsole, in English. Never a text copy of the screen. The same goes
for the README. The highlights stay a plain list: no heading or text per highlight, the list of
commits explains the rest.
A patch release is just a sentence or two, for example: "A small patch. The menu no longer closes
when you press Enter." The list of commits follows on its own.
- Write for users: what they notice, not how the code does it. Friendly and simple.
- 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>)"`.
+15 -11
View File
@@ -1,4 +1,4 @@
# middleclick-autoscroll - build and install # middleclick-autoscroll: build and install
# #
# Everything here is plain shell; "building" only means compiling the gettext # Everything here is plain shell; "building" only means compiling the gettext
# catalogs and rendering the man page. Both targets degrade to a no-op when # catalogs and rendering the man page. Both targets degrade to a no-op when
@@ -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.2.0 VERSION ?= 1.5.3
PREFIX ?= /usr PREFIX ?= /usr
DESTDIR ?= DESTDIR ?=
@@ -19,17 +19,21 @@ LOCALEDIR ?= $(DATADIR)/locale
MANDIR ?= $(DATADIR)/man MANDIR ?= $(DATADIR)/man
# Where systemd looks for user units. For a normal install into /usr this is # Where systemd looks for user units. For a normal install into /usr this is
# asked of systemd itself, because the answer is not the same everywhere - a # asked of systemd itself, because the answer is not the same everywhere. A
# distribution that still keeps /lib separate from /usr/lib says so here - and # distribution that still keeps /lib separate from /usr/lib says so here. It is
# only guessed at when there is no systemd installed to ask. # only guessed at when there is no systemd installed to ask.
# #
# A build with a prefix of its own keeps the units under that prefix instead. # A build with a prefix of its own keeps the units under that prefix instead,
# systemd searches $(PREFIX)/lib/systemd/user as well, and a file outside the # and under share rather than lib. systemd searches both for a system prefix,
# prefix it was asked for is not this build's to place. # but only share for a home one: $XDG_DATA_HOME/systemd/user is a search path
# and ~/.local/lib/systemd/user is not, so an install into ~/.local that put
# the units in lib would leave the watcher impossible to enable. That prefix is
# the one an atomic distribution leaves a user: there is no writing to /usr on
# Bazzite or Silverblue without layering a package and rebooting.
ifeq ($(PREFIX),/usr) ifeq ($(PREFIX),/usr)
USERUNITDIR ?= $(shell pkg-config --variable=systemduserunitdir systemd 2>/dev/null || echo /usr/lib/systemd/user) USERUNITDIR ?= $(shell pkg-config --variable=systemduserunitdir systemd 2>/dev/null || echo /usr/lib/systemd/user)
else else
USERUNITDIR ?= $(PREFIX)/lib/systemd/user USERUNITDIR ?= $(DATADIR)/systemd/user
endif endif
LINGUAS := de LINGUAS := de
@@ -57,14 +61,14 @@ po/%.mo: po/%.po
ifdef MSGFMT ifdef MSGFMT
$(MSGFMT) --check --output-file=$@ $< $(MSGFMT) --check --output-file=$@ $<
else else
@echo "msgfmt not found - skipping $@" @echo "msgfmt not found, skipping $@"
endif endif
$(MANPAGE): doc/middleclick-autoscroll.1.scd $(MANPAGE): doc/middleclick-autoscroll.1.scd
ifdef SCDOC ifdef SCDOC
$(SCDOC) < $< > $@ $(SCDOC) < $< > $@
else else
@echo "scdoc not found - skipping $@" @echo "scdoc not found, skipping $@"
endif endif
# Syntax-check every shell file, and run shellcheck when it is available. # Syntax-check every shell file, and run shellcheck when it is available.
@@ -76,7 +80,7 @@ check:
shellcheck -x -e SC1090,SC1091 src/middleclick-autoscroll $(LIBS); \ shellcheck -x -e SC1090,SC1091 src/middleclick-autoscroll $(LIBS); \
echo "ok shellcheck"; \ echo "ok shellcheck"; \
else \ else \
echo "shellcheck not found - skipped"; \ echo "shellcheck not found, skipped"; \
fi fi
@if command -v desktop-file-validate >/dev/null 2>&1; then \ @if command -v desktop-file-validate >/dev/null 2>&1; then \
echo "ok desktop-file-validate (nothing to check)"; \ echo "ok desktop-file-validate (nothing to check)"; \
+107 -69
View File
@@ -1,118 +1,156 @@
# middleclick-autoscroll <p align="center">
<img width="200" src="res/middleclick-autoscroll.svg" alt="middleclick-autoscroll">
</p>
Middle-click autoscroll for Linux. Hold the middle mouse button, move the <h1 align="center">middleclick-autoscroll</h1>
mouse, the page scrolls — like on Windows. Works with browsers, Electron apps,
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 <h3 align="center">Middle-click autoscroll in every app that supports it.</h3>
out what's Chromium-based, and handles it.
## Installing <p align="center">
Browsers, Electron apps like Discord and Spotify, Steam and anything else built on Chromium.
</p>
**Arch, CachyOS, EndeavourOS, Manjaro** <h5 align="center">
<a href="#install">Install</a> |
<a href="#how-to-use">How to use</a> |
<a href="https://github.com/LoonixTools/middleclick-autoscroll/issues">Report a bug</a>
</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>
</p>
## Install
<details>
<summary><b>Arch</b></summary>
```bash ```bash
paru -S middleclick-autoscroll yay -S middleclick-autoscroll
``` ```
**Debian, Ubuntu, Linux Mint, Pop!_OS** </details>
<details>
<summary><b>Fedora</b></summary>
```bash
sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \
https://loonixtools.github.io/middleclick-autoscroll/middleclick-autoscroll.repo
sudo dnf install middleclick-autoscroll
```
</details>
<details>
<summary><b>Bazzite</b></summary>
```bash
sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \
https://loonixtools.github.io/middleclick-autoscroll/middleclick-autoscroll.repo
sudo rpm-ostree install middleclick-autoscroll
systemctl reboot
```
</details>
<details>
<summary><b>Debian</b>, Ubuntu</summary>
```bash ```bash
sudo install -d -m 0755 /etc/apt/keyrings sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://felitendo.github.io/middleclick-autoscroll/KEY.gpg \ curl -fsSL https://loonixtools.github.io/middleclick-autoscroll/KEY.gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/middleclick-autoscroll.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/middleclick-autoscroll.gpg
echo "deb [signed-by=/etc/apt/keyrings/middleclick-autoscroll.gpg] https://felitendo.github.io/middleclick-autoscroll/deb ./" \ echo "deb [signed-by=/etc/apt/keyrings/middleclick-autoscroll.gpg] https://loonixtools.github.io/middleclick-autoscroll/deb ./" \
| sudo tee /etc/apt/sources.list.d/middleclick-autoscroll.list | sudo tee /etc/apt/sources.list.d/middleclick-autoscroll.list
sudo apt update && sudo apt install middleclick-autoscroll sudo apt update && sudo apt install middleclick-autoscroll
``` ```
**Fedora, RHEL, CentOS Stream** </details>
<details>
<summary><b>openSUSE</b></summary>
```bash ```bash
sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \ sudo rpm --import https://loonixtools.github.io/middleclick-autoscroll/KEY.gpg
https://felitendo.github.io/middleclick-autoscroll/middleclick-autoscroll.repo
sudo dnf install middleclick-autoscroll
```
**openSUSE**
```bash
sudo rpm --import https://felitendo.github.io/middleclick-autoscroll/KEY.gpg
sudo zypper addrepo --gpgcheck --refresh \ sudo zypper addrepo --gpgcheck --refresh \
https://felitendo.github.io/middleclick-autoscroll/rpm middleclick-autoscroll https://loonixtools.github.io/middleclick-autoscroll/rpm middleclick-autoscroll
sudo zypper install middleclick-autoscroll sudo zypper install middleclick-autoscroll
``` ```
Then run `middleclick-autoscroll enable`. That's it — nothing else to configure. </details>
Updates come through your package manager like anything else. Without a package, ## How to use
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 ```bash
everything. middleclick-autoscroll
```
## How it works <p align="center">
<img src="res/screenshots/menu.png" alt="The middleclick-autoscroll menu in Konsole: autoscroll on, 19 of 19 applications covered" width="620">
</p>
Blink (the engine in Chromium, Electron, and CEF) already has autoscroll, but Press **1**. Autoscroll _✨just works✨_, like on Windows.
it's off on Linux because middle click does primary-selection paste there. One
flag turns it on: <details>
<summary>Applications</summary>
**3** lists every app it found. Space turns one on or off.
<p align="center">
<img src="res/screenshots/applications.png" alt="The applications list in Konsole, each with how it gets autoscroll" width="620">
</p>
</details>
## More
<details>
<summary>How it works</summary>
Chromium, Electron and CEF all have autoscroll built in, but it is off on Linux. One 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 Setting that for every app by hand is a pain and breaks with updates, so this tool does it for you.
flag files, desktop entries, Flatpaks, snaps, autostart entries, Steam — so it New apps are picked up on their own. AppImages are covered too, without unpacking or starting them.
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 Some browsers want a different name. Brave and Helium ship the feature under their own, so browsers
relevant directories. Without systemd, `middleclick-autoscroll apply` does the are handed all of the names at once. A name a browser does not know is ignored.
same thing manually.
## Steam Steam is its own case. It builds the command line for its interface itself, and repairs every file
that looks changed. So the flag goes into the script that starts the web helper, written to keep the
file at the size and date Steam recorded. Steam finds nothing to repair, and autoscroll works in the
store and the library. After a Steam update the patch is put back on its own.
Steam's web UI supports autoscroll but has no way to pass extra arguments to its </details>
helper, so the program patches the script that starts it. Steam checks its own
files at every start and repairs whatever looks changed, so the patch is written
to look unchanged: the bytes the argument costs come back out of the script's
comments and the timestamp is restored, leaving a file exactly as long and as old
as Steam left it. It survives however you start Steam — the menu, a desktop
shortcut, a game launcher, a terminal.
As a fallback, for the case where that isn't possible, `-noverifyfiles` goes on <details>
everything that starts Steam: its launcher, its autostart entry, and the <summary>Commands</summary>
shortcuts it writes for single games. That one means Steam won't auto-repair
damaged files on its own — you can turn it off separately in the settings.
## Commands | | |
| Command | |
|---|---| |---|---|
| `middleclick-autoscroll` | Interactive menu | | `middleclick-autoscroll` | The menu |
| `… enable` | Turn on, apply, start watching | | `… enable` | Turn on |
| `… disable` | Undo everything | | `… disable` | Undo everything |
| `… apply` | Apply to new apps | | `… apply` | Pick up new apps |
| `… apply --rebuild` | Redo from scratch | | `… status` | What is covered |
| `… status` | What's covered | | `… list` | All apps and how they are handled |
| `… list` | All apps and how they're handled |
See `man middleclick-autoscroll` for more. </details>
## Building from source <details>
<summary>Build from source</summary>
```bash ```bash
make make
sudo make install sudo make install
``` ```
Optionally needs `msgfmt` (gettext) for translations and `scdoc` for the man Optional: `msgfmt` for translations, `scdoc` for the man page.
page. Supports `PREFIX` and `DESTDIR`. `make check` runs syntax checks and
shellcheck.
See [packaging/README.md](packaging/README.md) for release builds and repo </details>
signing.
## License
GPL-3.0-or-later. GPL-3.0-or-later.
+115 -36
View File
@@ -18,7 +18,19 @@ 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, because 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. Two
browsers know the feature under a name of their own and ignore Chromium's:
Helium, which is asked for *HeliumMiddleClickAutoscroll* as well, and Brave,
which is asked for *MiddelButtonClickAutoscroll* (spelled the way Brave spells
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 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.
@@ -43,7 +55,7 @@ is refused.
watcher calls; running it by hand is only needed when the watcher is off. watcher calls; running it by hand is only needed when the watcher is off.
Entries left behind by an application that has since been uninstalled are Entries left behind by an application that has since been uninstalled are
removed here too - the entry shadowing it lives in the user's home, where removed here too. The entry shadowing it lives in the user's home, where
the package manager that removed the application cannot see it. the package manager that removed the application cannot see it.
With *--rebuild* everything is taken back first and written again from With *--rebuild* everything is taken back first and written again from
@@ -71,10 +83,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
@@ -85,8 +97,8 @@ Two ways, chosen per application.
For applications that ship their own binary with no wrapper, and for For applications that ship their own binary with no wrapper, and for
everything inside a Flatpak or a snap, a copy of the desktop entry with the everything inside a Flatpak or a snap, a copy of the desktop entry with the
argument appended is written to _~/.local/share/applications_, where it argument appended is written to _~/.local/share/applications_, where it
shadows the system one. Entries that already live there - AppImages, web shadows the system one. Entries that already live there (AppImages, web
app shortcuts - are edited in place, with the original kept. app shortcuts) are edited in place, with the original kept.
This is the route everything takes on the distributions whose Chromium This is the route everything takes on the distributions whose Chromium
wrappers keep their equivalent file under _/etc_, where it is the system's wrappers keep their equivalent file under _/etc_, where it is the system's
@@ -104,7 +116,7 @@ entry entirely. Those are patched in place as well.
Shortcuts on the desktop itself are patched in place too. Nothing in the XDG 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 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 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 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 from _~/.config/user-dirs.dirs_ rather than guessed; the watcher is told about
it in a drop-in written when autoscroll is turned on. it in a drop-in written when autoscroll is turned on.
@@ -122,32 +134,31 @@ _~/.local/share/Steam_ for Valve's own package and Arch's,
_~/.steam/debian-installation_ for Debian's, and the private tree of the _~/.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 and timestamp rather than by content - and restores whatever differs. So size and timestamp, not by content) and restores whatever differs. So
the patch is written to look untouched: the bytes the argument costs are taken the patch is written to look untouched: the bytes the argument costs are taken
back out of the script's own comments and the timestamp is put back afterwards, back out of the script's own comments and the timestamp is put back afterwards,
leaving a file exactly as long and exactly as old as Steam left it. A client leaving a file exactly as long and exactly as old as Steam left it. A client
that checks its files finds nothing to repair, and the argument survives that checks its files finds nothing to repair, and the argument survives
however Steam was started - from the menu, from a game shortcut, from a however Steam was started: from the menu, from a game shortcut, from a
launcher like Heroic or Lutris, from a terminal. launcher like Heroic or Lutris, or from a terminal.
That is what has to work, because there is no way to make every possible way of That is what has to work, because it is the only thing that does. A patch that
starting Steam carry an argument. *-noverifyfiles* is the second line rather changes the length is never written, whatever else could be done to cover for
than the first: it covers the case where the script has no comments left to pay it: Steam checks its files at the shutdown it runs itself as well as at a start
for the argument and the patch has to grow the file. It goes on Steam's it was given arguments for, and that one carries no arguments of anybody's. One
launcher entry, on its entry in _~/.config/autostart_, which Steam writes as wrong length costs the whole client package downloaded, extracted and installed
soon as it is set to run at login, and on the shortcuts Steam writes for single again, and a client that quits at the end of it instead of coming up.
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.
A patch that does change the size is still held back while the client is So where the comments cannot pay for the argument, or a client update has
running: Steam puts its own copy back, the two would only undo each other, and changed how the helper is started, nothing is written and the status screen
the helper is started once, at the start, so patching again would not help that says Steam is not patched. Nothing goes on Steam's launcher entry, its
session anyway. It goes in at the next apply with Steam closed. A patch that autostart entry or the shortcuts it writes for single games either, and Steam
keeps the size has nothing to wait for and goes in either way. 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
if it cannot be made to.
A client update brings a new version of the script. The watcher notices and 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 patches it again, and the copy kept for undoing is replaced with the new
@@ -162,11 +173,66 @@ and has a configuration file with a slot for extra arguments; that slot is
where the flag goes. Installed as a plain package, a Flatpak or a snap, it is where the flag goes. Installed as a plain package, a Flatpak or a snap, it is
an ordinary desktop entry and needs nothing special. an ordinary desktop entry and needs nothing special.
# WHAT CANNOT BE DETECTED # MIDDLE-CLICK PASTE
An AppImage keeps its payload in a compressed filesystem, so there is no way to Inside a Chromium application the argument settles this on its own: with
tell from the outside whether it contains Chromium. Those are listed as *cannot autoscroll on, Blink stops pasting the primary selection on middle click,
tell* and left alone until they are switched on from the applications screen. because the button is doing something else now. Everywhere else on the desktop
middle click goes on pasting, so the desktop's own switch for it is turned off
too.
KDE has such a switch, and only for a Wayland session, where the paste is a
protocol the compositor either offers or does not:
~/.config/kwinrc, [Wayland], EnablePrimarySelection=false
It is written with *kwriteconfig6*, the same tool System Settings uses, because
kwinrc has a cascade behind it and entries a distribution can mark immutable;
an edit that reaches past all that writes something KDE goes on to ignore. KWin
reads the key once, while it starts, so the change is true from the next login
on rather than straight away.
What the key said before is recorded, and *disable* puts that back. A key that
already said *false* before any of this is left alone and not recorded either,
so a paste the user switched off themselves is never switched back on.
On X11 there is nothing to switch. The primary selection is part of X itself
and every toolkit reaches for it on its own; no one place can say no. The
status screen leaves the row out where there is nothing to report.
Turn it off under *Settings* to keep the desktop's middle-click paste.
# APPIMAGES
An AppImage is the AppImage runtime, an ordinary executable, with a squashfs
image appended to it. The payload is compressed, so none of the files that
identify Chromium is on disk where it could be found.
The names are, though. squashfs keeps the name of everything it holds in one
table of its own, a few kilobytes of it, and a name is all this question needs -
so that table is read and decompressed in place. Nothing is unpacked, and the
image is never run: this can happen from the watcher, and starting a program
because it was installed is not something a scan gets to do.
Two kinds of image are still out of reach. One is packed with *lzo* or *lz4*,
which squashfs stores as bare blocks that neither tool will read without the
framing their own file formats add. The other is the original AppImage layout,
which is an ISO9660 filesystem rather than a squashfs one. Both are listed as
*cannot tell* and left alone until they are switched on from the applications
screen.
# WHAT HAS NO FLAG AT ALL
Autoscroll is a Blink feature, so only an application drawn by Blink can be
given it. An application built on WebKitGTK is not one of those. That
includes Tauri on Linux, GNOME Web, and anything else linked against
libwebkit2gtk. WebKit has no equivalent feature to ask for, on any command line
or in any configuration file.
Such an application is identified as what it is and then left out of the list
entirely, rather than being offered as something that could be switched on. An
application that is absent from *list* is absent because there is nothing this
tool could do for it.
# FILES # FILES
@@ -180,9 +246,17 @@ _~/.local/state/middleclick-autoscroll/ledger_
_~/.local/state/middleclick-autoscroll/backup/_ _~/.local/state/middleclick-autoscroll/backup/_
Copies of the files that are edited in place rather than shadowed. Copies of the files that are edited in place rather than shadowed.
_~/.config/kwinrc_
KDE's own configuration. One key in it, *EnablePrimarySelection*, is
written when the desktop's middle-click paste is turned off, and put
back by *disable*.
_~/.cache/middleclick-autoscroll/detect_ _~/.cache/middleclick-autoscroll/detect_
Which programs were found to be Chromium, keyed by size and modification Which programs were found to be Chromium, keyed by size and modification
time. Safe to delete. 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.
Safe to delete.
# ENVIRONMENT # ENVIRONMENT
@@ -195,11 +269,16 @@ Bash 4.2 or newer and GNU coreutils. The watcher needs a systemd user session;
without one everything else works and applications installed later are picked without one everything else works and applications installed later are picked
up at the next *apply* rather than on their own. up at the next *apply* rather than on their own.
*gzip* is what reads the name table of an AppImage, and *xz* or *zstd* the ones
packed with those instead. None of the three is required: an image whose
compressor has no tool installed is listed as *cannot tell*, exactly like the
two formats there is no reader for at all.
# SEE ALSO # SEE ALSO
*systemctl*(1), *flatpak*(1), *snap*(8) *systemctl*(1), *flatpak*(1), *snap*(8), *kwriteconfig6*(1)
# AUTHORS # AUTHORS
Felitendo. Source and issue tracker at Felitendo. Source and issue tracker at
https://github.com/Felitendo/middleclick-autoscroll https://github.com/LoonixTools/middleclick-autoscroll
+12 -10
View File
@@ -1,7 +1,7 @@
# Packaging and releases # Packaging and releases
The Makefile installs everything; these only wrap what it produced. That is The Makefile installs everything; these only wrap what it produced. That is
deliberate — a packaging script that lists the files again is a second on purpose: a packaging script that lists the files again is a second
description of the layout, and two descriptions drift. description of the layout, and two descriptions drift.
| | | | | |
@@ -29,12 +29,14 @@ argument.
## Making a release ## Making a release
1. Bump `VERSION` in the Makefile. 1. Bump `VERSION` in the Makefile.
2. Commit, then `git tag vX.Y.Z && git push --tags`. 2. Add the release to `CHANGELOG.md`, in the format CLAUDE.md describes.
3. Commit, then `git tag vX.Y.Z && git push --tags`.
The `release` workflow builds both packages in a Debian and a Fedora container, The `release` workflow builds both packages in a Debian and a Fedora container,
refuses the tag if it disagrees with the Makefile, attaches the packages to a refuses the tag if it disagrees with the Makefile or has no changelog entry,
GitHub release, and adds them to the APT and RPM repositories on the `gh-pages` attaches the packages to a GitHub release with the entry as its notes, and adds
branch. Nothing else has to be done by hand. them to the APT and RPM repositories on the `gh-pages` branch. Nothing else has
to be done by hand.
## Trying the release path first ## Trying the release path first
@@ -44,14 +46,14 @@ gh workflow run release.yml -f dry_run=true
Builds both packages, builds both repositories with a key generated on the Builds both packages, builds both repositories with a key generated on the
spot, checks the three signatures it wrote, and then installs the packages back spot, checks the three signatures it wrote, and then installs the packages back
out of the repositories — apt on the runner, dnf in a Fedora container. Nothing out of the repositories (apt on the runner, dnf in a Fedora container). Nothing
is pushed and no release is made. This is worth running after any change to the is pushed and no release is made. This is worth running after any change to the
packaging, because the alternative is finding out from a tag. packaging, because the alternative is finding out from a tag.
## Setting up the signing, once ## Setting up the signing, once
The repositories are signed, so this needs a key. Make one that exists for The repositories are signed, so this needs a key. Make one that exists for
nothing else — not a personal key — and give it no passphrase: it lives as an nothing else (not a personal key) and give it no passphrase. It lives as an
encrypted repository secret, and `rpmsign` cannot be handed a passphrase encrypted repository secret, and `rpmsign` cannot be handed a passphrase
unattended. unattended.
@@ -66,7 +68,7 @@ gpg --armor --export-secret-keys 'middleclick-autoscroll repository' \
Without the secret the workflow still builds both packages and attaches them to Without the secret the workflow still builds both packages and attaches them to
the release; it says so in the log and leaves the repositories alone. the release; it says so in the log and leaves the repositories alone.
## Pointing Pages at it, once — and in this order ## Pointing Pages at it, once, in this order
The `gh-pages` branch does not exist until a release has put something on it, The `gh-pages` branch does not exist until a release has put something on it,
and a branch that does not exist cannot be picked in the Pages settings. So: and a branch that does not exist cannot be picked in the Pages settings. So:
@@ -77,8 +79,8 @@ and a branch that does not exist cannot be picked in the Pages settings. So:
root. root.
Doing it the other way round is a wall, and leaving Pages pointed at `main` Doing it the other way round is a wall, and leaving Pages pointed at `main`
serves the source tree at the address the install instructions name — the key serves the source tree at the address the install instructions name. Then the
and the indexes are 404 and nothing installs. key and the indexes are 404 and nothing installs.
## What users end up with ## What users end up with
+1 -1
View File
@@ -5,7 +5,7 @@
# #
# Everything the package contains comes out of `make install`. This only wraps # Everything the package contains comes out of `make install`. This only wraps
# what that produced, so there is exactly one description of where a file goes # what that produced, so there is exactly one description of where a file goes
# and it is the Makefile - a packaging script that lists the files again is a # and it is the Makefile. A packaging script that lists the files again is a
# second description, and the two drift. # second description, and the two drift.
# #
# Needs: make, dpkg-deb, msgfmt (gettext), scdoc. # Needs: make, dpkg-deb, msgfmt (gettext), scdoc.
+3 -3
View File
@@ -3,12 +3,12 @@
# Refuses a release whose tag and Makefile disagree. # Refuses a release whose tag and Makefile disagree.
# #
# The version is baked into the program at install time from the Makefile, and # The version is baked into the program at install time from the Makefile, and
# the packages take theirs from the same place - but the tag is what people see # the packages take theirs from the same place. But the tag is what people see
# and what the release is named after. A tag that says something else produces # and what the release is named after. A tag that says something else produces
# a package called 1.0.4 containing a program that reports 1.0.3, and nothing # a package called 1.0.4 containing a program that reports 1.0.3, and nothing
# would have complained. # would have complained.
# #
# Anything that is not a v-tag - a run started by hand from a branch - is not a # Anything that is not a v-tag (a run started by hand from a branch) is not a
# release and has nothing to check. # release and has nothing to check.
set -euo pipefail set -euo pipefail
@@ -19,7 +19,7 @@ ref="${1:-}"
case "$ref" in case "$ref" in
v[0-9]*) ;; v[0-9]*) ;;
*) *)
echo "not a release tag (${ref:-none}) - nothing to check against" echo "not a release tag (${ref:-none}), nothing to check against"
exit 0 exit 0
;; ;;
esac esac
+2 -2
View File
@@ -6,9 +6,9 @@ Architecture: all
Maintainer: Felitendo <felitendoyt@gmail.com> Maintainer: Felitendo <felitendoyt@gmail.com>
Depends: bash (>= 4.2), coreutils, findutils, grep, sed, mawk | gawk | original-awk Depends: bash (>= 4.2), coreutils, findutils, grep, sed, mawk | gawk | original-awk
Recommends: gettext-base, systemd, desktop-file-utils Recommends: gettext-base, systemd, desktop-file-utils
Homepage: https://github.com/Felitendo/middleclick-autoscroll Homepage: https://github.com/LoonixTools/middleclick-autoscroll
Description: middle-click autoscroll for Chromium-based applications Description: middle-click autoscroll for Chromium-based applications
Blink - the engine inside Chromium, Electron and CEF - has had Windows-style Blink (the engine inside Chromium, Electron and CEF) has had Windows-style
autoscroll for years: hold the middle mouse button, move the pointer, the page autoscroll for years: hold the middle mouse button, move the pointer, the page
scrolls. On Linux it is switched off, because middle click is already taken by scrolls. On Linux it is switched off, because middle click is already taken by
primary-selection paste. primary-selection paste.
+1 -1
View File
@@ -1,6 +1,6 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: middleclick-autoscroll Upstream-Name: middleclick-autoscroll
Source: https://github.com/Felitendo/middleclick-autoscroll Source: https://github.com/LoonixTools/middleclick-autoscroll
Files: * Files: *
Copyright: 2026 Felitendo Copyright: 2026 Felitendo
+5 -5
View File
@@ -64,15 +64,15 @@
<h1>middleclick-autoscroll</h1> <h1>middleclick-autoscroll</h1>
<p class="lead"> <p class="lead">
Middle-click autoscroll — hold the middle mouse button, move the pointer, the Middle-click autoscroll in every Chromium-based application on the system,
page scrolls — in every Chromium-based application on the system, and in and in anything installed later: hold the middle mouse button, move the
anything installed later. pointer, and the page scrolls.
</p> </p>
<p> <p>
This page is the package repository. Set it up once and every new version This page is the package repository. Set it up once and every new version
arrives with the rest of your system updates. The arrives with the rest of your system updates. The
<a href="https://github.com/Felitendo/middleclick-autoscroll">source and the <a href="https://github.com/LoonixTools/middleclick-autoscroll">source and the
documentation</a> are on GitHub. documentation</a> are on GitHub.
</p> </p>
@@ -103,7 +103,7 @@ sudo zypper install middleclick-autoscroll</code></pre>
<p> <p>
That is the whole setup. Nothing else has to be configured and no file has to That is the whole setup. Nothing else has to be configured and no file has to
be edited. Run <code>middleclick-autoscroll disable</code> before removing the be edited. Run <code>middleclick-autoscroll disable</code> before removing the
package — it puts back everything that was changed. package. It puts back everything that was changed.
</p> </p>
<footer> <footer>
+2 -2
View File
@@ -11,7 +11,7 @@
# Usage: publish-repos.sh <gh-pages checkout> <directory of new packages> # Usage: publish-repos.sh <gh-pages checkout> <directory of new packages>
# #
# Needs: dpkg-dev, apt-utils, createrepo-c, gpg, and a secret key already # Needs: dpkg-dev, apt-utils, createrepo-c, gpg, and a secret key already
# imported - its id is taken from the keyring. # imported. Its id is taken from the keyring.
set -euo pipefail set -euo pipefail
@@ -19,7 +19,7 @@ pages="$(cd -- "$1" && pwd)"
incoming="$(cd -- "$2" && pwd)" incoming="$(cd -- "$2" && pwd)"
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
base_url="${MCA_REPO_URL:-https://felitendo.github.io/middleclick-autoscroll}" base_url="${MCA_REPO_URL:-https://loonixtools.github.io/middleclick-autoscroll}"
keyid="$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/ { print $5; exit }')" keyid="$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/ { print $5; exit }')"
[[ -n $keyid ]] || { echo "$0: no secret key in the keyring" >&2; exit 1; } [[ -n $keyid ]] || { echo "$0: no secret key in the keyring" >&2; exit 1; }
+2 -2
View File
@@ -9,7 +9,7 @@ Release: 1%{?dist}
Summary: Middle-click autoscroll for Chromium-based applications Summary: Middle-click autoscroll for Chromium-based applications
License: GPL-3.0-or-later License: GPL-3.0-or-later
URL: https://github.com/Felitendo/middleclick-autoscroll URL: https://github.com/LoonixTools/middleclick-autoscroll
Source0: %{name}-%{version}.tar.gz Source0: %{name}-%{version}.tar.gz
BuildArch: noarch BuildArch: noarch
@@ -33,7 +33,7 @@ Recommends: systemd
Recommends: desktop-file-utils Recommends: desktop-file-utils
%description %description
Blink - the engine inside Chromium, Electron and CEF - has had Windows-style Blink (the engine inside Chromium, Electron and CEF) has had Windows-style
autoscroll for years: hold the middle mouse button, move the pointer, the page autoscroll for years: hold the middle mouse button, move the pointer, the page
scrolls. On Linux it is switched off, because middle click is already taken by scrolls. On Linux it is switched off, because middle click is already taken by
primary-selection paste. primary-selection paste.
+53 -30
View File
@@ -5,7 +5,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: middleclick-autoscroll 1.0.3\n" "Project-Id-Version: middleclick-autoscroll 1.0.3\n"
"Report-Msgid-Bugs-To: https://github.com/Felitendo/middleclick-autoscroll/issues\n" "Report-Msgid-Bugs-To: https://github.com/LoonixTools/middleclick-autoscroll/"
"issues\n"
"POT-Creation-Date: 2026-08-16 00:00+0200\n" "POT-Creation-Date: 2026-08-16 00:00+0200\n"
"PO-Revision-Date: 2026-08-16 00:00+0200\n" "PO-Revision-Date: 2026-08-16 00:00+0200\n"
"Last-Translator: Felitendo\n" "Last-Translator: Felitendo\n"
@@ -43,10 +44,6 @@ msgstr "nicht erkennbar"
msgid "not patched yet" msgid "not patched yet"
msgstr "noch nicht gepatcht" msgstr "noch nicht gepatcht"
msgid "waiting until Steam is closed"
msgstr "wartet, bis Steam geschlossen ist"
#. Status block #. Status block
msgid "Autoscroll" msgid "Autoscroll"
msgstr "Autoscroll" msgstr "Autoscroll"
@@ -60,12 +57,15 @@ msgstr "%d von %d"
msgid "Not identified" msgid "Not identified"
msgstr "Nicht erkannt" msgstr "Nicht erkannt"
msgid "%d - see the applications list" msgid "%d (see the applications list)"
msgstr "%d – siehe Anwendungsliste" msgstr "%d (siehe Anwendungsliste)"
msgid "Steam" msgid "Steam"
msgstr "Steam" msgstr "Steam"
msgid "Middle-click paste"
msgstr "Mittelklick-Einfügen"
msgid "New applications" msgid "New applications"
msgstr "Neue Anwendungen" msgstr "Neue Anwendungen"
@@ -75,7 +75,6 @@ msgstr "Zuletzt angewendet"
msgid "Applications pick this up the next time they are started." msgid "Applications pick this up the next time they are started."
msgstr "Anwendungen übernehmen das beim nächsten Start." msgstr "Anwendungen übernehmen das beim nächsten Start."
#. Menu #. Menu
msgid "Turn autoscroll on or off" msgid "Turn autoscroll on or off"
msgstr "Autoscroll ein- oder ausschalten" msgstr "Autoscroll ein- oder ausschalten"
@@ -95,7 +94,6 @@ msgstr "Beenden"
msgid "Press any key to continue..." msgid "Press any key to continue..."
msgstr "Beliebige Taste drücken …" msgstr "Beliebige Taste drücken …"
#. Settings #. Settings
msgid "Electron and CEF applications" msgid "Electron and CEF applications"
msgstr "Electron- und CEF-Anwendungen" msgstr "Electron- und CEF-Anwendungen"
@@ -115,14 +113,17 @@ msgstr "Programme, die sich beim Anmelden selbst starten"
msgid "Spotify" msgid "Spotify"
msgstr "Spotify" msgstr "Spotify"
msgid "Turn off KDE's middle-click paste"
msgstr "KDEs Mittelklick-Einfügen abschalten"
msgid "Apply to newly installed applications" msgid "Apply to newly installed applications"
msgstr "Auf neu installierte Anwendungen anwenden" msgstr "Auf neu installierte Anwendungen anwenden"
msgid "Additional Chromium arguments" msgid "Additional Chromium arguments"
msgstr "Zusätzliche Chromium-Argumente" msgstr "Zusätzliche Chromium-Argumente"
msgid "Up/Down select - Space or Right changes - q goes back" msgid "Up/Down: select, Space or Right: change, q: back"
msgstr "Hoch/Runter wählt – Leertaste oder Rechts ändert – q geht zurück" msgstr "Hoch/Runter: wählen, Leertaste oder Rechts: ändern, q: zurück"
msgid "Arguments separated by spaces, empty for none:" msgid "Arguments separated by spaces, empty for none:"
msgstr "Argumente mit Leerzeichen getrennt, leer für keine:" msgstr "Argumente mit Leerzeichen getrennt, leer für keine:"
@@ -130,7 +131,6 @@ msgstr "Argumente mit Leerzeichen getrennt, leer für keine:"
msgid "Could not save the setting." msgid "Could not save the setting."
msgstr "Einstellung konnte nicht gespeichert werden." msgstr "Einstellung konnte nicht gespeichert werden."
#. Applications screen #. Applications screen
msgid "Looking at the installed applications..." msgid "Looking at the installed applications..."
msgstr "Installierte Anwendungen werden durchgesehen …" msgstr "Installierte Anwendungen werden durchgesehen …"
@@ -139,30 +139,34 @@ msgid "No Chromium-based applications found."
msgstr "Keine Chromium-basierten Anwendungen gefunden." msgstr "Keine Chromium-basierten Anwendungen gefunden."
msgid "Anything not identified is left alone until it is turned on here." msgid "Anything not identified is left alone until it is turned on here."
msgstr "Nicht erkannte Anwendungen bleiben unangetastet, bis sie hier eingeschaltet werden." msgstr ""
"Nicht erkannte Anwendungen bleiben unangetastet, bis sie hier eingeschaltet "
"werden."
msgid "Autoscroll is off - this is what would be covered." msgid "Autoscroll is off. This is what would be covered."
msgstr "Autoscroll ist aus – das hier wäre abgedeckt." msgstr "Autoscroll ist aus. Das hier wäre abgedeckt."
msgid "Up/Down select - Space turns one on or off - q goes back"
msgstr "Hoch/Runter wählt – Leertaste schaltet einzeln ein oder aus – q geht zurück"
msgid "Up/Down: select, Space: turn on or off, q: back"
msgstr "Hoch/Runter: wählen, Leertaste: ein- oder ausschalten, q: zurück"
#. Results #. Results
msgid "Autoscroll is on for %d applications." msgid "Autoscroll is on for %d applications."
msgstr "Autoscroll ist für %d Anwendungen an." msgstr "Autoscroll ist für %d Anwendungen an."
msgid "Applications that are already running pick this up when they are restarted." msgid ""
"Applications that are already running pick this up when they are restarted."
msgstr "Bereits laufende Anwendungen übernehmen das nach einem Neustart." msgstr "Bereits laufende Anwendungen übernehmen das nach einem Neustart."
msgid "Steam's launcher now skips its file check, which is what keeps the change from being undone at every start."
msgstr "Steams Startereintrag überspringt jetzt die Dateiprüfung – nur so bleibt die Änderung bei jedem Start erhalten."
msgid "Could not start the watcher for newly installed applications." msgid "Could not start the watcher for newly installed applications."
msgstr "Die Überwachung neu installierter Anwendungen konnte nicht gestartet werden." msgstr ""
"Die Überwachung neu installierter Anwendungen konnte nicht gestartet werden."
msgid "No systemd user session - newly installed applications will not be picked up on their own." msgid ""
msgstr "Keine systemd-Benutzersitzung – neu installierte Anwendungen werden nicht von selbst erfasst." "No systemd user session, so newly installed applications will not be picked "
"up on their own."
msgstr ""
"Keine systemd-Benutzersitzung, daher werden neu installierte Anwendungen "
"nicht von selbst erfasst."
msgid "Autoscroll is off and everything that was changed has been put back." msgid "Autoscroll is off and everything that was changed has been put back."
msgstr "Autoscroll ist aus, alle Änderungen wurden zurückgenommen." msgstr "Autoscroll ist aus, alle Änderungen wurden zurückgenommen."
@@ -179,12 +183,23 @@ msgstr "Auf %d Anwendungen angewendet."
msgid "Already applied to %d applications." msgid "Already applied to %d applications."
msgstr "Bereits auf %d Anwendungen angewendet." msgstr "Bereits auf %d Anwendungen angewendet."
msgid "Steam starts its interface in a way this version does not recognise; leaving it alone." msgid ""
msgstr "Steam startet seine Oberfläche auf eine Art, die diese Version nicht kennt; Steam bleibt unangetastet." "Steam starts its interface in a way this version cannot change without Steam "
"noticing; leaving it alone."
msgstr ""
"Steam startet seine Oberfläche auf eine Art, die diese Version nicht ändern "
"kann, ohne dass Steam es merkt; Steam bleibt unangetastet."
msgid "Steam is running and has put its own file back; the change waits until Steam is closed." msgid ""
msgstr "Steam läuft und hat seine eigene Datei wiederhergestellt; die Änderung wird nachgeholt, sobald Steam geschlossen ist." "Steam's interface cannot be given the flag without leaving a file Steam "
"would reinstall; the change has been taken back."
msgstr ""
"Steams Oberfläche lässt sich das Flag nicht geben, ohne eine Datei zu "
"hinterlassen, die Steam neu installieren würde; die Änderung wurde "
"zurückgenommen."
msgid "Middle click stops pasting at the next login."
msgstr "Mittelklick fügt ab der nächsten Anmeldung nichts mehr ein."
#. Help #. Help
msgid "Usage: middleclick-autoscroll [command]" msgid "Usage: middleclick-autoscroll [command]"
@@ -223,7 +238,6 @@ msgstr "Ohne Befehl wird ein interaktives Menü angezeigt."
msgid "Unknown command: %s" msgid "Unknown command: %s"
msgstr "Unbekannter Befehl: %s" msgstr "Unbekannter Befehl: %s"
#. Time #. Time
msgid "never" msgid "never"
msgstr "nie" msgstr "nie"
@@ -231,11 +245,20 @@ msgstr "nie"
msgid "just now" msgid "just now"
msgstr "gerade eben" msgstr "gerade eben"
msgid "1 minute ago"
msgstr "vor 1 Minute"
msgid "%d minutes ago" msgid "%d minutes ago"
msgstr "vor %d Minuten" msgstr "vor %d Minuten"
msgid "1 hour ago"
msgstr "vor 1 Stunde"
msgid "%d hours ago" msgid "%d hours ago"
msgstr "vor %d Stunden" msgstr "vor %d Stunden"
msgid "1 day ago"
msgstr "vor 1 Tag"
msgid "%d days ago" msgid "%d days ago"
msgstr "vor %d Tagen" msgstr "vor %d Tagen"
+34 -21
View File
@@ -5,7 +5,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: middleclick-autoscroll 1.0.3\n" "Project-Id-Version: middleclick-autoscroll 1.0.3\n"
"Report-Msgid-Bugs-To: https://github.com/Felitendo/middleclick-autoscroll/issues\n" "Report-Msgid-Bugs-To: https://github.com/LoonixTools/middleclick-autoscroll/"
"issues\n"
"POT-Creation-Date: 2026-08-16 00:00+0200\n" "POT-Creation-Date: 2026-08-16 00:00+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
@@ -43,10 +44,6 @@ msgstr ""
msgid "not patched yet" msgid "not patched yet"
msgstr "" msgstr ""
msgid "waiting until Steam is closed"
msgstr ""
#. Status block #. Status block
msgid "Autoscroll" msgid "Autoscroll"
msgstr "" msgstr ""
@@ -60,12 +57,15 @@ msgstr ""
msgid "Not identified" msgid "Not identified"
msgstr "" msgstr ""
msgid "%d - see the applications list" msgid "%d (see the applications list)"
msgstr "" msgstr ""
msgid "Steam" msgid "Steam"
msgstr "" msgstr ""
msgid "Middle-click paste"
msgstr ""
msgid "New applications" msgid "New applications"
msgstr "" msgstr ""
@@ -75,7 +75,6 @@ msgstr ""
msgid "Applications pick this up the next time they are started." msgid "Applications pick this up the next time they are started."
msgstr "" msgstr ""
#. Menu #. Menu
msgid "Turn autoscroll on or off" msgid "Turn autoscroll on or off"
msgstr "" msgstr ""
@@ -95,7 +94,6 @@ msgstr ""
msgid "Press any key to continue..." msgid "Press any key to continue..."
msgstr "" msgstr ""
#. Settings #. Settings
msgid "Electron and CEF applications" msgid "Electron and CEF applications"
msgstr "" msgstr ""
@@ -115,13 +113,16 @@ msgstr ""
msgid "Spotify" msgid "Spotify"
msgstr "" msgstr ""
msgid "Turn off KDE's middle-click paste"
msgstr ""
msgid "Apply to newly installed applications" msgid "Apply to newly installed applications"
msgstr "" msgstr ""
msgid "Additional Chromium arguments" msgid "Additional Chromium arguments"
msgstr "" msgstr ""
msgid "Up/Down select - Space or Right changes - q goes back" msgid "Up/Down: select, Space or Right: change, q: back"
msgstr "" msgstr ""
msgid "Arguments separated by spaces, empty for none:" msgid "Arguments separated by spaces, empty for none:"
@@ -130,7 +131,6 @@ msgstr ""
msgid "Could not save the setting." msgid "Could not save the setting."
msgstr "" msgstr ""
#. Applications screen #. Applications screen
msgid "Looking at the installed applications..." msgid "Looking at the installed applications..."
msgstr "" msgstr ""
@@ -141,27 +141,26 @@ msgstr ""
msgid "Anything not identified is left alone until it is turned on here." msgid "Anything not identified is left alone until it is turned on here."
msgstr "" msgstr ""
msgid "Autoscroll is off - this is what would be covered." msgid "Autoscroll is off. This is what would be covered."
msgstr "" msgstr ""
msgid "Up/Down select - Space turns one on or off - q goes back" msgid "Up/Down: select, Space: turn on or off, q: back"
msgstr "" msgstr ""
#. Results #. Results
msgid "Autoscroll is on for %d applications." msgid "Autoscroll is on for %d applications."
msgstr "" msgstr ""
msgid "Applications that are already running pick this up when they are restarted." msgid ""
msgstr "" "Applications that are already running pick this up when they are restarted."
msgid "Steam's launcher now skips its file check, which is what keeps the change from being undone at every start."
msgstr "" msgstr ""
msgid "Could not start the watcher for newly installed applications." msgid "Could not start the watcher for newly installed applications."
msgstr "" msgstr ""
msgid "No systemd user session - newly installed applications will not be picked up on their own." msgid ""
"No systemd user session, so newly installed applications will not be picked "
"up on their own."
msgstr "" msgstr ""
msgid "Autoscroll is off and everything that was changed has been put back." msgid "Autoscroll is off and everything that was changed has been put back."
@@ -179,12 +178,18 @@ msgstr ""
msgid "Already applied to %d applications." msgid "Already applied to %d applications."
msgstr "" msgstr ""
msgid "Steam starts its interface in a way this version does not recognise; leaving it alone." msgid ""
"Steam starts its interface in a way this version cannot change without Steam "
"noticing; leaving it alone."
msgstr "" msgstr ""
msgid "Steam is running and has put its own file back; the change waits until Steam is closed." msgid ""
"Steam's interface cannot be given the flag without leaving a file Steam "
"would reinstall; the change has been taken back."
msgstr "" msgstr ""
msgid "Middle click stops pasting at the next login."
msgstr ""
#. Help #. Help
msgid "Usage: middleclick-autoscroll [command]" msgid "Usage: middleclick-autoscroll [command]"
@@ -223,7 +228,6 @@ msgstr ""
msgid "Unknown command: %s" msgid "Unknown command: %s"
msgstr "" msgstr ""
#. Time #. Time
msgid "never" msgid "never"
msgstr "" msgstr ""
@@ -231,11 +235,20 @@ msgstr ""
msgid "just now" msgid "just now"
msgstr "" msgstr ""
msgid "1 minute ago"
msgstr ""
msgid "%d minutes ago" msgid "%d minutes ago"
msgstr "" msgstr ""
msgid "1 hour ago"
msgstr ""
msgid "%d hours ago" msgid "%d hours ago"
msgstr "" msgstr ""
msgid "1 day ago"
msgstr ""
msgid "%d days ago" msgid "%d days ago"
msgstr "" msgstr ""
+27
View File
@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8.7 17.8 437.4 437.4" width="512" height="512">
<defs>
<linearGradient id="shell" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#dfe7f0"/></linearGradient>
<linearGradient id="wheel" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#5fd0ff"/><stop offset="1" stop-color="#0c62a0"/></linearGradient>
<radialGradient id="glow" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="#2aa7e8" stop-opacity=".55"/><stop offset="1" stop-color="#2aa7e8" stop-opacity="0"/></radialGradient>
<filter id="soft" x="-20%" y="-20%" width="140%" height="150%"><feDropShadow dx="0" dy="10" stdDeviation="12" flood-color="#04213a" flood-opacity=".35"/></filter>
<linearGradient id="disc" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#5fd0ff"/><stop offset="1" stop-color="#0c62a0"/></linearGradient>
</defs>
<g filter="url(#soft)">
<circle cx="200" cy="200" r="150" fill="url(#disc)"/>
<g fill="#ffffff">
<path d="M200 70 L236 118 L164 118 Z"/>
<path d="M200 330 L236 282 L164 282 Z"/>
<path d="M70 200 L118 164 L118 236 Z" opacity=".6"/>
<path d="M330 200 L282 164 L282 236 Z" opacity=".6"/>
<circle cx="200" cy="200" r="22"/>
</g>
</g>
<g transform="translate(150 150) scale(.62) rotate(-12 256 266)" filter="url(#soft)">
<rect x="166" y="96" width="180" height="340" rx="90" fill="url(#shell)" stroke="#c9d5e3" stroke-width="4"/>
<path d="M170 228 C 210 240, 302 240, 342 228" fill="none" stroke="#c9d5e3" stroke-width="5" stroke-linecap="round"/>
<path d="M256 100 L256 236" stroke="#c9d5e3" stroke-width="5"/>
<circle cx="256" cy="168" r="70" fill="url(#glow)"/>
<rect x="236" y="128" width="40" height="80" rx="20" fill="url(#wheel)"/>
<g stroke="#ffffff" stroke-opacity=".55" stroke-width="5" stroke-linecap="round"><line x1="246" y1="148" x2="266" y2="148"/><line x1="246" y1="168" x2="266" y2="168"/><line x1="246" y1="188" x2="266" y2="188"/></g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

+5 -5
View File
@@ -5,7 +5,7 @@ Documentation=man:middleclick-autoscroll(1)
[Path] [Path]
# Every way an application can turn up: a package, a Flatpak, a snap, something # Every way an application can turn up: a package, a Flatpak, a snap, something
# installed by hand. Watching where the launchers land means there is no hook # installed by hand. Watching where the launchers land means there is no hook
# to write per package manager - pacman, apt, dnf, zypper, flatpak, snapd and # to write per package manager: pacman, apt, dnf, zypper, flatpak, snapd and
# an AppImage manager all end up here. # an AppImage manager all end up here.
# #
# A directory that does not exist on this system costs nothing: systemd watches # A directory that does not exist on this system costs nothing: systemd watches
@@ -23,10 +23,10 @@ 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 # 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 # game somebody asks for one, and starting a game is a Steam start. But the
# name is translated and cannot be written down here. `enable` adds it in a # folder's name is translated and cannot be written down here. `enable` adds it
# drop-in, from the name this system uses. # 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
+33 -29
View File
@@ -4,7 +4,7 @@
# #
# The routing lives in mca_route and is used twice: once by mca_apply to patch # The routing lives in mca_route and is used twice: once by mca_apply to patch
# and once by the applications screen to show what would happen. Keeping it in # and once by the applications screen to show what would happen. Keeping it in
# one function is what makes that screen trustworthy - it is not a description # one function is what makes that screen trustworthy: it is not a description
# of the behaviour, it is the behaviour. # of the behaviour, it is the behaviour.
MCA_CHANGES=0 MCA_CHANGES=0
@@ -56,7 +56,7 @@ mca_route() {
# mca_apply # mca_apply
# Brings everything into line with the current settings. Safe to run as often # Brings everything into line with the current settings. Safe to run as often
# as it likes to be - it writes only what differs, which is what keeps the # as it likes to be. It writes only what differs, which is what keeps the
# watcher from chasing its own changes. # watcher from chasing its own changes.
mca_apply() { mca_apply() {
local i id file prog kind packaging route steam_done=0 local i id file prog kind packaging route steam_done=0
@@ -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,16 @@ 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" "$packaging" # Nothing is done to the entry itself: what Steam needs is
# in its own installation, and the same patch serves every
# way of starting the client.
if (( ! steam_done )); then if (( ! steam_done )); then
mca_steam_apply mca_steam_apply
steam_done=1 steam_done=1
@@ -102,40 +114,31 @@ mca_apply() {
esac esac
done done
# Steam is worth patching even when its desktop entry is missing - a user # Steam is worth patching even when its desktop entry is missing. A user
# who starts it from a script or a game launcher still gets the interface. # who starts it from a script or a game launcher still gets the interface.
if [[ $CFG_STEAM == yes ]] && (( ! steam_done )) && mca_steam_installed; then if [[ $CFG_STEAM == yes ]] && (( ! steam_done )) && mca_steam_installed; then
mca_steam_apply mca_steam_apply
fi fi
# The shortcuts Steam writes for single games. They are not applications # Autostart entries. A Chromium application that starts itself at login
# and are not offered as ones, but starting a game with Steam closed is a # points straight at its binary and never reads the entry in the menu, so
# Steam start like any other: without the switch the client finds the # Discord at login used to behave differently from Discord from the menu.
# patched helper script, puts its own back, and the interface loses if [[ $CFG_AUTOSTART == yes ]]; then
# autoscroll for the rest of the session.
if [[ $CFG_STEAM == yes ]]; then
for i in "${!MCA_STEAM_LINKS[@]}"; do
mca_steam_desktop_apply "${MCA_STEAM_LINKS[i]}" \
"${MCA_STEAM_LINK_FILES[i]}" "${MCA_STEAM_LINK_PACK[i]}"
done
fi
# Steam's autostart entry carries Steam's own switch and follows the Steam
# setting, not this one - leaving it out while Steam is patched is what puts
# the client in an update loop - so both are checked inside.
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 # Shortcuts on the desktop itself, which nothing above has seen: the XDG
# search path does not go there - and a game started from one starts Steam # search path does not go there. Each entry is gated on its own, so there is
# without its switch, which is the difference between autoscroll working # nothing to check out here.
# 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 mca_shortcuts_apply
[[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply [[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply
# Not about any one application: middle click goes on pasting everywhere
# else on the desktop, and KDE is the one desktop that can be told not to.
# See kde.sh.
[[ $CFG_PASTE == yes ]] && mca_kde_paste_apply
mca_prune_orphans mca_prune_orphans
if (( MCA_CHANGES )) && mca_have update-desktop-database; then if (( MCA_CHANGES )) && mca_have update-desktop-database; then
@@ -143,6 +146,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
} }
@@ -187,8 +191,8 @@ mca_watch_enabled() {
# _mca_watch_dropin_write # _mca_watch_dropin_write
# Returns 0 when the drop-in changed and systemd has to be told, 1 when it was # 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 # already right. That is the same contract as mca_write_if_changed, for the
# reason: the watcher runs after every desktop entry that appears anywhere on # same reason: the watcher runs after every desktop entry that appears anywhere on
# the system, and a daemon-reload each time would be absurd. # the system, and a daemon-reload each time would be absurd.
_mca_watch_dropin_write() { _mca_watch_dropin_write() {
local dir file="$MCA_UNIT_DROPIN/desktop.conf" content local dir file="$MCA_UNIT_DROPIN/desktop.conf" content
+80 -13
View File
@@ -3,7 +3,7 @@
# Paths, translations, output helpers and the change ledger. # Paths, translations, output helpers and the change ledger.
# #
# Everything here runs unprivileged. This tool only ever writes inside the # Everything here runs unprivileged. This tool only ever writes inside the
# user's own home - flag files, desktop entries, Steam's own scripts - so there # user's own home (flag files, desktop entries, Steam's own scripts), so there
# is no system state to guard and nothing that needs root. # is no system state to guard and nothing that needs root.
MCA_VERSION="@VERSION@" MCA_VERSION="@VERSION@"
@@ -13,11 +13,53 @@ 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 # Two browsers do not answer to the Chromium name. Helium ships the feature
# list that an app (or the user) already carries. # under its own name and sets the runtime flag from that alone. Brave takes
# the generated feature away and switches the flag on from one of its own,
# spelled MiddelButtonClickAutoscroll. The typo is in Brave's source, and the
# correct spelling does nothing. So browsers are asked for all three. 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,MiddelButtonClickAutoscroll"
# 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,MiddelButtonClickAutoscroll"
# Bumped when the flags above change, and when what gets written where does.
# 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.
#
# 3 is what takes -noverifyfiles back out of the Steam entries an older version
# put it in.
#
# 4 adds Brave's name to the browser list, which an entry edited in place would
# otherwise never get.
MCA_FLAG_SCHEME=4
MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}" MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}"
MCA_LOCALEDIR="${MCA_LOCALEDIR:-@LOCALEDIR@}" MCA_LOCALEDIR="${MCA_LOCALEDIR:-@LOCALEDIR@}"
@@ -45,7 +87,7 @@ MCA_BACKUPDIR="${MCA_STATEDIR}/backup"
MCA_LEDGER="${MCA_STATEDIR}/ledger" MCA_LEDGER="${MCA_STATEDIR}/ledger"
# The folder the desktop itself shows. Nothing in the XDG search path looks at # 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 # it, so an entry that lives only there is invisible to a scan. A game
# shortcut dragged onto the desktop is exactly that. # shortcut dragged onto the desktop is exactly that.
# #
# Its name is translated: Schreibtisch, Bureau, Escritorio. The name in use is # Its name is translated: Schreibtisch, Bureau, Escritorio. The name in use is
@@ -175,8 +217,8 @@ fi
# Set by mca_bad and mca_note. The menu redraws straight after an action, which # Set by mca_bad and mca_note. The menu redraws straight after an action, which
# would wipe the screen; this marks that something was printed the user still # would wipe the screen; this marks that something was printed the user still
# has to read. A plain success needs no acknowledgement - the status block at # has to read. A plain success needs no acknowledgement, because the status
# the top of the menu already shows the new state. # block at the top of the menu already shows the new state.
MCA_UI_NEEDS_ACK='' MCA_UI_NEEDS_ACK=''
# --quiet silences progress chatter; errors still go to stderr. Used by the # --quiet silences progress chatter; errors still go to stderr. Used by the
@@ -202,10 +244,13 @@ mca_time_ago() {
delta=$(( now - ts )) delta=$(( now - ts ))
(( delta < 0 )) && delta=0 (( delta < 0 )) && delta=0
if (( delta < 60 )); then mca_msg "just now" if (( delta < 60 )); then mca_msg "just now"
elif (( delta < 3600 )); then mca_msg "%d minutes ago" "$(( delta / 60 ))" elif (( delta < 120 )); then mca_msg "1 minute ago"
elif (( delta < 86400 )); then mca_msg "%d hours ago" "$(( delta / 3600 ))" elif (( delta < 3600 )); then mca_msg "%d minutes ago" "$(( delta / 60 ))"
else mca_msg "%d days ago" "$(( delta / 86400 ))" elif (( delta < 7200 )); then mca_msg "1 hour ago"
elif (( delta < 86400 )); then mca_msg "%d hours ago" "$(( delta / 3600 ))"
elif (( delta < 172800 )); then mca_msg "1 day ago"
else mca_msg "%d days ago" "$(( delta / 86400 ))"
fi fi
printf '\n' printf '\n'
} }
@@ -233,7 +278,7 @@ mca_state_write() {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# The change ledger # The change ledger
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Reverting by pattern - "delete every desktop entry that mentions the flag" - # Reverting by pattern ("delete every desktop entry that mentions the flag")
# would also delete entries the user wrote by hand. So every change is recorded # would also delete entries the user wrote by hand. So every change is recorded
# instead, and `revert` replays the ledger backwards. # instead, and `revert` replays the ledger backwards.
# #
@@ -243,9 +288,12 @@ mca_state_write() {
# shadow <generated desktop entry> <source entry it was generated from> # shadow <generated desktop entry> <source entry it was generated from>
# inplace <edited file> <basename of its backup copy> # inplace <edited file> <basename of its backup copy>
# flags <flag file> created | appended | merged # flags <flag file> created | appended | merged
# kwin <kwinrc> what EnablePrimarySelection said before
# #
# `inplace` and `flags` differ in how they are undone: a backup is restored # The kinds differ in how they are undone: a backup is restored wholesale, a
# wholesale, a flag file only loses the one line that was added to it. # 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.
mca_ledger_add() { mca_ledger_add() {
local kind="$1" path="$2" detail="${3:-}" local kind="$1" path="$2" detail="${3:-}"
@@ -258,6 +306,25 @@ mca_ledger_add() {
printf '%s\t%s\t%s\n' "$kind" "$path" "$detail" >> "$MCA_LEDGER" printf '%s\t%s\t%s\n' "$kind" "$path" "$detail" >> "$MCA_LEDGER"
} }
# mca_ledger_has <path>
# Whether the ledger still knows about that file. Worth asking because the copy
# kept beside it is named after the path and outlives a ledger that was lost or
# truncated: a file nothing has a record of any more is one nothing would ever
# look at again.
mca_ledger_has() {
[[ -f $MCA_LEDGER ]] || return 1
awk -F'\t' -v p="$1" '$2 == p { found = 1 } END { exit !found }' "$MCA_LEDGER"
}
# mca_ledger_detail <path>
# The third column of that file's line, empty when the ledger has none. What it
# means is the kind's business; for `kwin` it is the value to put back.
mca_ledger_detail() {
[[ -f $MCA_LEDGER ]] || return 1
awk -F'\t' -v p="$1" '$2 == p { d = $3 } END { if (!length(d)) exit 1; print d }' \
"$MCA_LEDGER"
}
mca_ledger_forget() { mca_ledger_forget() {
local path="$1" tmp local path="$1" tmp
[[ -f $MCA_LEDGER ]] || return 0 [[ -f $MCA_LEDGER ]] || return 0
+10 -1
View File
@@ -144,6 +144,7 @@ mca_config_load() {
CFG_STEAM=no; mca_config_bool PatchSteam yes && CFG_STEAM=yes CFG_STEAM=no; mca_config_bool PatchSteam yes && CFG_STEAM=yes
CFG_SPOTIFY=no; mca_config_bool PatchSpotify yes && CFG_SPOTIFY=yes CFG_SPOTIFY=no; mca_config_bool PatchSpotify yes && CFG_SPOTIFY=yes
CFG_WATCH=no; mca_config_bool WatchNewApps yes && CFG_WATCH=yes CFG_WATCH=no; mca_config_bool WatchNewApps yes && CFG_WATCH=yes
CFG_PASTE=no; mca_config_bool DisablePaste yes && CFG_PASTE=yes
CFG_EXTRA_FLAGS="$(mca_config_get ExtraFlags '')" CFG_EXTRA_FLAGS="$(mca_config_get ExtraFlags '')"
@@ -154,10 +155,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'
} }
+470 -91
View File
@@ -4,7 +4,7 @@
# #
# The detection is deliberately conservative. A wrong "yes" appends an unknown # The detection is deliberately conservative. A wrong "yes" appends an unknown
# argument to something that is not Chromium, and plenty of programs treat an # argument to something that is not Chromium, and plenty of programs treat an
# unrecognised argument as a file name to open - so every rule here is a # unrecognised argument as a file name to open. So every rule here is a
# positive one, and anything that cannot be identified is reported as unknown # positive one, and anything that cannot be identified is reported as unknown
# rather than guessed at. # rather than guessed at.
@@ -13,7 +13,7 @@
# swiftshader libraries) and plain CEF (libcef). # swiftshader libraries) and plain CEF (libcef).
# #
# Deliberately not in here: libEGL.so and libffmpeg.so. Chromium ships both, but # Deliberately not in here: libEGL.so and libffmpeg.so. Chromium ships both, but
# so does the system - /usr/lib/libEGL.so exists on any machine with Mesa - and # so does the system (/usr/lib/libEGL.so exists on any machine with Mesa), and
# a marker that can be somebody else's file is not a marker. # a marker that can be somebody else's file is not a marker.
MCA_MARKERS=( MCA_MARKERS=(
chrome_crashpad_handler chrome-sandbox chrome_100_percent.pak chrome_crashpad_handler chrome-sandbox chrome_100_percent.pak
@@ -21,6 +21,22 @@ MCA_MARKERS=(
libvk_swiftshader.so LICENSES.chromium.html libcef.so libvk_swiftshader.so LICENSES.chromium.html libcef.so
) )
# The markers that still mean something when the whole of a tree is searched
# rather than just the directory the binary sits in. That is the case inside a
# Flatpak, a snap or an AppImage, where there is no "next to the binary".
#
# Three of the names above are missing here on purpose. icudtl.dat is ICU's
# data file and not Chromium's: a Flutter application ships one in data/, and
# a whole-tree search would find it. snapshot_blob.bin and resources.pak are
# the same kind of shared name. Next to a binary they are still evidence,
# because nothing but Chromium unpacks its payload there. A few directories
# deeper they are not.
MCA_MARKERS_STRICT=(
chrome_crashpad_handler chrome-sandbox chrome_100_percent.pak
v8_context_snapshot.bin libvk_swiftshader.so LICENSES.chromium.html
libcef.so app.asar
)
# Shared directories, where a marker belongs to the system rather than to the # Shared directories, where a marker belongs to the system rather than to the
# program that happens to live there. An application ships its payload in a # program that happens to live there. An application ships its payload in a
# directory of its own; nothing unpacks Chromium straight into /usr/lib. # directory of its own; nothing unpacks Chromium straight into /usr/lib.
@@ -58,14 +74,21 @@ MCA_SNAP_DIRS=(/snap /var/lib/snapd/snap)
# CHROMIUM_FLAGS and CHROME_WRAPPER earn their place: they are the variables # CHROMIUM_FLAGS and CHROME_WRAPPER earn their place: they are the variables
# the Debian, Fedora and openSUSE Chromium wrappers and Google's own Chrome # the Debian, Fedora and openSUSE Chromium wrappers and Google's own Chrome
# wrapper build their command line out of, and nothing else sets them. # wrapper build their command line out of, and nothing else sets them.
MCA_SCRIPT_HINTS='ELECTRON_|app\.asar|chrome-sandbox|libcef|enable-blink-features|ozone-platform-hint|CHROMIUM_FLAGS|CHROME_WRAPPER|CHROME_VERSION_EXTRA' #
# updater_bootstrap is Discord's, and it is the only thing left that names it.
# Discord no longer ships its application: /usr/bin/discord is a few lines of
# shell that run that bootstrap, which downloads Electron into the user's
# config directory on first start and execs it from there. So there is nothing
# next to the launcher to find, and the path it hands over to is built out of
# $HOME and the version it just downloaded.
MCA_SCRIPT_HINTS='ELECTRON_|app\.asar|chrome-sandbox|libcef|enable-blink-features|ozone-platform-hint|CHROMIUM_FLAGS|CHROME_WRAPPER|CHROME_VERSION_EXTRA|updater_bootstrap'
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Desktop entries # Desktop entries
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Where Flatpak and snapd put the launchers they export. Both add these to # Where Flatpak and snapd put the launchers they export. Both add these to
# XDG_DATA_DIRS themselves, through a file in /etc/profile.d - but only for a # XDG_DATA_DIRS themselves, through a file in /etc/profile.d. But only for a
# session that was started after they were installed, and only for a session # session that was started after they were installed, and only for a session
# manager that reads it at all. They are appended, after everything XDG names, # manager that reads it at all. They are appended, after everything XDG names,
# so a directory that is already in the search path keeps its own position and # so a directory that is already in the search path keeps its own position and
@@ -77,7 +100,7 @@ mca_extra_desktop_dirs() {
/var/lib/snapd/desktop/applications /var/lib/snapd/desktop/applications
} }
# The directories a desktop entry can come from, most specific first - which is # The directories a desktop entry can come from, most specific first. This is
# also XDG lookup order, so the first file found for an id is the one that is # also XDG lookup order, so the first file found for an id is the one that is
# actually used. # actually used.
mca_desktop_dirs() { mca_desktop_dirs() {
@@ -139,7 +162,7 @@ _mca_desktop_read() {
# mca_exec_program <exec line> # mca_exec_program <exec line>
# The program a desktop entry actually starts: the first token that is not an # The program a desktop entry actually starts: the first token that is not an
# environment prefix, resolved to an absolute path. The result is left in # environment prefix, resolved to an absolute path. The result is left in
# MCA_PROG rather than printed - the scan calls this for every desktop entry on # MCA_PROG rather than printed. The scan calls this for every desktop entry on
# the system, and a command substitution each time is a fork each time. # the system, and a command substitution each time is a fork each time.
# #
# Fails for entries this tool has no safe way to rewrite: anything routed # Fails for entries this tool has no safe way to rewrite: anything routed
@@ -215,7 +238,7 @@ mca_exec_flatpak_id() {
# Whether an entry starts something inside Steam rather than starting Steam # Whether an entry starts something inside Steam rather than starting Steam
# itself: it carries a steam:// address of its own. Steam writes one of those # itself: it carries a steam:// address of its own. Steam writes one of those
# for every game somebody asks for a shortcut to, and the client's own entry # for every game somebody asks for a shortcut to, and the client's own entry
# never has one - it takes an address from the outside, through %U. # never has one. It takes an address from the outside, through %U.
mca_exec_is_steam_link() { mca_exec_is_steam_link() {
local line="$1" prog="$2" local line="$1" prog="$2"
@@ -223,21 +246,60 @@ mca_exec_is_steam_link() {
mca_prog_is_steam "$prog" mca_prog_is_steam "$prog"
} }
# _mca_prog_is_steam_name <program>
# The launcher under one of the names Valve and the distributions give it.
_mca_prog_is_steam_name() {
case "${1##*/}" in
steam|steam-runtime|steam-native|steam-jupiter) return 0 ;;
esac
return 1
}
# _mca_prog_is_steam_wrapper <program>
# Whether a script in front of the client is a way of starting Steam. People
# put one there to add a switch of their own, and an entry pointing at it is a
# Steam start like any other. But the script is not named after Steam, so
# nothing above recognises it.
#
# Getting this wrong is worse than it sounds: such a script tends to mention
# the flag it is there to add, which is one of the markers that say "Chromium"
# to the hint scan. The entry then ends up being handled as an application and
# is given the flag on its command line, where Steam ignores it, instead of
# being handed to the Steam module that knows how to reach the web helper.
#
# Only the handover is followed, and only one step of it: the client's own
# launcher is already recognised by name, so a wrapper in front of it is the
# whole of what is left.
_mca_prog_is_steam_wrapper() {
local prog="$1" head='' target
[[ $prog == /* && -f $prog && -r $prog ]] || return 1
# Two characters, read in the shell: the scan asks this about every program
# on the system, and most of them are binaries whose first line is the whole
# file.
read -r -N 2 head < "$prog" 2>/dev/null || return 1
[[ $head == '#!' ]] || return 1
target="$(_mca_script_target "$prog")" || return 1
[[ -n $target ]] || return 1
_mca_prog_is_steam_name "$target"
}
# mca_prog_is_steam <program> # mca_prog_is_steam <program>
# Whether running this program starts the Steam client. Every packaging is in # Whether running this program starts the Steam client. Every packaging is in
# here and every name Valve and the distributions give the launcher, because # 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 # the answer decides whether an entry gets Steam's own switch. An entry that
# that starts Steam without it undoes the web helper patch on the way up. # 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 # The program is what a scan leaves behind: an absolute path for a native
# install, flatpak:<id> or snap:<name> for the other two. # install, flatpak:<id> or snap:<name> for the other two.
mca_prog_is_steam() { mca_prog_is_steam() {
local prog="$1" local prog="$1"
case "${prog##*/}" in _mca_prog_is_steam_name "$prog" && return 0
steam|steam-runtime|steam-native|steam-jupiter) return 0 ;; [[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]] && return 0
esac _mca_prog_is_steam_wrapper "$prog"
[[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]]
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -270,14 +332,27 @@ _mca_stat_batch() {
return 0 return 0
} }
# The first line of the cache file, and the reason it is there: the verdicts
# below it are keyed on size and mtime, so an entry for a file that has not
# changed is never looked at again. A cache written when a verdict meant
# something else (before an AppImage could come out as anything but "no")
# would therefore keep answering the old way forever. Bumping this is how such
# a cache gets dropped instead.
MCA_CACHE_FORMAT='# middleclick-autoscroll detect 2'
_mca_cache_load() { _mca_cache_load() {
local path stamp verdict local path stamp verdict first=1
(( MCA_CACHE_LOADED )) && return 0 (( MCA_CACHE_LOADED )) && return 0
MCA_CACHE_LOADED=1 MCA_CACHE_LOADED=1
[[ -r "$MCA_CACHEDIR/detect" ]] || return 0 [[ -r "$MCA_CACHEDIR/detect" ]] || return 0
while IFS=$'\t' read -r path stamp verdict; do while IFS=$'\t' read -r path stamp verdict; do
if (( first )); then
first=0
[[ $path == "$MCA_CACHE_FORMAT" ]] || return 0
continue
fi
[[ -n $path && -n $stamp ]] || continue [[ -n $path && -n $stamp ]] || continue
MCA_DETECT_CACHE["$path"]="$stamp"$'\t'"$verdict" MCA_DETECT_CACHE["$path"]="$stamp"$'\t'"$verdict"
done < "$MCA_CACHEDIR/detect" done < "$MCA_CACHEDIR/detect"
@@ -292,6 +367,7 @@ mca_cache_flush() {
mkdir -p "$MCA_CACHEDIR" 2>/dev/null || return 0 mkdir -p "$MCA_CACHEDIR" 2>/dev/null || return 0
tmp="$(mktemp "$MCA_CACHEDIR/detect.XXXXXX")" || return 0 tmp="$(mktemp "$MCA_CACHEDIR/detect.XXXXXX")" || return 0
printf '%s\n' "$MCA_CACHE_FORMAT" > "$tmp"
for path in "${!MCA_DETECT_CACHE[@]}"; do for path in "${!MCA_DETECT_CACHE[@]}"; do
entry="${MCA_DETECT_CACHE[$path]}" entry="${MCA_DETECT_CACHE[$path]}"
printf '%s\t%s\n' "$path" "$entry" >> "$tmp" printf '%s\t%s\n' "$path" "$entry" >> "$tmp"
@@ -311,7 +387,7 @@ _mca_has_markers() {
[[ $dir == "$s" ]] && return 1 [[ $dir == "$s" ]] && return 1
done done
for s in "${MCA_SYSTEM_DIR_GLOBS[@]}"; do for s in "${MCA_SYSTEM_DIR_GLOBS[@]}"; do
# Unquoted on purpose - these are patterns, not names. # Unquoted on purpose: these are patterns, not names.
# shellcheck disable=SC2053 # shellcheck disable=SC2053
[[ $dir == $s ]] && return 1 [[ $dir == $s ]] && return 1
done done
@@ -323,6 +399,26 @@ _mca_has_markers() {
return 1 return 1
} }
# _mca_find_markers <directory> <depth>
# The same question for a whole tree, which is the shape a Flatpak, a snap and
# an unpacked AppImage come in: everything the application ships is somewhere
# under one root and there is no single directory that is "next to the binary".
# Hence the narrower list, see MCA_MARKERS_STRICT.
_mca_find_markers() {
local root="$1" depth="$2" m
local -a names=()
[[ -d $root ]] || return 1
for m in "${MCA_MARKERS_STRICT[@]}"; do
(( ${#names[@]} )) && names+=(-o)
names+=(-name "$m")
done
[[ -n "$(find "$root" -maxdepth "$depth" \
\( "${names[@]}" \) -print -quit 2>/dev/null)" ]]
}
# The plain assignments the script made before it handed over, for # The plain assignments the script made before it handed over, for
# _mca_script_subst to read. A variable of its own rather than something passed # _mca_script_subst to read. A variable of its own rather than something passed
# around: every caller of _mca_script_target reads it through a command # around: every caller of _mca_script_target reads it through a command
@@ -334,9 +430,9 @@ declare -A MCA_SCRIPT_VARS=()
# The text with $NAME and ${NAME} replaced by what the script assigned to them, # The text with $NAME and ${NAME} replaced by what the script assigned to them,
# left in MCA_SUBST. # left in MCA_SUBST.
# #
# Fails as soon as something turns up that only a running shell could work out # Fails as soon as something turns up that only a running shell could work out,
# - a positional parameter, a name the script never set, a default value. That # like a positional parameter, a name the script never set or a default value.
# is the point: an unresolvable path has to come out as no path at all, never # That is the point: an unresolvable path has to come out as no path at all, never
# as a wrong one. # as a wrong one.
MCA_SUBST='' MCA_SUBST=''
@@ -376,8 +472,8 @@ _mca_script_subst() {
# openSUSE all set the directory and the program name into variables at the top # openSUSE all set the directory and the program name into variables at the top
# of the script and end on `exec -a "$APPNAME" "$LIBDIR/$APPNAME"`. Without # of the script and end on `exec -a "$APPNAME" "$LIBDIR/$APPNAME"`. Without
# resolving those there is nothing to follow, and the answer would have to come # resolving those there is nothing to follow, and the answer would have to come
# from the hint scan - a weaker kind of evidence than finding the binary and # from the hint scan, which is weaker evidence than finding the binary and its
# its markers. # markers.
_mca_script_target() { _mca_script_target() {
local script="$1" line tok name val skip=0 local script="$1" line tok name val skip=0
local -a tokens local -a tokens
@@ -448,10 +544,10 @@ _mca_script_target() {
# mca_flags_candidates <program> # mca_flags_candidates <program>
# The flag files a launcher reads, in the order it reads them, taken from the # The flag files a launcher reads, in the order it reads them, taken from the
# launcher itself rather than assumed from the distribution. Where the wrappers # launcher itself rather than assumed from the distribution. Arch's Electron
# follow that convention - Arch's Electron and Chromium packages do, and take # and Chromium packages follow that convention and take extra arguments from
# extra arguments from $XDG_CONFIG_HOME/<name>-flags.conf - it is a far better # $XDG_CONFIG_HOME/<name>-flags.conf. Where the wrappers do that, it is a far
# place to inject than a desktop entry: it survives package updates and it # better place to inject than a desktop entry: it survives package updates and it
# applies to a launch from the terminal too. Where they do not, this finds # applies to a launch from the terminal too. Where they do not, this finds
# nothing and the caller falls back to the desktop entry. # nothing and the caller falls back to the desktop entry.
mca_flags_candidates() { mca_flags_candidates() {
@@ -464,7 +560,7 @@ mca_flags_candidates() {
# A wrapper that only execs another wrapper (heroic -> electron43) inherits # A wrapper that only execs another wrapper (heroic -> electron43) inherits
# that one's flag files, plus the specific name it builds from a variable # that one's flag files, plus the specific name it builds from a variable
# at runtime and therefore never writes down - which is why that one is # at runtime and therefore never writes down. That is why that one is
# derived from the target's own name rather than found. # derived from the target's own name rather than found.
# #
# It is derived only once the target has shown that it reads a flag file at # It is derived only once the target has shown that it reads a flag file at
@@ -482,18 +578,295 @@ mca_flags_candidates() {
fi fi
} }
# mca_is_chromium <program> # ---------------------------------------------------------------------------
# Succeeds when the program is a Chromium, CEF or Electron process. # Looking inside an AppImage
mca_is_chromium() { # ---------------------------------------------------------------------------
local prog="$1" verdict real stamp cached #
# An AppImage is the AppImage runtime (an ordinary ELF executable) with a
# squashfs image appended to it. The payload is compressed, so nothing the
# application ships is on disk where the marker check could find it, and for a
# long time that made every AppImage a "cannot tell".
#
# It does not have to be. squashfs keeps the names of everything it holds in
# one table of its own, the table is a few kilobytes, and the names are the
# whole of what this question needs. So that table is read and inflated here -
# rather than unpacking a hundred megabytes, and rather than running the image
# to ask it, which is the one thing a scan started by a path unit must never
# do.
#
# Everything below fails closed. The moment a field is not where it should be,
# or the compressor is one there is no tool for, the answer goes back to
# "cannot tell" and the application is left to the applications screen.
[[ -n $prog && -e $prog ]] || return 1 # _mca_bytes <file> <offset> <count>
# Count bytes from an offset, as numbers, in MCA_BYTES. od rather than a shell
# read because a variable cannot hold a NUL byte and these headers are full of
# them. And a whole header at a time rather than a field at a time, because
# the first question below is asked about every program on the system and one
# od per field would be four forks per program.
MCA_BYTES=()
_mca_bytes() {
local file="$1" off="$2" count="$3"
MCA_BYTES=()
# read stops on end of input rather than on the delimiter it was given, so
# it reports failure for input that is perfectly complete. The count is
# the check.
read -r -d '' -a MCA_BYTES \
< <(od -An -tu1 -j"$off" -N"$count" -v -- "$file" 2>/dev/null)
(( ${#MCA_BYTES[@]} == count ))
}
# _mca_slice <file> <offset> <length>
# Length bytes from an offset, on stdout.
_mca_slice() {
dd if="$1" bs=65536 iflag=skip_bytes,count_bytes \
skip="$2" count="$3" status=none 2>/dev/null
}
# _mca_word <index>
# One 64-bit little-endian field out of MCA_BYTES, in MCA_WORD. Assigns rather
# than prints for the same reason _mca_bytes reads a whole header at a time:
# printing means a command substitution, and that is a fork per field.
#
# Fails for the all-ones value squashfs writes down for a table that is not in
# the image, which is a case the callers below have to tell from a real offset
# and bash arithmetic (signed, 64-bit) cannot represent.
MCA_WORD=0
_mca_word() {
local at="$1"
(( ${#MCA_BYTES[@]} >= at + 8 )) || return 1
(( MCA_BYTES[at+7] == 255 && MCA_BYTES[at+6] == 255 )) && return 1
MCA_WORD=$(( (MCA_BYTES[at] | MCA_BYTES[at+1] << 8 \
| MCA_BYTES[at+2] << 16 | MCA_BYTES[at+3] << 24) \
| ((MCA_BYTES[at+4] | MCA_BYTES[at+5] << 8 \
| MCA_BYTES[at+6] << 16 | MCA_BYTES[at+7] << 24) << 32) ))
return 0
}
# _mca_appimage_offset <file>
# Where the appended image starts, in MCA_APPIMAGE_AT: directly after the ELF,
# which is where its section header table ends. This is how the runtime works
# it out for --appimage-offset, and it is exact.
#
# Searching for the squashfs magic instead would not be: the runtime carries a
# copy of it in its own code. Neither is the AppImage magic at byte 8 of the
# ELF header a way in. It is there to be zeroed, and a build pipeline that
# does not want its image picked up by a desktop integration daemon does
# exactly that. So the offset is computed for any ELF at all and the caller
# finds out whether there is an image at it.
MCA_APPIMAGE_AT=0
_mca_appimage_offset() {
local shoff shentsize shnum
MCA_APPIMAGE_AT=0
_mca_bytes "$1" 0 64 || return 1
# \x7fELF, and then the class byte: 2 for the 64-bit header, 1 for the
# 32-bit one, which puts every field after it somewhere else.
(( MCA_BYTES[0] == 127 && MCA_BYTES[1] == 69 \
&& MCA_BYTES[2] == 76 && MCA_BYTES[3] == 70 )) || return 1
if (( MCA_BYTES[4] == 2 )); then
_mca_word 40 || return 1
shoff="$MCA_WORD"
shentsize=$(( MCA_BYTES[58] | MCA_BYTES[59] << 8 ))
shnum=$(( MCA_BYTES[60] | MCA_BYTES[61] << 8 ))
elif (( MCA_BYTES[4] == 1 )); then
shoff=$(( MCA_BYTES[32] | MCA_BYTES[33] << 8 \
| MCA_BYTES[34] << 16 | MCA_BYTES[35] << 24 ))
shentsize=$(( MCA_BYTES[46] | MCA_BYTES[47] << 8 ))
shnum=$(( MCA_BYTES[48] | MCA_BYTES[49] << 8 ))
else
return 1
fi
(( shoff > 0 && shentsize > 0 && shnum > 0 )) || return 1
MCA_APPIMAGE_AT=$(( shoff + shentsize * shnum ))
return 0
}
# _mca_inflate <compressor> <file> <offset> <length>
# One squashfs metadata block, decompressed onto stdout.
#
# lzo and lz4 are missing because squashfs stores them as bare blocks and
# neither lzop nor the lz4 tool will read one without the framing their own
# file format puts around it. An image compressed with either comes out as
# "cannot tell", which is where it started.
_mca_inflate() {
local comp="$1" file="$2" off="$3" len="$4"
case "$comp" in
1)
# squashfs stores a zlib stream and gzip only reads its own
# container, but underneath both are the same deflate data with a
# different wrapper around it. So the wrapper is swapped: zlib's
# two header bytes are dropped and a minimal gzip header put in
# front. gzip then writes every byte of the block and complains
# about the trailer it did not get, which is why its status is
# thrown away here and the check is on the output instead.
(( len > 2 )) || return 1
{
printf '\037\213\010\000\000\000\000\000\000\003'
_mca_slice "$file" $(( off + 2 )) $(( len - 2 ))
} | { gzip -dc 2>/dev/null || true; }
;;
2)
mca_have xz || return 1
_mca_slice "$file" "$off" "$len" \
| { xz -dc --format=lzma 2>/dev/null || true; }
;;
4)
mca_have xz || return 1
_mca_slice "$file" "$off" "$len" | { xz -dc 2>/dev/null || true; }
;;
6)
mca_have zstd || return 1
_mca_slice "$file" "$off" "$len" | { zstd -dc 2>/dev/null || true; }
;;
*) return 1 ;;
esac
}
# _mca_squashfs_names <file> <offset>
# The directory table of the image at that offset, inflated onto stdout. It is
# not parsed: the names sit in it as plain text between the records that
# describe them, and a name is all the caller is looking for.
#
# Fails unless the walk lands exactly on the end of the table. That is the
# integrity check: the block sizes adding up to the table's own length is what
# says the fields were read from a real superblock, and that the output is the
# whole of the names rather than some of them. It is also why the caller has
# to collect this before trusting it, never pipe it.
_mca_squashfs_names() {
local file="$1" base="$2"
local comp dir_start end field header size pos
_mca_bytes "$file" "$base" 96 || return 1
comp=$(( MCA_BYTES[20] | MCA_BYTES[21] << 8 ))
_mca_word 72 || return 1
dir_start="$MCA_WORD"
(( dir_start > 0 )) || return 1
# Where the names stop: the first table squashfs writes after them. Which
# one that is depends on the image. There is no fragment table when
# nothing was packed into a fragment, and no export table unless it was
# asked for. So they are tried in the order they are written and the
# first one that is actually there wins. bytes_used closes the list for an
# image that has none of them.
end=0
for field in 80 88 48 40; do
_mca_word "$field" || continue
(( MCA_WORD > dir_start )) || continue
end="$MCA_WORD"
break
done
(( end > dir_start )) || return 1
# Names for a hundred thousand files would still fit in a fraction of
# this. A table that claims more than it is a table that was misread.
(( end - dir_start > 8388608 )) && return 1
pos=$(( base + dir_start ))
end=$(( base + end ))
while (( pos < end )); do
_mca_bytes "$file" "$pos" 2 || return 1
header=$(( MCA_BYTES[0] | MCA_BYTES[1] << 8 ))
size=$(( header & 0x7fff ))
(( size > 0 && pos + 2 + size <= end )) || return 1
# The top bit says the block was stored as it is, which squashfs does
# for the ones compression made no smaller.
if (( header & 0x8000 )); then
_mca_slice "$file" $(( pos + 2 )) "$size" || return 1
else
_mca_inflate "$comp" "$file" $(( pos + 2 )) "$size" || return 1
fi
pos=$(( pos + 2 + size ))
done
(( pos == end ))
}
# _mca_appimage_verdict <file>
# What the image appended to this file contains, left in MCA_APPIMAGE:
# yes - Chromium, CEF or Electron
# no - the names of everything inside were read and none of them is
# unknown - there is an image, but its contents could not be read
# none - not an AppImage; there is nothing appended to this ELF
MCA_APPIMAGE=none
_mca_appimage_verdict() {
local file="$1" base names m
local -a args=()
MCA_APPIMAGE=none
_mca_appimage_offset "$file" || return 0
base="$MCA_APPIMAGE_AT"
# hsqs, the squashfs magic. Nothing there means nothing was appended, so
# this is an ordinary executable and not an AppImage at all.
_mca_bytes "$file" "$base" 4 || return 0
(( MCA_BYTES[0] == 104 && MCA_BYTES[1] == 115 \
&& MCA_BYTES[2] == 113 && MCA_BYTES[3] == 115 )) || return 0
# From here on there is an image, so the worst this can end on is "cannot
# tell". Never "no", which would be an answer about contents that were
# never read.
MCA_APPIMAGE=unknown
names="$(mktemp "${TMPDIR:-/tmp}/mca-names.XXXXXX")" || return 0
# Nothing at all in the table means every block failed to inflate (a
# compressor whose tool is not installed). That is a "cannot tell" too.
if _mca_squashfs_names "$file" "$base" > "$names" 2>/dev/null \
&& [[ -s $names ]]
then
for m in "${MCA_MARKERS_STRICT[@]}"; do args+=(-e "$m"); done
if grep -qaF "${args[@]}" -- "$names"; then
MCA_APPIMAGE=yes
else
MCA_APPIMAGE=no
fi
fi
rm -f -- "$names"
return 0
}
# mca_detect_verdict <program>
# What running this program starts, left in MCA_VERDICT:
# yes - Chromium, CEF or Electron
# 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
#
# Assigns rather than returns three states through an exit code, and is the one
# place the memo and the on-disk cache are consulted.
MCA_VERDICT=''
mca_detect_verdict() {
local prog="$1" real stamp cached
MCA_VERDICT=no
[[ -n $prog && -e $prog ]] || return 0
# Memoized under the path as given, so the same launcher named twice in a # Memoized under the path as given, so the same launcher named twice in a
# scan costs nothing at all the second time. # scan costs nothing at all the second time.
if [[ -n ${MCA_DETECT_MEMO[$prog]+set} ]]; then if [[ -n ${MCA_DETECT_MEMO[$prog]+set} ]]; then
[[ ${MCA_DETECT_MEMO[$prog]} == yes ]] MCA_VERDICT="${MCA_DETECT_MEMO[$prog]}"
return $? return 0
fi fi
_mca_cache_load _mca_cache_load
@@ -507,26 +880,45 @@ mca_is_chromium() {
if [[ -n $stamp && -n ${MCA_DETECT_CACHE[$prog]+set} ]]; then if [[ -n $stamp && -n ${MCA_DETECT_CACHE[$prog]+set} ]]; then
cached="${MCA_DETECT_CACHE[$prog]}" cached="${MCA_DETECT_CACHE[$prog]}"
if [[ "${cached%%$'\t'*}" == "$stamp" ]]; then if [[ "${cached%%$'\t'*}" == "$stamp" ]]; then
verdict="${cached#*$'\t'}" MCA_VERDICT="${cached#*$'\t'}"
MCA_DETECT_MEMO[$prog]="$verdict" MCA_DETECT_MEMO[$prog]="$MCA_VERDICT"
[[ $verdict == yes ]] return 0
return $?
fi fi
fi fi
real="$(readlink -f -- "$prog" 2>/dev/null)" || real="$prog" real="$(readlink -f -- "$prog" 2>/dev/null)" || real="$prog"
verdict=no MCA_DETECT_UNSURE=0
if _mca_detect_uncached "$real"; then verdict=yes; fi if _mca_detect_uncached "$real"; then
MCA_VERDICT=yes
elif (( MCA_DETECT_UNSURE )); then
MCA_VERDICT=unknown
else
MCA_VERDICT=no
fi
MCA_DETECT_MEMO[$prog]="$verdict" MCA_DETECT_MEMO[$prog]="$MCA_VERDICT"
if [[ -n $stamp ]]; then if [[ -n $stamp ]]; then
MCA_DETECT_CACHE["$prog"]="$stamp"$'\t'"$verdict" MCA_DETECT_CACHE["$prog"]="$stamp"$'\t'"$MCA_VERDICT"
MCA_CACHE_DIRTY=1 MCA_CACHE_DIRTY=1
fi fi
[[ $verdict == yes ]] return 0
} }
# mca_is_chromium <program>
# Succeeds when the program is a Chromium, CEF or Electron process. "Cannot
# tell" is not that, so it fails here. Anything that has to treat the two
# differently asks mca_detect_verdict instead.
mca_is_chromium() {
mca_detect_verdict "$1"
[[ $MCA_VERDICT == yes ]]
}
# Set by _mca_detect_uncached when it reaches the end without finding anything
# and the reason is that something could not be read, rather than that there
# was nothing there. Only mca_detect_verdict reads it, straight after the call.
MCA_DETECT_UNSURE=0
_mca_detect_uncached() { _mca_detect_uncached() {
local real="$1" depth="${2:-0}" dir target local real="$1" depth="${2:-0}" dir target
@@ -546,20 +938,33 @@ _mca_detect_uncached() {
fi fi
# A binary. Everything Chromium ships is unpacked next to it, either in the # A binary. Everything Chromium ships is unpacked next to it, either in the
# same directory or - for /opt/thing/bin/Thing layouts - one level up. # same directory or (for /opt/thing/bin/Thing layouts) one level up.
dir="$(dirname -- "$real")" dir="$(dirname -- "$real")"
_mca_has_markers "$dir" && return 0 _mca_has_markers "$dir" && return 0
[[ ${dir##*/} == bin ]] && _mca_has_markers "${dir%/*}" && return 0 [[ ${dir##*/} == bin ]] && _mca_has_markers "${dir%/*}" && return 0
# 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.
_mca_appimage_verdict "$real"
case "$MCA_APPIMAGE" in
yes) return 0 ;;
no) return 1 ;;
esac
# Last resort: Chromium's own argument table is in the binary. -m1 stops at # Last resort: Chromium's own argument table is in the binary. -m1 stops at
# the first hit, so this reads far less than the file size suggests. # the first hit, so this reads far less than the file size suggests.
#
# AppImages are the one thing this cannot see through - their payload is a
# compressed filesystem - which is why they come out as unknown and are
# left to the applications screen.
grep -qaFm1 -- 'enable-blink-features' "$real" 2>/dev/null && return 0 grep -qaFm1 -- 'enable-blink-features' "$real" 2>/dev/null && return 0
grep -qaFm1 -- 'CHROME_VERSION_EXTRA' "$real" 2>/dev/null && return 0 grep -qaFm1 -- 'CHROME_VERSION_EXTRA' "$real" 2>/dev/null && return 0
# What is left is an image that could not be read (an unsupported
# compressor) or the older AppImage layout, which is an ISO9660 filesystem
# and has no name table of this shape at all. Either way the answer is not
# "no", it is "nobody looked".
if [[ $MCA_APPIMAGE == unknown || $real == *.AppImage || $real == *.appimage ]]; then
MCA_DETECT_UNSURE=1
fi
return 1 return 1
} }
@@ -576,16 +981,6 @@ MCA_PROGS=() # resolved program, or a Flatpak app id or a snap name
MCA_KINDS=() # app | browser | steam | unknown | no MCA_KINDS=() # app | browser | steam | unknown | no
MCA_PACKAGING=() # native | flatpak | snap MCA_PACKAGING=() # native | flatpak | snap
# The shortcuts Steam writes for single games. Not applications of their own - a
# game is whatever engine it was built with, and none of those reads a Chromium
# argument - so they are kept apart from the list rather than listed as
# something that got switched on. They do start Steam, which is why they are
# kept at all: the Steam module gives them Steam's own switch.
MCA_STEAM_LINKS=() # desktop file id
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
# has to remember to invalidate this. # has to remember to invalidate this.
@@ -603,10 +998,9 @@ 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_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
# they can be asked for all at once. # they can be asked for all at once.
while IFS= read -r dir; do while IFS= read -r dir; do
[[ -d $dir ]] || continue [[ -d $dir ]] || continue
@@ -643,18 +1037,12 @@ mca_scan() {
prog="snap:$MCA_PROG" prog="snap:$MCA_PROG"
fi fi
if mca_exec_is_steam_link "$exec_line" "$prog"; then # The shortcuts Steam writes for single games are not
MCA_STEAM_LINKS+=("$id") # applications of their own. A game is whatever engine it was
MCA_STEAM_LINK_FILES+=("$file") # built with, and none of those reads a Chromium argument. And
if [[ $prog == flatpak:* ]]; then # starting the client through one needs nothing on its command
MCA_STEAM_LINK_PACK+=(flatpak) # line either.
elif [[ $prog == snap:* ]]; then mca_exec_is_steam_link "$exec_line" "$prog" && continue
MCA_STEAM_LINK_PACK+=(snap)
else
MCA_STEAM_LINK_PACK+=(native)
fi
continue
fi
c_ids+=("$id"); c_files+=("$file"); c_names+=("$name") c_ids+=("$id"); c_files+=("$file"); c_names+=("$name")
c_progs+=("$prog") c_progs+=("$prog")
@@ -666,8 +1054,8 @@ mca_scan() {
MCA_STAT=() MCA_STAT=()
_mca_stat_batch "${c_stat[@]}" _mca_stat_batch "${c_stat[@]}"
# Pass two: decide what each one is. What it does - an application or a # Pass two: decide what each one is. What it does (an application or a
# browser - and how it was packaged are two separate questions: a Chromium # browser) and how it was packaged are two separate questions: a Chromium
# installed as a snap is still a browser, and somebody who has turned # installed as a snap is still a browser, and somebody who has turned
# browsers off means that one too. # browsers off means that one too.
for i in "${!c_ids[@]}"; do for i in "${!c_ids[@]}"; do
@@ -695,10 +1083,12 @@ mca_scan() {
# 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
elif mca_is_chromium "$prog"; then else
(( c_browser[i] )) && kind=browser || kind=app mca_detect_verdict "$prog"
elif [[ $prog == *.AppImage || $prog == *.appimage ]]; then case "$MCA_VERDICT" in
kind=unknown yes) (( c_browser[i] )) && kind=browser || kind=app ;;
unknown) kind=unknown ;;
esac
fi fi
[[ $kind == no ]] && continue [[ $kind == no ]] && continue
@@ -748,8 +1138,8 @@ mca_desktop_is_browser() {
} }
# mca_snap_name <program> # mca_snap_name <program>
# The snap an executable belongs to, left in MCA_PROG - assigned rather than # The snap an executable belongs to, left in MCA_PROG. It is assigned rather
# printed for the same reason mca_exec_program is: the scan asks this about # than printed for the same reason mca_exec_program is: the scan asks this about
# every desktop entry on the system, and a command substitution per entry is a # every desktop entry on the system, and a command substitution per entry is a
# fork per entry. # fork per entry.
# #
@@ -791,13 +1181,7 @@ mca_snap_is_chromium() {
for d in "${MCA_SNAP_DIRS[@]}"; do for d in "${MCA_SNAP_DIRS[@]}"; do
root="$d/$name/current" root="$d/$name/current"
[[ -d $root ]] || continue if _mca_find_markers "$root" 5; then
if [[ -n "$(find "$root" -maxdepth 5 \
\( -name 'chrome_crashpad_handler' -o -name 'app.asar' \
-o -name 'libcef.so' -o -name 'v8_context_snapshot.bin' \
-o -name 'chrome-sandbox' \) \
-print -quit 2>/dev/null)" ]]
then
MCA_DETECT_MEMO[snap:$name]=yes MCA_DETECT_MEMO[snap:$name]=yes
return 0 return 0
fi fi
@@ -820,12 +1204,7 @@ mca_flatpak_is_chromium() {
fi fi
loc="$(flatpak info --show-location "$id" 2>/dev/null)" loc="$(flatpak info --show-location "$id" 2>/dev/null)"
if [[ -n $loc && -d "$loc/files" ]] \ if [[ -n $loc ]] && _mca_find_markers "$loc/files" 4; then
&& [[ -n "$(find "$loc/files" -maxdepth 4 \
\( -name 'chrome_crashpad_handler' -o -name 'app.asar' \
-o -name 'libcef.so' -o -name 'v8_context_snapshot.bin' \) \
-print -quit 2>/dev/null)" ]]
then
MCA_DETECT_MEMO[flatpak:$id]=yes MCA_DETECT_MEMO[flatpak:$id]=yes
return 0 return 0
fi fi
+167
View File
@@ -0,0 +1,167 @@
# shellcheck shell=bash
#
# KDE's middle-click paste.
#
# Inside a Chromium application the flag settles this on its own: with
# autoscroll on, Blink stops pasting the primary selection on middle click,
# because the button is doing something else now. Everywhere else on the
# desktop middle click goes on pasting, which is the behaviour this whole
# program exists to get away from. So the desktop's own switch for it is
# turned off too.
#
# KWin has such a switch, and only for a Wayland session, where the paste is a
# protocol the compositor either offers or does not:
#
# ~/.config/kwinrc, [Wayland], EnablePrimarySelection=false
#
# On X11 there is nothing to switch. The primary selection is part of X itself
# and every toolkit reaches for it on its own; no one place can say no.
#
# KWin reads the key once, while it starts, so what is written here is true
# from the next login on rather than straight away.
MCA_KWINRC="${MCA_XDG_CONFIG}/kwinrc"
MCA_KWIN_GROUP="Wayland"
MCA_KWIN_KEY="EnablePrimarySelection"
# The kconfig command line tools, whichever generation this session has. They
# are what System Settings writes kwinrc with, and worth the fork: the file has
# a cascade behind it (/etc/xdg, the kdedefaults directory) and entries a
# distribution can mark immutable, and an edit that reaches past all that with
# sed writes something KDE goes on to ignore.
MCA_KDE_TOOL=''
MCA_KDE_TOOL_LOOKED=0
_mca_kde_tool() {
local gen
if (( ! MCA_KDE_TOOL_LOOKED )); then
MCA_KDE_TOOL_LOOKED=1
for gen in 6 5; do
if mca_have "kreadconfig$gen" && mca_have "kwriteconfig$gen"; then
MCA_KDE_TOOL="$gen"
break
fi
done
fi
[[ -n $MCA_KDE_TOOL ]] || return 1
printf '%s\n' "$MCA_KDE_TOOL"
}
# mca_kde_session
# Whether this is a KWin Wayland session.
mca_kde_session() {
local desktop="${XDG_CURRENT_DESKTOP:-}"
# The watcher is a systemd service, and one started before the session
# imported its environment sees none of these variables. A running
# kwin_wayland answers both halves of the question at once, and is what
# such a run ends up going by.
if [[ -z $desktop && -z ${KDE_FULL_SESSION:-} ]]; then
mca_have pgrep || return 1
pgrep -u "$(id -u)" -x kwin_wayland > /dev/null 2>&1
return
fi
# XDG_CURRENT_DESKTOP is the one every desktop sets, and the one to go by
# wherever it is set; KDE_FULL_SESSION is what answers when it is not.
[[ -z $desktop || ${desktop^^} == *KDE* ]] || return 1
[[ ${XDG_SESSION_TYPE:-} == wayland || -n ${WAYLAND_DISPLAY:-} ]]
}
mca_kde_available() {
mca_kde_session && _mca_kde_tool > /dev/null
}
# mca_kde_read [file]
# What the key says now, lowercased, empty when it is not set at all.
mca_kde_read() {
local file="${1:-$MCA_KWINRC}" gen val
gen="$(_mca_kde_tool)" || return 1
# kreadconfig creates the file it is pointed at, and the status screen
# (which asks on every redraw) has no business writing anything. A file
# that is not there has no value in it to read anyway.
[[ -f $file ]] || return 0
val="$("kreadconfig$gen" --file "$file" --group "$MCA_KWIN_GROUP" \
--key "$MCA_KWIN_KEY" 2>/dev/null)"
printf '%s\n' "${val,,}"
}
# mca_kde_write [file] <value>
# An empty value takes the key back out rather than writing nothing into it -
# a key that is missing and a key that says nothing are not the same thing to
# KDE, and only the first one is what an untouched kwinrc looks like.
mca_kde_write() {
local file="$1" value="$2" gen
gen="$(_mca_kde_tool)" || return 1
if [[ -z $value ]]; then
"kwriteconfig$gen" --file "$file" --group "$MCA_KWIN_GROUP" \
--key "$MCA_KWIN_KEY" --delete 2>/dev/null
else
"kwriteconfig$gen" --file "$file" --group "$MCA_KWIN_GROUP" \
--key "$MCA_KWIN_KEY" "$value" 2>/dev/null
fi
}
# mca_kde_paste_apply
# Turns the paste off, and remembers what the key said before so that undoing
# puts that back rather than a guess.
#
# A key that already says false without the ledger knowing about it was set by
# the user or by the distribution. That one is left alone and nothing is
# recorded, so `disable` cannot switch a paste back on that this program never
# switched off.
mca_kde_paste_apply() {
local current previous changed=0
mca_kde_available || return 1
current="$(mca_kde_read)" || return 1
if mca_ledger_has "$MCA_KWINRC"; then
# Ours already. What goes back is the value recorded the first time
# round, not what reading the file now would say. That is false, and
# false is what is being undone.
previous="$(mca_ledger_detail "$MCA_KWINRC")" || previous=unset
elif [[ $current == false ]]; then
return 0
else
previous="${current:-unset}"
fi
if [[ $current != false ]]; then
mca_kde_write "$MCA_KWINRC" false || return 1
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
changed=1
fi
mca_ledger_add kwin "$MCA_KWINRC" "$previous"
(( changed )) && mca_note "$(mca_msg "Middle click stops pasting at the next login.")"
return 0
}
# mca_kde_paste_revert <file> <previous value>
mca_kde_paste_revert() {
local file="$1" previous="$2"
_mca_kde_tool > /dev/null || return 1
[[ -f $file ]] || return 1
# Only what is still recognisably ours. A key somebody has since put a
# value of their own into is theirs now.
[[ "$(mca_kde_read "$file")" == false ]] || return 1
[[ $previous == unset ]] && previous=''
mca_kde_write "$file" "$previous" || return 1
# Nothing was in the file but the key, which means there was no file before
# this program wrote one. An empty kwinrc says nothing that its absence
# does not say better.
[[ -s $file ]] || rm -f -- "$file"
return 0
}
+21 -11
View File
@@ -32,7 +32,7 @@ mca_ui_term_restore() {
} }
# Runs an action with the terminal handed back to normal line mode, so anything # Runs an action with the terminal handed back to normal line mode, so anything
# it prints - or prompts for - behaves the way a program expects. # it prints or prompts for behaves the way a program expects.
mca_ui_cooked() { mca_ui_cooked() {
mca_ui_term_restore mca_ui_term_restore
"$@" "$@"
@@ -126,8 +126,8 @@ mca_pause() {
# _mca_row <label> <value> # _mca_row <label> <value>
# printf's %-28s pads by bytes, so a label containing "ü" comes out one column # printf's %-28s pads by bytes, so a label containing "ü" comes out one column
# short. ${#s} counts characters in a UTF-8 locale, so the padding is computed # short. ${#s} counts characters in a UTF-8 locale, so the padding is computed
# here instead - and applied inline, because command substitution would eat the # here instead. It is applied inline, because command substitution would eat
# trailing spaces again. # the trailing spaces again.
_mca_row() { _mca_row() {
local label="$1" value="$2" pad local label="$1" value="$2" pad
pad=$(( 30 - ${#label} )) pad=$(( 30 - ${#label} ))
@@ -172,15 +172,12 @@ mca_ui_status() {
if (( MCA_N_UNKNOWN )); then if (( MCA_N_UNKNOWN )); then
_mca_row "$(mca_msg "Not identified")" \ _mca_row "$(mca_msg "Not identified")" \
"${MCA_C_DIM}$(mca_msg "%d - see the applications list" "$MCA_N_UNKNOWN")${MCA_C_RESET}" "${MCA_C_DIM}$(mca_msg "%d (see the applications list)" "$MCA_N_UNKNOWN")${MCA_C_RESET}"
fi fi
if mca_steam_installed; then if mca_steam_installed; then
if mca_steam_patched; then if mca_steam_patched; then
_mca_row "$(mca_msg "Steam")" "$(_mca_onoff yes)" _mca_row "$(mca_msg "Steam")" "$(_mca_onoff yes)"
elif [[ $CFG_STEAM == yes && $CFG_ENABLED == yes ]] && mca_steam_waiting; then
_mca_row "$(mca_msg "Steam")" \
"${MCA_C_DIM}$(mca_msg "waiting until Steam is closed")${MCA_C_RESET}"
elif [[ $CFG_STEAM == yes && $CFG_ENABLED == yes ]]; then elif [[ $CFG_STEAM == yes && $CFG_ENABLED == yes ]]; then
_mca_row "$(mca_msg "Steam")" \ _mca_row "$(mca_msg "Steam")" \
"${MCA_C_YELLOW}$(mca_msg "not patched yet")${MCA_C_RESET}" "${MCA_C_YELLOW}$(mca_msg "not patched yet")${MCA_C_RESET}"
@@ -197,6 +194,18 @@ mca_ui_status() {
fi fi
fi fi
# Not an ON/OFF like the rows around it: what is being reported is the state
# of the paste, not of a switch, and off is the state this program is after.
if mca_kde_available; then
if [[ "$(mca_kde_read)" == false ]]; then
_mca_row "$(mca_msg "Middle-click paste")" \
"${MCA_C_GREEN}$(mca_msg "off")${MCA_C_RESET}"
else
_mca_row "$(mca_msg "Middle-click paste")" \
"${MCA_C_DIM}$(mca_msg "on")${MCA_C_RESET}"
fi
fi
_mca_row "$(mca_msg "Last applied")" "$(mca_time_ago "$last")" _mca_row "$(mca_msg "Last applied")" "$(mca_time_ago "$last")"
if [[ $CFG_ENABLED == yes ]]; then if [[ $CFG_ENABLED == yes ]]; then
@@ -219,6 +228,7 @@ MCA_SETTINGS=(
"PatchAutostart|bool|yes|Programs that start themselves at login" "PatchAutostart|bool|yes|Programs that start themselves at login"
"PatchSteam|bool|yes|Steam" "PatchSteam|bool|yes|Steam"
"PatchSpotify|bool|yes|Spotify" "PatchSpotify|bool|yes|Spotify"
"DisablePaste|bool|yes|Turn off KDE's middle-click paste"
"WatchNewApps|bool|yes|Apply to newly installed applications" "WatchNewApps|bool|yes|Apply to newly installed applications"
"ExtraFlags|text||Additional Chromium arguments" "ExtraFlags|text||Additional Chromium arguments"
) )
@@ -284,7 +294,7 @@ mca_ui_settings() {
local title hint local title hint
mca_msg_into "$locale" "Settings"; title="$MCA_MSG_RESULT" mca_msg_into "$locale" "Settings"; title="$MCA_MSG_RESULT"
mca_msg_into "$locale" "Up/Down select - Space or Right changes - q goes back" mca_msg_into "$locale" "Up/Down: select, Space or Right: change, q: back"
hint="$MCA_MSG_RESULT" hint="$MCA_MSG_RESULT"
local clearseq local clearseq
@@ -377,11 +387,11 @@ mca_ui_apps() {
local title hint legend warn local title hint legend warn
mca_msg_into "$locale" "Applications"; title="$MCA_MSG_RESULT" mca_msg_into "$locale" "Applications"; title="$MCA_MSG_RESULT"
mca_msg_into "$locale" "Up/Down select - Space turns one on or off - q goes back" mca_msg_into "$locale" "Up/Down: select, Space: turn on or off, q: back"
hint="$MCA_MSG_RESULT" hint="$MCA_MSG_RESULT"
mca_msg_into "$locale" "Anything not identified is left alone until it is turned on here." mca_msg_into "$locale" "Anything not identified is left alone until it is turned on here."
legend="$MCA_MSG_RESULT" legend="$MCA_MSG_RESULT"
mca_msg_into "$locale" "Autoscroll is off - this is what would be covered." mca_msg_into "$locale" "Autoscroll is off. This is what would be covered."
warn="$MCA_MSG_RESULT" warn="$MCA_MSG_RESULT"
# The per-row labels are resolved once, here. Looking them up inside the # The per-row labels are resolved once, here. Looking them up inside the
@@ -544,7 +554,7 @@ mca_ui_menu() {
fi fi
;; ;;
q|Q) mca_ui_term_restore; trap - EXIT INT TERM; return 0 ;; q|Q) mca_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
# Anything else - Enter, arrow keys, stray characters - just # Anything else (Enter, arrow keys, stray characters) just
# redraws. Escape is deliberately not a quit key, so a mistyped # redraws. Escape is deliberately not a quit key, so a mistyped
# arrow key cannot close the menu. # arrow key cannot close the menu.
*) ;; *) ;;
+149 -89
View File
@@ -4,19 +4,19 @@
# #
# There are two ways in, and the order matters: # There are two ways in, and the order matters:
# #
# 1. A flag file. Where the distribution wraps Electron and Chromium in a # 1. A flag file. Some distributions wrap Electron and Chromium in a
# launcher script that reads extra arguments from # launcher script that reads extra arguments from
# $XDG_CONFIG_HOME/<name>-flags.conf - Arch and its derivatives do, and it # $XDG_CONFIG_HOME/<name>-flags.conf. Arch and its derivatives do, and
# is the convention their packages follow - this is the good one: it is # their packages follow that convention. Where it exists, this is the good
# the supported way to pass arguments, it survives package upgrades # way in: it is the supported way to pass arguments, it survives package upgrades
# untouched, and it applies however the program is started, including from # untouched, and it applies however the program is started, including from
# a terminal. # a terminal.
# #
# Nothing is assumed about which distribution this is. The launcher itself # Nothing is assumed about which distribution this is. The launcher itself
# is read, and the route is taken only for a launcher that really does # is read, and the route is taken only for a launcher that really does
# name such a file. Elsewhere - Debian, Ubuntu, Fedora, openSUSE, where # name such a file. On Debian, Ubuntu, Fedora and openSUSE the
# the equivalent file lives under /etc and is the system's to write - # equivalent file lives under /etc and is the system's to write. There is
# there is no flag file to use and route 2 answers for everything. # no flag file to use there, and route 2 answers for everything.
# #
# 2. A desktop entry. For applications that ship their own binary with no # 2. A desktop entry. For applications that ship their own binary with no
# wrapper, and for everything a Flatpak or a snap contains, there is # wrapper, and for everything a Flatpak or a snap contains, there is
@@ -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
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -37,6 +71,11 @@ MCA_MARK_END='# <<< middleclick-autoscroll'
# Where an argument has to go: after the program and its own arguments, but # Where an argument has to go: after the program and its own arguments, but
# before the first field code (%U and friends) or Flatpak's @@ markers, which # before the first field code (%U and friends) or Flatpak's @@ markers, which
# are placeholders the launcher expands rather than arguments. # are placeholders the launcher expands rather than arguments.
#
# A bare "--" stops it too. That is where Chromium stops reading switches and
# starts treating everything as an address, so a flag put after it is read as a
# page to open and the feature never comes on. Discord's entry is exactly that
# shape: `discord --url -- %u`.
_mca_exec_insert_at() { _mca_exec_insert_at() {
local rest="$1" tok idx=0 local rest="$1" tok idx=0
@@ -45,7 +84,7 @@ _mca_exec_insert_at() {
[[ -n $rest ]] || break [[ -n $rest ]] || break
tok="${rest%% *}" tok="${rest%% *}"
case "$tok" in case "$tok" in
%[a-zA-Z]|@@|@@u) printf '%s\n' "$idx"; return 0 ;; %[a-zA-Z]|@@|@@u|--) printf '%s\n' "$idx"; return 0 ;;
esac esac
rest="${rest:${#tok}}" rest="${rest:${#tok}}"
idx=$(( idx + ${#tok} )) idx=$(( idx + ${#tok} ))
@@ -70,28 +109,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 +160,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,29 +176,31 @@ 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
# None exists yet. The last candidate is the wrapper's own fallback - the # None exists yet. The last candidate is the wrapper's own fallback (the
# generic electron-flags.conf rather than electron43-flags.conf - so # generic electron-flags.conf rather than electron43-flags.conf). So
# 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 +211,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 +235,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 +270,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 +290,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"
@@ -271,7 +324,7 @@ MCA_MARK_SHADOW='X-MCA-Generated'
MCA_MARK_INPLACE='X-MCA-Patched' MCA_MARK_INPLACE='X-MCA-Patched'
# _mca_desktop_transform <file> <marker> [flags] [position] # _mca_desktop_transform <file> <marker> [flags] [position]
# The whole file with every Exec line rewritten - the main one and the one in # The whole file with every Exec line rewritten: the main one and the one in
# each Desktop Action, because those are the right-click menu entries and a # each Desktop Action, because those are the right-click menu entries and a
# user who starts Steam from "Library" expects the same behaviour there. # user who starts Steam from "Library" expects the same behaviour there.
_mca_desktop_transform() { _mca_desktop_transform() {
@@ -312,8 +365,8 @@ mca_desktop_apply() {
local target="$MCA_APPDIR/$id.desktop" content backup local target="$MCA_APPDIR/$id.desktop" content backup
if [[ "$src" == "$target" ]]; then if [[ "$src" == "$target" ]]; then
# The user's own entry - an AppImage, a web app shortcut, something # The user's own entry, like an AppImage, a web app shortcut or
# installed by hand. There is nowhere to shadow it from, so it is # something installed by hand. There is nowhere to shadow it from, so it is
# edited directly and the original is kept. # edited directly and the original is kept.
grep -q "^$MCA_MARK_INPLACE=" "$src" 2>/dev/null && return 0 grep -q "^$MCA_MARK_INPLACE=" "$src" 2>/dev/null && return 0
@@ -335,7 +388,7 @@ mca_desktop_apply() {
mkdir -p "$MCA_APPDIR" 2>/dev/null || return 1 mkdir -p "$MCA_APPDIR" 2>/dev/null || return 1
# Something is already shadowing this entry. Unless it is a shadow of ours, # Something is already shadowing this entry. Unless it is a shadow of ours,
# it is the user's own file - possibly one we edited in place earlier - and # it is the user's own file (possibly one we edited in place earlier), and
# overwriting it here would lose it. # overwriting it here would lose it.
if [[ -e $target ]] && ! grep -q "^$MCA_MARK_SHADOW=" "$target" 2>/dev/null; then if [[ -e $target ]] && ! grep -q "^$MCA_MARK_SHADOW=" "$target" 2>/dev/null; then
return 1 return 1
@@ -359,17 +412,8 @@ mca_desktop_apply() {
# entry in the menu completely, which is why Discord launched at login used # entry in the menu completely, which is why Discord launched at login used
# to behave differently from Discord launched from the menu. # to behave differently from Discord launched from the menu.
# #
# The desktop folder, where a shortcut somebody dragged out of the menu - # The desktop folder, where a shortcut somebody dragged out of the menu
# or asked Steam for - lives and nowhere else. # (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, # Neither can be shadowed from anywhere, so both are edited where they stand,
# with the original kept. # with the original kept.
@@ -378,16 +422,27 @@ mca_desktop_apply() {
# One desktop entry that lives outside the XDG search path, edited where it is # One desktop entry that lives outside the XDG search path, edited where it is
# because there is nowhere to shadow it from. # because there is nowhere to shadow it from.
# #
# Anything that starts Steam gets Steam's own switch; a Chromium application # What decides whether an application here is in scope differs by where the
# gets the flag. What decides whether a Chromium application here is in scope # entry came from, which is what the gate says: an autostart entry follows the
# differs by where the entry came from, which is what the gate says: an # autostart setting, a shortcut follows the same rules as the application it is
# autostart entry follows the autostart setting, a shortcut follows the same # a shortcut to.
# rules as the application it is a shortcut to.
_mca_entry_patch_inplace() { _mca_entry_patch_inplace() {
local file="$1" gate="$2" local file="$1" gate="$2"
local id prog packaging=native content backup kind local id prog packaging=native content backup kind copy
grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null && return 0 # Already ours, and normally that is the end of it: what is in there is what
# this version put there. Not so for a file the ledger has no record of -
# nothing would ever look at that one again, neither the undo a change of
# flags goes through nor this, which leaves a marked file alone, and it would
# keep an answer from an older version for good. The copy taken before the
# edit is named after the path and is still there, so the file goes back to
# what it was and is written again below.
if grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null; then
mca_ledger_has "$file" && return 0
copy="$MCA_BACKUPDIR/$(mca_backup_name "$file")"
[[ -f $copy ]] || return 0
cp -p -- "$copy" "$file" 2>/dev/null || return 0
fi
_mca_desktop_read "$file" _mca_desktop_read "$file"
[[ -n $DE_EXEC ]] || return 0 [[ -n $DE_EXEC ]] || return 0
@@ -408,28 +463,29 @@ _mca_entry_patch_inplace() {
packaging=snap packaging=snap
fi fi
if mca_prog_is_steam "$prog"; then # An entry that starts Steam is left where it is. Steam takes nothing
[[ $CFG_STEAM == yes ]] || return 0 # from a command line that would help, and what it does take belongs to
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \ # the web helper script in its own installation.
"$MCA_STEAM_LAUNCH_FLAG" "$(mca_steam_flag_position "$packaging")")" mca_prog_is_steam "$prog" && return 0
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 else
case "$packaging" in id="${file##*/}"; id="${id%.desktop}"
flatpak) mca_flatpak_is_chromium "${prog#flatpak:}" || return 0 ;; mca_kind_wanted "$kind" "$id" "$packaging" || return 0
snap) mca_snap_is_chromium "${prog#snap:}" || return 0 ;;
*) mca_is_chromium "$prog" || return 0 ;;
esac
if [[ $gate == autostart ]]; then
[[ $CFG_AUTOSTART == yes ]] || return 0
else
id="${file##*/}"; id="${id%.desktop}"
mca_desktop_is_browser && kind=browser || kind=app
mca_kind_wanted "$kind" "$id" "$packaging" || return 0
fi
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE")"
fi fi
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \
"$(mca_flags "$kind")")"
[[ -n $content ]] || return 0 [[ -n $content ]] || return 0
[[ "$content" == "$(< "$file")" ]] && return 0 [[ "$content" == "$(< "$file")" ]] && return 0
@@ -517,7 +573,7 @@ mca_spotify_apply() {
# 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. A file that was edited where it stood and has # 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 # since been deleted is the same problem from the other side: its record would
# have `disable` put the file back. # have `disable` put the file back.
@@ -595,6 +651,10 @@ mca_revert_all() {
mca_steam_revert "$path" "$detail" \ mca_steam_revert "$path" "$detail" \
&& MCA_CHANGES=$(( MCA_CHANGES + 1 )) && MCA_CHANGES=$(( MCA_CHANGES + 1 ))
;; ;;
kwin)
mca_kde_paste_revert "$path" "$detail" \
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
;;
esac esac
done < <(tac "$MCA_LEDGER" 2>/dev/null) done < <(tac "$MCA_LEDGER" 2>/dev/null)
+61 -135
View File
@@ -2,41 +2,35 @@
# #
# Steam. # Steam.
# #
# Steam's interface is CEF, so the feature is there - but Steam builds the # Steam's interface is CEF, so the feature is there. But Steam builds the
# command line for its web helper itself and offers no way to add to it. The # command line for its web helper itself and offers no way to add to it. The
# only place to get an argument in is the shell script that starts the helper, # only place to get an argument in is the shell script that starts the helper,
# which lives inside Steam's own installation: # which lives inside Steam's own installation:
# #
# ~/.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 and timestamp rather than by content - and restores whatever differs. # 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 # So the patch is written to look untouched: the bytes the flag costs are taken
# back out of the script's comments and the timestamp is put back afterwards, # back out of the script's comments and the timestamp is put back afterwards,
# which leaves a file that is byte-for-byte the size Steam recorded and as old # which leaves a file that is byte-for-byte the size Steam recorded and as old
# as Steam left it. A client that verifies its files finds nothing to repair # 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 # and the flag survives however Steam was started: from the menu, from a game
# shortcut, from a launcher like Heroic, from a terminal. # shortcut, from a launcher like Heroic, or from a terminal.
# #
# That is the part that has to work, because there is no way to make every # A patch that changes the length is not an option, and no switch covers for
# possible way of starting Steam carry an argument. -noverifyfiles on the # one. Steam checks its files at a start it was given arguments for, but also
# launcher entries is the second line rather than the first: it covers the case # at the shutdown it runs itself. That one has no -noverifyfiles on its
# where the script has no comments left to pay for the flag and the patch has # command line, whatever the session was started with. What a single wrong
# to grow the file. The trade-off is real and belongs to the user, so it is a # length costs is the whole client package downloaded, extracted and installed
# switch of its own rather than part of the general application handling: with # again, and a client that quits at the end of it instead of coming up. So the
# verification off, Steam no longer repairs a damaged installation by itself. # flag either fits in the space the comments give back, or it is not written at
# # all: no autoscroll in the interface is a smaller thing than a client that
# A patch that does change the size still has to stay out of the way of a # reinstalls itself.
# 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.
MCA_STEAM_LAUNCH_FLAG='-noverifyfiles'
# Every place a Steam installation is known to live, resolved and de-duplicated # Every place a Steam installation is known to live, resolved and de-duplicated
# because ~/.steam/steam is normally a symlink into ~/.local/share. # because ~/.steam/steam is normally a symlink into ~/.local/share.
# #
@@ -88,24 +82,6 @@ mca_steam_installed() {
return 1 return 1
} }
# mca_steam_running
# Steam records its own process id beside its installation while it runs. The
# file outlives a crash, so the id is checked rather than believed.
mca_steam_running() {
local f pid
for f in \
"$HOME/.steam/steam.pid" \
"$HOME/.var/app/com.valvesoftware.Steam/.steam/steam.pid" \
"$HOME/snap/steam/common/.steam/steam.pid"
do
[[ -r $f ]] || continue
pid="$(< "$f")"
[[ $pid =~ ^[0-9]+$ ]] || continue
kill -0 "$pid" 2>/dev/null && return 0
done
return 1
}
# mca_steam_script_patched <script> # mca_steam_script_patched <script>
mca_steam_script_patched() { mca_steam_script_patched() {
grep -q -- "$MCA_FEATURE" "$1" 2>/dev/null grep -q -- "$MCA_FEATURE" "$1" 2>/dev/null
@@ -120,26 +96,6 @@ mca_steam_patched() {
return 1 return 1
} }
# mca_steam_deferred <script>
# Whether the patch is being held back rather than simply missing: the script
# was patched before, its own copy is back, and Steam is still running. See
# mca_steam_apply for why that is left alone.
mca_steam_deferred() {
[[ -e "$MCA_BACKUPDIR/$(mca_backup_name "$1")" ]] && mca_steam_running
}
# mca_steam_waiting
# The same question for the status screen, which has no script in hand.
mca_steam_waiting() {
local root script
while IFS= read -r root; do
script="$(mca_steam_script "$root")" || continue
mca_steam_script_patched "$script" && continue
mca_steam_deferred "$script" && return 0
done < <(mca_steam_roots)
return 1
}
# _mca_steam_exec_line <script> # _mca_steam_exec_line <script>
# The line number of the command that starts the web helper: the last # The line number of the command that starts the web helper: the last
# uncommented line that both names steamwebhelper and forwards "$@". Both the # uncommented line that both names steamwebhelper and forwards "$@". Both the
@@ -154,27 +110,25 @@ _mca_steam_exec_line() {
' "$1" 2>/dev/null ' "$1" 2>/dev/null
} }
# _mca_steam_build <script> <flags> <reclaim: 0|1> # _mca_steam_build <script> <flags>
# The patched script on stdout: the flags appended to the line that starts the # 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 # web helper, and the same number of bytes taken back out of the script's
# the script's comments, so that the file Steam finds is the length Steam wrote # comments, so that the file Steam finds is the length Steam wrote down.
# down. Everything else - setting up the container runtime, deciding whether # Everything else is left alone, like setting up the container runtime or
# the sandbox can be used - is left alone. # deciding whether the sandbox can be used.
# #
# Comments are eaten from the bottom up, so the header that says what the # 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 # 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. # 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. # Fails when the comments are too short to pay for the flags.
_mca_steam_build() { _mca_steam_build() {
local script="$1" flags="$2" reclaim="$3" lineno need=0 local script="$1" flags="$2" lineno need
lineno="$(_mca_steam_exec_line "$script")" lineno="$(_mca_steam_exec_line "$script")"
[[ -n $lineno ]] || return 1 [[ -n $lineno ]] || return 1
if (( reclaim )); then need="$(printf '%s' " $flags" | wc -c)"
need="$(printf '%s' " $flags" | wc -c)" need="${need//[^0-9]/}"
need="${need//[^0-9]/}"
fi
# LC_ALL=C so awk counts bytes: a comment with an accent in it is shorter # 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. # in characters than it is on disk, and it is the disk that Steam measures.
@@ -222,44 +176,50 @@ _mca_steam_refresh_backup() {
cp -p -- "$script" "$MCA_BACKUPDIR/$name" 2>/dev/null || true cp -p -- "$script" "$MCA_BACKUPDIR/$name" 2>/dev/null || true
} }
# _mca_steam_reneutralise <script> <flags> # _mca_steam_settle <script> <flags>
# An earlier version of this program patched by appending and left the script # The script is already patched. Whether that is a patch Steam can live with is
# longer than Steam expects, which is a patch that only survives because # a question of length: an earlier version of this program appended the flag
# -noverifyfiles is there to stop Steam looking. If the same flags can be made # where the comments could not pay for it, which leaves the script longer than
# to fit, they are: the file goes back to the length and the timestamp Steam # the file Steam wrote down. A client that checks its files reinstalls itself
# wrote down and stops depending on that switch. # over that, then quits instead of starting.
# #
# The new copy is built from the untouched original that was kept when the # Rebuilt from the untouched original kept when the script was first patched,
# script was first patched, never from the patched file - patching a patched # never from the patched file, the flag either fits this time or the original
# file is how a flag ends up in there twice. # goes back and Steam is left with a client it recognises.
_mca_steam_reneutralise() { _mca_steam_settle() {
local script="$1" flags="$2" name copy out local script="$1" flags="$2" name copy out
name="$(mca_backup_name "$script")" name="$(mca_backup_name "$script")"
copy="$MCA_BACKUPDIR/$name" copy="$MCA_BACKUPDIR/$name"
[[ -f $copy ]] || return 1
# Already the length Steam expects: there is nothing to gain. # Nothing to compare against, so nothing can be said about the length.
(( $(wc -c < "$script") == $(wc -c < "$copy") )) && return 1 [[ -f $copy ]] || return 0
out="$(_mca_steam_build "$copy" "$flags" 1)" || return 1 # Already the length Steam expects: the patch is a good one.
_mca_steam_same_size "$copy" "$out" || return 1 (( $(wc -c < "$script") == $(wc -c < "$copy") )) && return 0
if mca_write_if_changed "$script" "$out"$'\n'; then if out="$(_mca_steam_build "$copy" "$flags")" && _mca_steam_same_size "$copy" "$out"; then
MCA_CHANGES=$(( MCA_CHANGES + 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
fi fi
chmod +x -- "$script" 2>/dev/null || true
touch -r "$copy" -- "$script" 2>/dev/null || true mca_steam_revert "$script" "$name"
mca_ledger_add steam "$script" "$name" mca_ledger_forget "$script"
mca_note "$(mca_msg "Steam's interface cannot be given the flag without leaving a file Steam would reinstall; the change has been taken back.")"
return 0 return 0
} }
# mca_steam_apply # mca_steam_apply
# Puts the flags into every Steam installation that has the script, preferring # Puts the flags into every Steam installation that has the script, and only
# the patch that keeps the file's size and falling back to the one that does # where that can be done without changing how long the file is. Returns 1 when
# not. Returns 1 when there is no Steam to patch at all. # there is no Steam to patch at all.
mca_steam_apply() { mca_steam_apply() {
local root script backup flags found=0 out neutral local root script backup flags found=0 out
flags="$(mca_flags)" flags="$(mca_flags)"
@@ -268,35 +228,21 @@ mca_steam_apply() {
found=1 found=1
if mca_steam_script_patched "$script"; then if mca_steam_script_patched "$script"; then
_mca_steam_reneutralise "$script" "$flags" || true _mca_steam_settle "$script" "$flags"
continue continue
fi fi
_mca_steam_refresh_backup "$script" _mca_steam_refresh_backup "$script"
neutral=1 # Either the comments can pay for the flag or nothing is written.
if ! { out="$(_mca_steam_build "$script" "$flags" 1)" \ # 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"; } && _mca_steam_same_size "$script" "$out"; }
then then
neutral=0 mca_note "$(mca_msg "Steam starts its interface in a way this version cannot change without Steam noticing; leaving it alone.")"
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.")"
continue continue
fi fi
@@ -331,7 +277,7 @@ mca_steam_revert() {
return 0 return 0
fi fi
# No backup to fall back on - take the flags back out of the exec line. # No backup to fall back on, so take the flags back out of the exec line.
[[ -f $script ]] || return 1 [[ -f $script ]] || return 1
local tmp local tmp
tmp="$(mktemp "${script}.XXXXXX")" || return 1 tmp="$(mktemp "${script}.XXXXXX")" || return 1
@@ -339,23 +285,3 @@ mca_steam_revert() {
chmod +x -- "$script" 2>/dev/null || true chmod +x -- "$script" 2>/dev/null || true
return 0 return 0
} }
# 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
# them, so the launcher entry carries that switch.
mca_steam_desktop_apply() {
mca_desktop_apply "$1" "$2" "$MCA_STEAM_LAUNCH_FLAG" \
"$(mca_steam_flag_position "${3:-native}")"
}
+4 -8
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# middleclick-autoscroll - middle-click autoscroll for Chromium-based # middleclick-autoscroll: middle-click autoscroll for Chromium-based
# applications on Linux # applications on Linux
# #
# Blink has had Windows-style autoscroll for years, behind a runtime flag that # 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 # 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 # 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 # 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 # 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.
@@ -17,7 +17,7 @@ set -uo pipefail
MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}" MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}"
for _mod in common config detect patch steam apply menu; do for _mod in common config detect patch steam kde apply menu; do
# shellcheck source=/dev/null # shellcheck source=/dev/null
if ! source "$MCA_LIBDIR/$_mod.sh"; then if ! source "$MCA_LIBDIR/$_mod.sh"; then
printf 'middleclick-autoscroll: cannot load %s/%s.sh\n' "$MCA_LIBDIR" "$_mod" >&2 printf 'middleclick-autoscroll: cannot load %s/%s.sh\n' "$MCA_LIBDIR" "$_mod" >&2
@@ -50,16 +50,12 @@ mca_do_enable() {
if mca_watch_available; then if mca_watch_available; then
mca_watch_enable || mca_note "$(mca_msg "Could not start the watcher for newly installed applications.")" mca_watch_enable || mca_note "$(mca_msg "Could not start the watcher for newly installed applications.")"
else else
mca_note "$(mca_msg "No systemd user session - newly installed applications will not be picked up on their own.")" mca_note "$(mca_msg "No systemd user session, so newly installed applications will not be picked up on their own.")"
fi fi
fi fi
mca_ok "$(mca_msg "Autoscroll is on for %d applications." "$MCA_N_ON")" mca_ok "$(mca_msg "Autoscroll is on for %d applications." "$MCA_N_ON")"
if [[ $CFG_STEAM == yes ]] && mca_steam_installed; then
mca_note "$(mca_msg "Steam's launcher now skips its file check, which is what keeps the change from being undone at every start.")"
fi
mca_note "$(mca_msg "Applications that are already running pick this up when they are restarted.")" mca_note "$(mca_msg "Applications that are already running pick this up when they are restarted.")"
} }