Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af82ffec78 | ||
|
|
b7e0ff6fd1 | ||
|
|
ced6f57dfd | ||
|
|
21e29d99c3 | ||
|
|
c31c7b92e5 | ||
|
|
0bbf0cbd9f | ||
|
|
70f6748f3c | ||
|
|
aaa41f0384 | ||
|
|
a053b4e555 | ||
|
|
36498f2acf | ||
|
|
bd84c6645a | ||
|
|
f42d80598f | ||
|
|
4004023297 | ||
|
|
9896260636 | ||
|
|
5b1fc84710 |
No files matched your search
@@ -0,0 +1,28 @@
|
||||
name: check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: syntax and shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# The Makefile skips whatever is missing rather than failing, so the
|
||||
# tools it looks for have to be here or the check checks less than it
|
||||
# appears to.
|
||||
- name: Install the tools the Makefile looks for
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
shellcheck gettext scdoc
|
||||
|
||||
- run: make check
|
||||
|
||||
- name: Build the catalogs and the man page
|
||||
run: make build
|
||||
@@ -0,0 +1,256 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: >-
|
||||
Build the repositories with a throwaway key and install from them,
|
||||
without publishing anything. This is how the release path gets
|
||||
exercised without cutting a tag.
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deb:
|
||||
name: Debian package
|
||||
runs-on: ubuntu-latest
|
||||
container: debian:stable
|
||||
steps:
|
||||
- name: Install the build tools
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates git make gettext scdoc dpkg-dev
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Check the tag against the Makefile
|
||||
run: packaging/check-version.sh "${{ github.ref_name }}"
|
||||
|
||||
- run: packaging/build-deb.sh
|
||||
|
||||
- name: Look inside what was built
|
||||
run: |
|
||||
dpkg-deb --info dist/*.deb
|
||||
dpkg-deb --contents dist/*.deb
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: deb
|
||||
path: dist/*.deb
|
||||
if-no-files-found: error
|
||||
|
||||
rpm:
|
||||
name: RPM package
|
||||
runs-on: ubuntu-latest
|
||||
container: fedora:latest
|
||||
steps:
|
||||
- name: Install the build tools
|
||||
run: |
|
||||
dnf install -y --setopt=install_weak_deps=False \
|
||||
git make gettext scdoc tar rpm-build rpm-sign systemd-rpm-macros
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Check the tag against the Makefile
|
||||
run: packaging/check-version.sh "${{ github.ref_name }}"
|
||||
|
||||
- run: packaging/build-rpm.sh
|
||||
|
||||
# Signed here rather than alongside the APT repository, because this is
|
||||
# the one place with a native rpm-sign. A dry run signs with a key it
|
||||
# makes on the spot, so the command itself is still exercised.
|
||||
- name: Sign the package
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
gpg --batch --passphrase '' --quick-generate-key \
|
||||
'dry run <dry-run@example.invalid>' rsa2048 sign never
|
||||
elif [ -n "${GPG_PRIVATE_KEY:-}" ]; then
|
||||
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
else
|
||||
echo "No GPG_PRIVATE_KEY secret; leaving the package unsigned."
|
||||
exit 0
|
||||
fi
|
||||
keyid="$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/ { print $5; exit }')"
|
||||
rpmsign --define "_gpg_name $keyid" --addsign dist/*.rpm
|
||||
gpg --armor --export "$keyid" > dist/rpm-signer.asc
|
||||
rpm --import dist/rpm-signer.asc
|
||||
rpm --checksig dist/*.rpm
|
||||
|
||||
- name: Look inside what was built
|
||||
run: |
|
||||
rpm -qip dist/*.rpm
|
||||
rpm -qlp dist/*.rpm
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: rpm
|
||||
path: |
|
||||
dist/*.rpm
|
||||
dist/rpm-signer.asc
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Release and repositories
|
||||
needs: [deb, rpm]
|
||||
if: startsWith(github.ref, 'refs/tags/v') || inputs.dry_run
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: incoming
|
||||
merge-multiple: true
|
||||
|
||||
- name: Attach the packages to the release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release create "${{ github.ref_name }}" \
|
||||
--title "${{ github.ref_name }}" \
|
||||
--generate-notes \
|
||||
incoming/*.deb incoming/*.rpm \
|
||||
|| gh release upload "${{ github.ref_name }}" \
|
||||
incoming/*.deb incoming/*.rpm --clobber
|
||||
|
||||
- name: Install the repository tools
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
dpkg-dev apt-utils createrepo-c
|
||||
|
||||
- name: Get a signing key
|
||||
id: key
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
gpg --batch --passphrase '' --quick-generate-key \
|
||||
'dry run <dry-run@example.invalid>' rsa2048 sign never
|
||||
elif [ -n "${GPG_PRIVATE_KEY:-}" ]; then
|
||||
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
else
|
||||
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."
|
||||
exit 0
|
||||
fi
|
||||
echo "present=yes" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check out the published repositories
|
||||
if: steps.key.outputs.present == 'yes' && !inputs.dry_run
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: gh-pages
|
||||
path: pages
|
||||
continue-on-error: true
|
||||
|
||||
- name: Update the repositories
|
||||
if: steps.key.outputs.present == 'yes'
|
||||
run: |
|
||||
# First release, or a dry run: there is no branch to start from.
|
||||
if [ ! -d pages/.git ]; then
|
||||
rm -rf pages && mkdir pages
|
||||
git -C pages init -q -b gh-pages
|
||||
git -C pages remote add origin "https://github.com/${{ github.repository }}.git"
|
||||
fi
|
||||
rm -f incoming/rpm-signer.asc
|
||||
packaging/publish-repos.sh pages incoming
|
||||
|
||||
- name: Check that what was written can be verified
|
||||
if: steps.key.outputs.present == 'yes'
|
||||
run: |
|
||||
find pages -type f -not -path '*/.git/*' | sort
|
||||
echo '--- Release ---'; cat pages/deb/Release
|
||||
echo '--- Packages ---'; cat pages/deb/Packages
|
||||
gpg --verify pages/deb/InRelease
|
||||
gpg --verify pages/deb/Release.gpg pages/deb/Release
|
||||
gpg --verify pages/rpm/repodata/repomd.xml.asc pages/rpm/repodata/repomd.xml
|
||||
|
||||
# The proof that the repository works is apt reading it: the signature,
|
||||
# the index, the dependencies and the program that comes out the far end.
|
||||
- name: Install from the repository that was just built
|
||||
if: inputs.dry_run
|
||||
run: |
|
||||
sudo install -d -m 0755 /etc/apt/keyrings
|
||||
sudo gpg --dearmor -o /etc/apt/keyrings/middleclick-autoscroll.gpg < pages/KEY.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/middleclick-autoscroll.gpg] file://$PWD/pages/deb ./" \
|
||||
| sudo tee /etc/apt/sources.list.d/middleclick-autoscroll.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y middleclick-autoscroll
|
||||
middleclick-autoscroll --version
|
||||
middleclick-autoscroll list
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: inputs.dry_run
|
||||
with:
|
||||
name: pages
|
||||
path: pages
|
||||
include-hidden-files: true
|
||||
|
||||
- name: Push them
|
||||
if: steps.key.outputs.present == 'yes' && !inputs.dry_run
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
cd pages
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add -A
|
||||
git diff --quiet --cached && { echo "nothing changed"; exit 0; }
|
||||
git commit -q -m "Publish ${{ github.ref_name }}"
|
||||
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" HEAD:gh-pages
|
||||
|
||||
verify-dnf:
|
||||
name: Install from the RPM repository
|
||||
needs: publish
|
||||
if: inputs.dry_run
|
||||
runs-on: ubuntu-latest
|
||||
container: fedora:latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: pages
|
||||
path: pages
|
||||
|
||||
# A real run signs the package and the repository metadata with the one
|
||||
# key from the secret. A dry run has no secret, so each job made a key of
|
||||
# its own and both public halves are needed to check both signatures.
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: rpm
|
||||
path: signer
|
||||
|
||||
- name: Install from the repository that was just built
|
||||
run: |
|
||||
rpm --import pages/KEY.gpg
|
||||
rpm --import signer/rpm-signer.asc
|
||||
cat > /etc/yum.repos.d/middleclick-autoscroll.repo <<EOF
|
||||
[middleclick-autoscroll]
|
||||
name=middleclick-autoscroll
|
||||
baseurl=file://$PWD/pages/rpm
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
repo_gpgcheck=1
|
||||
gpgkey=file://$PWD/pages/KEY.gpg
|
||||
EOF
|
||||
# util-linux is for runuser below; the base image does not carry it,
|
||||
# and util-linux-core is not the half that has it.
|
||||
dnf install -y middleclick-autoscroll util-linux
|
||||
|
||||
# As somebody, not as root: running it as root is refused, which is
|
||||
# the point of it, and a container is root by default.
|
||||
useradd -m tester
|
||||
runuser -u tester -- middleclick-autoscroll --version
|
||||
runuser -u tester -- middleclick-autoscroll list
|
||||
@@ -1,3 +1,6 @@
|
||||
# build artifacts
|
||||
po/*.mo
|
||||
doc/*.1
|
||||
|
||||
# packages
|
||||
dist/
|
||||
@@ -8,7 +8,7 @@
|
||||
# Overridable so a packager can pass the version it is actually building
|
||||
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
|
||||
# straight from a checkout, and is what a release tag has to carry.
|
||||
VERSION ?= 1.0.0
|
||||
VERSION ?= 1.2.0
|
||||
|
||||
PREFIX ?= /usr
|
||||
DESTDIR ?=
|
||||
@@ -17,7 +17,20 @@ DATADIR ?= $(PREFIX)/share
|
||||
LIBDIR ?= $(DATADIR)/middleclick-autoscroll/lib
|
||||
LOCALEDIR ?= $(DATADIR)/locale
|
||||
MANDIR ?= $(DATADIR)/man
|
||||
|
||||
# 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
|
||||
# distribution that still keeps /lib separate from /usr/lib says so here - and
|
||||
# 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.
|
||||
# systemd searches $(PREFIX)/lib/systemd/user as well, and a file outside the
|
||||
# prefix it was asked for is not this build's to place.
|
||||
ifeq ($(PREFIX),/usr)
|
||||
USERUNITDIR ?= $(shell pkg-config --variable=systemduserunitdir systemd 2>/dev/null || echo /usr/lib/systemd/user)
|
||||
else
|
||||
USERUNITDIR ?= $(PREFIX)/lib/systemd/user
|
||||
endif
|
||||
|
||||
LINGUAS := de
|
||||
MOFILES := $(patsubst %,po/%.mo,$(LINGUAS))
|
||||
@@ -28,12 +41,18 @@ LIBS := $(wildcard src/lib/*.sh)
|
||||
MSGFMT := $(shell command -v msgfmt 2>/dev/null)
|
||||
SCDOC := $(shell command -v scdoc 2>/dev/null)
|
||||
|
||||
.PHONY: all build install uninstall check clean
|
||||
.PHONY: all build install uninstall check clean version
|
||||
|
||||
all: build
|
||||
|
||||
build: $(MOFILES) $(MANPAGE)
|
||||
|
||||
# The one place the version is written down, for everything that has to agree
|
||||
# with it: the packaging scripts, and the release workflow checking that the
|
||||
# tag it was handed says the same thing.
|
||||
version:
|
||||
@echo $(VERSION)
|
||||
|
||||
po/%.mo: po/%.po
|
||||
ifdef MSGFMT
|
||||
$(MSGFMT) --check --output-file=$@ $<
|
||||
|
||||
@@ -1,181 +1,103 @@
|
||||
# middleclick-autoscroll
|
||||
|
||||
Middle-click autoscroll — hold the middle mouse button, move the pointer, the
|
||||
page scrolls — for every Chromium-based application on the system. Discord,
|
||||
Vesktop, Equibop, VS Code, Obsidian, Signal, Spotify, Steam, Chromium-based
|
||||
browsers, and whatever gets installed next week.
|
||||
Middle-click autoscroll for Linux. Hold the middle mouse button, move the
|
||||
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
|
||||
out what's Chromium-based, and handles it.
|
||||
|
||||
## Installing
|
||||
|
||||
**Arch, CachyOS, EndeavourOS, Manjaro**
|
||||
|
||||
```bash
|
||||
paru -S middleclick-autoscroll
|
||||
middleclick-autoscroll enable
|
||||
```
|
||||
|
||||
That is the whole setup. Nothing else has to be configured, and no file has to
|
||||
be edited.
|
||||
**Debian, Ubuntu, Linux Mint, Pop!_OS**
|
||||
|
||||
## Why this needs a program at all
|
||||
```bash
|
||||
sudo install -d -m 0755 /etc/apt/keyrings
|
||||
curl -fsSL https://felitendo.github.io/middleclick-autoscroll/KEY.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 ./" \
|
||||
| sudo tee /etc/apt/sources.list.d/middleclick-autoscroll.list
|
||||
sudo apt update && sudo apt install middleclick-autoscroll
|
||||
```
|
||||
|
||||
Blink — the engine inside Chromium, Electron and CEF — has had Windows-style
|
||||
autoscroll for years. On Linux it is switched off, because middle click is
|
||||
already taken by primary-selection paste. One command line argument turns it
|
||||
back on:
|
||||
**Fedora, RHEL, CentOS Stream**
|
||||
|
||||
```bash
|
||||
sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \
|
||||
https://felitendo.github.io/middleclick-autoscroll/middleclick-autoscroll.repo
|
||||
sudo dnf install middleclick-autoscroll
|
||||
```
|
||||
|
||||
**openSUSE**
|
||||
|
||||
```bash
|
||||
sudo rpm --import https://felitendo.github.io/middleclick-autoscroll/KEY.gpg
|
||||
sudo zypper addrepo --gpgcheck --refresh \
|
||||
https://felitendo.github.io/middleclick-autoscroll/rpm middleclick-autoscroll
|
||||
sudo zypper install middleclick-autoscroll
|
||||
```
|
||||
|
||||
Then run `middleclick-autoscroll enable`. That's it — nothing else to configure.
|
||||
|
||||
Updates come through your package manager like anything else. Without a package,
|
||||
build [from source](#building-from-source) or grab a `.deb`/`.rpm` from the
|
||||
[releases page](https://github.com/Felitendo/middleclick-autoscroll/releases).
|
||||
|
||||
Run `middleclick-autoscroll disable` before removing the package — it undoes
|
||||
everything.
|
||||
|
||||
## How it works
|
||||
|
||||
Blink (the engine in Chromium, Electron, and CEF) already has autoscroll, but
|
||||
it's off on Linux because middle click does primary-selection paste there. One
|
||||
flag turns it on:
|
||||
|
||||
```
|
||||
--enable-blink-features=MiddleClickAutoscroll
|
||||
```
|
||||
|
||||
Getting that argument into one application is a five-minute job. Getting it into
|
||||
all of them, in a way that survives the next package upgrade, is not:
|
||||
Doing that for one app is a five-minute job. Doing it for *all* of them — across
|
||||
flag files, desktop entries, Flatpaks, snaps, autostart entries, Steam — so it
|
||||
survives upgrades, is not. That's what this does.
|
||||
|
||||
- Some applications read a flag file, some don't.
|
||||
- Some ship their own copy of Electron, some use the system one.
|
||||
- Flatpaks see none of the host's configuration.
|
||||
- An application that starts itself at login uses a different entry than the one
|
||||
in the menu, and Discord launched at login used to behave differently from
|
||||
Discord launched by hand.
|
||||
- Steam takes no arguments for its interface at all, and puts back any file you
|
||||
change.
|
||||
- Every upgrade can undo the lot.
|
||||
|
||||
## The interface
|
||||
|
||||
`middleclick-autoscroll` on its own:
|
||||
|
||||
```
|
||||
Middle-Click Autoscroll
|
||||
|
||||
Autoscroll ON
|
||||
|
||||
Applications covered 14 of 15
|
||||
Not identified 1 - see the applications list
|
||||
Steam ON
|
||||
New applications ON
|
||||
Last applied 3 minutes ago
|
||||
|
||||
Applications pick this up the next time they are started.
|
||||
|
||||
[1] Turn autoscroll on or off
|
||||
[2] Apply now
|
||||
[3] Applications
|
||||
[4] Settings
|
||||
[q] Quit
|
||||
```
|
||||
|
||||
**[3] Applications** lists everything that was found, how each one is handled,
|
||||
and lets a single application be switched off — or an unrecognised one switched
|
||||
on — with the space bar:
|
||||
|
||||
```
|
||||
Applications
|
||||
|
||||
▸ Vesktop on (flag file)
|
||||
Discord on (flag file)
|
||||
Code - OSS on (flag file)
|
||||
Obsidian on (flag file)
|
||||
Signal on (flag file)
|
||||
Spotify on (launcher)
|
||||
Steam on (Steam)
|
||||
Cursor cannot tell
|
||||
```
|
||||
|
||||
**[4] Settings** has the categories — Electron and CEF applications, browsers,
|
||||
Flatpaks, autostart entries, Steam, Spotify, whether to watch for new
|
||||
applications, and a field for extra Chromium arguments if you want any.
|
||||
|
||||
There is a configuration file behind all of this. You are never asked to open
|
||||
it.
|
||||
|
||||
## Where the argument actually goes
|
||||
|
||||
Two routes, picked per application.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Flag file** | Arch's Electron and Chromium wrappers read extra arguments from `~/.config/<name>-flags.conf`. This is the good one: it is the supported way to pass arguments, it survives package upgrades untouched, and it applies to a launch from a terminal as much as one from the menu. |
|
||||
| **Desktop entry** | For applications that ship their own binary with no wrapper, and for Flatpaks, a copy of the entry with the argument appended goes into `~/.local/share/applications`, where it shadows the system one. |
|
||||
|
||||
Entries that already live in `~/.local/share/applications` — AppImages, web app
|
||||
shortcuts — are edited in place and the original is kept. So are the entries in
|
||||
`~/.config/autostart`, so an application that starts itself at login gets the
|
||||
same treatment as one started from the menu.
|
||||
|
||||
An `--enable-blink-features` that is already there is **extended**, never
|
||||
repeated. Chromium keeps only the last occurrence of that option, so a second
|
||||
one would silently switch off whatever the first one enabled.
|
||||
New apps are picked up within a second by a systemd path unit that watches the
|
||||
relevant directories. Without systemd, `middleclick-autoscroll apply` does the
|
||||
same thing manually.
|
||||
|
||||
## Steam
|
||||
|
||||
Steam's interface is CEF and supports the feature perfectly well, but Steam
|
||||
builds the command line for its web helper itself and offers no way to add to
|
||||
it. The only place an argument fits is the script that starts the helper, inside
|
||||
Steam's own installation:
|
||||
Steam's web UI supports autoscroll but has no way to pass extra arguments to its
|
||||
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.
|
||||
|
||||
```
|
||||
~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
```
|
||||
|
||||
Steam checksums that script at every start and restores it when it differs, so
|
||||
its launcher entry also gets `-noverifyfiles`. **The trade-off is real**: with
|
||||
verification off, Steam no longer repairs a damaged installation by itself. That
|
||||
is why Steam is a switch of its own rather than part of the general handling —
|
||||
turn it off in the settings and Steam is left completely alone.
|
||||
|
||||
The script comes back on every client update. The watcher notices and puts the
|
||||
patch back.
|
||||
|
||||
Starting Steam from a terminal without `-noverifyfiles` undoes it for that one
|
||||
session; the next start from the menu has it again.
|
||||
|
||||
## Applications installed later
|
||||
|
||||
A systemd user path unit watches every directory a launcher can appear in —
|
||||
`/usr/share/applications`, the Flatpak exports, `~/.local/share/applications`,
|
||||
`~/.config/autostart` — plus Steam's helper script. Anything new is handled
|
||||
within a second of being installed, whether it came from pacman, the AUR,
|
||||
Flatpak or an AppImage manager. There is no hook to install per package manager.
|
||||
|
||||
## What it will not guess
|
||||
|
||||
An AppImage keeps its payload in a compressed filesystem, so there is no way to
|
||||
tell from the outside whether Chromium is in there. Those show up as **cannot
|
||||
tell** and are left alone until you switch them on from the applications screen.
|
||||
|
||||
Detection is deliberately conservative everywhere else too. A wrong "yes" would
|
||||
append an unknown argument to something that is not Chromium, and plenty of
|
||||
programs treat an unrecognised argument as a file name to open.
|
||||
|
||||
## Undoing it
|
||||
|
||||
```bash
|
||||
middleclick-autoscroll disable
|
||||
```
|
||||
|
||||
Every change is recorded in a ledger as it is made, and `disable` replays it
|
||||
backwards: generated entries are deleted, edited files are restored from their
|
||||
backups, flag files that only ever contained our line are removed, and a flag
|
||||
file that was merged into loses exactly the one feature that was added to it.
|
||||
Files that were not touched by this program are not touched by it now either.
|
||||
|
||||
Run this before uninstalling the package.
|
||||
As a fallback, for the case where that isn't possible, `-noverifyfiles` goes on
|
||||
everything that starts Steam: its launcher, its autostart entry, and the
|
||||
shortcuts it writes for single games. That one means Steam won't auto-repair
|
||||
damaged files on its own — you can turn it off separately in the settings.
|
||||
|
||||
## Commands
|
||||
|
||||
| | |
|
||||
| Command | |
|
||||
|---|---|
|
||||
| `middleclick-autoscroll` | the menu above |
|
||||
| `… enable` | turn it on, apply, start watching |
|
||||
| `… disable` | turn it off and put everything back |
|
||||
| `… apply` | apply to anything new (this is what the watcher calls) |
|
||||
| `… status` | what is covered |
|
||||
| `… list` | every application that was found and how it is handled |
|
||||
| `middleclick-autoscroll` | Interactive menu |
|
||||
| `… enable` | Turn on, apply, start watching |
|
||||
| `… disable` | Undo everything |
|
||||
| `… apply` | Apply to new apps |
|
||||
| `… apply --rebuild` | Redo from scratch |
|
||||
| `… status` | What's covered |
|
||||
| `… list` | All apps and how they're handled |
|
||||
|
||||
See `man middleclick-autoscroll` for the details.
|
||||
|
||||
## Requirements
|
||||
|
||||
Arch or an Arch derivative (CachyOS, EndeavourOS, Manjaro), bash, systemd for
|
||||
the watcher. Nothing outside your home directory is ever written to, and running
|
||||
it as root is refused.
|
||||
See `man middleclick-autoscroll` for more.
|
||||
|
||||
## Building from source
|
||||
|
||||
@@ -184,7 +106,12 @@ make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
`make check` runs `bash -n` and, if installed, `shellcheck` over every script.
|
||||
Optionally needs `msgfmt` (gettext) for translations and `scdoc` for the man
|
||||
page. Supports `PREFIX` and `DESTDIR`. `make check` runs syntax checks and
|
||||
shellcheck.
|
||||
|
||||
See [packaging/README.md](packaging/README.md) for release builds and repo
|
||||
signing.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ middleclick-autoscroll(1)
|
||||
|
||||
# NAME
|
||||
|
||||
middleclick-autoscroll - middle-click autoscroll for Chromium-based applications
|
||||
middleclick-autoscroll - middle-click autoscroll in every application that supports it
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
@@ -18,7 +18,9 @@ on the command line.
|
||||
|
||||
*middleclick-autoscroll* finds every Chromium-based application on the system
|
||||
and puts that argument somewhere the application will actually read it, then
|
||||
keeps doing so for anything installed later.
|
||||
keeps doing so for anything installed later. It works on any distribution:
|
||||
which of the routes below an application takes is read off its launcher, not
|
||||
assumed from where the launcher came from.
|
||||
|
||||
Run without a command it shows an interactive menu. Everything it can be told
|
||||
is reachable from there; the configuration file behind it does not need to be
|
||||
@@ -36,10 +38,17 @@ is refused.
|
||||
*disable*
|
||||
Turn it off and put every file that was changed back the way it was.
|
||||
|
||||
*apply*
|
||||
*apply* [*--rebuild*]
|
||||
Apply to anything that has appeared since the last run. This is what the
|
||||
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
|
||||
removed here too - the entry shadowing it lives in the user's home, where
|
||||
the package manager that removed the application cannot see it.
|
||||
|
||||
With *--rebuild* everything is taken back first and written again from
|
||||
scratch. That is the repair: it does not care what looks correct already.
|
||||
|
||||
*status*
|
||||
How many applications are covered, whether Steam is patched, and when the
|
||||
last run was.
|
||||
@@ -59,7 +68,7 @@ is refused.
|
||||
Two ways, chosen per application.
|
||||
|
||||
*Flag file*
|
||||
Arch's Electron and Chromium wrappers read extra arguments from
|
||||
Where the launcher reads extra arguments from
|
||||
_$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
|
||||
applies to a launch from a terminal as much as one from the menu. An
|
||||
@@ -67,19 +76,39 @@ Two ways, chosen per application.
|
||||
rather than duplicated - Chromium keeps only the last occurrence of that
|
||||
option, so a second one would switch the first one off.
|
||||
|
||||
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
|
||||
launcher does is read off the launcher itself, never assumed from the
|
||||
distribution: only one that really names such a file takes this route.
|
||||
|
||||
*Desktop entry*
|
||||
For applications that ship their own binary with no wrapper, and for
|
||||
Flatpaks, a copy of the desktop entry with the argument appended is written
|
||||
to _~/.local/share/applications_, where it shadows the system one. Entries
|
||||
that already live there - AppImages, web app shortcuts - are edited in
|
||||
place, with the original kept. Every generated entry is marked with
|
||||
*X-MCA-Generated*; an entry that is not marked is somebody else's and is
|
||||
left alone.
|
||||
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
|
||||
shadows the system one. Entries that already live there - AppImages, web
|
||||
app shortcuts - are edited in place, with the original kept.
|
||||
|
||||
This is the route everything takes on the distributions whose Chromium
|
||||
wrappers keep their equivalent file under _/etc_, where it is the system's
|
||||
to write and not the user's: Debian, Ubuntu, Fedora and openSUSE among
|
||||
them.
|
||||
|
||||
A generated entry is marked *X-MCA-Generated* and an entry edited in place
|
||||
*X-MCA-Patched*. The two are never confused: the first is deleted when
|
||||
undoing, the second is restored from its backup. An entry with neither
|
||||
marker belongs to somebody else and is left alone.
|
||||
|
||||
Programs that start themselves at login write their own entry into
|
||||
_~/.config/autostart_ pointing straight at their binary, bypassing the menu
|
||||
entry entirely. Those are patched in place as well.
|
||||
|
||||
Shortcuts on the desktop itself are patched in place too. Nothing in the XDG
|
||||
search path looks at that folder, so a shortcut that lives only there would
|
||||
otherwise be invisible - and Steam puts one there for every game somebody asks
|
||||
for a shortcut to. The folder's name is translated, and the name in use is read
|
||||
from _~/.config/user-dirs.dirs_ rather than guessed; the watcher is told about
|
||||
it in a drop-in written when autoscroll is turned on.
|
||||
|
||||
# STEAM
|
||||
|
||||
Steam's interface is CEF and supports the feature, but Steam builds the command
|
||||
@@ -88,22 +117,50 @@ helper, inside Steam's own installation:
|
||||
|
||||
~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
|
||||
Steam checksums that script at every start and restores it when it differs, so
|
||||
its launcher entry also gets *-noverifyfiles*. The trade-off 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.
|
||||
Where that installation is depends on how Steam was installed:
|
||||
_~/.local/share/Steam_ for Valve's own package and Arch's,
|
||||
_~/.steam/debian-installation_ for Debian's, and the private tree of the
|
||||
sandbox for the Flatpak and the snap. All of them are looked at.
|
||||
|
||||
Starting Steam from a terminal without *-noverifyfiles* undoes the patch for
|
||||
that session. The watcher notices and puts it back, so the next start has it
|
||||
again.
|
||||
Steam compares the installed files against its manifest at every start - by
|
||||
size and timestamp rather than by content - and restores whatever differs. So
|
||||
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,
|
||||
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
|
||||
however Steam was started - from the menu, from a game shortcut, from a
|
||||
launcher like Heroic or Lutris, from a terminal.
|
||||
|
||||
That is what has to work, because there is no way to make every possible way of
|
||||
starting Steam carry an argument. *-noverifyfiles* is the second line rather
|
||||
than the first: it covers the case where the script has no comments left to pay
|
||||
for the argument and the patch has to grow the file. It goes on Steam's
|
||||
launcher entry, on its entry in _~/.config/autostart_, which Steam writes as
|
||||
soon as it is set to run at login, and on the shortcuts Steam writes for single
|
||||
games, on the desktop and in the menu alike. A game is not an application this
|
||||
program has anything to offer and none of them is listed under *Applications*,
|
||||
but starting one is a Steam start like any other. The trade-off of the switch
|
||||
is that Steam no longer repairs a damaged installation on its own; that is why
|
||||
Steam is a switch of its own in the settings.
|
||||
|
||||
A patch that does change the size is still held back while the client is
|
||||
running: Steam puts its own copy back, the two would only undo each other, and
|
||||
the helper is started once, at the start, so patching again would not help that
|
||||
session anyway. It goes in at the next apply with Steam closed. A patch that
|
||||
keeps the size has nothing to wait for and goes in either way.
|
||||
|
||||
A client update brings a new version of the script. The watcher notices and
|
||||
patches it again, and the copy kept for undoing is replaced with the new
|
||||
version at the same time, so undoing puts back the script Steam last shipped
|
||||
rather than the one from before the update.
|
||||
|
||||
# SPOTIFY
|
||||
|
||||
The official client is CEF rather than Electron. Installed through
|
||||
*spotify-launcher*, it is started by a program that builds its own command line
|
||||
and has a configuration file with a slot for extra arguments; that slot is
|
||||
where the flag goes. Installed as a plain package, it is an ordinary desktop
|
||||
entry and needs nothing special.
|
||||
where the flag goes. Installed as a plain package, a Flatpak or a snap, it is
|
||||
an ordinary desktop entry and needs nothing special.
|
||||
|
||||
# WHAT CANNOT BE DETECTED
|
||||
|
||||
@@ -132,9 +189,15 @@ _~/.cache/middleclick-autoscroll/detect_
|
||||
*NO_COLOR*
|
||||
Disables colour.
|
||||
|
||||
# REQUIREMENTS
|
||||
|
||||
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
|
||||
up at the next *apply* rather than on their own.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
*systemctl*(1), *flatpak*(1)
|
||||
*systemctl*(1), *flatpak*(1), *snap*(8)
|
||||
|
||||
# AUTHORS
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Packaging and releases
|
||||
|
||||
The Makefile installs everything; these only wrap what it produced. That is
|
||||
deliberate — a packaging script that lists the files again is a second
|
||||
description of the layout, and two descriptions drift.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `deb/control`, `deb/copyright` | metadata for the Debian binary package |
|
||||
| `rpm/middleclick-autoscroll.spec` | the RPM spec |
|
||||
| `build-deb.sh`, `build-rpm.sh` | build one package into `dist/` |
|
||||
| `check-version.sh` | refuses a tag that disagrees with the Makefile |
|
||||
| `publish-repos.sh` | regenerates the APT and RPM repositories |
|
||||
| `pages/` | the landing page and the `.repo` file served from GitHub Pages |
|
||||
|
||||
Neither package carries a maintainer script. The units are enabled per user by
|
||||
the program itself, and there is nothing to do as root at install time.
|
||||
|
||||
## Building one by hand
|
||||
|
||||
```bash
|
||||
packaging/build-deb.sh # needs dpkg-dev, gettext, scdoc
|
||||
packaging/build-rpm.sh # needs rpm-build, gettext, scdoc, systemd-rpm-macros
|
||||
```
|
||||
|
||||
Both take the version from `make version` unless one is passed as the first
|
||||
argument.
|
||||
|
||||
## Making a release
|
||||
|
||||
1. Bump `VERSION` in the Makefile.
|
||||
2. Commit, then `git tag vX.Y.Z && git push --tags`.
|
||||
|
||||
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
|
||||
GitHub release, and adds 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
|
||||
|
||||
```bash
|
||||
gh workflow run release.yml -f dry_run=true
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
packaging, because the alternative is finding out from a tag.
|
||||
|
||||
## Setting up the signing, once
|
||||
|
||||
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
|
||||
encrypted repository secret, and `rpmsign` cannot be handed a passphrase
|
||||
unattended.
|
||||
|
||||
```bash
|
||||
gpg --batch --passphrase '' --quick-generate-key \
|
||||
'middleclick-autoscroll repository <felitendoyt@gmail.com>' rsa4096 sign never
|
||||
|
||||
gpg --armor --export-secret-keys 'middleclick-autoscroll repository' \
|
||||
| gh secret set GPG_PRIVATE_KEY
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Pointing Pages at it, once — and in this order
|
||||
|
||||
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:
|
||||
|
||||
1. Set the secret, above.
|
||||
2. Tag a release. The workflow creates the branch and fills it.
|
||||
3. *Then* set **Pages** to deploy from a branch and pick `gh-pages` at the
|
||||
root.
|
||||
|
||||
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
|
||||
and the indexes are 404 and nothing installs.
|
||||
|
||||
## What users end up with
|
||||
|
||||
The public key is published as `KEY.gpg` beside the repositories, and the
|
||||
landing page carries the setup lines for each distribution. After that, a new
|
||||
version arrives with `apt upgrade`, `dnf upgrade` or `zypper up` like anything
|
||||
else.
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Builds the binary package for Debian, Ubuntu and their derivatives into
|
||||
# dist/.
|
||||
#
|
||||
# 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
|
||||
# and it is the Makefile - a packaging script that lists the files again is a
|
||||
# second description, and the two drift.
|
||||
#
|
||||
# Needs: make, dpkg-deb, msgfmt (gettext), scdoc.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
version="${1:-$(make -s -C "$here" version)}"
|
||||
name=middleclick-autoscroll
|
||||
|
||||
# A package without its man page or its translations is not a package this
|
||||
# should be quietly willing to produce: the Makefile skips both when the tools
|
||||
# are missing, and the result would look like a successful build.
|
||||
for tool in msgfmt scdoc dpkg-deb; do
|
||||
command -v "$tool" > /dev/null || { echo "$0: $tool is not installed" >&2; exit 1; }
|
||||
done
|
||||
|
||||
root="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$root"' EXIT
|
||||
|
||||
make -C "$here" install \
|
||||
DESTDIR="$root" \
|
||||
PREFIX=/usr \
|
||||
VERSION="$version" \
|
||||
USERUNITDIR=/usr/lib/systemd/user
|
||||
|
||||
install -d "$root/DEBIAN"
|
||||
sed "s|@VERSION@|$version|g" "$here/packaging/deb/control" > "$root/DEBIAN/control"
|
||||
install -Dm644 "$here/packaging/deb/copyright" \
|
||||
"$root/usr/share/doc/$name/copyright"
|
||||
|
||||
# Relative to the package root, and DEBIAN/ itself is not part of the contents.
|
||||
# Sorted in the C locale so the file comes out the same whatever the locale of
|
||||
# the machine that built it.
|
||||
( cd "$root" && find . -type f ! -path './DEBIAN/*' -printf '%P\0' \
|
||||
| LC_ALL=C sort -z | xargs -0 md5sum > DEBIAN/md5sums )
|
||||
|
||||
mkdir -p "$here/dist"
|
||||
out="$here/dist/${name}_${version}_all.deb"
|
||||
dpkg-deb --root-owner-group --build "$root" "$out" > /dev/null
|
||||
|
||||
echo "$out"
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Builds the binary package for Fedora, RHEL, openSUSE and their derivatives
|
||||
# into dist/.
|
||||
#
|
||||
# The spec takes the version as a macro rather than carrying one of its own,
|
||||
# for the same reason the Debian control file has a placeholder: the Makefile
|
||||
# is where the version is written down.
|
||||
#
|
||||
# Needs: rpmbuild, make, msgfmt (gettext), scdoc, systemd-rpm-macros.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
version="${1:-$(make -s -C "$here" version)}"
|
||||
name=middleclick-autoscroll
|
||||
|
||||
command -v rpmbuild > /dev/null || { echo "$0: rpmbuild is not installed" >&2; exit 1; }
|
||||
|
||||
top="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$top"' EXIT
|
||||
mkdir -p "$top"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS}
|
||||
|
||||
# The working tree as it is, not as it was committed: a package built from a
|
||||
# checkout has to contain what is in that checkout.
|
||||
tar czf "$top/SOURCES/$name-$version.tar.gz" \
|
||||
--transform "s,^\\.,$name-$version," \
|
||||
--exclude=./.git --exclude=./dist --exclude=./po/'*.mo' \
|
||||
-C "$here" .
|
||||
|
||||
rpmbuild \
|
||||
--define "_topdir $top" \
|
||||
--define "_version $version" \
|
||||
-bb "$here/packaging/rpm/$name.spec" > /dev/null
|
||||
|
||||
mkdir -p "$here/dist"
|
||||
cp "$top"/RPMS/noarch/*.rpm "$here/dist/"
|
||||
|
||||
ls "$here/dist/$name-$version"*.rpm
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Refuses a release whose tag and Makefile disagree.
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
# would have complained.
|
||||
#
|
||||
# Anything that is not a v-tag - a run started by hand from a branch - is not a
|
||||
# release and has nothing to check.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ref="${1:-}"
|
||||
|
||||
case "$ref" in
|
||||
v[0-9]*) ;;
|
||||
*)
|
||||
echo "not a release tag (${ref:-none}) - nothing to check against"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
tag_version="${ref#v}"
|
||||
make_version="$(make -s -C "$here" version)"
|
||||
|
||||
if [[ $tag_version != "$make_version" ]]; then
|
||||
echo "tag $ref says $tag_version, the Makefile says $make_version" >&2
|
||||
echo "Bump VERSION in the Makefile to match the tag, or retag." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$ref matches the Makefile"
|
||||
@@ -0,0 +1,24 @@
|
||||
Package: middleclick-autoscroll
|
||||
Version: @VERSION@
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Maintainer: Felitendo <felitendoyt@gmail.com>
|
||||
Depends: bash (>= 4.2), coreutils, findutils, grep, sed, mawk | gawk | original-awk
|
||||
Recommends: gettext-base, systemd, desktop-file-utils
|
||||
Homepage: https://github.com/Felitendo/middleclick-autoscroll
|
||||
Description: middle-click autoscroll for Chromium-based applications
|
||||
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
|
||||
scrolls. On Linux it is switched off, because middle click is already taken by
|
||||
primary-selection paste.
|
||||
.
|
||||
This turns it back on for every application on the system that can do it, and
|
||||
for anything installed later. There is no list of supported applications to
|
||||
keep up to date: every launcher is examined, the ones running on Chromium
|
||||
underneath are identified by what they ship rather than by their name, and the
|
||||
argument goes wherever that particular application will actually read it.
|
||||
.
|
||||
Nothing outside the user's home directory is ever written to. Run
|
||||
"middleclick-autoscroll disable" before removing this package, so that
|
||||
everything it changed is put back.
|
||||
@@ -0,0 +1,20 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: middleclick-autoscroll
|
||||
Source: https://github.com/Felitendo/middleclick-autoscroll
|
||||
|
||||
Files: *
|
||||
Copyright: 2026 Felitendo
|
||||
License: GPL-3+
|
||||
|
||||
License: GPL-3+
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU General Public License as published by the Free Software
|
||||
Foundation, either version 3 of the License, or (at your option) any later
|
||||
version.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
.
|
||||
On Debian systems the full text of the GNU General Public License version 3 can
|
||||
be found in /usr/share/common-licenses/GPL-3.
|
||||
@@ -0,0 +1,115 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>middleclick-autoscroll</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--fg: #1b1f23;
|
||||
--muted: #57606a;
|
||||
--rule: #d8dee4;
|
||||
--code-bg: #f6f8fa;
|
||||
--accent: #1793d1;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: #0d1117;
|
||||
--fg: #e6edf3;
|
||||
--muted: #9198a1;
|
||||
--rule: #30363d;
|
||||
--code-bg: #161b22;
|
||||
--accent: #58a6ff;
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0 auto;
|
||||
padding: 3rem 1.25rem 5rem;
|
||||
max-width: 46rem;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
h1 { font-size: 1.6rem; margin: 0 0 .4rem; }
|
||||
h2 {
|
||||
font-size: 1.05rem; margin: 2.5rem 0 .6rem;
|
||||
padding-bottom: .3rem; border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
p.lead { color: var(--muted); margin: 0 0 2rem; }
|
||||
p { margin: 0 0 1rem; }
|
||||
a { color: var(--accent); }
|
||||
code {
|
||||
background: var(--code-bg); padding: .12em .35em;
|
||||
border-radius: 4px; font-size: .9em;
|
||||
}
|
||||
pre {
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 6px;
|
||||
padding: .9rem 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
pre code { background: none; padding: 0; font-size: .85rem; }
|
||||
footer {
|
||||
margin-top: 3.5rem; padding-top: 1rem;
|
||||
border-top: 1px solid var(--rule);
|
||||
color: var(--muted); font-size: .9rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>middleclick-autoscroll</h1>
|
||||
<p class="lead">
|
||||
Middle-click autoscroll — hold the middle mouse button, move the pointer, the
|
||||
page scrolls — in every Chromium-based application on the system, and in
|
||||
anything installed later.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This page is the package repository. Set it up once and every new version
|
||||
arrives with the rest of your system updates. The
|
||||
<a href="https://github.com/Felitendo/middleclick-autoscroll">source and the
|
||||
documentation</a> are on GitHub.
|
||||
</p>
|
||||
|
||||
<h2>Debian, Ubuntu, Linux Mint, Pop!_OS</h2>
|
||||
<pre><code>sudo install -d -m 0755 /etc/apt/keyrings
|
||||
curl -fsSL @BASEURL@/KEY.gpg \
|
||||
| sudo gpg --dearmor -o /etc/apt/keyrings/middleclick-autoscroll.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/middleclick-autoscroll.gpg] @BASEURL@/deb ./" \
|
||||
| sudo tee /etc/apt/sources.list.d/middleclick-autoscroll.list
|
||||
sudo apt update
|
||||
sudo apt install middleclick-autoscroll</code></pre>
|
||||
|
||||
<h2>Fedora, RHEL, CentOS Stream</h2>
|
||||
<pre><code>sudo curl -fsSL -o /etc/yum.repos.d/middleclick-autoscroll.repo \
|
||||
@BASEURL@/middleclick-autoscroll.repo
|
||||
sudo dnf install middleclick-autoscroll</code></pre>
|
||||
|
||||
<h2>openSUSE</h2>
|
||||
<pre><code>sudo rpm --import @BASEURL@/KEY.gpg
|
||||
sudo zypper addrepo --gpgcheck --refresh @BASEURL@/rpm middleclick-autoscroll
|
||||
sudo zypper install middleclick-autoscroll</code></pre>
|
||||
|
||||
<h2>Arch, CachyOS, EndeavourOS, Manjaro</h2>
|
||||
<pre><code>paru -S middleclick-autoscroll</code></pre>
|
||||
|
||||
<h2>Then, once</h2>
|
||||
<pre><code>middleclick-autoscroll enable</code></pre>
|
||||
<p>
|
||||
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
|
||||
package — it puts back everything that was changed.
|
||||
</p>
|
||||
|
||||
<footer>
|
||||
GPL-3.0-or-later. Packages are signed; the public key is
|
||||
<a href="@BASEURL@/KEY.gpg">KEY.gpg</a>.
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
[middleclick-autoscroll]
|
||||
name=middleclick-autoscroll
|
||||
baseurl=@BASEURL@/rpm
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
repo_gpgcheck=1
|
||||
gpgkey=@BASEURL@/KEY.gpg
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Puts the packages that were just built into the APT and RPM repositories on
|
||||
# the gh-pages branch, and regenerates the indexes over everything that is
|
||||
# there.
|
||||
#
|
||||
# Old versions are kept rather than replaced. An index built over all of them
|
||||
# is what lets somebody pin a version or go back to one, and it costs a few
|
||||
# hundred kilobytes.
|
||||
#
|
||||
# Usage: publish-repos.sh <gh-pages checkout> <directory of new packages>
|
||||
#
|
||||
# Needs: dpkg-dev, apt-utils, createrepo-c, gpg, and a secret key already
|
||||
# imported - its id is taken from the keyring.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
pages="$(cd -- "$1" && pwd)"
|
||||
incoming="$(cd -- "$2" && pwd)"
|
||||
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
base_url="${MCA_REPO_URL:-https://felitendo.github.io/middleclick-autoscroll}"
|
||||
|
||||
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; }
|
||||
|
||||
mkdir -p "$pages/deb" "$pages/rpm"
|
||||
cp -- "$incoming"/*.deb "$pages/deb/"
|
||||
cp -- "$incoming"/*.rpm "$pages/rpm/"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# APT
|
||||
# ---------------------------------------------------------------------------
|
||||
# A flat repository: the packages and their index sit in one directory and the
|
||||
# sources line ends in "./". There is one distribution here and it is the same
|
||||
# package for all of them, so the suite and component machinery of a pool
|
||||
# layout would describe nothing.
|
||||
(
|
||||
cd "$pages/deb"
|
||||
|
||||
# The old index must be gone before the new one is written: apt-ftparchive
|
||||
# hashes every file in the directory, and a Release that hashes the
|
||||
# previous Release is a Release that cannot be verified.
|
||||
rm -f Packages Packages.gz Release Release.gpg InRelease
|
||||
|
||||
dpkg-scanpackages --multiversion . > Packages
|
||||
gzip -9kf Packages
|
||||
|
||||
apt-ftparchive \
|
||||
-o APT::FTPArchive::Release::Origin=middleclick-autoscroll \
|
||||
-o APT::FTPArchive::Release::Label=middleclick-autoscroll \
|
||||
-o APT::FTPArchive::Release::Suite=stable \
|
||||
-o APT::FTPArchive::Release::Codename=stable \
|
||||
-o APT::FTPArchive::Release::Architectures=all \
|
||||
-o APT::FTPArchive::Release::Components=main \
|
||||
release . > Release
|
||||
|
||||
# Both signatures: InRelease is what current apt fetches, Release.gpg is
|
||||
# what an older one falls back to.
|
||||
gpg --batch --yes --local-user "$keyid" --clearsign --output InRelease Release
|
||||
gpg --batch --yes --local-user "$keyid" --detach-sign --armor --output Release.gpg Release
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RPM
|
||||
# ---------------------------------------------------------------------------
|
||||
(
|
||||
cd "$pages/rpm"
|
||||
createrepo_c --quiet --update .
|
||||
rm -f repodata/repomd.xml.asc
|
||||
gpg --batch --yes --local-user "$keyid" --detach-sign --armor repodata/repomd.xml
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The key and the landing page
|
||||
# ---------------------------------------------------------------------------
|
||||
gpg --armor --export "$keyid" > "$pages/KEY.gpg"
|
||||
|
||||
sed "s|@BASEURL@|$base_url|g" "$here/packaging/pages/index.html" > "$pages/index.html"
|
||||
sed "s|@BASEURL@|$base_url|g" "$here/packaging/pages/middleclick-autoscroll.repo" \
|
||||
> "$pages/middleclick-autoscroll.repo"
|
||||
|
||||
# Pages would otherwise hand the whole directory to Jekyll, which drops every
|
||||
# file whose name starts with an underscore and can rewrite the rest.
|
||||
touch "$pages/.nojekyll"
|
||||
|
||||
echo "signed with $keyid"
|
||||
ls -1 "$pages/deb" "$pages/rpm"
|
||||
@@ -0,0 +1,72 @@
|
||||
# Built with `packaging/build-rpm.sh`, which passes the version in rather than
|
||||
# editing this file: the Makefile is where the version is written down, and two
|
||||
# places that have to agree are one place too many.
|
||||
%global upstream_version %{?_version}%{!?_version:1.0.3}
|
||||
|
||||
Name: middleclick-autoscroll
|
||||
Version: %{upstream_version}
|
||||
Release: 1%{?dist}
|
||||
Summary: Middle-click autoscroll for Chromium-based applications
|
||||
|
||||
License: GPL-3.0-or-later
|
||||
URL: https://github.com/Felitendo/middleclick-autoscroll
|
||||
Source0: %{name}-%{version}.tar.gz
|
||||
|
||||
BuildArch: noarch
|
||||
BuildRequires: make
|
||||
BuildRequires: gettext
|
||||
BuildRequires: scdoc
|
||||
# For %{_userunitdir}, which is where the watcher's user units belong.
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
Requires: bash >= 4.2
|
||||
Requires: coreutils
|
||||
Requires: findutils
|
||||
Requires: grep
|
||||
Requires: sed
|
||||
Requires: gawk
|
||||
|
||||
# The interface falls back to English without gettext and picks up new
|
||||
# applications at the next `apply` without systemd, so neither is required.
|
||||
Recommends: /usr/bin/gettext
|
||||
Recommends: systemd
|
||||
Recommends: desktop-file-utils
|
||||
|
||||
%description
|
||||
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
|
||||
scrolls. On Linux it is switched off, because middle click is already taken by
|
||||
primary-selection paste.
|
||||
|
||||
This turns it back on for every application on the system that can do it, and
|
||||
for anything installed later. There is no list of supported applications to keep
|
||||
up to date: every launcher is examined, the ones running on Chromium underneath
|
||||
are identified by what they ship rather than by their name, and the argument goes
|
||||
wherever that particular application will actually read it.
|
||||
|
||||
Nothing outside the user's home directory is ever written to. Run
|
||||
"middleclick-autoscroll disable" before removing this package, so that everything
|
||||
it changed is put back.
|
||||
|
||||
%prep
|
||||
%autosetup -n %{name}-%{version}
|
||||
|
||||
%build
|
||||
%make_build VERSION=%{upstream_version}
|
||||
|
||||
%install
|
||||
%make_install PREFIX=%{_prefix} VERSION=%{upstream_version} \
|
||||
USERUNITDIR=%{_userunitdir}
|
||||
|
||||
%find_lang %{name}
|
||||
|
||||
%files -f %{name}.lang
|
||||
%license LICENSE
|
||||
%doc %{_datadir}/doc/%{name}/README.md
|
||||
%{_bindir}/%{name}
|
||||
%{_datadir}/%{name}/
|
||||
%{_userunitdir}/%{name}.path
|
||||
%{_userunitdir}/%{name}.service
|
||||
%{_mandir}/man1/%{name}.1*
|
||||
|
||||
%changelog
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: middleclick-autoscroll 1.0.0\n"
|
||||
"Project-Id-Version: middleclick-autoscroll 1.0.3\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/Felitendo/middleclick-autoscroll/issues\n"
|
||||
"POT-Creation-Date: 2026-08-16 00:00+0200\n"
|
||||
"PO-Revision-Date: 2026-08-16 00:00+0200\n"
|
||||
@@ -43,6 +43,9 @@ msgstr "nicht erkennbar"
|
||||
msgid "not patched yet"
|
||||
msgstr "noch nicht gepatcht"
|
||||
|
||||
msgid "waiting until Steam is closed"
|
||||
msgstr "wartet, bis Steam geschlossen ist"
|
||||
|
||||
|
||||
#. Status block
|
||||
msgid "Autoscroll"
|
||||
@@ -77,8 +80,8 @@ msgstr "Anwendungen übernehmen das beim nächsten Start."
|
||||
msgid "Turn autoscroll on or off"
|
||||
msgstr "Autoscroll ein- oder ausschalten"
|
||||
|
||||
msgid "Apply now"
|
||||
msgstr "Jetzt anwenden"
|
||||
msgid "Re-apply everything"
|
||||
msgstr "Alles neu anwenden"
|
||||
|
||||
msgid "Applications"
|
||||
msgstr "Anwendungen"
|
||||
@@ -103,6 +106,9 @@ msgstr "Chromium-basierte Browser"
|
||||
msgid "Flatpak applications"
|
||||
msgstr "Flatpak-Anwendungen"
|
||||
|
||||
msgid "Snap applications"
|
||||
msgstr "Snap-Anwendungen"
|
||||
|
||||
msgid "Programs that start themselves at login"
|
||||
msgstr "Programme, die sich beim Anmelden selbst starten"
|
||||
|
||||
@@ -164,6 +170,9 @@ msgstr "Autoscroll ist aus, alle Änderungen wurden zurückgenommen."
|
||||
msgid "Autoscroll is off. Turn it on first."
|
||||
msgstr "Autoscroll ist aus. Erst einschalten."
|
||||
|
||||
msgid "Applied again to %d applications."
|
||||
msgstr "Erneut auf %d Anwendungen angewendet."
|
||||
|
||||
msgid "Applied to %d applications."
|
||||
msgstr "Auf %d Anwendungen angewendet."
|
||||
|
||||
@@ -173,6 +182,9 @@ msgstr "Bereits auf %d Anwendungen angewendet."
|
||||
msgid "Steam starts its interface in a way this version does not recognise; leaving it alone."
|
||||
msgstr "Steam startet seine Oberfläche auf eine Art, die diese Version nicht kennt; Steam bleibt unangetastet."
|
||||
|
||||
msgid "Steam is running and has put its own file back; the change waits until Steam is closed."
|
||||
msgstr "Steam läuft und hat seine eigene Datei wiederhergestellt; die Änderung wird nachgeholt, sobald Steam geschlossen ist."
|
||||
|
||||
|
||||
#. Help
|
||||
msgid "Usage: middleclick-autoscroll [command]"
|
||||
@@ -190,6 +202,9 @@ msgstr "Ausschalten und alles zurücknehmen"
|
||||
msgid "Apply to anything installed since the last run"
|
||||
msgstr "Auf alles anwenden, was seit dem letzten Lauf dazugekommen ist"
|
||||
|
||||
msgid "Take everything back and apply it again"
|
||||
msgstr "Alles zurücknehmen und neu anwenden"
|
||||
|
||||
msgid "Show what is covered"
|
||||
msgstr "Anzeigen, was abgedeckt ist"
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: middleclick-autoscroll 1.0.0\n"
|
||||
"Project-Id-Version: middleclick-autoscroll 1.0.3\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/Felitendo/middleclick-autoscroll/issues\n"
|
||||
"POT-Creation-Date: 2026-08-16 00:00+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
@@ -43,6 +43,9 @@ msgstr ""
|
||||
msgid "not patched yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "waiting until Steam is closed"
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. Status block
|
||||
msgid "Autoscroll"
|
||||
@@ -77,7 +80,7 @@ msgstr ""
|
||||
msgid "Turn autoscroll on or off"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apply now"
|
||||
msgid "Re-apply everything"
|
||||
msgstr ""
|
||||
|
||||
msgid "Applications"
|
||||
@@ -103,6 +106,9 @@ msgstr ""
|
||||
msgid "Flatpak applications"
|
||||
msgstr ""
|
||||
|
||||
msgid "Snap applications"
|
||||
msgstr ""
|
||||
|
||||
msgid "Programs that start themselves at login"
|
||||
msgstr ""
|
||||
|
||||
@@ -164,6 +170,9 @@ msgstr ""
|
||||
msgid "Autoscroll is off. Turn it on first."
|
||||
msgstr ""
|
||||
|
||||
msgid "Applied again to %d applications."
|
||||
msgstr ""
|
||||
|
||||
msgid "Applied to %d applications."
|
||||
msgstr ""
|
||||
|
||||
@@ -173,6 +182,9 @@ msgstr ""
|
||||
msgid "Steam starts its interface in a way this version does not recognise; leaving it alone."
|
||||
msgstr ""
|
||||
|
||||
msgid "Steam is running and has put its own file back; the change waits until Steam is closed."
|
||||
msgstr ""
|
||||
|
||||
|
||||
#. Help
|
||||
msgid "Usage: middleclick-autoscroll [command]"
|
||||
@@ -190,6 +202,9 @@ msgstr ""
|
||||
msgid "Apply to anything installed since the last run"
|
||||
msgstr ""
|
||||
|
||||
msgid "Take everything back and apply it again"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show what is covered"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -3,21 +3,39 @@ Description=Watch for newly installed Chromium-based applications
|
||||
Documentation=man:middleclick-autoscroll(1)
|
||||
|
||||
[Path]
|
||||
# Every way an application can turn up: a package, a Flatpak, 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
|
||||
# to write per package manager - pacman, flatpak and an AppImage manager all
|
||||
# end up here.
|
||||
# to write per package manager - pacman, apt, dnf, zypper, flatpak, snapd and
|
||||
# an AppImage manager all end up here.
|
||||
#
|
||||
# A directory that does not exist on this system costs nothing: systemd watches
|
||||
# the nearest parent that does and picks the path up when it appears.
|
||||
PathModified=/usr/share/applications
|
||||
PathModified=/usr/local/share/applications
|
||||
PathModified=%h/.local/share/applications
|
||||
PathModified=%h/.config/autostart
|
||||
PathModified=/var/lib/flatpak/exports/share/applications
|
||||
PathModified=%h/.local/share/flatpak/exports/share/applications
|
||||
PathModified=/var/lib/snapd/desktop/applications
|
||||
|
||||
# NixOS and Guix build the search path out of profiles rather than /usr.
|
||||
PathModified=/run/current-system/sw/share/applications
|
||||
PathModified=%h/.nix-profile/share/applications
|
||||
PathModified=%h/.guix-profile/share/applications
|
||||
|
||||
# The desktop folder is watched too - Steam writes a shortcut there for every
|
||||
# game somebody asks for one, and starting a game is a Steam start - but its
|
||||
# name is translated and cannot be written down here. `enable` adds it in a
|
||||
# drop-in, from the name this system uses.
|
||||
|
||||
# Steam replaces the script that starts its interface on every client update,
|
||||
# which takes the patch with it.
|
||||
# which takes the patch with it. Where that installation lives depends on how
|
||||
# Steam was installed.
|
||||
PathChanged=%h/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
PathChanged=%h/.steam/debian-installation/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
PathChanged=%h/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
PathChanged=%h/.var/app/com.valvesoftware.Steam/data/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
PathChanged=%h/snap/steam/common/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
|
||||
Unit=middleclick-autoscroll.service
|
||||
|
||||
|
||||
+88
-18
@@ -8,10 +8,9 @@
|
||||
# of the behaviour, it is the behaviour.
|
||||
|
||||
MCA_CHANGES=0
|
||||
MCA_TOUCHED=()
|
||||
MCA_ROUTES=()
|
||||
|
||||
# mca_route <kind> <id> <program>
|
||||
# mca_route <kind> <id> <program> [packaging]
|
||||
# What happens to one application, left in MCA_ROUTE:
|
||||
# flags - the launcher reads a flag file; write it there
|
||||
# desktop - no flag file; shadow or edit the desktop entry
|
||||
@@ -25,7 +24,7 @@ MCA_ROUTES=()
|
||||
MCA_ROUTE=''
|
||||
|
||||
mca_route() {
|
||||
local kind="$1" id="$2" prog="$3"
|
||||
local kind="$1" id="$2" prog="$3" packaging="${4:-native}"
|
||||
|
||||
if [[ $kind == steam ]]; then
|
||||
if mca_config_list_has Skip "$id" || [[ $CFG_STEAM != yes ]]; then
|
||||
@@ -36,14 +35,14 @@ mca_route() {
|
||||
return
|
||||
fi
|
||||
|
||||
if ! mca_kind_wanted "$kind" "$id"; then
|
||||
if ! mca_kind_wanted "$kind" "$id" "$packaging"; then
|
||||
[[ $kind == unknown ]] && MCA_ROUTE=unknown || MCA_ROUTE=off
|
||||
return
|
||||
fi
|
||||
|
||||
# A Flatpak has its own copy of everything and none of the host's wrappers,
|
||||
# so the desktop entry is the only way in.
|
||||
if [[ $kind == flatpak ]]; then
|
||||
# A Flatpak or a snap carries its own copy of everything and sees none of
|
||||
# the host's wrappers, so the desktop entry is the only way in.
|
||||
if [[ $packaging != native ]]; then
|
||||
MCA_ROUTE=desktop
|
||||
return
|
||||
fi
|
||||
@@ -60,10 +59,9 @@ mca_route() {
|
||||
# as it likes to be - it writes only what differs, which is what keeps the
|
||||
# watcher from chasing its own changes.
|
||||
mca_apply() {
|
||||
local i id file prog kind route steam_done=0
|
||||
local i id file prog kind packaging route steam_done=0
|
||||
|
||||
MCA_CHANGES=0
|
||||
MCA_TOUCHED=()
|
||||
MCA_ROUTES=()
|
||||
MCA_N_ON=0; MCA_N_OFF=0; MCA_N_UNKNOWN=0
|
||||
|
||||
@@ -75,8 +73,9 @@ mca_apply() {
|
||||
file="${MCA_FILES[i]}"
|
||||
prog="${MCA_PROGS[i]}"
|
||||
kind="${MCA_KINDS[i]}"
|
||||
packaging="${MCA_PACKAGING[i]}"
|
||||
|
||||
mca_route "$kind" "$id" "$prog"
|
||||
mca_route "$kind" "$id" "$prog" "$packaging"
|
||||
route="$MCA_ROUTE"
|
||||
MCA_ROUTES[i]="$route"
|
||||
|
||||
@@ -94,7 +93,7 @@ mca_apply() {
|
||||
mca_desktop_apply "$id" "$file"
|
||||
;;
|
||||
steam)
|
||||
mca_steam_desktop_apply "$id" "$file"
|
||||
mca_steam_desktop_apply "$id" "$file" "$packaging"
|
||||
if (( ! steam_done )); then
|
||||
mca_steam_apply
|
||||
steam_done=1
|
||||
@@ -109,15 +108,41 @@ mca_apply() {
|
||||
mca_steam_apply
|
||||
fi
|
||||
|
||||
[[ $CFG_AUTOSTART == yes ]] && mca_autostart_apply
|
||||
# The shortcuts Steam writes for single games. They are not applications
|
||||
# and are not offered as ones, but starting a game with Steam closed is a
|
||||
# Steam start like any other: without the switch the client finds the
|
||||
# patched helper script, puts its own back, and the interface loses
|
||||
# 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
|
||||
fi
|
||||
|
||||
# Shortcuts on the desktop itself. Nothing above has seen them - the XDG
|
||||
# search path does not go there - and a game started from one starts Steam
|
||||
# without its switch, which is the difference between autoscroll working
|
||||
# and autoscroll working most of the time. Each entry is gated on its own,
|
||||
# so there is nothing to check out here.
|
||||
mca_shortcuts_apply
|
||||
|
||||
[[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply
|
||||
|
||||
mca_prune_orphans
|
||||
|
||||
if (( MCA_CHANGES )) && mca_have update-desktop-database; then
|
||||
update-desktop-database "$MCA_APPDIR" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
mca_state_write last_apply "$(date +%s)"
|
||||
mca_state_write covered "${#MCA_TOUCHED[@]}"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -131,7 +156,6 @@ mca_revert() {
|
||||
update-desktop-database "$MCA_APPDIR" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
mca_state_write covered 0
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -139,13 +163,19 @@ mca_revert() {
|
||||
# The watcher
|
||||
# ---------------------------------------------------------------------------
|
||||
# A systemd user path unit watching every directory a desktop entry can appear
|
||||
# in. That covers a package installed with pacman, a Flatpak, an AppImage
|
||||
# registered by hand and a Steam client update, without a hook per package
|
||||
# manager.
|
||||
# in. That covers a package from whatever the distribution's package manager
|
||||
# is, a Flatpak, a snap, an AppImage registered by hand and a Steam client
|
||||
# update, without a hook per package manager.
|
||||
|
||||
MCA_UNIT_PATH="middleclick-autoscroll.path"
|
||||
MCA_UNIT_SERVICE="middleclick-autoscroll.service"
|
||||
|
||||
# The desktop folder is watched like every other directory an entry can turn up
|
||||
# in, but its name is translated and the unit that ships with the package
|
||||
# cannot know it. So that one path is written here, into a drop-in, from the
|
||||
# name this system actually uses.
|
||||
MCA_UNIT_DROPIN="${MCA_XDG_CONFIG}/systemd/user/${MCA_UNIT_PATH}.d"
|
||||
|
||||
mca_watch_available() {
|
||||
mca_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} || -S "/run/user/$(id -u)/bus" ]]
|
||||
}
|
||||
@@ -155,17 +185,56 @@ mca_watch_enabled() {
|
||||
systemctl --user is-enabled --quiet "$MCA_UNIT_PATH" 2>/dev/null
|
||||
}
|
||||
|
||||
# _mca_watch_dropin_write
|
||||
# Returns 0 when the drop-in changed and systemd has to be told, 1 when it was
|
||||
# already right - the same contract as mca_write_if_changed, and for the same
|
||||
# reason: the watcher runs after every desktop entry that appears anywhere on
|
||||
# the system, and a daemon-reload each time would be absurd.
|
||||
_mca_watch_dropin_write() {
|
||||
local dir file="$MCA_UNIT_DROPIN/desktop.conf" content
|
||||
|
||||
dir="$(mca_desktop_folder)"
|
||||
[[ -d $dir ]] || { _mca_watch_dropin_remove; return $?; }
|
||||
|
||||
# A per cent sign starts a specifier in a unit file and has to be doubled
|
||||
# to mean itself. Rare in a folder name, fatal when it happens.
|
||||
content="[Path]"$'\n'"PathModified=${dir//%/%%}"$'\n'
|
||||
|
||||
mca_write_if_changed "$file" "$content"
|
||||
}
|
||||
|
||||
_mca_watch_dropin_remove() {
|
||||
[[ -e "$MCA_UNIT_DROPIN/desktop.conf" ]] || return 1
|
||||
rm -f -- "$MCA_UNIT_DROPIN/desktop.conf"
|
||||
rmdir "$MCA_UNIT_DROPIN" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
mca_watch_enable() {
|
||||
mca_watch_available || return 1
|
||||
_mca_watch_dropin_write
|
||||
systemctl --user daemon-reload > /dev/null 2>&1 || true
|
||||
systemctl --user enable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || return 1
|
||||
systemctl --user enable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_watch_refresh
|
||||
# Keeps a running watcher in step with a desktop folder that has been renamed,
|
||||
# which is what a change of session language does to it.
|
||||
mca_watch_refresh() {
|
||||
mca_watch_available || return 0
|
||||
_mca_watch_dropin_write || return 0
|
||||
systemctl --user daemon-reload > /dev/null 2>&1 || true
|
||||
systemctl --user restart "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
|
||||
return 0
|
||||
}
|
||||
|
||||
mca_watch_disable() {
|
||||
mca_watch_available || return 0
|
||||
systemctl --user disable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
|
||||
systemctl --user disable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
||||
_mca_watch_dropin_remove && systemctl --user daemon-reload > /dev/null 2>&1
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -187,7 +256,8 @@ mca_count_routes() {
|
||||
MCA_ROUTES=()
|
||||
|
||||
for i in "${!MCA_IDS[@]}"; do
|
||||
mca_route "${MCA_KINDS[i]}" "${MCA_IDS[i]}" "${MCA_PROGS[i]}"
|
||||
mca_route "${MCA_KINDS[i]}" "${MCA_IDS[i]}" "${MCA_PROGS[i]}" \
|
||||
"${MCA_PACKAGING[i]}"
|
||||
route="$MCA_ROUTE"
|
||||
MCA_ROUTES[i]="$route"
|
||||
case "$route" in
|
||||
|
||||
+40
-19
@@ -34,7 +34,7 @@ MCA_CACHEDIR="${MCA_CACHEDIR:-${MCA_XDG_CACHE}/${MCA_NAME}}"
|
||||
|
||||
# Where generated desktop entries go. A file here shadows the one with the same
|
||||
# name in /usr/share/applications, which is how an application gets extra
|
||||
# command line arguments without touching anything pacman owns.
|
||||
# command line arguments without touching a file the package manager owns.
|
||||
MCA_APPDIR="${MCA_XDG_DATA}/applications"
|
||||
|
||||
# Copies of every file that is edited in place rather than shadowed.
|
||||
@@ -44,6 +44,29 @@ MCA_BACKUPDIR="${MCA_STATEDIR}/backup"
|
||||
# else. See mca_ledger_add.
|
||||
MCA_LEDGER="${MCA_STATEDIR}/ledger"
|
||||
|
||||
# The folder the desktop itself shows. Nothing in the XDG search path looks at
|
||||
# it, so an entry that lives only there is invisible to a scan - and a game
|
||||
# shortcut dragged onto the desktop is exactly that.
|
||||
#
|
||||
# Its name is translated: Schreibtisch, Bureau, Escritorio. The name in use is
|
||||
# in the file xdg-user-dirs writes, so it is read rather than guessed, and
|
||||
# ~/Desktop is only the fallback for a system that has no such file.
|
||||
mca_desktop_folder() {
|
||||
local file="$MCA_XDG_CONFIG/user-dirs.dirs" line dir=''
|
||||
|
||||
if [[ -r $file ]]; then
|
||||
while IFS= read -r line; do
|
||||
[[ $line == XDG_DESKTOP_DIR=* ]] || continue
|
||||
dir="${line#XDG_DESKTOP_DIR=}"
|
||||
dir="${dir%\"}"; dir="${dir#\"}"
|
||||
done < "$file"
|
||||
dir="${dir/#\$HOME/$HOME}"
|
||||
fi
|
||||
|
||||
[[ $dir == /* ]] || dir="$HOME/Desktop"
|
||||
printf '%s\n' "${dir%/}"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translations
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -60,8 +83,15 @@ mca_ui_locale() {
|
||||
[[ -z $l ]] && l="${LANG:-}"
|
||||
fi
|
||||
|
||||
if [[ -z $l && -r /etc/locale.conf ]]; then
|
||||
l="$(sed -n 's/^LANG=//p' /etc/locale.conf | tr -d '"' | head -n1)"
|
||||
# systemd writes /etc/locale.conf and most distributions use it; Debian and
|
||||
# Ubuntu keep the same LANG= line in /etc/default/locale instead.
|
||||
if [[ -z $l ]]; then
|
||||
local f
|
||||
for f in /etc/locale.conf /etc/default/locale; do
|
||||
[[ -r $f ]] || continue
|
||||
l="$(sed -n 's/^LANG=//p' "$f" | tr -d '"' | head -n1)"
|
||||
[[ -n $l ]] && break
|
||||
done
|
||||
fi
|
||||
|
||||
printf '%s\n' "${l:-C}"
|
||||
@@ -158,7 +188,6 @@ mca_head() { printf '\n%s%s%s\n\n' "$MCA_C_BOLD$MCA_C_BLUE" "$*" "$MCA_C_RESET";
|
||||
mca_ok() { [[ -n $MCA_QUIET ]] || printf '%s✔%s %s\n' "$MCA_C_GREEN" "$MCA_C_RESET" "$*"; }
|
||||
mca_bad() { MCA_UI_NEEDS_ACK=1; printf '%s✘%s %s\n' "$MCA_C_RED" "$MCA_C_RESET" "$*" >&2; }
|
||||
mca_note() { MCA_UI_NEEDS_ACK=1; [[ -n $MCA_QUIET ]] || printf '%s•%s %s\n' "$MCA_C_DIM" "$MCA_C_RESET" "$*"; }
|
||||
mca_debug() { [[ -n ${MCA_DEBUG:-} ]] && printf 'debug: %s\n' "$*" >&2; return 0; }
|
||||
|
||||
mca_have() { command -v "$1" > /dev/null 2>&1; }
|
||||
|
||||
@@ -239,21 +268,13 @@ mca_ledger_forget() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_ledger_has <path>
|
||||
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>
|
||||
mca_ledger_detail() {
|
||||
[[ -f $MCA_LEDGER ]] || return 1
|
||||
awk -F'\t' -v p="$1" '$2 == p { print $3; exit }' "$MCA_LEDGER"
|
||||
}
|
||||
|
||||
mca_ledger_count() {
|
||||
[[ -f $MCA_LEDGER ]] || { printf '0\n'; return; }
|
||||
grep -c . "$MCA_LEDGER" 2>/dev/null || printf '0\n'
|
||||
# mca_backup_name <file>
|
||||
# The name a backup copy of that file is stored under. Derived from the path
|
||||
# rather than remembered, so a caller can ask whether a file has been backed up
|
||||
# before without reading the ledger.
|
||||
mca_backup_name() {
|
||||
printf '%s' "$1" | sed 's|/|%|g'
|
||||
}
|
||||
|
||||
# mca_backup <file>
|
||||
@@ -263,7 +284,7 @@ mca_ledger_count() {
|
||||
# patched version.
|
||||
mca_backup() {
|
||||
local file="$1" name
|
||||
name="$(printf '%s' "$file" | sed 's|/|%|g')"
|
||||
name="$(mca_backup_name "$file")"
|
||||
|
||||
mkdir -p "$MCA_BACKUPDIR" 2>/dev/null || return 1
|
||||
if [[ ! -e "$MCA_BACKUPDIR/$name" ]]; then
|
||||
|
||||
@@ -139,6 +139,7 @@ mca_config_load() {
|
||||
CFG_APPS=no; mca_config_bool PatchApps yes && CFG_APPS=yes
|
||||
CFG_BROWSERS=no; mca_config_bool PatchBrowsers yes && CFG_BROWSERS=yes
|
||||
CFG_FLATPAK=no; mca_config_bool PatchFlatpak yes && CFG_FLATPAK=yes
|
||||
CFG_SNAP=no; mca_config_bool PatchSnap yes && CFG_SNAP=yes
|
||||
CFG_AUTOSTART=no; mca_config_bool PatchAutostart yes && CFG_AUTOSTART=yes
|
||||
CFG_STEAM=no; mca_config_bool PatchSteam yes && CFG_STEAM=yes
|
||||
CFG_SPOTIFY=no; mca_config_bool PatchSpotify yes && CFG_SPOTIFY=yes
|
||||
|
||||
+332
-42
@@ -27,9 +27,23 @@ MCA_MARKERS=(
|
||||
MCA_SYSTEM_DIRS=(
|
||||
/ /bin /lib /lib32 /lib64 /sbin /usr /usr/bin /usr/lib /usr/lib32
|
||||
/usr/lib64 /usr/libexec /usr/sbin /usr/local /usr/local/bin
|
||||
/usr/local/lib /opt
|
||||
/usr/local/lib /usr/local/libexec /opt
|
||||
)
|
||||
|
||||
# The same thing for the layouts that put a machine triplet in the path.
|
||||
# Debian and Ubuntu keep the shared libraries in /usr/lib/x86_64-linux-gnu
|
||||
# rather than /usr/lib, so that directory is every bit as shared as /usr/lib is
|
||||
# elsewhere and a marker sitting in it belongs to nobody in particular.
|
||||
MCA_SYSTEM_DIR_GLOBS=(
|
||||
'/usr/lib/*-linux-gnu*' '/usr/lib32/*-linux-gnu*'
|
||||
'/usr/lib64/*-linux-gnu*' '/usr/local/lib/*-linux-gnu*'
|
||||
)
|
||||
|
||||
# Where snapd mounts the installed snaps. /snap is the usual place and the one
|
||||
# the shims point into; distributions that keep /snap free of a top-level
|
||||
# directory use the second.
|
||||
MCA_SNAP_DIRS=(/snap /var/lib/snapd/snap)
|
||||
|
||||
# Strings in a launcher script that mean it starts a Chromium or Electron
|
||||
# process, for the wrappers whose command line is assembled out of variables and
|
||||
# cannot be followed from the outside.
|
||||
@@ -38,34 +52,51 @@ MCA_SYSTEM_DIRS=(
|
||||
# "chromium" on its own is not one of them: /usr/bin/xdg-open lists every
|
||||
# browser it knows how to start, and that is not a browser.
|
||||
#
|
||||
# The flag file convention is an Arch packaging habit and says nothing about the
|
||||
# engine either, so it is not in here.
|
||||
MCA_SCRIPT_HINTS='ELECTRON_|app\.asar|chrome-sandbox|libcef|enable-blink-features|ozone-platform-hint'
|
||||
# The flag file convention is one distribution's packaging habit and says
|
||||
# nothing about the engine either, so it is not in here.
|
||||
#
|
||||
# CHROMIUM_FLAGS and CHROME_WRAPPER earn their place: they are the variables
|
||||
# the Debian, Fedora and openSUSE Chromium wrappers and Google's own Chrome
|
||||
# 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'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Desktop entries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 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
|
||||
# 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,
|
||||
# so a directory that is already in the search path keeps its own position and
|
||||
# the ones that were missing are still scanned.
|
||||
mca_extra_desktop_dirs() {
|
||||
printf '%s\n' \
|
||||
"$MCA_XDG_DATA/flatpak/exports/share/applications" \
|
||||
/var/lib/flatpak/exports/share/applications \
|
||||
/var/lib/snapd/desktop/applications
|
||||
}
|
||||
|
||||
# The directories a desktop entry can come from, most specific first - which is
|
||||
# also XDG lookup order, so the first file found for an id is the one that is
|
||||
# actually used.
|
||||
mca_desktop_dirs() {
|
||||
local dirs="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" d
|
||||
local -A seen=()
|
||||
|
||||
while IFS= read -r d; do
|
||||
[[ -n $d ]] || continue
|
||||
d="${d%/}"
|
||||
[[ -n ${seen[$d]+set} ]] && continue
|
||||
seen[$d]=1
|
||||
printf '%s\n' "$d"
|
||||
done < <(
|
||||
printf '%s\n' "$MCA_APPDIR"
|
||||
while IFS= read -r -d: d; do
|
||||
[[ -n $d ]] && printf '%s/applications\n' "${d%/}"
|
||||
done <<< "${dirs}:"
|
||||
}
|
||||
|
||||
# mca_desktop_get <file> <key>
|
||||
# A single value from the [Desktop Entry] group. Desktop Action groups repeat
|
||||
# the same keys, and reading past the first group would pick up the wrong one.
|
||||
mca_desktop_get() {
|
||||
local file="$1" key="$2"
|
||||
awk -v key="$key" '
|
||||
/^[[:space:]]*\[/ { inentry = ($0 ~ /^[[:space:]]*\[Desktop Entry\][[:space:]]*$/); next }
|
||||
inentry && index($0, key "=") == 1 { print substr($0, length(key) + 2); exit }
|
||||
' "$file" 2>/dev/null
|
||||
mca_extra_desktop_dirs
|
||||
)
|
||||
}
|
||||
|
||||
# _mca_desktop_read <file>
|
||||
@@ -97,6 +128,9 @@ _mca_desktop_read() {
|
||||
Hidden=*) DE_HIDDEN="${line#Hidden=}" ;;
|
||||
Categories=*) DE_CATEGORIES="${line#Categories=}" ;;
|
||||
MimeType=*) DE_MIME="${line#MimeType=}" ;;
|
||||
# Only a generated shadow. An entry we edited in place carries
|
||||
# X-MCA-Patched and is still the application's real entry, so it
|
||||
# has to stay in the scan.
|
||||
X-MCA-Generated=*) DE_OURS=1 ;;
|
||||
esac
|
||||
done < "$file"
|
||||
@@ -177,6 +211,35 @@ mca_exec_flatpak_id() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# mca_exec_is_steam_link <exec line> <program>
|
||||
# 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
|
||||
# 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.
|
||||
mca_exec_is_steam_link() {
|
||||
local line="$1" prog="$2"
|
||||
|
||||
[[ $line == *steam://* ]] || return 1
|
||||
mca_prog_is_steam "$prog"
|
||||
}
|
||||
|
||||
# mca_prog_is_steam <program>
|
||||
# Whether running this program starts the Steam client. Every packaging is in
|
||||
# here and every name Valve and the distributions give the launcher, because
|
||||
# the answer decides whether an entry gets Steam's own switch - and an entry
|
||||
# that starts Steam without it undoes the web helper patch on the way up.
|
||||
#
|
||||
# The program is what a scan leaves behind: an absolute path for a native
|
||||
# install, flatpak:<id> or snap:<name> for the other two.
|
||||
mca_prog_is_steam() {
|
||||
local prog="$1"
|
||||
|
||||
case "${prog##*/}" in
|
||||
steam|steam-runtime|steam-native|steam-jupiter) return 0 ;;
|
||||
esac
|
||||
[[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Is this Chromium?
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -247,6 +310,11 @@ _mca_has_markers() {
|
||||
for s in "${MCA_SYSTEM_DIRS[@]}"; do
|
||||
[[ $dir == "$s" ]] && return 1
|
||||
done
|
||||
for s in "${MCA_SYSTEM_DIR_GLOBS[@]}"; do
|
||||
# Unquoted on purpose - these are patterns, not names.
|
||||
# shellcheck disable=SC2053
|
||||
[[ $dir == $s ]] && return 1
|
||||
done
|
||||
|
||||
for m in "${MCA_MARKERS[@]}"; do
|
||||
[[ -e "$dir/$m" ]] && return 0
|
||||
@@ -255,23 +323,118 @@ _mca_has_markers() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# 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
|
||||
# around: every caller of _mca_script_target reads it through a command
|
||||
# substitution, so each call already works on a copy and there is nothing here
|
||||
# that two of them could collide over.
|
||||
declare -A MCA_SCRIPT_VARS=()
|
||||
|
||||
# _mca_script_subst <text>
|
||||
# The text with $NAME and ${NAME} replaced by what the script assigned to them,
|
||||
# left in MCA_SUBST.
|
||||
#
|
||||
# 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
|
||||
# is the point: an unresolvable path has to come out as no path at all, never
|
||||
# as a wrong one.
|
||||
MCA_SUBST=''
|
||||
|
||||
_mca_script_subst() {
|
||||
local text="$1" out='' rest name
|
||||
|
||||
while [[ $text == *'$'* ]]; do
|
||||
out+="${text%%\$*}"
|
||||
rest="${text#*\$}"
|
||||
|
||||
if [[ $rest == '{'* ]]; then
|
||||
[[ $rest == *'}'* ]] || return 1
|
||||
name="${rest%%\}*}"; name="${name#\{}"
|
||||
rest="${rest#*\}}"
|
||||
else
|
||||
name="${rest%%[!A-Za-z0-9_]*}"
|
||||
rest="${rest:${#name}}"
|
||||
fi
|
||||
|
||||
[[ $name =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || return 1
|
||||
[[ -n ${MCA_SCRIPT_VARS[$name]+set} ]] || return 1
|
||||
|
||||
out+="${MCA_SCRIPT_VARS[$name]}"
|
||||
text="$rest"
|
||||
done
|
||||
|
||||
MCA_SUBST="$out$text"
|
||||
return 0
|
||||
}
|
||||
|
||||
# _mca_script_target <script>
|
||||
# The program a wrapper script hands over to, so a chain like
|
||||
# heroic -> electron43 -> /usr/lib/electron43/electron can be followed.
|
||||
#
|
||||
# The assignments above the exec line are followed as well, because that is the
|
||||
# shape the Chromium wrappers outside Arch have: Debian, Ubuntu, Fedora and
|
||||
# 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
|
||||
# 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
|
||||
# its markers.
|
||||
_mca_script_target() {
|
||||
local script="$1" line tok
|
||||
local script="$1" line tok name val skip=0
|
||||
local -a tokens
|
||||
local re_assign='^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)=([^[:space:];|&()`]*)[[:space:]]*$'
|
||||
|
||||
MCA_SCRIPT_VARS=()
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [[ $line =~ $re_assign ]]; then
|
||||
name="${BASH_REMATCH[2]}"
|
||||
val="${BASH_REMATCH[3]}"
|
||||
|
||||
# One pair of quotes around the whole value is ordinary and means
|
||||
# nothing here. Single quotes also mean the value is literal, so
|
||||
# there is nothing left to expand.
|
||||
if [[ $val == \'*\' ]]; then
|
||||
MCA_SCRIPT_VARS[$name]="${val:1:${#val}-2}"
|
||||
continue
|
||||
fi
|
||||
[[ $val == \"*\" ]] && val="${val:1:${#val}-2}"
|
||||
|
||||
if _mca_script_subst "$val"; then
|
||||
MCA_SCRIPT_VARS[$name]="$MCA_SUBST"
|
||||
else
|
||||
# Not resolvable, so anything built from it must not be either.
|
||||
unset "MCA_SCRIPT_VARS[$name]"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
[[ $line =~ ^[[:space:]]*exec[[:space:]]+(.*)$ ]] || continue
|
||||
read -r -a tokens <<< "${BASH_REMATCH[1]}"
|
||||
skip=0
|
||||
for tok in "${tokens[@]}"; do
|
||||
[[ $tok == env ]] && continue
|
||||
(( skip )) && { skip=0; continue; }
|
||||
case "$tok" in
|
||||
env) continue ;;
|
||||
# `exec -a NAME PROG` renames the process. NAME is not a
|
||||
# program, and it is usually the wrapper's own name, so
|
||||
# following it would lead straight back here.
|
||||
-a|--argv0) skip=1; continue ;;
|
||||
-*) continue ;;
|
||||
esac
|
||||
[[ $tok == *=* && $tok != /* ]] && continue
|
||||
[[ $tok == -* ]] && continue
|
||||
# Anything still carrying a shell variable cannot be resolved from
|
||||
# the outside; the hint scan has to answer for those.
|
||||
[[ $tok == *'$'* ]] && return 1
|
||||
|
||||
tok="${tok%\"}"; tok="${tok#\"}"
|
||||
tok="${tok%\'}"; tok="${tok#\'}"
|
||||
|
||||
# Anything still carrying a shell variable is resolved from the
|
||||
# assignments above, or not at all; the hint scan answers for the
|
||||
# wrappers that build their command line some other way.
|
||||
if [[ $tok == *'$'* ]]; then
|
||||
_mca_script_subst "$tok" || return 1
|
||||
tok="$MCA_SUBST"
|
||||
fi
|
||||
[[ -n $tok ]] || return 1
|
||||
|
||||
if [[ $tok == /* ]]; then
|
||||
printf '%s\n' "$tok"
|
||||
else
|
||||
@@ -284,13 +447,15 @@ _mca_script_target() {
|
||||
}
|
||||
|
||||
# mca_flags_candidates <program>
|
||||
# The flag files a launcher reads, in the order it reads them. Arch's Electron
|
||||
# and Chromium wrappers all take extra arguments from
|
||||
# $XDG_CONFIG_HOME/<name>-flags.conf, which is a far better place to inject than
|
||||
# a desktop entry: it survives package updates and it applies to a launch from
|
||||
# the terminal too.
|
||||
# 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
|
||||
# follow that convention - Arch's Electron and Chromium packages do, and take
|
||||
# extra arguments from $XDG_CONFIG_HOME/<name>-flags.conf - it is a far 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
|
||||
# nothing and the caller falls back to the desktop entry.
|
||||
mca_flags_candidates() {
|
||||
local prog="$1" depth="${2:-0}" target
|
||||
local prog="$1" depth="${2:-0}" target inherited
|
||||
(( depth > 3 )) && return 0
|
||||
[[ -r $prog ]] || return 0
|
||||
head -c2 -- "$prog" 2>/dev/null | grep -q '#!' || return 0
|
||||
@@ -298,10 +463,22 @@ mca_flags_candidates() {
|
||||
grep -oE '[A-Za-z0-9_.+-]+-flags\.conf' -- "$prog" 2>/dev/null
|
||||
|
||||
# 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
|
||||
# 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
|
||||
# all. Every launcher that can be followed is not a launcher that reads
|
||||
# one: the Chromium wrappers outside Arch do not, and neither does an
|
||||
# ordinary program that simply execs its own binary. Naming a file for
|
||||
# those would put the flag somewhere nothing ever looks and skip the
|
||||
# desktop entry that would have worked.
|
||||
if target="$(_mca_script_target "$prog")" && [[ -n $target ]]; then
|
||||
inherited="$(mca_flags_candidates "$target" $(( depth + 1 )))"
|
||||
if [[ -n $inherited ]]; then
|
||||
printf '%s-flags.conf\n' "${target##*/}"
|
||||
mca_flags_candidates "$target" $(( depth + 1 ))
|
||||
printf '%s\n' "$inherited"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -395,12 +572,23 @@ _mca_detect_uncached() {
|
||||
MCA_IDS=() # desktop file id, without the .desktop suffix
|
||||
MCA_FILES=() # the desktop entry that is in effect for that id
|
||||
MCA_NAMES=() # display name
|
||||
MCA_PROGS=() # resolved program, or a flatpak app id
|
||||
MCA_KINDS=() # app | browser | flatpak | steam | unknown | no
|
||||
MCA_PROGS=() # resolved program, or a Flatpak app id or a snap name
|
||||
MCA_KINDS=() # app | browser | steam | unknown | no
|
||||
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
|
||||
# then redraws from what it found. Anything that changes the answer - applying,
|
||||
# reverting - invalidates it explicitly.
|
||||
# then redraws from what it found. Applying rescans on its own, so nothing else
|
||||
# has to remember to invalidate this.
|
||||
MCA_SCANNED=0
|
||||
|
||||
mca_scan_once() {
|
||||
@@ -408,14 +596,14 @@ mca_scan_once() {
|
||||
mca_scan
|
||||
}
|
||||
|
||||
mca_scan_invalidate() { MCA_SCANNED=0; }
|
||||
|
||||
mca_scan() {
|
||||
local dir file id name exec_line prog kind i
|
||||
local dir file id name exec_line prog kind packaging i
|
||||
local -A seen=()
|
||||
local -a c_ids=() c_files=() c_names=() c_progs=() c_browser=() c_stat=()
|
||||
|
||||
MCA_IDS=(); MCA_FILES=(); MCA_NAMES=(); MCA_PROGS=(); MCA_KINDS=()
|
||||
MCA_PACKAGING=()
|
||||
MCA_STEAM_LINKS=(); MCA_STEAM_LINK_FILES=(); MCA_STEAM_LINK_PACK=()
|
||||
|
||||
# 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
|
||||
@@ -451,6 +639,21 @@ mca_scan() {
|
||||
if [[ ${prog##*/} == flatpak ]]; then
|
||||
mca_exec_flatpak_id "$exec_line" || continue
|
||||
prog="flatpak:$MCA_PROG"
|
||||
elif mca_snap_name "$prog"; then
|
||||
prog="snap:$MCA_PROG"
|
||||
fi
|
||||
|
||||
if mca_exec_is_steam_link "$exec_line" "$prog"; then
|
||||
MCA_STEAM_LINKS+=("$id")
|
||||
MCA_STEAM_LINK_FILES+=("$file")
|
||||
if [[ $prog == flatpak:* ]]; then
|
||||
MCA_STEAM_LINK_PACK+=(flatpak)
|
||||
elif [[ $prog == snap:* ]]; then
|
||||
MCA_STEAM_LINK_PACK+=(snap)
|
||||
else
|
||||
MCA_STEAM_LINK_PACK+=(native)
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
c_ids+=("$id"); c_files+=("$file"); c_names+=("$name")
|
||||
@@ -463,15 +666,32 @@ mca_scan() {
|
||||
MCA_STAT=()
|
||||
_mca_stat_batch "${c_stat[@]}"
|
||||
|
||||
# Pass two: decide what each one is.
|
||||
# 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
|
||||
# installed as a snap is still a browser, and somebody who has turned
|
||||
# browsers off means that one too.
|
||||
for i in "${!c_ids[@]}"; do
|
||||
prog="${c_progs[i]}"
|
||||
kind=no
|
||||
packaging=native
|
||||
|
||||
if [[ $prog == flatpak:* ]]; then
|
||||
packaging=flatpak
|
||||
if mca_prog_is_steam "$prog"; then
|
||||
kind=steam
|
||||
elif mca_flatpak_is_chromium "${prog#flatpak:}"; then
|
||||
(( c_browser[i] )) && kind=browser || kind=app
|
||||
fi
|
||||
prog="${prog#flatpak:}"
|
||||
mca_flatpak_is_chromium "$prog" && kind=flatpak
|
||||
elif [[ ${prog##*/} == steam || ${prog##*/} == steam-runtime ]]; then
|
||||
elif [[ $prog == snap:* ]]; then
|
||||
prog="${prog#snap:}"
|
||||
packaging=snap
|
||||
if [[ $prog == steam ]]; then
|
||||
kind=steam
|
||||
elif mca_snap_is_chromium "$prog"; then
|
||||
(( c_browser[i] )) && kind=browser || kind=app
|
||||
fi
|
||||
elif mca_prog_is_steam "$prog"; then
|
||||
# Steam is Chromium inside, but nothing about it can be changed
|
||||
# from a command line argument; it has its own module.
|
||||
kind=steam
|
||||
@@ -488,6 +708,7 @@ mca_scan() {
|
||||
MCA_NAMES+=("${c_names[i]}")
|
||||
MCA_PROGS+=("$prog")
|
||||
MCA_KINDS+=("$kind")
|
||||
MCA_PACKAGING+=("$packaging")
|
||||
done
|
||||
|
||||
mca_cache_flush
|
||||
@@ -526,6 +747,66 @@ mca_desktop_is_browser() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# mca_snap_name <program>
|
||||
# The snap an executable belongs to, left in MCA_PROG - assigned rather 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
|
||||
# fork per entry.
|
||||
#
|
||||
# /snap/bin/<name> is the shim snapd puts in PATH and is a symlink to snapd
|
||||
# itself, so following it lands on /usr/bin/snap and says nothing whatever
|
||||
# about the application. The name is the only thing that carries information,
|
||||
# and it is what leads to the mounted tree below.
|
||||
mca_snap_name() {
|
||||
local prog="$1" rest d
|
||||
|
||||
for d in "${MCA_SNAP_DIRS[@]}"; do
|
||||
if [[ $prog == "$d/bin/"* ]]; then
|
||||
rest="${prog#"$d/bin/"}"
|
||||
# A snap that ships several programs names them <snap>.<app>.
|
||||
MCA_PROG="${rest%%.*}"
|
||||
return 0
|
||||
fi
|
||||
if [[ $prog == "$d/"* ]]; then
|
||||
rest="${prog#"$d/"}"
|
||||
MCA_PROG="${rest%%/*}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# mca_snap_is_chromium <snap name>
|
||||
# A snap keeps everything it ships inside its own mounted revision, so the
|
||||
# marker check works the same way there as anywhere else once that tree has
|
||||
# been located. "current" is the symlink snapd keeps pointing at the revision
|
||||
# that will actually be started.
|
||||
mca_snap_is_chromium() {
|
||||
local name="$1" d root
|
||||
|
||||
if [[ -n ${MCA_DETECT_MEMO[snap:$name]+set} ]]; then
|
||||
[[ ${MCA_DETECT_MEMO[snap:$name]} == yes ]]
|
||||
return $?
|
||||
fi
|
||||
|
||||
for d in "${MCA_SNAP_DIRS[@]}"; do
|
||||
root="$d/$name/current"
|
||||
[[ -d $root ]] || continue
|
||||
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
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
MCA_DETECT_MEMO[snap:$name]=no
|
||||
return 1
|
||||
}
|
||||
|
||||
# mca_flatpak_is_chromium <app id>
|
||||
# Flatpak keeps every application in its own tree, so the marker check works the
|
||||
# same way once that tree has been located.
|
||||
@@ -553,19 +834,28 @@ mca_flatpak_is_chromium() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# mca_kind_wanted <kind> <id>
|
||||
# mca_kind_wanted <kind> <id> [packaging]
|
||||
# Whether the current settings say this entry should be patched. Skip beats
|
||||
# everything, an explicit include beats detection, and detection beats nothing.
|
||||
#
|
||||
# Packaging is a gate in front of the category rather than a category of its
|
||||
# own: a Flatpak or a snap sees none of the host's configuration and is worth
|
||||
# switching off as a group, but it is still an application or a browser and
|
||||
# whichever of those the user turned off applies to it too.
|
||||
mca_kind_wanted() {
|
||||
local kind="$1" id="$2"
|
||||
local kind="$1" id="$2" packaging="${3:-native}"
|
||||
|
||||
mca_config_list_has Skip "$id" && return 1
|
||||
mca_config_list_has Include "$id" && return 0
|
||||
|
||||
case "$packaging" in
|
||||
flatpak) [[ $CFG_FLATPAK == yes ]] || return 1 ;;
|
||||
snap) [[ $CFG_SNAP == yes ]] || return 1 ;;
|
||||
esac
|
||||
|
||||
case "$kind" in
|
||||
app) [[ $CFG_APPS == yes ]] ;;
|
||||
browser) [[ $CFG_BROWSERS == yes ]] ;;
|
||||
flatpak) [[ $CFG_FLATPAK == yes && $CFG_APPS == yes ]] ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
+41
-11
@@ -178,6 +178,9 @@ mca_ui_status() {
|
||||
if mca_steam_installed; then
|
||||
if mca_steam_patched; then
|
||||
_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
|
||||
_mca_row "$(mca_msg "Steam")" \
|
||||
"${MCA_C_YELLOW}$(mca_msg "not patched yet")${MCA_C_RESET}"
|
||||
@@ -212,6 +215,7 @@ MCA_SETTINGS=(
|
||||
"PatchApps|bool|yes|Electron and CEF applications"
|
||||
"PatchBrowsers|bool|yes|Chromium-based browsers"
|
||||
"PatchFlatpak|bool|yes|Flatpak applications"
|
||||
"PatchSnap|bool|yes|Snap applications"
|
||||
"PatchAutostart|bool|yes|Programs that start themselves at login"
|
||||
"PatchSteam|bool|yes|Steam"
|
||||
"PatchSpotify|bool|yes|Spotify"
|
||||
@@ -371,12 +375,32 @@ mca_ui_apps() {
|
||||
|
||||
locale="$(mca_ui_locale)"
|
||||
|
||||
local title hint legend
|
||||
local title hint legend warn
|
||||
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"
|
||||
hint="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "Anything not identified is left alone until it is turned on here."
|
||||
legend="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "Autoscroll is off - this is what would be covered."
|
||||
warn="$MCA_MSG_RESULT"
|
||||
|
||||
# The per-row labels are resolved once, here. Looking them up inside the
|
||||
# drawing loop is a fork per row per keypress, and that is enough to make
|
||||
# the arrow keys feel like the screen is reloading.
|
||||
local l_off l_cannot l_on l_steam l_flagfile l_launcher
|
||||
mca_msg_into "$locale" "off"; l_off="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "cannot tell"; l_cannot="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "on"; l_on="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "Steam"; l_steam="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "flag file"; l_flagfile="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "launcher"; l_launcher="$MCA_MSG_RESULT"
|
||||
|
||||
local s_off="${MCA_C_DIM}${l_off}${MCA_C_RESET}"
|
||||
local s_cannot="${MCA_C_DIM}${l_cannot}${MCA_C_RESET}"
|
||||
local s_on="${MCA_C_GREEN}${l_on}${MCA_C_RESET}"
|
||||
local s_steam="${s_on} ${MCA_C_DIM}(${l_steam})${MCA_C_RESET}"
|
||||
local s_flags="${s_on} ${MCA_C_DIM}(${l_flagfile})${MCA_C_RESET}"
|
||||
local s_desktop="${s_on} ${MCA_C_DIM}(${l_launcher})${MCA_C_RESET}"
|
||||
|
||||
local clearseq
|
||||
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
|
||||
@@ -409,11 +433,11 @@ mca_ui_apps() {
|
||||
local marker selected="${MCA_C_BLUE}▸${MCA_C_RESET} " shown
|
||||
for i in "${!labels[@]}"; do
|
||||
case "${states[i]}" in
|
||||
off) shown="${MCA_C_DIM}$(mca_msg "off")${MCA_C_RESET}" ;;
|
||||
unknown) shown="${MCA_C_DIM}$(mca_msg "cannot tell")${MCA_C_RESET}" ;;
|
||||
steam) shown="${MCA_C_GREEN}$(mca_msg "on")${MCA_C_RESET} ${MCA_C_DIM}($(mca_msg "Steam"))${MCA_C_RESET}" ;;
|
||||
flags) shown="${MCA_C_GREEN}$(mca_msg "on")${MCA_C_RESET} ${MCA_C_DIM}($(mca_msg "flag file"))${MCA_C_RESET}" ;;
|
||||
*) shown="${MCA_C_GREEN}$(mca_msg "on")${MCA_C_RESET} ${MCA_C_DIM}($(mca_msg "launcher"))${MCA_C_RESET}" ;;
|
||||
off) shown="$s_off" ;;
|
||||
unknown) shown="$s_cannot" ;;
|
||||
steam) shown="$s_steam" ;;
|
||||
flags) shown="$s_flags" ;;
|
||||
*) shown="$s_desktop" ;;
|
||||
esac
|
||||
pad=$(( 34 - ${#labels[i]} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
@@ -426,7 +450,7 @@ mca_ui_apps() {
|
||||
# Without this the list reads as a list of what is switched on, which
|
||||
# it is not while the whole thing is off.
|
||||
if [[ $CFG_ENABLED != yes ]]; then
|
||||
frame+=" ${MCA_C_YELLOW}$(mca_msg "Autoscroll is off - this is what would be covered.")${MCA_C_RESET}"$'\n'
|
||||
frame+=" ${MCA_C_YELLOW}${warn}${MCA_C_RESET}"$'\n'
|
||||
fi
|
||||
frame+=" ${MCA_C_DIM}${legend}${MCA_C_RESET}"$'\n'
|
||||
frame+=" ${MCA_C_DIM}${hint}${MCA_C_RESET}"$'\n'
|
||||
@@ -475,7 +499,7 @@ mca_ui_menu() {
|
||||
mca_ui_status
|
||||
printf '\n'
|
||||
printf ' [1] %s\n' "$(mca_msg "Turn autoscroll on or off")"
|
||||
printf ' [2] %s\n' "$(mca_msg "Apply now")"
|
||||
printf ' [2] %s\n' "$(mca_msg "Re-apply everything")"
|
||||
printf ' [3] %s\n' "$(mca_msg "Applications")"
|
||||
printf ' [4] %s\n' "$(mca_msg "Settings")"
|
||||
printf ' [q] %s\n' "$(mca_msg "Quit")"
|
||||
@@ -499,18 +523,24 @@ mca_ui_menu() {
|
||||
fi
|
||||
[[ -n $MCA_UI_NEEDS_ACK ]] && mca_pause
|
||||
;;
|
||||
# Not a plain apply: with the watcher running there is never
|
||||
# anything left for one to do, and a menu entry that answers
|
||||
# "already done" every time is not an action. This is the repair -
|
||||
# everything is taken back and written again from scratch, which is
|
||||
# what fixes an application that drifted, a flag file somebody
|
||||
# edited, or Steam after it restored its own script.
|
||||
2)
|
||||
mca_ui_cooked mca_do_apply
|
||||
mca_ui_cooked mca_do_apply --rebuild
|
||||
mca_pause
|
||||
;;
|
||||
3)
|
||||
if mca_ui_apps; then
|
||||
[[ $CFG_ENABLED == yes ]] && mca_ui_cooked mca_do_apply --changed
|
||||
[[ $CFG_ENABLED == yes ]] && mca_ui_cooked mca_do_apply --rebuild
|
||||
fi
|
||||
;;
|
||||
4)
|
||||
if mca_ui_settings; then
|
||||
[[ $CFG_ENABLED == yes ]] && mca_ui_cooked mca_do_apply --changed
|
||||
[[ $CFG_ENABLED == yes ]] && mca_ui_cooked mca_do_apply --rebuild
|
||||
fi
|
||||
;;
|
||||
q|Q) mca_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
|
||||
|
||||
+183
-45
@@ -4,15 +4,25 @@
|
||||
#
|
||||
# There are two ways in, and the order matters:
|
||||
#
|
||||
# 1. A flag file. Arch's Electron and Chromium wrappers read extra arguments
|
||||
# from $XDG_CONFIG_HOME/<name>-flags.conf. This is the good one - it is the
|
||||
# supported way to pass arguments, it survives package upgrades untouched,
|
||||
# and it applies however the program is started, including from a terminal.
|
||||
# 1. A flag file. Where the distribution wraps Electron and Chromium in a
|
||||
# launcher script that reads extra arguments from
|
||||
# $XDG_CONFIG_HOME/<name>-flags.conf - Arch and its derivatives do, and it
|
||||
# is the convention their packages follow - this is the good one: it is
|
||||
# the supported way to pass arguments, it survives package upgrades
|
||||
# untouched, and it applies however the program is started, including from
|
||||
# a terminal.
|
||||
#
|
||||
# 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
|
||||
# name such a file. Elsewhere - Debian, Ubuntu, Fedora, openSUSE, where
|
||||
# the equivalent file lives under /etc and is the system's to write -
|
||||
# there is no flag file to use and route 2 answers for everything.
|
||||
#
|
||||
# 2. A desktop entry. For applications that ship their own binary with no
|
||||
# wrapper - most vendor packages, every Flatpak - there is nowhere else to
|
||||
# put an argument, so a copy of the entry with the flag appended goes into
|
||||
# ~/.local/share/applications, where it shadows the system one.
|
||||
# wrapper, and for everything a Flatpak or a snap contains, there is
|
||||
# nowhere else to put an argument, so a copy of the entry with the flag
|
||||
# appended goes into ~/.local/share/applications, where it shadows the
|
||||
# system one.
|
||||
#
|
||||
# Nothing outside $HOME is ever written to.
|
||||
|
||||
@@ -195,9 +205,6 @@ _mca_flags_write() {
|
||||
mca_ledger_add flags "$file" block
|
||||
fi
|
||||
|
||||
# Every application whose flag file was touched counts as covered, so the
|
||||
# status line can say how many there are.
|
||||
MCA_TOUCHED+=("$file")
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -255,12 +262,20 @@ mca_flags_revert() {
|
||||
# Desktop entries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# _mca_desktop_transform <file> [flags] [position]
|
||||
# A generated entry and an entry that was edited in place look similar and must
|
||||
# never be confused: the first one is ours to delete, the second one is the
|
||||
# user's file with one line changed and has to be restored from its backup. They
|
||||
# carry different markers so that the scan, the "already done" checks and the
|
||||
# undo can all tell them apart.
|
||||
MCA_MARK_SHADOW='X-MCA-Generated'
|
||||
MCA_MARK_INPLACE='X-MCA-Patched'
|
||||
|
||||
# _mca_desktop_transform <file> <marker> [flags] [position]
|
||||
# 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
|
||||
# user who starts Steam from "Library" expects the same behaviour there.
|
||||
_mca_desktop_transform() {
|
||||
local file="$1" flags="${2:-}" where="${3:-before-fields}"
|
||||
local file="$1" marker="$2" flags="${3:-}" where="${4:-before-fields}"
|
||||
local line rest new marked=0
|
||||
|
||||
while IFS= read -r line || [[ -n $line ]]; do
|
||||
@@ -283,7 +298,7 @@ _mca_desktop_transform() {
|
||||
printf '%s\n' "$line"
|
||||
|
||||
if (( ! marked )) && [[ $line =~ ^[[:space:]]*\[Desktop\ Entry\][[:space:]]*$ ]]; then
|
||||
printf 'X-MCA-Generated=%s\n' "$MCA_VERSION"
|
||||
printf '%s=%s\n' "$marker" "$MCA_VERSION"
|
||||
marked=1
|
||||
fi
|
||||
done < "$file"
|
||||
@@ -300,9 +315,9 @@ mca_desktop_apply() {
|
||||
# The user's own entry - an AppImage, a web app shortcut, something
|
||||
# installed by hand. There is nowhere to shadow it from, so it is
|
||||
# edited directly and the original is kept.
|
||||
grep -q '^X-MCA-Generated=' "$src" 2>/dev/null && return 0
|
||||
grep -q "^$MCA_MARK_INPLACE=" "$src" 2>/dev/null && return 0
|
||||
|
||||
content="$(_mca_desktop_transform "$src" "$flags" "$where")"
|
||||
content="$(_mca_desktop_transform "$src" "$MCA_MARK_INPLACE" "$flags" "$where")"
|
||||
[[ -n $content ]] || return 1
|
||||
[[ "$content" == "$(< "$src")" ]] && return 0
|
||||
|
||||
@@ -311,18 +326,18 @@ mca_desktop_apply() {
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add inplace "$src" "$backup"
|
||||
MCA_TOUCHED+=("$src")
|
||||
return 0
|
||||
fi
|
||||
|
||||
content="$(_mca_desktop_transform "$src" "$flags" "$where")"
|
||||
content="$(_mca_desktop_transform "$src" "$MCA_MARK_SHADOW" "$flags" "$where")"
|
||||
[[ -n $content ]] || return 1
|
||||
|
||||
mkdir -p "$MCA_APPDIR" 2>/dev/null || return 1
|
||||
|
||||
# Something is already shadowing this entry. If it is not ours it is the
|
||||
# user's own override and is left exactly as it is.
|
||||
if [[ -e $target ]] && ! grep -q '^X-MCA-Generated=' "$target" 2>/dev/null; then
|
||||
# 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
|
||||
# overwriting it here would lose it.
|
||||
if [[ -e $target ]] && ! grep -q "^$MCA_MARK_SHADOW=" "$target" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -330,44 +345,121 @@ mca_desktop_apply() {
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add shadow "$target" "$src"
|
||||
MCA_TOUCHED+=("$target")
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Autostart entries
|
||||
# Entries outside the search path
|
||||
# ---------------------------------------------------------------------------
|
||||
# Applications that start themselves at login write their own entry into
|
||||
# ~/.config/autostart, pointing straight at their binary. Those bypass the
|
||||
# desktop entry in the menu completely, which is why Discord launched at login
|
||||
# used to behave differently from Discord launched from the menu.
|
||||
# Two directories hold desktop entries that no part of the XDG search path
|
||||
# looks at, so nothing the scan does can reach them:
|
||||
#
|
||||
# ~/.config/autostart, where applications that start themselves at login
|
||||
# write their own entry, pointing straight at their binary. Those bypass the
|
||||
# entry in the menu completely, which is why Discord launched at login used
|
||||
# to behave differently from Discord launched from the menu.
|
||||
#
|
||||
# The desktop folder, where a shortcut somebody dragged out of the menu -
|
||||
# or asked Steam for - lives and nowhere else.
|
||||
#
|
||||
# Steam is in both, even though it is not a Chromium process itself: its
|
||||
# entries need -noverifyfiles exactly like the menu one. The autostart entry is
|
||||
# the one most likely to exist, because Steam writes it as soon as "run at
|
||||
# startup" is ticked; without it a Steam started at login restores the patched
|
||||
# web helper script and the watcher patches it back, over and over. The desktop
|
||||
# ones are the gap behind "autoscroll works, except sometimes": starting a game
|
||||
# from the desktop is a Steam start like any other, and a Steam start without
|
||||
# the switch costs the interface its autoscroll for the rest of the session.
|
||||
#
|
||||
# Neither can be shadowed from anywhere, so both are edited where they stand,
|
||||
# with the original kept.
|
||||
|
||||
# _mca_entry_patch_inplace <file> <gate: autostart|apps>
|
||||
# One desktop entry that lives outside the XDG search path, edited where it is
|
||||
# because there is nowhere to shadow it from.
|
||||
#
|
||||
# Anything that starts Steam gets Steam's own switch; a Chromium application
|
||||
# gets the flag. What decides whether a Chromium application here is in scope
|
||||
# differs by where the entry came from, which is what the gate says: an
|
||||
# autostart entry follows the autostart setting, a shortcut follows the same
|
||||
# rules as the application it is a shortcut to.
|
||||
_mca_entry_patch_inplace() {
|
||||
local file="$1" gate="$2"
|
||||
local id prog packaging=native content backup kind
|
||||
|
||||
grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null && return 0
|
||||
|
||||
_mca_desktop_read "$file"
|
||||
[[ -n $DE_EXEC ]] || return 0
|
||||
[[ $DE_HIDDEN == true ]] && return 0
|
||||
|
||||
mca_exec_program "$DE_EXEC" || return 0
|
||||
prog="$MCA_PROG"
|
||||
|
||||
# A Flatpak or a snap entry names the wrapper rather than the application.
|
||||
# The scan resolves those into an id; outside the scan the same has to
|
||||
# happen here, or every Flatpak looks like the flatpak program itself.
|
||||
if [[ ${prog##*/} == flatpak ]]; then
|
||||
mca_exec_flatpak_id "$DE_EXEC" || return 0
|
||||
prog="flatpak:$MCA_PROG"
|
||||
packaging=flatpak
|
||||
elif mca_snap_name "$prog"; then
|
||||
prog="snap:$MCA_PROG"
|
||||
packaging=snap
|
||||
fi
|
||||
|
||||
if mca_prog_is_steam "$prog"; then
|
||||
[[ $CFG_STEAM == yes ]] || return 0
|
||||
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \
|
||||
"$MCA_STEAM_LAUNCH_FLAG" "$(mca_steam_flag_position "$packaging")")"
|
||||
else
|
||||
case "$packaging" in
|
||||
flatpak) mca_flatpak_is_chromium "${prog#flatpak:}" || return 0 ;;
|
||||
snap) mca_snap_is_chromium "${prog#snap:}" || return 0 ;;
|
||||
*) mca_is_chromium "$prog" || return 0 ;;
|
||||
esac
|
||||
|
||||
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
|
||||
|
||||
[[ -n $content ]] || return 0
|
||||
[[ "$content" == "$(< "$file")" ]] && return 0
|
||||
|
||||
backup="$(mca_backup "$file")" || return 0
|
||||
if mca_write_if_changed "$file" "$content"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add inplace "$file" "$backup"
|
||||
}
|
||||
|
||||
mca_autostart_apply() {
|
||||
local dir="$MCA_XDG_CONFIG/autostart" file exec_line prog content backup
|
||||
local dir="$MCA_XDG_CONFIG/autostart" file
|
||||
|
||||
[[ -d $dir ]] || return 0
|
||||
|
||||
for file in "$dir"/*.desktop; do
|
||||
[[ -f $file ]] || continue
|
||||
grep -q '^X-MCA-Generated=' "$file" 2>/dev/null && continue
|
||||
_mca_entry_patch_inplace "$file" autostart
|
||||
done
|
||||
}
|
||||
|
||||
exec_line="$(mca_desktop_get "$file" Exec)"
|
||||
[[ -n $exec_line ]] || continue
|
||||
mca_shortcuts_apply() {
|
||||
local dir file
|
||||
dir="$(mca_desktop_folder)"
|
||||
|
||||
mca_exec_program "$exec_line" || continue
|
||||
prog="$MCA_PROG"
|
||||
mca_is_chromium "$prog" || continue
|
||||
[[ -d $dir ]] || return 0
|
||||
|
||||
content="$(_mca_desktop_transform "$file")"
|
||||
[[ -n $content ]] || continue
|
||||
[[ "$content" == "$(< "$file")" ]] && continue
|
||||
|
||||
backup="$(mca_backup "$file")" || continue
|
||||
if mca_write_if_changed "$file" "$content"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add inplace "$file" "$backup"
|
||||
MCA_TOUCHED+=("$file")
|
||||
for file in "$dir"/*.desktop; do
|
||||
[[ -f $file ]] || continue
|
||||
_mca_entry_patch_inplace "$file" apps
|
||||
done
|
||||
}
|
||||
|
||||
@@ -412,7 +504,53 @@ mca_spotify_apply() {
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add inplace "$file" "${backup:-}"
|
||||
MCA_TOUCHED+=("$file")
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Leftovers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# mca_prune_orphans
|
||||
# Forgets changes there is no longer anything to undo.
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
# looks at what is there now. A file that was edited where it stood and has
|
||||
# since been deleted is the same problem from the other side: its record would
|
||||
# have `disable` put the file back.
|
||||
mca_prune_orphans() {
|
||||
local kind path source line
|
||||
local -a lines=()
|
||||
|
||||
[[ -f $MCA_LEDGER ]] || return 0
|
||||
mapfile -t lines < "$MCA_LEDGER"
|
||||
|
||||
for line in "${lines[@]}"; do
|
||||
IFS=$'\t' read -r kind path source <<< "$line"
|
||||
|
||||
# An entry that was edited where it stood and has since been deleted -
|
||||
# a game shortcut dragged to the wastebasket, an autostart entry the
|
||||
# application removed. There is nothing left to put back, and keeping
|
||||
# the record would have `disable` recreate a file the user threw away.
|
||||
if [[ $kind == inplace && ! -e $path ]]; then
|
||||
[[ -n $source ]] && rm -f -- "$MCA_BACKUPDIR/$source"
|
||||
mca_ledger_forget "$path"
|
||||
continue
|
||||
fi
|
||||
|
||||
[[ $kind == shadow && -n $source ]] || continue
|
||||
[[ -e $source ]] && continue
|
||||
|
||||
# Only ever remove something still recognisably ours.
|
||||
if [[ -f $path ]] && grep -q "^$MCA_MARK_SHADOW=" "$path" 2>/dev/null; then
|
||||
rm -f -- "$path"
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_forget "$path"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -431,7 +569,7 @@ mca_revert_all() {
|
||||
case "$kind" in
|
||||
shadow)
|
||||
# Only remove what is still recognisably ours.
|
||||
if [[ -f $path ]] && grep -q '^X-MCA-Generated=' "$path" 2>/dev/null; then
|
||||
if [[ -f $path ]] && grep -q "^$MCA_MARK_SHADOW=" "$path" 2>/dev/null; then
|
||||
rm -f -- "$path"
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
|
||||
+223
-25
@@ -9,20 +9,41 @@
|
||||
#
|
||||
# ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
#
|
||||
# Steam checksums that script at every start and restores it when it differs,
|
||||
# which is why the launcher gets -noverifyfiles as well. The trade-off is real
|
||||
# and belongs to the user, so this is a switch of its own rather than part of
|
||||
# the general application handling: with verification off, Steam no longer
|
||||
# repairs a damaged installation by itself.
|
||||
# Steam compares the installed files against its manifest at every start - by
|
||||
# size and timestamp rather than by content - and restores whatever differs.
|
||||
# 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,
|
||||
# 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
|
||||
# and the flag survives however Steam was started - from the menu, from a game
|
||||
# shortcut, from a launcher like Heroic, from a terminal.
|
||||
#
|
||||
# That is the part that has to work, because there is no way to make every
|
||||
# possible way of starting Steam carry an argument. -noverifyfiles on the
|
||||
# launcher entries is the second line rather than the first: it covers the case
|
||||
# where the script has no comments left to pay for the flag and the patch has
|
||||
# to grow the file. The trade-off is real and belongs to the user, so it is a
|
||||
# switch of its own rather than part of the general application handling: with
|
||||
# verification off, Steam no longer repairs a damaged installation by itself.
|
||||
#
|
||||
# A patch that does change the size still has to stay out of the way of a
|
||||
# client that is already running, or the two programs spend the session undoing
|
||||
# each other: Steam restores the script, the watcher patches it again, Steam
|
||||
# restores it again, and the client never gets past its update dialog. A patch
|
||||
# that keeps the size is invisible to that check and goes in either way.
|
||||
#
|
||||
# The file comes back on every client update, and the watcher re-applies the
|
||||
# patch when that happens.
|
||||
|
||||
MCA_STEAM_EXEC='exec ./steamwebhelper "$@"'
|
||||
MCA_STEAM_LAUNCH_FLAG='-noverifyfiles'
|
||||
|
||||
# Every place a Steam installation is known to live, resolved and de-duplicated
|
||||
# because ~/.steam/steam is normally a symlink into ~/.local/share.
|
||||
#
|
||||
# Which of these is the real one depends on how Steam was installed: Valve's
|
||||
# own package and the Arch one use ~/.local/share/Steam, Debian's puts it in
|
||||
# ~/.steam/debian-installation, and the Flatpak and the snap each keep it
|
||||
# inside the private tree their sandbox gives them.
|
||||
mca_steam_roots() {
|
||||
local candidates=(
|
||||
"$MCA_XDG_DATA/Steam"
|
||||
@@ -30,6 +51,8 @@ mca_steam_roots() {
|
||||
"$HOME/.steam/root"
|
||||
"$HOME/.steam/debian-installation"
|
||||
"$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"
|
||||
"$HOME/.var/app/com.valvesoftware.Steam/data/Steam"
|
||||
"$HOME/snap/steam/common/.local/share/Steam"
|
||||
)
|
||||
local dir real
|
||||
local -A seen=()
|
||||
@@ -65,6 +88,24 @@ mca_steam_installed() {
|
||||
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() {
|
||||
grep -q -- "$MCA_FEATURE" "$1" 2>/dev/null
|
||||
@@ -79,6 +120,26 @@ mca_steam_patched() {
|
||||
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>
|
||||
# The line number of the command that starts the web helper: the last
|
||||
# uncommented line that both names steamwebhelper and forwards "$@". Both the
|
||||
@@ -93,12 +154,112 @@ _mca_steam_exec_line() {
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# _mca_steam_build <script> <flags> <reclaim: 0|1>
|
||||
# The patched script on stdout: the flags appended to the line that starts the
|
||||
# web helper, and - when asked for - the same number of bytes taken back out of
|
||||
# the script's comments, so that the file Steam finds is the length Steam wrote
|
||||
# down. Everything else - setting up the container runtime, deciding whether
|
||||
# the sandbox can be used - is left alone.
|
||||
#
|
||||
# Comments are eaten from the bottom up, so the header that says what the
|
||||
# script is for is the last thing to lose anything, and the '#' itself always
|
||||
# stays: a line that loses it stops being a comment and starts being a command.
|
||||
# Fails when the comments are too short to pay for the flags.
|
||||
_mca_steam_build() {
|
||||
local script="$1" flags="$2" reclaim="$3" lineno need=0
|
||||
|
||||
lineno="$(_mca_steam_exec_line "$script")"
|
||||
[[ -n $lineno ]] || return 1
|
||||
|
||||
if (( reclaim )); then
|
||||
need="$(printf '%s' " $flags" | wc -c)"
|
||||
need="${need//[^0-9]/}"
|
||||
fi
|
||||
|
||||
# LC_ALL=C so awk counts bytes: a comment with an accent in it is shorter
|
||||
# in characters than it is on disk, and it is the disk that Steam measures.
|
||||
LC_ALL=C awk -v n="$lineno" -v f=" $flags" -v need="$need" '
|
||||
{ lines[NR] = (NR == n ? $0 f : $0) }
|
||||
END {
|
||||
for (i = NR; i >= 2 && need > 0; i--) {
|
||||
if (i == n) continue
|
||||
p = index(lines[i], "#")
|
||||
if (p == 0) continue
|
||||
# Only a line that is nothing but a comment; a trailing one
|
||||
# sits behind a command that has to stay intact.
|
||||
if (substr(lines[i], 1, p - 1) ~ /[^ \t]/) continue
|
||||
can = length(lines[i]) - p
|
||||
if (can <= 0) continue
|
||||
if (can > need) can = need
|
||||
lines[i] = substr(lines[i], 1, length(lines[i]) - can)
|
||||
need -= can
|
||||
}
|
||||
if (need > 0) exit 1
|
||||
for (i = 1; i <= NR; i++) print lines[i]
|
||||
}
|
||||
' "$script"
|
||||
}
|
||||
|
||||
# _mca_steam_same_size <script> <content>
|
||||
# Whether writing that content leaves the file at the size it has now. Measured
|
||||
# rather than worked out: a script without a final newline gains one on the way
|
||||
# through awk, and one byte is all it takes for Steam to notice.
|
||||
_mca_steam_same_size() {
|
||||
local script="$1" content="$2"
|
||||
(( $(wc -c < "$script") == $(printf '%s\n' "$content" | wc -c) ))
|
||||
}
|
||||
|
||||
# _mca_steam_refresh_backup <script>
|
||||
# A client update brings a new version of the script, and the copy kept from
|
||||
# before it is no longer what putting it back means. Only ever done while the
|
||||
# script is unpatched, so the copy that replaces it is Steam's own.
|
||||
_mca_steam_refresh_backup() {
|
||||
local script="$1" name
|
||||
name="$(mca_backup_name "$script")"
|
||||
|
||||
[[ -e "$MCA_BACKUPDIR/$name" ]] || return 0
|
||||
cmp -s -- "$script" "$MCA_BACKUPDIR/$name" && return 0
|
||||
cp -p -- "$script" "$MCA_BACKUPDIR/$name" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# _mca_steam_reneutralise <script> <flags>
|
||||
# An earlier version of this program patched by appending and left the script
|
||||
# longer than Steam expects, which is a patch that only survives because
|
||||
# -noverifyfiles is there to stop Steam looking. If the same flags can be made
|
||||
# to fit, they are: the file goes back to the length and the timestamp Steam
|
||||
# wrote down and stops depending on that switch.
|
||||
#
|
||||
# The new copy is built from the untouched original that was kept when the
|
||||
# script was first patched, never from the patched file - patching a patched
|
||||
# file is how a flag ends up in there twice.
|
||||
_mca_steam_reneutralise() {
|
||||
local script="$1" flags="$2" name copy out
|
||||
|
||||
name="$(mca_backup_name "$script")"
|
||||
copy="$MCA_BACKUPDIR/$name"
|
||||
[[ -f $copy ]] || return 1
|
||||
|
||||
# Already the length Steam expects: there is nothing to gain.
|
||||
(( $(wc -c < "$script") == $(wc -c < "$copy") )) && return 1
|
||||
|
||||
out="$(_mca_steam_build "$copy" "$flags" 1)" || return 1
|
||||
_mca_steam_same_size "$copy" "$out" || return 1
|
||||
|
||||
if mca_write_if_changed "$script" "$out"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
chmod +x -- "$script" 2>/dev/null || true
|
||||
touch -r "$copy" -- "$script" 2>/dev/null || true
|
||||
mca_ledger_add steam "$script" "$name"
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_steam_apply
|
||||
# Appends the flags to the line that starts the web helper. Everything else in
|
||||
# the script - setting up the container runtime, deciding whether the sandbox
|
||||
# can be used - is left alone.
|
||||
# Puts the flags into every Steam installation that has the script, preferring
|
||||
# the patch that keeps the file's size and falling back to the one that does
|
||||
# not. Returns 1 when there is no Steam to patch at all.
|
||||
mca_steam_apply() {
|
||||
local root script backup flags lineno found=0 out
|
||||
local root script backup flags found=0 out neutral
|
||||
|
||||
flags="$(mca_flags)"
|
||||
|
||||
@@ -106,26 +267,52 @@ mca_steam_apply() {
|
||||
script="$(mca_steam_script "$root")" || continue
|
||||
found=1
|
||||
|
||||
mca_steam_script_patched "$script" && { MCA_TOUCHED+=("$script"); continue; }
|
||||
|
||||
lineno="$(_mca_steam_exec_line "$script")"
|
||||
if [[ -z $lineno ]]; then
|
||||
# A client update changed how the helper is started. Leaving the
|
||||
# file alone is the only safe answer: this script is what starts
|
||||
# Steam's entire interface.
|
||||
mca_note "$(mca_msg "Steam starts its interface in a way this version does not recognise; leaving it alone.")"
|
||||
if mca_steam_script_patched "$script"; then
|
||||
_mca_steam_reneutralise "$script" "$flags" || true
|
||||
continue
|
||||
fi
|
||||
|
||||
out="$(awk -v n="$lineno" -v f=" $flags" 'NR == n { $0 = $0 f } { print }' "$script")" || continue
|
||||
_mca_steam_refresh_backup "$script"
|
||||
|
||||
neutral=1
|
||||
if ! { out="$(_mca_steam_build "$script" "$flags" 1)" \
|
||||
&& _mca_steam_same_size "$script" "$out"; }
|
||||
then
|
||||
neutral=0
|
||||
out="$(_mca_steam_build "$script" "$flags" 0)" || {
|
||||
# A client update changed how the helper is started. Leaving
|
||||
# the file alone is the only safe answer: this script is what
|
||||
# starts Steam's entire interface.
|
||||
mca_note "$(mca_msg "Steam starts its interface in a way this version does not recognise; leaving it alone.")"
|
||||
continue
|
||||
}
|
||||
fi
|
||||
|
||||
# The patch has to grow the file, and it is not patched now but was
|
||||
# before: Steam has just put its own copy back. Patching it again
|
||||
# while the client watches is what turns one size mismatch into an
|
||||
# endless update dialog, and it would not help this session anyway -
|
||||
# the helper is started once, at the start. The patch waits for the
|
||||
# next apply with Steam closed. A patch that keeps the size has
|
||||
# nothing to wait for.
|
||||
if (( ! neutral )) && mca_steam_deferred "$script"; then
|
||||
mca_note "$(mca_msg "Steam is running and has put its own file back; the change waits until Steam is closed.")"
|
||||
continue
|
||||
fi
|
||||
|
||||
backup="$(mca_backup "$script")" || continue
|
||||
if mca_write_if_changed "$script" "$out"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
chmod +x -- "$script" 2>/dev/null || true
|
||||
|
||||
# Steam looks at when the file was last written as well as at how big
|
||||
# it is, and the copy taken before the edit still carries the original
|
||||
# timestamp. Putting it back costs nothing and removes the other half
|
||||
# of what the client would notice.
|
||||
touch -r "$MCA_BACKUPDIR/$backup" -- "$script" 2>/dev/null || true
|
||||
|
||||
mca_ledger_add steam "$script" "$backup"
|
||||
MCA_TOUCHED+=("$script")
|
||||
done < <(mca_steam_roots)
|
||||
|
||||
(( found ))
|
||||
@@ -153,11 +340,22 @@ mca_steam_revert() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_steam_desktop_apply <id> <source entry>
|
||||
# mca_steam_flag_position <packaging>
|
||||
# Where Steam's own switch goes on a command line. Straight after the program
|
||||
# for a native or snap entry, because Steam reads its options before the
|
||||
# steam:// argument that the right-click actions pass. For a Flatpak the first
|
||||
# token is flatpak itself, which would take the switch for one of its own and
|
||||
# refuse to start, so there it goes at the end of the arguments - after the
|
||||
# application id, where flatpak hands everything on to Steam.
|
||||
mca_steam_flag_position() {
|
||||
[[ ${1:-native} == flatpak ]] && { printf 'before-fields\n'; return; }
|
||||
printf 'after-program\n'
|
||||
}
|
||||
|
||||
# mca_steam_desktop_apply <id> <source entry> [packaging]
|
||||
# Steam restores its own files at every start unless it is told not to verify
|
||||
# them, so the launcher entry carries that switch. It goes directly after the
|
||||
# program because Steam reads its options before the steam:// argument that the
|
||||
# right-click actions pass.
|
||||
# them, so the launcher entry carries that switch.
|
||||
mca_steam_desktop_apply() {
|
||||
mca_desktop_apply "$1" "$2" "$MCA_STEAM_LAUNCH_FLAG" after-program
|
||||
mca_desktop_apply "$1" "$2" "$MCA_STEAM_LAUNCH_FLAG" \
|
||||
"$(mca_steam_flag_position "${3:-native}")"
|
||||
}
|
||||
@@ -76,11 +76,11 @@ mca_do_disable() {
|
||||
}
|
||||
|
||||
mca_do_apply() {
|
||||
local changed=0 arg
|
||||
local rebuild=0 arg
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--changed) changed=1 ;;
|
||||
--rebuild) rebuild=1 ;;
|
||||
--quiet) MCA_QUIET=1 ;;
|
||||
esac
|
||||
done
|
||||
@@ -95,25 +95,36 @@ mca_do_apply() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# A settings change can take an application out of scope as well as bring
|
||||
# one in, and only a full undo can tell the difference, so that path starts
|
||||
# from a clean slate. The watcher never does this: rewriting files that are
|
||||
# already correct would make it trigger itself.
|
||||
if (( changed )); then
|
||||
# Start from a clean slate. Needed after a settings change, which can take
|
||||
# an application out of scope as well as bring one in and where only a full
|
||||
# undo can tell the difference, and it is what makes the repair a repair:
|
||||
# whatever drifted is written again rather than left because it is "already
|
||||
# applied".
|
||||
#
|
||||
# The watcher never does this. Rewriting files that are already correct
|
||||
# would change their timestamps, and it is watching those directories.
|
||||
if (( rebuild )); then
|
||||
mca_revert
|
||||
fi
|
||||
|
||||
mca_apply
|
||||
|
||||
if (( MCA_CHANGES )); then
|
||||
if (( rebuild )); then
|
||||
mca_ok "$(mca_msg "Applied again to %d applications." "$MCA_N_ON")"
|
||||
mca_say " $(mca_msg "Applications that are already running pick this up when they are restarted.")"
|
||||
elif (( MCA_CHANGES )); then
|
||||
mca_ok "$(mca_msg "Applied to %d applications." "$MCA_N_ON")"
|
||||
mca_say " $(mca_msg "Applications that are already running pick this up when they are restarted.")"
|
||||
else
|
||||
mca_ok "$(mca_msg "Already applied to %d applications." "$MCA_N_ON")"
|
||||
fi
|
||||
|
||||
if [[ $CFG_WATCH == yes ]] && mca_watch_available && ! mca_watch_enabled; then
|
||||
if [[ $CFG_WATCH == yes ]] && mca_watch_available; then
|
||||
if mca_watch_enabled; then
|
||||
mca_watch_refresh
|
||||
else
|
||||
mca_watch_enable || true
|
||||
fi
|
||||
elif [[ $CFG_WATCH != yes ]] && mca_watch_enabled; then
|
||||
mca_watch_disable
|
||||
fi
|
||||
@@ -160,6 +171,7 @@ mca_do_help() {
|
||||
enable $(mca_msg "Turn autoscroll on")
|
||||
disable $(mca_msg "Turn it off and put everything back")
|
||||
apply $(mca_msg "Apply to anything installed since the last run")
|
||||
apply --rebuild $(mca_msg "Take everything back and apply it again")
|
||||
status $(mca_msg "Show what is covered")
|
||||
list $(mca_msg "List every application that was found")
|
||||
-h, --help $(mca_msg "Show this help")
|
||||
|
||||
Reference in new issue
Block a user