Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ced6f57dfd | ||
|
|
21e29d99c3 | ||
|
|
c31c7b92e5 | ||
|
|
0bbf0cbd9f | ||
|
|
70f6748f3c | ||
|
|
aaa41f0384 | ||
|
|
a053b4e555 | ||
|
|
36498f2acf | ||
|
|
bd84c6645a | ||
|
|
f42d80598f | ||
|
|
4004023297 |
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.2
|
||||
VERSION ?= 1.1.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,185 +1,95 @@
|
||||
# middleclick-autoscroll
|
||||
|
||||
Middle-click autoscroll — hold the middle mouse button, move the pointer, the
|
||||
page scrolls — in every application on the system that can do it, and in
|
||||
anything installed later.
|
||||
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.
|
||||
|
||||
There is no list of supported applications to check against and none to keep up
|
||||
to date. Every launcher on the system is examined, the ones running on Chromium
|
||||
underneath are identified by what they ship rather than by their name, and each
|
||||
of them is handled.
|
||||
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:
|
||||
|
||||
```
|
||||
~/.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.
|
||||
Steam's web UI supports autoscroll but has no way to pass extra arguments to its
|
||||
helper. The program patches the helper launch script and adds `-noverifyfiles`
|
||||
to Steam's launcher so the patch sticks. That means Steam won't auto-repair
|
||||
damaged files on its own — you can turn this 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
|
||||
|
||||
@@ -188,7 +98,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
|
||||
|
||||
|
||||
@@ -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,12 +76,22 @@ 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.
|
||||
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
|
||||
@@ -91,22 +110,40 @@ 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, not by content - and restores whatever differs, so its launcher entry
|
||||
gets *-noverifyfiles*. So does its entry in _~/.config/autostart_, which Steam
|
||||
writes as soon as it is set to run at login: that entry bypasses the menu one
|
||||
entirely, and without the switch a Steam started at login spends the session in
|
||||
an update dialog. 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.
|
||||
|
||||
The shortcuts Steam writes for single games carry the switch too. A game is not
|
||||
an application this program has anything to offer and none of them is listed
|
||||
under *Applications*, but starting one with Steam closed is a Steam start like
|
||||
any other, and without the switch it costs the interface its autoscroll for the
|
||||
rest of the session.
|
||||
|
||||
Starting Steam some other way - from a terminal, from a script - leaves the
|
||||
switch out, and Steam puts its own copy of the script back for that session.
|
||||
The patch returns at the next apply with Steam closed. It is deliberately not
|
||||
repeated while the client is running: the two would only undo each other, and
|
||||
the helper is started once, at the start, so it would not help that session
|
||||
anyway.
|
||||
|
||||
# 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
|
||||
|
||||
@@ -135,9 +172,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.2\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.2\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,33 @@ 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
|
||||
|
||||
# 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/snap/steam/common/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
|
||||
Unit=middleclick-autoscroll.service
|
||||
|
||||
|
||||
+33
-13
@@ -10,7 +10,7 @@
|
||||
MCA_CHANGES=0
|
||||
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
|
||||
@@ -24,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
|
||||
@@ -35,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
|
||||
@@ -59,7 +59,7 @@ 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_ROUTES=()
|
||||
@@ -73,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"
|
||||
|
||||
@@ -107,9 +108,27 @@ 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]}"
|
||||
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
|
||||
[[ $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
|
||||
@@ -135,9 +154,9 @@ 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"
|
||||
@@ -183,7 +202,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
|
||||
|
||||
+19
-4
@@ -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.
|
||||
@@ -60,8 +60,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}"
|
||||
@@ -239,6 +246,14 @@ mca_ledger_forget() {
|
||||
}
|
||||
|
||||
|
||||
# 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>
|
||||
# Copies a file aside before it is edited in place, and prints the name the
|
||||
# copy was stored under. Existing backups are never overwritten: the first copy
|
||||
@@ -246,7 +261,7 @@ mca_ledger_forget() {
|
||||
# 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
|
||||
|
||||
+300
-26
@@ -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,23 +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_extra_desktop_dirs
|
||||
)
|
||||
}
|
||||
|
||||
# mca_desktop_get <file> <key>
|
||||
@@ -180,6 +222,21 @@ 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
|
||||
case "${prog##*/}" in
|
||||
steam|steam-runtime) return 0 ;;
|
||||
esac
|
||||
[[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Is this Chromium?
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -250,6 +307,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
|
||||
@@ -258,23 +320,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
|
||||
@@ -287,13 +444,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
|
||||
@@ -301,10 +460,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
|
||||
}
|
||||
|
||||
@@ -398,8 +569,17 @@ _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
|
||||
|
||||
# A scan reads every desktop entry on the system, so the menu does it once and
|
||||
# then redraws from what it found. Applying rescans on its own, so nothing else
|
||||
@@ -412,11 +592,13 @@ mca_scan_once() {
|
||||
}
|
||||
|
||||
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=()
|
||||
|
||||
# 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
|
||||
@@ -452,6 +634,14 @@ 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")
|
||||
continue
|
||||
fi
|
||||
|
||||
c_ids+=("$id"); c_files+=("$file"); c_names+=("$name")
|
||||
@@ -464,14 +654,28 @@ 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
|
||||
prog="${prog#flatpak:}"
|
||||
mca_flatpak_is_chromium "$prog" && kind=flatpak
|
||||
packaging=flatpak
|
||||
mca_flatpak_is_chromium "$prog" \
|
||||
&& { (( c_browser[i] )) && kind=browser || kind=app; }
|
||||
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 [[ ${prog##*/} == steam || ${prog##*/} == steam-runtime ]]; then
|
||||
# Steam is Chromium inside, but nothing about it can be changed
|
||||
# from a command line argument; it has its own module.
|
||||
@@ -489,6 +693,7 @@ mca_scan() {
|
||||
MCA_NAMES+=("${c_names[i]}")
|
||||
MCA_PROGS+=("$prog")
|
||||
MCA_KINDS+=("$kind")
|
||||
MCA_PACKAGING+=("$packaging")
|
||||
done
|
||||
|
||||
mca_cache_flush
|
||||
@@ -527,6 +732,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.
|
||||
@@ -554,19 +819,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
|
||||
}
|
||||
+14
-4
@@ -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"
|
||||
@@ -495,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")"
|
||||
@@ -519,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 ;;
|
||||
|
||||
+65
-8
@@ -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.
|
||||
|
||||
@@ -345,6 +355,12 @@ mca_desktop_apply() {
|
||||
# ~/.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.
|
||||
#
|
||||
# Steam is here too, even though it is not a Chromium process itself: its entry
|
||||
# needs -noverifyfiles exactly like the menu one, and it is the entry 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.
|
||||
|
||||
mca_autostart_apply() {
|
||||
local dir="$MCA_XDG_CONFIG/autostart" file exec_line prog content backup
|
||||
@@ -360,9 +376,16 @@ mca_autostart_apply() {
|
||||
|
||||
mca_exec_program "$exec_line" || continue
|
||||
prog="$MCA_PROG"
|
||||
mca_is_chromium "$prog" || continue
|
||||
|
||||
if [[ ${prog##*/} == steam || ${prog##*/} == steam-runtime ]]; then
|
||||
[[ $CFG_STEAM == yes ]] || continue
|
||||
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \
|
||||
"$MCA_STEAM_LAUNCH_FLAG" after-program)"
|
||||
else
|
||||
[[ $CFG_AUTOSTART == yes ]] || continue
|
||||
mca_is_chromium "$prog" || continue
|
||||
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE")"
|
||||
fi
|
||||
[[ -n $content ]] || continue
|
||||
[[ "$content" == "$(< "$file")" ]] && continue
|
||||
|
||||
@@ -418,6 +441,40 @@ mca_spotify_apply() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Leftovers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# mca_prune_orphans
|
||||
# Removes generated entries whose application is gone.
|
||||
#
|
||||
# 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.
|
||||
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"
|
||||
[[ $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
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Undo
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+68
-5
@@ -9,11 +9,19 @@
|
||||
#
|
||||
# ~/.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, not by content - and restores whatever 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.
|
||||
#
|
||||
# Every way of starting Steam has to carry that switch, 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. So the autostart entry gets it as well as the menu one (see
|
||||
# mca_autostart_apply), and the patch below stays out of the way of a client
|
||||
# that is already running.
|
||||
#
|
||||
# The file comes back on every client update, and the watcher re-applies the
|
||||
# patch when that happens.
|
||||
@@ -22,6 +30,11 @@ 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"
|
||||
@@ -29,6 +42,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=()
|
||||
@@ -64,6 +79,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
|
||||
@@ -78,6 +111,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
|
||||
@@ -107,6 +160,16 @@ mca_steam_apply() {
|
||||
|
||||
mca_steam_script_patched "$script" && continue
|
||||
|
||||
# Not patched now, but patched 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.
|
||||
if 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
|
||||
|
||||
lineno="$(_mca_steam_exec_line "$script")"
|
||||
if [[ -z $lineno ]]; then
|
||||
# A client update changed how the helper is started. Leaving the
|
||||
|
||||
@@ -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,17 +95,24 @@ 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
|
||||
@@ -160,6 +167,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