Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af82ffec78 | ||
|
|
b7e0ff6fd1 | ||
|
|
ced6f57dfd | ||
|
|
21e29d99c3 | ||
|
|
c31c7b92e5 | ||
|
|
0bbf0cbd9f | ||
|
|
70f6748f3c | ||
|
|
aaa41f0384 | ||
|
|
a053b4e555 | ||
|
|
36498f2acf | ||
|
|
bd84c6645a | ||
|
|
f42d80598f |
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
|
# build artifacts
|
||||||
po/*.mo
|
po/*.mo
|
||||||
doc/*.1
|
doc/*.1
|
||||||
|
|
||||||
|
# packages
|
||||||
|
dist/
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
# Overridable so a packager can pass the version it is actually building
|
# Overridable so a packager can pass the version it is actually building
|
||||||
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
|
# (`make VERSION=$pkgver`). The literal below is the fallback for builds
|
||||||
# straight from a checkout, and is what a release tag has to carry.
|
# straight from a checkout, and is what a release tag has to carry.
|
||||||
VERSION ?= 1.0.3
|
VERSION ?= 1.2.0
|
||||||
|
|
||||||
PREFIX ?= /usr
|
PREFIX ?= /usr
|
||||||
DESTDIR ?=
|
DESTDIR ?=
|
||||||
@@ -17,7 +17,20 @@ DATADIR ?= $(PREFIX)/share
|
|||||||
LIBDIR ?= $(DATADIR)/middleclick-autoscroll/lib
|
LIBDIR ?= $(DATADIR)/middleclick-autoscroll/lib
|
||||||
LOCALEDIR ?= $(DATADIR)/locale
|
LOCALEDIR ?= $(DATADIR)/locale
|
||||||
MANDIR ?= $(DATADIR)/man
|
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
|
USERUNITDIR ?= $(PREFIX)/lib/systemd/user
|
||||||
|
endif
|
||||||
|
|
||||||
LINGUAS := de
|
LINGUAS := de
|
||||||
MOFILES := $(patsubst %,po/%.mo,$(LINGUAS))
|
MOFILES := $(patsubst %,po/%.mo,$(LINGUAS))
|
||||||
@@ -28,12 +41,18 @@ LIBS := $(wildcard src/lib/*.sh)
|
|||||||
MSGFMT := $(shell command -v msgfmt 2>/dev/null)
|
MSGFMT := $(shell command -v msgfmt 2>/dev/null)
|
||||||
SCDOC := $(shell command -v scdoc 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
|
all: build
|
||||||
|
|
||||||
build: $(MOFILES) $(MANPAGE)
|
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
|
po/%.mo: po/%.po
|
||||||
ifdef MSGFMT
|
ifdef MSGFMT
|
||||||
$(MSGFMT) --check --output-file=$@ $<
|
$(MSGFMT) --check --output-file=$@ $<
|
||||||
|
|||||||
@@ -1,186 +1,103 @@
|
|||||||
# middleclick-autoscroll
|
# middleclick-autoscroll
|
||||||
|
|
||||||
Middle-click autoscroll — hold the middle mouse button, move the pointer, the
|
Middle-click autoscroll for Linux. Hold the middle mouse button, move the
|
||||||
page scrolls — in every application on the system that can do it, and in
|
mouse, the page scrolls — like on Windows. Works with browsers, Electron apps,
|
||||||
anything installed later.
|
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
|
No app list to maintain. The program looks at what's actually installed, figures
|
||||||
to date. Every launcher on the system is examined, the ones running on Chromium
|
out what's Chromium-based, and handles it.
|
||||||
underneath are identified by what they ship rather than by their name, and each
|
|
||||||
of them is handled.
|
## Installing
|
||||||
|
|
||||||
|
**Arch, CachyOS, EndeavourOS, Manjaro**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
paru -S middleclick-autoscroll
|
paru -S middleclick-autoscroll
|
||||||
middleclick-autoscroll enable
|
|
||||||
```
|
```
|
||||||
|
|
||||||
That is the whole setup. Nothing else has to be configured, and no file has to
|
**Debian, Ubuntu, Linux Mint, Pop!_OS**
|
||||||
be edited.
|
|
||||||
|
|
||||||
## 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
|
**Fedora, RHEL, CentOS Stream**
|
||||||
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
|
```bash
|
||||||
back on:
|
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
|
--enable-blink-features=MiddleClickAutoscroll
|
||||||
```
|
```
|
||||||
|
|
||||||
Getting that argument into one application is a five-minute job. Getting it into
|
Doing that for one app is a five-minute job. Doing it for *all* of them — across
|
||||||
all of them, in a way that survives the next package upgrade, is not:
|
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.
|
New apps are picked up within a second by a systemd path unit that watches the
|
||||||
- Some ship their own copy of Electron, some use the system one.
|
relevant directories. Without systemd, `middleclick-autoscroll apply` does the
|
||||||
- Flatpaks see none of the host's configuration.
|
same thing manually.
|
||||||
- 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] Re-apply everything
|
|
||||||
[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.
|
|
||||||
|
|
||||||
## Steam
|
## Steam
|
||||||
|
|
||||||
Steam's interface is CEF and supports the feature perfectly well, but Steam
|
Steam's web UI supports autoscroll but has no way to pass extra arguments to its
|
||||||
builds the command line for its web helper itself and offers no way to add to
|
helper, so the program patches the script that starts it. Steam checks its own
|
||||||
it. The only place an argument fits is the script that starts the helper, inside
|
files at every start and repairs whatever looks changed, so the patch is written
|
||||||
Steam's own installation:
|
to look unchanged: the bytes the argument costs come back out of the script's
|
||||||
|
comments and the timestamp is restored, leaving a file exactly as long and as old
|
||||||
|
as Steam left it. It survives however you start Steam — the menu, a desktop
|
||||||
|
shortcut, a game launcher, a terminal.
|
||||||
|
|
||||||
```
|
As a fallback, for the case where that isn't possible, `-noverifyfiles` goes on
|
||||||
~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
everything that starts Steam: its launcher, its autostart entry, and the
|
||||||
```
|
shortcuts it writes for single games. That one means Steam won't auto-repair
|
||||||
|
damaged files on its own — you can turn it off separately in the settings.
|
||||||
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.
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
| | |
|
| Command | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `middleclick-autoscroll` | the menu above |
|
| `middleclick-autoscroll` | Interactive menu |
|
||||||
| `… enable` | turn it on, apply, start watching |
|
| `… enable` | Turn on, apply, start watching |
|
||||||
| `… disable` | turn it off and put everything back |
|
| `… disable` | Undo everything |
|
||||||
| `… apply` | apply to anything new (this is what the watcher calls) |
|
| `… apply` | Apply to new apps |
|
||||||
| `… apply --rebuild` | take everything back and apply it again, to repair a mess |
|
| `… apply --rebuild` | Redo from scratch |
|
||||||
| `… status` | what is covered |
|
| `… status` | What's covered |
|
||||||
| `… list` | every application that was found and how it is handled |
|
| `… list` | All apps and how they're handled |
|
||||||
|
|
||||||
See `man middleclick-autoscroll` for the details.
|
See `man middleclick-autoscroll` for more.
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
|
|
||||||
@@ -189,7 +106,12 @@ make
|
|||||||
sudo make install
|
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
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ on the command line.
|
|||||||
|
|
||||||
*middleclick-autoscroll* finds every Chromium-based application on the system
|
*middleclick-autoscroll* finds every Chromium-based application on the system
|
||||||
and puts that argument somewhere the application will actually read it, then
|
and puts that argument somewhere the application will actually read it, then
|
||||||
keeps doing so for anything installed later.
|
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
|
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
|
is reachable from there; the configuration file behind it does not need to be
|
||||||
@@ -66,7 +68,7 @@ is refused.
|
|||||||
Two ways, chosen per application.
|
Two ways, chosen per application.
|
||||||
|
|
||||||
*Flag file*
|
*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
|
_$XDG_CONFIG_HOME/<name>-flags.conf_. This is the preferred route: it is
|
||||||
the supported way to pass arguments, it survives package upgrades, and it
|
the supported way to pass arguments, it survives package upgrades, and it
|
||||||
applies to a launch from a terminal as much as one from the menu. An
|
applies to a launch from a terminal as much as one from the menu. An
|
||||||
@@ -74,12 +76,22 @@ Two ways, chosen per application.
|
|||||||
rather than duplicated - Chromium keeps only the last occurrence of that
|
rather than duplicated - Chromium keeps only the last occurrence of that
|
||||||
option, so a second one would switch the first one off.
|
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*
|
*Desktop entry*
|
||||||
For applications that ship their own binary with no wrapper, and for
|
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
|
everything inside a Flatpak or a snap, a copy of the desktop entry with the
|
||||||
to _~/.local/share/applications_, where it shadows the system one. Entries
|
argument appended is written to _~/.local/share/applications_, where it
|
||||||
that already live there - AppImages, web app shortcuts - are edited in
|
shadows the system one. Entries that already live there - AppImages, web
|
||||||
place, with the original kept.
|
app shortcuts - are edited in place, with the original kept.
|
||||||
|
|
||||||
|
This is the route everything takes on the distributions whose Chromium
|
||||||
|
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
|
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
|
*X-MCA-Patched*. The two are never confused: the first is deleted when
|
||||||
@@ -90,6 +102,13 @@ Programs that start themselves at login write their own entry into
|
|||||||
_~/.config/autostart_ pointing straight at their binary, bypassing the menu
|
_~/.config/autostart_ pointing straight at their binary, bypassing the menu
|
||||||
entry entirely. Those are patched in place as well.
|
entry entirely. Those are patched in place as well.
|
||||||
|
|
||||||
|
Shortcuts on the desktop itself are patched in place too. Nothing in the XDG
|
||||||
|
search path looks at that folder, so a shortcut that lives only there would
|
||||||
|
otherwise be invisible - and Steam puts one there for every game somebody asks
|
||||||
|
for a shortcut to. The folder's name is translated, and the name in use is read
|
||||||
|
from _~/.config/user-dirs.dirs_ rather than guessed; the watcher is told about
|
||||||
|
it in a drop-in written when autoscroll is turned on.
|
||||||
|
|
||||||
# STEAM
|
# STEAM
|
||||||
|
|
||||||
Steam's interface is CEF and supports the feature, but Steam builds the command
|
Steam's interface is CEF and supports the feature, but Steam builds the command
|
||||||
@@ -98,22 +117,50 @@ helper, inside Steam's own installation:
|
|||||||
|
|
||||||
~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||||
|
|
||||||
Steam checksums that script at every start and restores it when it differs, so
|
Where that installation is depends on how Steam was installed:
|
||||||
its launcher entry also gets *-noverifyfiles*. The trade-off is that Steam no
|
_~/.local/share/Steam_ for Valve's own package and Arch's,
|
||||||
longer repairs a damaged installation on its own; that is why Steam is a switch
|
_~/.steam/debian-installation_ for Debian's, and the private tree of the
|
||||||
of its own in the settings.
|
sandbox for the Flatpak and the snap. All of them are looked at.
|
||||||
|
|
||||||
Starting Steam from a terminal without *-noverifyfiles* undoes the patch for
|
Steam compares the installed files against its manifest at every start - by
|
||||||
that session. The watcher notices and puts it back, so the next start has it
|
size and timestamp rather than by content - and restores whatever differs. So
|
||||||
again.
|
the patch is written to look untouched: the bytes the argument costs are taken
|
||||||
|
back out of the script's own comments and the timestamp is put back afterwards,
|
||||||
|
leaving a file exactly as long and exactly as old as Steam left it. A client
|
||||||
|
that checks its files finds nothing to repair, and the argument survives
|
||||||
|
however Steam was started - from the menu, from a game shortcut, from a
|
||||||
|
launcher like Heroic or Lutris, from a terminal.
|
||||||
|
|
||||||
|
That is what has to work, because there is no way to make every possible way of
|
||||||
|
starting Steam carry an argument. *-noverifyfiles* is the second line rather
|
||||||
|
than the first: it covers the case where the script has no comments left to pay
|
||||||
|
for the argument and the patch has to grow the file. It goes on Steam's
|
||||||
|
launcher entry, on its entry in _~/.config/autostart_, which Steam writes as
|
||||||
|
soon as it is set to run at login, and on the shortcuts Steam writes for single
|
||||||
|
games, on the desktop and in the menu alike. A game is not an application this
|
||||||
|
program has anything to offer and none of them is listed under *Applications*,
|
||||||
|
but starting one is a Steam start like any other. The trade-off of the switch
|
||||||
|
is that Steam no longer repairs a damaged installation on its own; that is why
|
||||||
|
Steam is a switch of its own in the settings.
|
||||||
|
|
||||||
|
A patch that does change the size is still held back while the client is
|
||||||
|
running: Steam puts its own copy back, the two would only undo each other, and
|
||||||
|
the helper is started once, at the start, so patching again would not help that
|
||||||
|
session anyway. It goes in at the next apply with Steam closed. A patch that
|
||||||
|
keeps the size has nothing to wait for and goes in either way.
|
||||||
|
|
||||||
|
A client update brings a new version of the script. The watcher notices and
|
||||||
|
patches it again, and the copy kept for undoing is replaced with the new
|
||||||
|
version at the same time, so undoing puts back the script Steam last shipped
|
||||||
|
rather than the one from before the update.
|
||||||
|
|
||||||
# SPOTIFY
|
# SPOTIFY
|
||||||
|
|
||||||
The official client is CEF rather than Electron. Installed through
|
The official client is CEF rather than Electron. Installed through
|
||||||
*spotify-launcher*, it is started by a program that builds its own command line
|
*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
|
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
|
where the flag goes. Installed as a plain package, a Flatpak or a snap, it is
|
||||||
entry and needs nothing special.
|
an ordinary desktop entry and needs nothing special.
|
||||||
|
|
||||||
# WHAT CANNOT BE DETECTED
|
# WHAT CANNOT BE DETECTED
|
||||||
|
|
||||||
@@ -142,9 +189,15 @@ _~/.cache/middleclick-autoscroll/detect_
|
|||||||
*NO_COLOR*
|
*NO_COLOR*
|
||||||
Disables colour.
|
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
|
# SEE ALSO
|
||||||
|
|
||||||
*systemctl*(1), *flatpak*(1)
|
*systemctl*(1), *flatpak*(1), *snap*(8)
|
||||||
|
|
||||||
# AUTHORS
|
# 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
|
||||||
@@ -43,6 +43,9 @@ msgstr "nicht erkennbar"
|
|||||||
msgid "not patched yet"
|
msgid "not patched yet"
|
||||||
msgstr "noch nicht gepatcht"
|
msgstr "noch nicht gepatcht"
|
||||||
|
|
||||||
|
msgid "waiting until Steam is closed"
|
||||||
|
msgstr "wartet, bis Steam geschlossen ist"
|
||||||
|
|
||||||
|
|
||||||
#. Status block
|
#. Status block
|
||||||
msgid "Autoscroll"
|
msgid "Autoscroll"
|
||||||
@@ -103,6 +106,9 @@ msgstr "Chromium-basierte Browser"
|
|||||||
msgid "Flatpak applications"
|
msgid "Flatpak applications"
|
||||||
msgstr "Flatpak-Anwendungen"
|
msgstr "Flatpak-Anwendungen"
|
||||||
|
|
||||||
|
msgid "Snap applications"
|
||||||
|
msgstr "Snap-Anwendungen"
|
||||||
|
|
||||||
msgid "Programs that start themselves at login"
|
msgid "Programs that start themselves at login"
|
||||||
msgstr "Programme, die sich beim Anmelden selbst starten"
|
msgstr "Programme, die sich beim Anmelden selbst starten"
|
||||||
|
|
||||||
@@ -176,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."
|
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."
|
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
|
#. Help
|
||||||
msgid "Usage: middleclick-autoscroll [command]"
|
msgid "Usage: middleclick-autoscroll [command]"
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ msgstr ""
|
|||||||
msgid "not patched yet"
|
msgid "not patched yet"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "waiting until Steam is closed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
#. Status block
|
#. Status block
|
||||||
msgid "Autoscroll"
|
msgid "Autoscroll"
|
||||||
@@ -103,6 +106,9 @@ msgstr ""
|
|||||||
msgid "Flatpak applications"
|
msgid "Flatpak applications"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Snap applications"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Programs that start themselves at login"
|
msgid "Programs that start themselves at login"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -176,6 +182,9 @@ msgstr ""
|
|||||||
msgid "Steam starts its interface in a way this version does not recognise; leaving it alone."
|
msgid "Steam starts its interface in a way this version does not recognise; leaving it alone."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Steam is running and has put its own file back; the change waits until Steam is closed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
#. Help
|
#. Help
|
||||||
msgid "Usage: middleclick-autoscroll [command]"
|
msgid "Usage: middleclick-autoscroll [command]"
|
||||||
|
|||||||
@@ -3,21 +3,39 @@ Description=Watch for newly installed Chromium-based applications
|
|||||||
Documentation=man:middleclick-autoscroll(1)
|
Documentation=man:middleclick-autoscroll(1)
|
||||||
|
|
||||||
[Path]
|
[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
|
# 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
|
# to write per package manager - pacman, apt, dnf, zypper, flatpak, snapd and
|
||||||
# end up here.
|
# 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/share/applications
|
||||||
PathModified=/usr/local/share/applications
|
PathModified=/usr/local/share/applications
|
||||||
PathModified=%h/.local/share/applications
|
PathModified=%h/.local/share/applications
|
||||||
PathModified=%h/.config/autostart
|
PathModified=%h/.config/autostart
|
||||||
PathModified=/var/lib/flatpak/exports/share/applications
|
PathModified=/var/lib/flatpak/exports/share/applications
|
||||||
PathModified=%h/.local/share/flatpak/exports/share/applications
|
PathModified=%h/.local/share/flatpak/exports/share/applications
|
||||||
|
PathModified=/var/lib/snapd/desktop/applications
|
||||||
|
|
||||||
|
# NixOS and Guix build the search path out of profiles rather than /usr.
|
||||||
|
PathModified=/run/current-system/sw/share/applications
|
||||||
|
PathModified=%h/.nix-profile/share/applications
|
||||||
|
PathModified=%h/.guix-profile/share/applications
|
||||||
|
|
||||||
|
# The desktop folder is watched too - Steam writes a shortcut there for every
|
||||||
|
# game somebody asks for one, and starting a game is a Steam start - but its
|
||||||
|
# name is translated and cannot be written down here. `enable` adds it in a
|
||||||
|
# drop-in, from the name this system uses.
|
||||||
|
|
||||||
# Steam replaces the script that starts its interface on every client update,
|
# 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/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||||
|
PathChanged=%h/.steam/debian-installation/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||||
PathChanged=%h/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
PathChanged=%h/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||||
|
PathChanged=%h/.var/app/com.valvesoftware.Steam/data/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||||
|
PathChanged=%h/snap/steam/common/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||||
|
|
||||||
Unit=middleclick-autoscroll.service
|
Unit=middleclick-autoscroll.service
|
||||||
|
|
||||||
|
|||||||
+86
-14
@@ -10,7 +10,7 @@
|
|||||||
MCA_CHANGES=0
|
MCA_CHANGES=0
|
||||||
MCA_ROUTES=()
|
MCA_ROUTES=()
|
||||||
|
|
||||||
# mca_route <kind> <id> <program>
|
# mca_route <kind> <id> <program> [packaging]
|
||||||
# What happens to one application, left in MCA_ROUTE:
|
# What happens to one application, left in MCA_ROUTE:
|
||||||
# flags - the launcher reads a flag file; write it there
|
# flags - the launcher reads a flag file; write it there
|
||||||
# desktop - no flag file; shadow or edit the desktop entry
|
# desktop - no flag file; shadow or edit the desktop entry
|
||||||
@@ -24,7 +24,7 @@ MCA_ROUTES=()
|
|||||||
MCA_ROUTE=''
|
MCA_ROUTE=''
|
||||||
|
|
||||||
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 [[ $kind == steam ]]; then
|
||||||
if mca_config_list_has Skip "$id" || [[ $CFG_STEAM != yes ]]; then
|
if mca_config_list_has Skip "$id" || [[ $CFG_STEAM != yes ]]; then
|
||||||
@@ -35,14 +35,14 @@ mca_route() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! mca_kind_wanted "$kind" "$id"; then
|
if ! mca_kind_wanted "$kind" "$id" "$packaging"; then
|
||||||
[[ $kind == unknown ]] && MCA_ROUTE=unknown || MCA_ROUTE=off
|
[[ $kind == unknown ]] && MCA_ROUTE=unknown || MCA_ROUTE=off
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# A Flatpak has its own copy of everything and none of the host's wrappers,
|
# A Flatpak or a snap carries its own copy of everything and sees none of
|
||||||
# so the desktop entry is the only way in.
|
# the host's wrappers, so the desktop entry is the only way in.
|
||||||
if [[ $kind == flatpak ]]; then
|
if [[ $packaging != native ]]; then
|
||||||
MCA_ROUTE=desktop
|
MCA_ROUTE=desktop
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -59,7 +59,7 @@ mca_route() {
|
|||||||
# as it likes to be - it writes only what differs, which is what keeps the
|
# as it likes to be - it writes only what differs, which is what keeps the
|
||||||
# watcher from chasing its own changes.
|
# watcher from chasing its own changes.
|
||||||
mca_apply() {
|
mca_apply() {
|
||||||
local i id file prog kind route steam_done=0
|
local i id file prog kind packaging route steam_done=0
|
||||||
|
|
||||||
MCA_CHANGES=0
|
MCA_CHANGES=0
|
||||||
MCA_ROUTES=()
|
MCA_ROUTES=()
|
||||||
@@ -73,8 +73,9 @@ mca_apply() {
|
|||||||
file="${MCA_FILES[i]}"
|
file="${MCA_FILES[i]}"
|
||||||
prog="${MCA_PROGS[i]}"
|
prog="${MCA_PROGS[i]}"
|
||||||
kind="${MCA_KINDS[i]}"
|
kind="${MCA_KINDS[i]}"
|
||||||
|
packaging="${MCA_PACKAGING[i]}"
|
||||||
|
|
||||||
mca_route "$kind" "$id" "$prog"
|
mca_route "$kind" "$id" "$prog" "$packaging"
|
||||||
route="$MCA_ROUTE"
|
route="$MCA_ROUTE"
|
||||||
MCA_ROUTES[i]="$route"
|
MCA_ROUTES[i]="$route"
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ mca_apply() {
|
|||||||
mca_desktop_apply "$id" "$file"
|
mca_desktop_apply "$id" "$file"
|
||||||
;;
|
;;
|
||||||
steam)
|
steam)
|
||||||
mca_steam_desktop_apply "$id" "$file"
|
mca_steam_desktop_apply "$id" "$file" "$packaging"
|
||||||
if (( ! steam_done )); then
|
if (( ! steam_done )); then
|
||||||
mca_steam_apply
|
mca_steam_apply
|
||||||
steam_done=1
|
steam_done=1
|
||||||
@@ -107,7 +108,32 @@ mca_apply() {
|
|||||||
mca_steam_apply
|
mca_steam_apply
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ $CFG_AUTOSTART == yes ]] && mca_autostart_apply
|
# The shortcuts Steam writes for single games. They are not applications
|
||||||
|
# and are not offered as ones, but starting a game with Steam closed is a
|
||||||
|
# Steam start like any other: without the switch the client finds the
|
||||||
|
# patched helper script, puts its own back, and the interface loses
|
||||||
|
# autoscroll for the rest of the session.
|
||||||
|
if [[ $CFG_STEAM == yes ]]; then
|
||||||
|
for i in "${!MCA_STEAM_LINKS[@]}"; do
|
||||||
|
mca_steam_desktop_apply "${MCA_STEAM_LINKS[i]}" \
|
||||||
|
"${MCA_STEAM_LINK_FILES[i]}" "${MCA_STEAM_LINK_PACK[i]}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Steam's autostart entry carries Steam's own switch and follows the Steam
|
||||||
|
# setting, not this one - leaving it out while Steam is patched is what puts
|
||||||
|
# the client in an update loop - so both are checked inside.
|
||||||
|
if [[ $CFG_AUTOSTART == yes || $CFG_STEAM == yes ]]; then
|
||||||
|
mca_autostart_apply
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Shortcuts on the desktop itself. Nothing above has seen them - the XDG
|
||||||
|
# search path does not go there - and a game started from one starts Steam
|
||||||
|
# without its switch, which is the difference between autoscroll working
|
||||||
|
# and autoscroll working most of the time. Each entry is gated on its own,
|
||||||
|
# so there is nothing to check out here.
|
||||||
|
mca_shortcuts_apply
|
||||||
|
|
||||||
[[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply
|
[[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply
|
||||||
|
|
||||||
mca_prune_orphans
|
mca_prune_orphans
|
||||||
@@ -137,13 +163,19 @@ mca_revert() {
|
|||||||
# The watcher
|
# The watcher
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# A systemd user path unit watching every directory a desktop entry can appear
|
# 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
|
# in. That covers a package from whatever the distribution's package manager
|
||||||
# registered by hand and a Steam client update, without a hook per package
|
# is, a Flatpak, a snap, an AppImage registered by hand and a Steam client
|
||||||
# manager.
|
# update, without a hook per package manager.
|
||||||
|
|
||||||
MCA_UNIT_PATH="middleclick-autoscroll.path"
|
MCA_UNIT_PATH="middleclick-autoscroll.path"
|
||||||
MCA_UNIT_SERVICE="middleclick-autoscroll.service"
|
MCA_UNIT_SERVICE="middleclick-autoscroll.service"
|
||||||
|
|
||||||
|
# The desktop folder is watched like every other directory an entry can turn up
|
||||||
|
# in, but its name is translated and the unit that ships with the package
|
||||||
|
# cannot know it. So that one path is written here, into a drop-in, from the
|
||||||
|
# name this system actually uses.
|
||||||
|
MCA_UNIT_DROPIN="${MCA_XDG_CONFIG}/systemd/user/${MCA_UNIT_PATH}.d"
|
||||||
|
|
||||||
mca_watch_available() {
|
mca_watch_available() {
|
||||||
mca_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} || -S "/run/user/$(id -u)/bus" ]]
|
mca_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} || -S "/run/user/$(id -u)/bus" ]]
|
||||||
}
|
}
|
||||||
@@ -153,17 +185,56 @@ mca_watch_enabled() {
|
|||||||
systemctl --user is-enabled --quiet "$MCA_UNIT_PATH" 2>/dev/null
|
systemctl --user is-enabled --quiet "$MCA_UNIT_PATH" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# _mca_watch_dropin_write
|
||||||
|
# Returns 0 when the drop-in changed and systemd has to be told, 1 when it was
|
||||||
|
# already right - the same contract as mca_write_if_changed, and for the same
|
||||||
|
# reason: the watcher runs after every desktop entry that appears anywhere on
|
||||||
|
# the system, and a daemon-reload each time would be absurd.
|
||||||
|
_mca_watch_dropin_write() {
|
||||||
|
local dir file="$MCA_UNIT_DROPIN/desktop.conf" content
|
||||||
|
|
||||||
|
dir="$(mca_desktop_folder)"
|
||||||
|
[[ -d $dir ]] || { _mca_watch_dropin_remove; return $?; }
|
||||||
|
|
||||||
|
# A per cent sign starts a specifier in a unit file and has to be doubled
|
||||||
|
# to mean itself. Rare in a folder name, fatal when it happens.
|
||||||
|
content="[Path]"$'\n'"PathModified=${dir//%/%%}"$'\n'
|
||||||
|
|
||||||
|
mca_write_if_changed "$file" "$content"
|
||||||
|
}
|
||||||
|
|
||||||
|
_mca_watch_dropin_remove() {
|
||||||
|
[[ -e "$MCA_UNIT_DROPIN/desktop.conf" ]] || return 1
|
||||||
|
rm -f -- "$MCA_UNIT_DROPIN/desktop.conf"
|
||||||
|
rmdir "$MCA_UNIT_DROPIN" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
mca_watch_enable() {
|
mca_watch_enable() {
|
||||||
mca_watch_available || return 1
|
mca_watch_available || return 1
|
||||||
|
_mca_watch_dropin_write
|
||||||
|
systemctl --user daemon-reload > /dev/null 2>&1 || true
|
||||||
systemctl --user enable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || return 1
|
systemctl --user enable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || return 1
|
||||||
systemctl --user enable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
systemctl --user enable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# mca_watch_refresh
|
||||||
|
# Keeps a running watcher in step with a desktop folder that has been renamed,
|
||||||
|
# which is what a change of session language does to it.
|
||||||
|
mca_watch_refresh() {
|
||||||
|
mca_watch_available || return 0
|
||||||
|
_mca_watch_dropin_write || return 0
|
||||||
|
systemctl --user daemon-reload > /dev/null 2>&1 || true
|
||||||
|
systemctl --user restart "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
mca_watch_disable() {
|
mca_watch_disable() {
|
||||||
mca_watch_available || return 0
|
mca_watch_available || return 0
|
||||||
systemctl --user disable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
|
systemctl --user disable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
|
||||||
systemctl --user disable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
systemctl --user disable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
||||||
|
_mca_watch_dropin_remove && systemctl --user daemon-reload > /dev/null 2>&1
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +256,8 @@ mca_count_routes() {
|
|||||||
MCA_ROUTES=()
|
MCA_ROUTES=()
|
||||||
|
|
||||||
for i in "${!MCA_IDS[@]}"; do
|
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"
|
route="$MCA_ROUTE"
|
||||||
MCA_ROUTES[i]="$route"
|
MCA_ROUTES[i]="$route"
|
||||||
case "$route" in
|
case "$route" in
|
||||||
|
|||||||
+42
-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
|
# 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
|
# 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"
|
MCA_APPDIR="${MCA_XDG_DATA}/applications"
|
||||||
|
|
||||||
# Copies of every file that is edited in place rather than shadowed.
|
# Copies of every file that is edited in place rather than shadowed.
|
||||||
@@ -44,6 +44,29 @@ MCA_BACKUPDIR="${MCA_STATEDIR}/backup"
|
|||||||
# else. See mca_ledger_add.
|
# else. See mca_ledger_add.
|
||||||
MCA_LEDGER="${MCA_STATEDIR}/ledger"
|
MCA_LEDGER="${MCA_STATEDIR}/ledger"
|
||||||
|
|
||||||
|
# The folder the desktop itself shows. Nothing in the XDG search path looks at
|
||||||
|
# it, so an entry that lives only there is invisible to a scan - and a game
|
||||||
|
# shortcut dragged onto the desktop is exactly that.
|
||||||
|
#
|
||||||
|
# Its name is translated: Schreibtisch, Bureau, Escritorio. The name in use is
|
||||||
|
# in the file xdg-user-dirs writes, so it is read rather than guessed, and
|
||||||
|
# ~/Desktop is only the fallback for a system that has no such file.
|
||||||
|
mca_desktop_folder() {
|
||||||
|
local file="$MCA_XDG_CONFIG/user-dirs.dirs" line dir=''
|
||||||
|
|
||||||
|
if [[ -r $file ]]; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ $line == XDG_DESKTOP_DIR=* ]] || continue
|
||||||
|
dir="${line#XDG_DESKTOP_DIR=}"
|
||||||
|
dir="${dir%\"}"; dir="${dir#\"}"
|
||||||
|
done < "$file"
|
||||||
|
dir="${dir/#\$HOME/$HOME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ $dir == /* ]] || dir="$HOME/Desktop"
|
||||||
|
printf '%s\n' "${dir%/}"
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Translations
|
# Translations
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -60,8 +83,15 @@ mca_ui_locale() {
|
|||||||
[[ -z $l ]] && l="${LANG:-}"
|
[[ -z $l ]] && l="${LANG:-}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z $l && -r /etc/locale.conf ]]; then
|
# systemd writes /etc/locale.conf and most distributions use it; Debian and
|
||||||
l="$(sed -n 's/^LANG=//p' /etc/locale.conf | tr -d '"' | head -n1)"
|
# 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
|
fi
|
||||||
|
|
||||||
printf '%s\n' "${l:-C}"
|
printf '%s\n' "${l:-C}"
|
||||||
@@ -239,6 +269,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>
|
# mca_backup <file>
|
||||||
# Copies a file aside before it is edited in place, and prints the name the
|
# 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
|
# copy was stored under. Existing backups are never overwritten: the first copy
|
||||||
@@ -246,7 +284,7 @@ mca_ledger_forget() {
|
|||||||
# patched version.
|
# patched version.
|
||||||
mca_backup() {
|
mca_backup() {
|
||||||
local file="$1" name
|
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
|
mkdir -p "$MCA_BACKUPDIR" 2>/dev/null || return 1
|
||||||
if [[ ! -e "$MCA_BACKUPDIR/$name" ]]; then
|
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_APPS=no; mca_config_bool PatchApps yes && CFG_APPS=yes
|
||||||
CFG_BROWSERS=no; mca_config_bool PatchBrowsers yes && CFG_BROWSERS=yes
|
CFG_BROWSERS=no; mca_config_bool PatchBrowsers yes && CFG_BROWSERS=yes
|
||||||
CFG_FLATPAK=no; mca_config_bool PatchFlatpak yes && CFG_FLATPAK=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_AUTOSTART=no; mca_config_bool PatchAutostart yes && CFG_AUTOSTART=yes
|
||||||
CFG_STEAM=no; mca_config_bool PatchSteam yes && CFG_STEAM=yes
|
CFG_STEAM=no; mca_config_bool PatchSteam yes && CFG_STEAM=yes
|
||||||
CFG_SPOTIFY=no; mca_config_bool PatchSpotify yes && CFG_SPOTIFY=yes
|
CFG_SPOTIFY=no; mca_config_bool PatchSpotify yes && CFG_SPOTIFY=yes
|
||||||
|
|||||||
+331
-42
@@ -27,9 +27,23 @@ MCA_MARKERS=(
|
|||||||
MCA_SYSTEM_DIRS=(
|
MCA_SYSTEM_DIRS=(
|
||||||
/ /bin /lib /lib32 /lib64 /sbin /usr /usr/bin /usr/lib /usr/lib32
|
/ /bin /lib /lib32 /lib64 /sbin /usr /usr/bin /usr/lib /usr/lib32
|
||||||
/usr/lib64 /usr/libexec /usr/sbin /usr/local /usr/local/bin
|
/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
|
# 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
|
# process, for the wrappers whose command line is assembled out of variables and
|
||||||
# cannot be followed from the outside.
|
# cannot be followed from the outside.
|
||||||
@@ -38,34 +52,51 @@ MCA_SYSTEM_DIRS=(
|
|||||||
# "chromium" on its own is not one of them: /usr/bin/xdg-open lists every
|
# "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.
|
# 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
|
# The flag file convention is one distribution's packaging habit and says
|
||||||
# engine either, so it is not in here.
|
# 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'
|
#
|
||||||
|
# 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
|
# 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
|
# 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
|
# also XDG lookup order, so the first file found for an id is the one that is
|
||||||
# actually used.
|
# actually used.
|
||||||
mca_desktop_dirs() {
|
mca_desktop_dirs() {
|
||||||
local dirs="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" d
|
local dirs="${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" d
|
||||||
printf '%s\n' "$MCA_APPDIR"
|
local -A seen=()
|
||||||
while IFS= read -r -d: d; do
|
|
||||||
[[ -n $d ]] && printf '%s/applications\n' "${d%/}"
|
|
||||||
done <<< "${dirs}:"
|
|
||||||
}
|
|
||||||
|
|
||||||
# mca_desktop_get <file> <key>
|
while IFS= read -r d; do
|
||||||
# A single value from the [Desktop Entry] group. Desktop Action groups repeat
|
[[ -n $d ]] || continue
|
||||||
# the same keys, and reading past the first group would pick up the wrong one.
|
d="${d%/}"
|
||||||
mca_desktop_get() {
|
[[ -n ${seen[$d]+set} ]] && continue
|
||||||
local file="$1" key="$2"
|
seen[$d]=1
|
||||||
awk -v key="$key" '
|
printf '%s\n' "$d"
|
||||||
/^[[:space:]]*\[/ { inentry = ($0 ~ /^[[:space:]]*\[Desktop Entry\][[:space:]]*$/); next }
|
done < <(
|
||||||
inentry && index($0, key "=") == 1 { print substr($0, length(key) + 2); exit }
|
printf '%s\n' "$MCA_APPDIR"
|
||||||
' "$file" 2>/dev/null
|
while IFS= read -r -d: d; do
|
||||||
|
[[ -n $d ]] && printf '%s/applications\n' "${d%/}"
|
||||||
|
done <<< "${dirs}:"
|
||||||
|
mca_extra_desktop_dirs
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# _mca_desktop_read <file>
|
# _mca_desktop_read <file>
|
||||||
@@ -180,6 +211,35 @@ mca_exec_flatpak_id() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# mca_exec_is_steam_link <exec line> <program>
|
||||||
|
# Whether an entry starts something inside Steam rather than starting Steam
|
||||||
|
# itself: it carries a steam:// address of its own. Steam writes one of those
|
||||||
|
# for every game somebody asks for a shortcut to, and the client's own entry
|
||||||
|
# never has one - it takes an address from the outside, through %U.
|
||||||
|
mca_exec_is_steam_link() {
|
||||||
|
local line="$1" prog="$2"
|
||||||
|
|
||||||
|
[[ $line == *steam://* ]] || return 1
|
||||||
|
mca_prog_is_steam "$prog"
|
||||||
|
}
|
||||||
|
|
||||||
|
# mca_prog_is_steam <program>
|
||||||
|
# Whether running this program starts the Steam client. Every packaging is in
|
||||||
|
# here and every name Valve and the distributions give the launcher, because
|
||||||
|
# the answer decides whether an entry gets Steam's own switch - and an entry
|
||||||
|
# that starts Steam without it undoes the web helper patch on the way up.
|
||||||
|
#
|
||||||
|
# The program is what a scan leaves behind: an absolute path for a native
|
||||||
|
# install, flatpak:<id> or snap:<name> for the other two.
|
||||||
|
mca_prog_is_steam() {
|
||||||
|
local prog="$1"
|
||||||
|
|
||||||
|
case "${prog##*/}" in
|
||||||
|
steam|steam-runtime|steam-native|steam-jupiter) return 0 ;;
|
||||||
|
esac
|
||||||
|
[[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]]
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Is this Chromium?
|
# Is this Chromium?
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -250,6 +310,11 @@ _mca_has_markers() {
|
|||||||
for s in "${MCA_SYSTEM_DIRS[@]}"; do
|
for s in "${MCA_SYSTEM_DIRS[@]}"; do
|
||||||
[[ $dir == "$s" ]] && return 1
|
[[ $dir == "$s" ]] && return 1
|
||||||
done
|
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
|
for m in "${MCA_MARKERS[@]}"; do
|
||||||
[[ -e "$dir/$m" ]] && return 0
|
[[ -e "$dir/$m" ]] && return 0
|
||||||
@@ -258,23 +323,118 @@ _mca_has_markers() {
|
|||||||
return 1
|
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>
|
# _mca_script_target <script>
|
||||||
# The program a wrapper script hands over to, so a chain like
|
# The program a wrapper script hands over to, so a chain like
|
||||||
# heroic -> electron43 -> /usr/lib/electron43/electron can be followed.
|
# 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() {
|
_mca_script_target() {
|
||||||
local script="$1" line tok
|
local script="$1" line tok name val skip=0
|
||||||
local -a tokens
|
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
|
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
|
[[ $line =~ ^[[:space:]]*exec[[:space:]]+(.*)$ ]] || continue
|
||||||
read -r -a tokens <<< "${BASH_REMATCH[1]}"
|
read -r -a tokens <<< "${BASH_REMATCH[1]}"
|
||||||
|
skip=0
|
||||||
for tok in "${tokens[@]}"; do
|
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 == *=* && $tok != /* ]] && continue
|
||||||
[[ $tok == -* ]] && continue
|
|
||||||
# Anything still carrying a shell variable cannot be resolved from
|
tok="${tok%\"}"; tok="${tok#\"}"
|
||||||
# the outside; the hint scan has to answer for those.
|
tok="${tok%\'}"; tok="${tok#\'}"
|
||||||
[[ $tok == *'$'* ]] && return 1
|
|
||||||
|
# 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
|
if [[ $tok == /* ]]; then
|
||||||
printf '%s\n' "$tok"
|
printf '%s\n' "$tok"
|
||||||
else
|
else
|
||||||
@@ -287,13 +447,15 @@ _mca_script_target() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# mca_flags_candidates <program>
|
# mca_flags_candidates <program>
|
||||||
# The flag files a launcher reads, in the order it reads them. Arch's Electron
|
# The flag files a launcher reads, in the order it reads them, taken from the
|
||||||
# and Chromium wrappers all take extra arguments from
|
# launcher itself rather than assumed from the distribution. Where the wrappers
|
||||||
# $XDG_CONFIG_HOME/<name>-flags.conf, which is a far better place to inject than
|
# follow that convention - Arch's Electron and Chromium packages do, and take
|
||||||
# a desktop entry: it survives package updates and it applies to a launch from
|
# extra arguments from $XDG_CONFIG_HOME/<name>-flags.conf - it is a far better
|
||||||
# the terminal too.
|
# 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() {
|
mca_flags_candidates() {
|
||||||
local prog="$1" depth="${2:-0}" target
|
local prog="$1" depth="${2:-0}" target inherited
|
||||||
(( depth > 3 )) && return 0
|
(( depth > 3 )) && return 0
|
||||||
[[ -r $prog ]] || return 0
|
[[ -r $prog ]] || return 0
|
||||||
head -c2 -- "$prog" 2>/dev/null | grep -q '#!' || return 0
|
head -c2 -- "$prog" 2>/dev/null | grep -q '#!' || return 0
|
||||||
@@ -301,10 +463,22 @@ mca_flags_candidates() {
|
|||||||
grep -oE '[A-Za-z0-9_.+-]+-flags\.conf' -- "$prog" 2>/dev/null
|
grep -oE '[A-Za-z0-9_.+-]+-flags\.conf' -- "$prog" 2>/dev/null
|
||||||
|
|
||||||
# A wrapper that only execs another wrapper (heroic -> electron43) inherits
|
# A wrapper that only execs another wrapper (heroic -> electron43) inherits
|
||||||
# that one's flag files, plus the specific name it builds from a variable.
|
# that one's flag files, plus the specific name it builds from a variable
|
||||||
|
# at runtime and therefore never writes down - which is why that one is
|
||||||
|
# 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
|
if target="$(_mca_script_target "$prog")" && [[ -n $target ]]; then
|
||||||
printf '%s-flags.conf\n' "${target##*/}"
|
inherited="$(mca_flags_candidates "$target" $(( depth + 1 )))"
|
||||||
mca_flags_candidates "$target" $(( depth + 1 ))
|
if [[ -n $inherited ]]; then
|
||||||
|
printf '%s-flags.conf\n' "${target##*/}"
|
||||||
|
printf '%s\n' "$inherited"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,8 +572,19 @@ _mca_detect_uncached() {
|
|||||||
MCA_IDS=() # desktop file id, without the .desktop suffix
|
MCA_IDS=() # desktop file id, without the .desktop suffix
|
||||||
MCA_FILES=() # the desktop entry that is in effect for that id
|
MCA_FILES=() # the desktop entry that is in effect for that id
|
||||||
MCA_NAMES=() # display name
|
MCA_NAMES=() # display name
|
||||||
MCA_PROGS=() # resolved program, or a flatpak app id
|
MCA_PROGS=() # resolved program, or a Flatpak app id or a snap name
|
||||||
MCA_KINDS=() # app | browser | flatpak | steam | unknown | no
|
MCA_KINDS=() # app | browser | steam | unknown | no
|
||||||
|
MCA_PACKAGING=() # native | flatpak | snap
|
||||||
|
|
||||||
|
# The shortcuts Steam writes for single games. Not applications of their own - a
|
||||||
|
# game is whatever engine it was built with, and none of those reads a Chromium
|
||||||
|
# argument - so they are kept apart from the list rather than listed as
|
||||||
|
# something that got switched on. They do start Steam, which is why they are
|
||||||
|
# kept at all: the Steam module gives them Steam's own switch.
|
||||||
|
MCA_STEAM_LINKS=() # desktop file id
|
||||||
|
MCA_STEAM_LINK_FILES=() # the entry that is in effect for it
|
||||||
|
MCA_STEAM_LINK_PACK=() # native | flatpak | snap, which decides where the
|
||||||
|
# switch goes on the command line
|
||||||
|
|
||||||
# A scan reads every desktop entry on the system, so the menu does it once and
|
# A scan reads every desktop entry on the system, so the menu does it once and
|
||||||
# then redraws from what it found. Applying rescans on its own, so nothing else
|
# then redraws from what it found. Applying rescans on its own, so nothing else
|
||||||
@@ -412,11 +597,13 @@ mca_scan_once() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mca_scan() {
|
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 seen=()
|
||||||
local -a c_ids=() c_files=() c_names=() c_progs=() c_browser=() c_stat=()
|
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_IDS=(); MCA_FILES=(); MCA_NAMES=(); MCA_PROGS=(); MCA_KINDS=()
|
||||||
|
MCA_PACKAGING=()
|
||||||
|
MCA_STEAM_LINKS=(); MCA_STEAM_LINK_FILES=(); MCA_STEAM_LINK_PACK=()
|
||||||
|
|
||||||
# Pass one: read the entries and work out what each of them starts. No
|
# Pass one: read the entries and work out what each of them starts. No
|
||||||
# detection yet - that needs a stat per program, and those are collected so
|
# detection yet - that needs a stat per program, and those are collected so
|
||||||
@@ -452,6 +639,21 @@ mca_scan() {
|
|||||||
if [[ ${prog##*/} == flatpak ]]; then
|
if [[ ${prog##*/} == flatpak ]]; then
|
||||||
mca_exec_flatpak_id "$exec_line" || continue
|
mca_exec_flatpak_id "$exec_line" || continue
|
||||||
prog="flatpak:$MCA_PROG"
|
prog="flatpak:$MCA_PROG"
|
||||||
|
elif mca_snap_name "$prog"; then
|
||||||
|
prog="snap:$MCA_PROG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if mca_exec_is_steam_link "$exec_line" "$prog"; then
|
||||||
|
MCA_STEAM_LINKS+=("$id")
|
||||||
|
MCA_STEAM_LINK_FILES+=("$file")
|
||||||
|
if [[ $prog == flatpak:* ]]; then
|
||||||
|
MCA_STEAM_LINK_PACK+=(flatpak)
|
||||||
|
elif [[ $prog == snap:* ]]; then
|
||||||
|
MCA_STEAM_LINK_PACK+=(snap)
|
||||||
|
else
|
||||||
|
MCA_STEAM_LINK_PACK+=(native)
|
||||||
|
fi
|
||||||
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
c_ids+=("$id"); c_files+=("$file"); c_names+=("$name")
|
c_ids+=("$id"); c_files+=("$file"); c_names+=("$name")
|
||||||
@@ -464,15 +666,32 @@ mca_scan() {
|
|||||||
MCA_STAT=()
|
MCA_STAT=()
|
||||||
_mca_stat_batch "${c_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
|
for i in "${!c_ids[@]}"; do
|
||||||
prog="${c_progs[i]}"
|
prog="${c_progs[i]}"
|
||||||
kind=no
|
kind=no
|
||||||
|
packaging=native
|
||||||
|
|
||||||
if [[ $prog == flatpak:* ]]; then
|
if [[ $prog == flatpak:* ]]; then
|
||||||
|
packaging=flatpak
|
||||||
|
if mca_prog_is_steam "$prog"; then
|
||||||
|
kind=steam
|
||||||
|
elif mca_flatpak_is_chromium "${prog#flatpak:}"; then
|
||||||
|
(( c_browser[i] )) && kind=browser || kind=app
|
||||||
|
fi
|
||||||
prog="${prog#flatpak:}"
|
prog="${prog#flatpak:}"
|
||||||
mca_flatpak_is_chromium "$prog" && kind=flatpak
|
elif [[ $prog == snap:* ]]; then
|
||||||
elif [[ ${prog##*/} == steam || ${prog##*/} == steam-runtime ]]; then
|
prog="${prog#snap:}"
|
||||||
|
packaging=snap
|
||||||
|
if [[ $prog == steam ]]; then
|
||||||
|
kind=steam
|
||||||
|
elif mca_snap_is_chromium "$prog"; then
|
||||||
|
(( c_browser[i] )) && kind=browser || kind=app
|
||||||
|
fi
|
||||||
|
elif mca_prog_is_steam "$prog"; then
|
||||||
# Steam is Chromium inside, but nothing about it can be changed
|
# Steam is Chromium inside, but nothing about it can be changed
|
||||||
# from a command line argument; it has its own module.
|
# from a command line argument; it has its own module.
|
||||||
kind=steam
|
kind=steam
|
||||||
@@ -489,6 +708,7 @@ mca_scan() {
|
|||||||
MCA_NAMES+=("${c_names[i]}")
|
MCA_NAMES+=("${c_names[i]}")
|
||||||
MCA_PROGS+=("$prog")
|
MCA_PROGS+=("$prog")
|
||||||
MCA_KINDS+=("$kind")
|
MCA_KINDS+=("$kind")
|
||||||
|
MCA_PACKAGING+=("$packaging")
|
||||||
done
|
done
|
||||||
|
|
||||||
mca_cache_flush
|
mca_cache_flush
|
||||||
@@ -527,6 +747,66 @@ mca_desktop_is_browser() {
|
|||||||
return 1
|
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>
|
# mca_flatpak_is_chromium <app id>
|
||||||
# Flatpak keeps every application in its own tree, so the marker check works the
|
# Flatpak keeps every application in its own tree, so the marker check works the
|
||||||
# same way once that tree has been located.
|
# same way once that tree has been located.
|
||||||
@@ -554,19 +834,28 @@ mca_flatpak_is_chromium() {
|
|||||||
return 1
|
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
|
# Whether the current settings say this entry should be patched. Skip beats
|
||||||
# everything, an explicit include beats detection, and detection beats nothing.
|
# 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() {
|
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 Skip "$id" && return 1
|
||||||
mca_config_list_has Include "$id" && return 0
|
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
|
case "$kind" in
|
||||||
app) [[ $CFG_APPS == yes ]] ;;
|
app) [[ $CFG_APPS == yes ]] ;;
|
||||||
browser) [[ $CFG_BROWSERS == yes ]] ;;
|
browser) [[ $CFG_BROWSERS == yes ]] ;;
|
||||||
flatpak) [[ $CFG_FLATPAK == yes && $CFG_APPS == yes ]] ;;
|
|
||||||
*) return 1 ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -178,6 +178,9 @@ mca_ui_status() {
|
|||||||
if mca_steam_installed; then
|
if mca_steam_installed; then
|
||||||
if mca_steam_patched; then
|
if mca_steam_patched; then
|
||||||
_mca_row "$(mca_msg "Steam")" "$(_mca_onoff yes)"
|
_mca_row "$(mca_msg "Steam")" "$(_mca_onoff yes)"
|
||||||
|
elif [[ $CFG_STEAM == yes && $CFG_ENABLED == yes ]] && mca_steam_waiting; then
|
||||||
|
_mca_row "$(mca_msg "Steam")" \
|
||||||
|
"${MCA_C_DIM}$(mca_msg "waiting until Steam is closed")${MCA_C_RESET}"
|
||||||
elif [[ $CFG_STEAM == yes && $CFG_ENABLED == yes ]]; then
|
elif [[ $CFG_STEAM == yes && $CFG_ENABLED == yes ]]; then
|
||||||
_mca_row "$(mca_msg "Steam")" \
|
_mca_row "$(mca_msg "Steam")" \
|
||||||
"${MCA_C_YELLOW}$(mca_msg "not patched yet")${MCA_C_RESET}"
|
"${MCA_C_YELLOW}$(mca_msg "not patched yet")${MCA_C_RESET}"
|
||||||
@@ -212,6 +215,7 @@ MCA_SETTINGS=(
|
|||||||
"PatchApps|bool|yes|Electron and CEF applications"
|
"PatchApps|bool|yes|Electron and CEF applications"
|
||||||
"PatchBrowsers|bool|yes|Chromium-based browsers"
|
"PatchBrowsers|bool|yes|Chromium-based browsers"
|
||||||
"PatchFlatpak|bool|yes|Flatpak applications"
|
"PatchFlatpak|bool|yes|Flatpak applications"
|
||||||
|
"PatchSnap|bool|yes|Snap applications"
|
||||||
"PatchAutostart|bool|yes|Programs that start themselves at login"
|
"PatchAutostart|bool|yes|Programs that start themselves at login"
|
||||||
"PatchSteam|bool|yes|Steam"
|
"PatchSteam|bool|yes|Steam"
|
||||||
"PatchSpotify|bool|yes|Spotify"
|
"PatchSpotify|bool|yes|Spotify"
|
||||||
|
|||||||
+132
-30
@@ -4,15 +4,25 @@
|
|||||||
#
|
#
|
||||||
# There are two ways in, and the order matters:
|
# There are two ways in, and the order matters:
|
||||||
#
|
#
|
||||||
# 1. A flag file. Arch's Electron and Chromium wrappers read extra arguments
|
# 1. A flag file. Where the distribution wraps Electron and Chromium in a
|
||||||
# from $XDG_CONFIG_HOME/<name>-flags.conf. This is the good one - it is the
|
# launcher script that reads extra arguments from
|
||||||
# supported way to pass arguments, it survives package upgrades untouched,
|
# $XDG_CONFIG_HOME/<name>-flags.conf - Arch and its derivatives do, and it
|
||||||
# and it applies however the program is started, including from a terminal.
|
# 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
|
# 2. A desktop entry. For applications that ship their own binary with no
|
||||||
# wrapper - most vendor packages, every Flatpak - there is nowhere else to
|
# wrapper, and for everything a Flatpak or a snap contains, there is
|
||||||
# put an argument, so a copy of the entry with the flag appended goes into
|
# nowhere else to put an argument, so a copy of the entry with the flag
|
||||||
# ~/.local/share/applications, where it shadows the system one.
|
# appended goes into ~/.local/share/applications, where it shadows the
|
||||||
|
# system one.
|
||||||
#
|
#
|
||||||
# Nothing outside $HOME is ever written to.
|
# Nothing outside $HOME is ever written to.
|
||||||
|
|
||||||
@@ -339,38 +349,117 @@ mca_desktop_apply() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Autostart entries
|
# Entries outside the search path
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Applications that start themselves at login write their own entry into
|
# Two directories hold desktop entries that no part of the XDG search path
|
||||||
# ~/.config/autostart, pointing straight at their binary. Those bypass the
|
# looks at, so nothing the scan does can reach them:
|
||||||
# desktop entry in the menu completely, which is why Discord launched at login
|
#
|
||||||
# used to behave differently from Discord launched from the menu.
|
# ~/.config/autostart, where applications that start themselves at login
|
||||||
|
# write their own entry, pointing straight at their binary. Those bypass the
|
||||||
|
# entry in the menu completely, which is why Discord launched at login used
|
||||||
|
# to behave differently from Discord launched from the menu.
|
||||||
|
#
|
||||||
|
# The desktop folder, where a shortcut somebody dragged out of the menu -
|
||||||
|
# or asked Steam for - lives and nowhere else.
|
||||||
|
#
|
||||||
|
# Steam is in both, even though it is not a Chromium process itself: its
|
||||||
|
# entries need -noverifyfiles exactly like the menu one. The autostart entry is
|
||||||
|
# the one most likely to exist, because Steam writes it as soon as "run at
|
||||||
|
# startup" is ticked; without it a Steam started at login restores the patched
|
||||||
|
# web helper script and the watcher patches it back, over and over. The desktop
|
||||||
|
# ones are the gap behind "autoscroll works, except sometimes": starting a game
|
||||||
|
# from the desktop is a Steam start like any other, and a Steam start without
|
||||||
|
# the switch costs the interface its autoscroll for the rest of the session.
|
||||||
|
#
|
||||||
|
# Neither can be shadowed from anywhere, so both are edited where they stand,
|
||||||
|
# with the original kept.
|
||||||
|
|
||||||
|
# _mca_entry_patch_inplace <file> <gate: autostart|apps>
|
||||||
|
# One desktop entry that lives outside the XDG search path, edited where it is
|
||||||
|
# because there is nowhere to shadow it from.
|
||||||
|
#
|
||||||
|
# Anything that starts Steam gets Steam's own switch; a Chromium application
|
||||||
|
# gets the flag. What decides whether a Chromium application here is in scope
|
||||||
|
# differs by where the entry came from, which is what the gate says: an
|
||||||
|
# autostart entry follows the autostart setting, a shortcut follows the same
|
||||||
|
# rules as the application it is a shortcut to.
|
||||||
|
_mca_entry_patch_inplace() {
|
||||||
|
local file="$1" gate="$2"
|
||||||
|
local id prog packaging=native content backup kind
|
||||||
|
|
||||||
|
grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null && return 0
|
||||||
|
|
||||||
|
_mca_desktop_read "$file"
|
||||||
|
[[ -n $DE_EXEC ]] || return 0
|
||||||
|
[[ $DE_HIDDEN == true ]] && return 0
|
||||||
|
|
||||||
|
mca_exec_program "$DE_EXEC" || return 0
|
||||||
|
prog="$MCA_PROG"
|
||||||
|
|
||||||
|
# A Flatpak or a snap entry names the wrapper rather than the application.
|
||||||
|
# The scan resolves those into an id; outside the scan the same has to
|
||||||
|
# happen here, or every Flatpak looks like the flatpak program itself.
|
||||||
|
if [[ ${prog##*/} == flatpak ]]; then
|
||||||
|
mca_exec_flatpak_id "$DE_EXEC" || return 0
|
||||||
|
prog="flatpak:$MCA_PROG"
|
||||||
|
packaging=flatpak
|
||||||
|
elif mca_snap_name "$prog"; then
|
||||||
|
prog="snap:$MCA_PROG"
|
||||||
|
packaging=snap
|
||||||
|
fi
|
||||||
|
|
||||||
|
if mca_prog_is_steam "$prog"; then
|
||||||
|
[[ $CFG_STEAM == yes ]] || return 0
|
||||||
|
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \
|
||||||
|
"$MCA_STEAM_LAUNCH_FLAG" "$(mca_steam_flag_position "$packaging")")"
|
||||||
|
else
|
||||||
|
case "$packaging" in
|
||||||
|
flatpak) mca_flatpak_is_chromium "${prog#flatpak:}" || return 0 ;;
|
||||||
|
snap) mca_snap_is_chromium "${prog#snap:}" || return 0 ;;
|
||||||
|
*) mca_is_chromium "$prog" || return 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ $gate == autostart ]]; then
|
||||||
|
[[ $CFG_AUTOSTART == yes ]] || return 0
|
||||||
|
else
|
||||||
|
id="${file##*/}"; id="${id%.desktop}"
|
||||||
|
mca_desktop_is_browser && kind=browser || kind=app
|
||||||
|
mca_kind_wanted "$kind" "$id" "$packaging" || return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n $content ]] || return 0
|
||||||
|
[[ "$content" == "$(< "$file")" ]] && return 0
|
||||||
|
|
||||||
|
backup="$(mca_backup "$file")" || return 0
|
||||||
|
if mca_write_if_changed "$file" "$content"$'\n'; then
|
||||||
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||||
|
fi
|
||||||
|
mca_ledger_add inplace "$file" "$backup"
|
||||||
|
}
|
||||||
|
|
||||||
mca_autostart_apply() {
|
mca_autostart_apply() {
|
||||||
local dir="$MCA_XDG_CONFIG/autostart" file exec_line prog content backup
|
local dir="$MCA_XDG_CONFIG/autostart" file
|
||||||
|
|
||||||
[[ -d $dir ]] || return 0
|
[[ -d $dir ]] || return 0
|
||||||
|
|
||||||
for file in "$dir"/*.desktop; do
|
for file in "$dir"/*.desktop; do
|
||||||
[[ -f $file ]] || continue
|
[[ -f $file ]] || continue
|
||||||
grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null && continue
|
_mca_entry_patch_inplace "$file" autostart
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
exec_line="$(mca_desktop_get "$file" Exec)"
|
mca_shortcuts_apply() {
|
||||||
[[ -n $exec_line ]] || continue
|
local dir file
|
||||||
|
dir="$(mca_desktop_folder)"
|
||||||
|
|
||||||
mca_exec_program "$exec_line" || continue
|
[[ -d $dir ]] || return 0
|
||||||
prog="$MCA_PROG"
|
|
||||||
mca_is_chromium "$prog" || continue
|
|
||||||
|
|
||||||
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE")"
|
for file in "$dir"/*.desktop; do
|
||||||
[[ -n $content ]] || continue
|
[[ -f $file ]] || continue
|
||||||
[[ "$content" == "$(< "$file")" ]] && continue
|
_mca_entry_patch_inplace "$file" apps
|
||||||
|
|
||||||
backup="$(mca_backup "$file")" || continue
|
|
||||||
if mca_write_if_changed "$file" "$content"$'\n'; then
|
|
||||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
||||||
fi
|
|
||||||
mca_ledger_add inplace "$file" "$backup"
|
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,13 +512,15 @@ mca_spotify_apply() {
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# mca_prune_orphans
|
# mca_prune_orphans
|
||||||
# Removes generated entries whose application is gone.
|
# Forgets changes there is no longer anything to undo.
|
||||||
#
|
#
|
||||||
# Uninstalling something deletes its entry from /usr/share/applications, but the
|
# Uninstalling something deletes its entry from /usr/share/applications, but the
|
||||||
# copy shadowing it is in the user's home and pacman knows nothing about it. It
|
# copy shadowing it is in the user's home and pacman knows nothing about it. It
|
||||||
# would sit in the menu forever, offering to start a program that is no longer
|
# would sit in the menu forever, offering to start a program that is no longer
|
||||||
# installed - and the watcher would not notice, because a plain apply only ever
|
# installed - and the watcher would not notice, because a plain apply only ever
|
||||||
# looks at what is there now.
|
# looks at what is there now. A file that was edited where it stood and has
|
||||||
|
# since been deleted is the same problem from the other side: its record would
|
||||||
|
# have `disable` put the file back.
|
||||||
mca_prune_orphans() {
|
mca_prune_orphans() {
|
||||||
local kind path source line
|
local kind path source line
|
||||||
local -a lines=()
|
local -a lines=()
|
||||||
@@ -439,6 +530,17 @@ mca_prune_orphans() {
|
|||||||
|
|
||||||
for line in "${lines[@]}"; do
|
for line in "${lines[@]}"; do
|
||||||
IFS=$'\t' read -r kind path source <<< "$line"
|
IFS=$'\t' read -r kind path source <<< "$line"
|
||||||
|
|
||||||
|
# An entry that was edited where it stood and has since been deleted -
|
||||||
|
# a game shortcut dragged to the wastebasket, an autostart entry the
|
||||||
|
# application removed. There is nothing left to put back, and keeping
|
||||||
|
# the record would have `disable` recreate a file the user threw away.
|
||||||
|
if [[ $kind == inplace && ! -e $path ]]; then
|
||||||
|
[[ -n $source ]] && rm -f -- "$MCA_BACKUPDIR/$source"
|
||||||
|
mca_ledger_forget "$path"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
[[ $kind == shadow && -n $source ]] || continue
|
[[ $kind == shadow && -n $source ]] || continue
|
||||||
[[ -e $source ]] && continue
|
[[ -e $source ]] && continue
|
||||||
|
|
||||||
|
|||||||
+223
-23
@@ -9,11 +9,28 @@
|
|||||||
#
|
#
|
||||||
# ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
# ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||||
#
|
#
|
||||||
# Steam checksums that script at every start and restores it when it differs,
|
# Steam compares the installed files against its manifest at every start - by
|
||||||
# which is why the launcher gets -noverifyfiles as well. The trade-off is real
|
# size and timestamp rather than by content - and restores whatever differs.
|
||||||
# and belongs to the user, so this is a switch of its own rather than part of
|
# So the patch is written to look untouched: the bytes the flag costs are taken
|
||||||
# the general application handling: with verification off, Steam no longer
|
# back out of the script's comments and the timestamp is put back afterwards,
|
||||||
# repairs a damaged installation by itself.
|
# which leaves a file that is byte-for-byte the size Steam recorded and as old
|
||||||
|
# as Steam left it. A client that verifies its files finds nothing to repair
|
||||||
|
# and the flag survives however Steam was started - from the menu, from a game
|
||||||
|
# shortcut, from a launcher like Heroic, from a terminal.
|
||||||
|
#
|
||||||
|
# That is the part that has to work, because there is no way to make every
|
||||||
|
# possible way of starting Steam carry an argument. -noverifyfiles on the
|
||||||
|
# launcher entries is the second line rather than the first: it covers the case
|
||||||
|
# where the script has no comments left to pay for the flag and the patch has
|
||||||
|
# to grow the file. The trade-off is real and belongs to the user, so it is a
|
||||||
|
# switch of its own rather than part of the general application handling: with
|
||||||
|
# verification off, Steam no longer repairs a damaged installation by itself.
|
||||||
|
#
|
||||||
|
# A patch that does change the size still has to stay out of the way of a
|
||||||
|
# client that is already running, or the two programs spend the session undoing
|
||||||
|
# each other: Steam restores the script, the watcher patches it again, Steam
|
||||||
|
# restores it again, and the client never gets past its update dialog. A patch
|
||||||
|
# that keeps the size is invisible to that check and goes in either way.
|
||||||
#
|
#
|
||||||
# The file comes back on every client update, and the watcher re-applies the
|
# The file comes back on every client update, and the watcher re-applies the
|
||||||
# patch when that happens.
|
# patch when that happens.
|
||||||
@@ -22,6 +39,11 @@ MCA_STEAM_LAUNCH_FLAG='-noverifyfiles'
|
|||||||
|
|
||||||
# Every place a Steam installation is known to live, resolved and de-duplicated
|
# Every place a Steam installation is known to live, resolved and de-duplicated
|
||||||
# because ~/.steam/steam is normally a symlink into ~/.local/share.
|
# because ~/.steam/steam is normally a symlink into ~/.local/share.
|
||||||
|
#
|
||||||
|
# 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() {
|
mca_steam_roots() {
|
||||||
local candidates=(
|
local candidates=(
|
||||||
"$MCA_XDG_DATA/Steam"
|
"$MCA_XDG_DATA/Steam"
|
||||||
@@ -29,6 +51,8 @@ mca_steam_roots() {
|
|||||||
"$HOME/.steam/root"
|
"$HOME/.steam/root"
|
||||||
"$HOME/.steam/debian-installation"
|
"$HOME/.steam/debian-installation"
|
||||||
"$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam"
|
"$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 dir real
|
||||||
local -A seen=()
|
local -A seen=()
|
||||||
@@ -64,6 +88,24 @@ mca_steam_installed() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# mca_steam_running
|
||||||
|
# Steam records its own process id beside its installation while it runs. The
|
||||||
|
# file outlives a crash, so the id is checked rather than believed.
|
||||||
|
mca_steam_running() {
|
||||||
|
local f pid
|
||||||
|
for f in \
|
||||||
|
"$HOME/.steam/steam.pid" \
|
||||||
|
"$HOME/.var/app/com.valvesoftware.Steam/.steam/steam.pid" \
|
||||||
|
"$HOME/snap/steam/common/.steam/steam.pid"
|
||||||
|
do
|
||||||
|
[[ -r $f ]] || continue
|
||||||
|
pid="$(< "$f")"
|
||||||
|
[[ $pid =~ ^[0-9]+$ ]] || continue
|
||||||
|
kill -0 "$pid" 2>/dev/null && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# mca_steam_script_patched <script>
|
# mca_steam_script_patched <script>
|
||||||
mca_steam_script_patched() {
|
mca_steam_script_patched() {
|
||||||
grep -q -- "$MCA_FEATURE" "$1" 2>/dev/null
|
grep -q -- "$MCA_FEATURE" "$1" 2>/dev/null
|
||||||
@@ -78,6 +120,26 @@ mca_steam_patched() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# mca_steam_deferred <script>
|
||||||
|
# Whether the patch is being held back rather than simply missing: the script
|
||||||
|
# was patched before, its own copy is back, and Steam is still running. See
|
||||||
|
# mca_steam_apply for why that is left alone.
|
||||||
|
mca_steam_deferred() {
|
||||||
|
[[ -e "$MCA_BACKUPDIR/$(mca_backup_name "$1")" ]] && mca_steam_running
|
||||||
|
}
|
||||||
|
|
||||||
|
# mca_steam_waiting
|
||||||
|
# The same question for the status screen, which has no script in hand.
|
||||||
|
mca_steam_waiting() {
|
||||||
|
local root script
|
||||||
|
while IFS= read -r root; do
|
||||||
|
script="$(mca_steam_script "$root")" || continue
|
||||||
|
mca_steam_script_patched "$script" && continue
|
||||||
|
mca_steam_deferred "$script" && return 0
|
||||||
|
done < <(mca_steam_roots)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# _mca_steam_exec_line <script>
|
# _mca_steam_exec_line <script>
|
||||||
# The line number of the command that starts the web helper: the last
|
# The line number of the command that starts the web helper: the last
|
||||||
# uncommented line that both names steamwebhelper and forwards "$@". Both the
|
# uncommented line that both names steamwebhelper and forwards "$@". Both the
|
||||||
@@ -92,12 +154,112 @@ _mca_steam_exec_line() {
|
|||||||
' "$1" 2>/dev/null
|
' "$1" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# _mca_steam_build <script> <flags> <reclaim: 0|1>
|
||||||
|
# The patched script on stdout: the flags appended to the line that starts the
|
||||||
|
# web helper, and - when asked for - the same number of bytes taken back out of
|
||||||
|
# the script's comments, so that the file Steam finds is the length Steam wrote
|
||||||
|
# down. Everything else - setting up the container runtime, deciding whether
|
||||||
|
# the sandbox can be used - is left alone.
|
||||||
|
#
|
||||||
|
# Comments are eaten from the bottom up, so the header that says what the
|
||||||
|
# script is for is the last thing to lose anything, and the '#' itself always
|
||||||
|
# stays: a line that loses it stops being a comment and starts being a command.
|
||||||
|
# Fails when the comments are too short to pay for the flags.
|
||||||
|
_mca_steam_build() {
|
||||||
|
local script="$1" flags="$2" reclaim="$3" lineno need=0
|
||||||
|
|
||||||
|
lineno="$(_mca_steam_exec_line "$script")"
|
||||||
|
[[ -n $lineno ]] || return 1
|
||||||
|
|
||||||
|
if (( reclaim )); then
|
||||||
|
need="$(printf '%s' " $flags" | wc -c)"
|
||||||
|
need="${need//[^0-9]/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# LC_ALL=C so awk counts bytes: a comment with an accent in it is shorter
|
||||||
|
# in characters than it is on disk, and it is the disk that Steam measures.
|
||||||
|
LC_ALL=C awk -v n="$lineno" -v f=" $flags" -v need="$need" '
|
||||||
|
{ lines[NR] = (NR == n ? $0 f : $0) }
|
||||||
|
END {
|
||||||
|
for (i = NR; i >= 2 && need > 0; i--) {
|
||||||
|
if (i == n) continue
|
||||||
|
p = index(lines[i], "#")
|
||||||
|
if (p == 0) continue
|
||||||
|
# Only a line that is nothing but a comment; a trailing one
|
||||||
|
# sits behind a command that has to stay intact.
|
||||||
|
if (substr(lines[i], 1, p - 1) ~ /[^ \t]/) continue
|
||||||
|
can = length(lines[i]) - p
|
||||||
|
if (can <= 0) continue
|
||||||
|
if (can > need) can = need
|
||||||
|
lines[i] = substr(lines[i], 1, length(lines[i]) - can)
|
||||||
|
need -= can
|
||||||
|
}
|
||||||
|
if (need > 0) exit 1
|
||||||
|
for (i = 1; i <= NR; i++) print lines[i]
|
||||||
|
}
|
||||||
|
' "$script"
|
||||||
|
}
|
||||||
|
|
||||||
|
# _mca_steam_same_size <script> <content>
|
||||||
|
# Whether writing that content leaves the file at the size it has now. Measured
|
||||||
|
# rather than worked out: a script without a final newline gains one on the way
|
||||||
|
# through awk, and one byte is all it takes for Steam to notice.
|
||||||
|
_mca_steam_same_size() {
|
||||||
|
local script="$1" content="$2"
|
||||||
|
(( $(wc -c < "$script") == $(printf '%s\n' "$content" | wc -c) ))
|
||||||
|
}
|
||||||
|
|
||||||
|
# _mca_steam_refresh_backup <script>
|
||||||
|
# A client update brings a new version of the script, and the copy kept from
|
||||||
|
# before it is no longer what putting it back means. Only ever done while the
|
||||||
|
# script is unpatched, so the copy that replaces it is Steam's own.
|
||||||
|
_mca_steam_refresh_backup() {
|
||||||
|
local script="$1" name
|
||||||
|
name="$(mca_backup_name "$script")"
|
||||||
|
|
||||||
|
[[ -e "$MCA_BACKUPDIR/$name" ]] || return 0
|
||||||
|
cmp -s -- "$script" "$MCA_BACKUPDIR/$name" && return 0
|
||||||
|
cp -p -- "$script" "$MCA_BACKUPDIR/$name" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# _mca_steam_reneutralise <script> <flags>
|
||||||
|
# An earlier version of this program patched by appending and left the script
|
||||||
|
# longer than Steam expects, which is a patch that only survives because
|
||||||
|
# -noverifyfiles is there to stop Steam looking. If the same flags can be made
|
||||||
|
# to fit, they are: the file goes back to the length and the timestamp Steam
|
||||||
|
# wrote down and stops depending on that switch.
|
||||||
|
#
|
||||||
|
# The new copy is built from the untouched original that was kept when the
|
||||||
|
# script was first patched, never from the patched file - patching a patched
|
||||||
|
# file is how a flag ends up in there twice.
|
||||||
|
_mca_steam_reneutralise() {
|
||||||
|
local script="$1" flags="$2" name copy out
|
||||||
|
|
||||||
|
name="$(mca_backup_name "$script")"
|
||||||
|
copy="$MCA_BACKUPDIR/$name"
|
||||||
|
[[ -f $copy ]] || return 1
|
||||||
|
|
||||||
|
# Already the length Steam expects: there is nothing to gain.
|
||||||
|
(( $(wc -c < "$script") == $(wc -c < "$copy") )) && return 1
|
||||||
|
|
||||||
|
out="$(_mca_steam_build "$copy" "$flags" 1)" || return 1
|
||||||
|
_mca_steam_same_size "$copy" "$out" || return 1
|
||||||
|
|
||||||
|
if mca_write_if_changed "$script" "$out"$'\n'; then
|
||||||
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||||
|
fi
|
||||||
|
chmod +x -- "$script" 2>/dev/null || true
|
||||||
|
touch -r "$copy" -- "$script" 2>/dev/null || true
|
||||||
|
mca_ledger_add steam "$script" "$name"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# mca_steam_apply
|
# mca_steam_apply
|
||||||
# Appends the flags to the line that starts the web helper. Everything else in
|
# Puts the flags into every Steam installation that has the script, preferring
|
||||||
# the script - setting up the container runtime, deciding whether the sandbox
|
# the patch that keeps the file's size and falling back to the one that does
|
||||||
# can be used - is left alone.
|
# not. Returns 1 when there is no Steam to patch at all.
|
||||||
mca_steam_apply() {
|
mca_steam_apply() {
|
||||||
local root script backup flags lineno found=0 out
|
local root script backup flags found=0 out neutral
|
||||||
|
|
||||||
flags="$(mca_flags)"
|
flags="$(mca_flags)"
|
||||||
|
|
||||||
@@ -105,24 +267,51 @@ mca_steam_apply() {
|
|||||||
script="$(mca_steam_script "$root")" || continue
|
script="$(mca_steam_script "$root")" || continue
|
||||||
found=1
|
found=1
|
||||||
|
|
||||||
mca_steam_script_patched "$script" && continue
|
if mca_steam_script_patched "$script"; then
|
||||||
|
_mca_steam_reneutralise "$script" "$flags" || true
|
||||||
lineno="$(_mca_steam_exec_line "$script")"
|
|
||||||
if [[ -z $lineno ]]; then
|
|
||||||
# A client update changed how the helper is started. Leaving the
|
|
||||||
# file alone is the only safe answer: this script is what starts
|
|
||||||
# Steam's entire interface.
|
|
||||||
mca_note "$(mca_msg "Steam starts its interface in a way this version does not recognise; leaving it alone.")"
|
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
out="$(awk -v n="$lineno" -v f=" $flags" 'NR == n { $0 = $0 f } { print }' "$script")" || continue
|
_mca_steam_refresh_backup "$script"
|
||||||
|
|
||||||
|
neutral=1
|
||||||
|
if ! { out="$(_mca_steam_build "$script" "$flags" 1)" \
|
||||||
|
&& _mca_steam_same_size "$script" "$out"; }
|
||||||
|
then
|
||||||
|
neutral=0
|
||||||
|
out="$(_mca_steam_build "$script" "$flags" 0)" || {
|
||||||
|
# A client update changed how the helper is started. Leaving
|
||||||
|
# the file alone is the only safe answer: this script is what
|
||||||
|
# starts Steam's entire interface.
|
||||||
|
mca_note "$(mca_msg "Steam starts its interface in a way this version does not recognise; leaving it alone.")"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The patch has to grow the file, and it is not patched now but was
|
||||||
|
# before: Steam has just put its own copy back. Patching it again
|
||||||
|
# while the client watches is what turns one size mismatch into an
|
||||||
|
# endless update dialog, and it would not help this session anyway -
|
||||||
|
# the helper is started once, at the start. The patch waits for the
|
||||||
|
# next apply with Steam closed. A patch that keeps the size has
|
||||||
|
# nothing to wait for.
|
||||||
|
if (( ! neutral )) && mca_steam_deferred "$script"; then
|
||||||
|
mca_note "$(mca_msg "Steam is running and has put its own file back; the change waits until Steam is closed.")"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
backup="$(mca_backup "$script")" || continue
|
backup="$(mca_backup "$script")" || continue
|
||||||
if mca_write_if_changed "$script" "$out"$'\n'; then
|
if mca_write_if_changed "$script" "$out"$'\n'; then
|
||||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||||
fi
|
fi
|
||||||
chmod +x -- "$script" 2>/dev/null || true
|
chmod +x -- "$script" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Steam looks at when the file was last written as well as at how big
|
||||||
|
# it is, and the copy taken before the edit still carries the original
|
||||||
|
# timestamp. Putting it back costs nothing and removes the other half
|
||||||
|
# of what the client would notice.
|
||||||
|
touch -r "$MCA_BACKUPDIR/$backup" -- "$script" 2>/dev/null || true
|
||||||
|
|
||||||
mca_ledger_add steam "$script" "$backup"
|
mca_ledger_add steam "$script" "$backup"
|
||||||
done < <(mca_steam_roots)
|
done < <(mca_steam_roots)
|
||||||
|
|
||||||
@@ -151,11 +340,22 @@ mca_steam_revert() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# mca_steam_desktop_apply <id> <source entry>
|
# mca_steam_flag_position <packaging>
|
||||||
|
# Where Steam's own switch goes on a command line. Straight after the program
|
||||||
|
# for a native or snap entry, because Steam reads its options before the
|
||||||
|
# steam:// argument that the right-click actions pass. For a Flatpak the first
|
||||||
|
# token is flatpak itself, which would take the switch for one of its own and
|
||||||
|
# refuse to start, so there it goes at the end of the arguments - after the
|
||||||
|
# application id, where flatpak hands everything on to Steam.
|
||||||
|
mca_steam_flag_position() {
|
||||||
|
[[ ${1:-native} == flatpak ]] && { printf 'before-fields\n'; return; }
|
||||||
|
printf 'after-program\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
# mca_steam_desktop_apply <id> <source entry> [packaging]
|
||||||
# Steam restores its own files at every start unless it is told not to verify
|
# Steam restores its own files at every start unless it is told not to verify
|
||||||
# them, so the launcher entry carries that switch. It goes directly after the
|
# them, so the launcher entry carries that switch.
|
||||||
# program because Steam reads its options before the steam:// argument that the
|
|
||||||
# right-click actions pass.
|
|
||||||
mca_steam_desktop_apply() {
|
mca_steam_desktop_apply() {
|
||||||
mca_desktop_apply "$1" "$2" "$MCA_STEAM_LAUNCH_FLAG" after-program
|
mca_desktop_apply "$1" "$2" "$MCA_STEAM_LAUNCH_FLAG" \
|
||||||
|
"$(mca_steam_flag_position "${3:-native}")"
|
||||||
}
|
}
|
||||||
@@ -119,8 +119,12 @@ mca_do_apply() {
|
|||||||
mca_ok "$(mca_msg "Already applied to %d applications." "$MCA_N_ON")"
|
mca_ok "$(mca_msg "Already applied to %d applications." "$MCA_N_ON")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $CFG_WATCH == yes ]] && mca_watch_available && ! mca_watch_enabled; then
|
if [[ $CFG_WATCH == yes ]] && mca_watch_available; then
|
||||||
mca_watch_enable || true
|
if mca_watch_enabled; then
|
||||||
|
mca_watch_refresh
|
||||||
|
else
|
||||||
|
mca_watch_enable || true
|
||||||
|
fi
|
||||||
elif [[ $CFG_WATCH != yes ]] && mca_watch_enabled; then
|
elif [[ $CFG_WATCH != yes ]] && mca_watch_enabled; then
|
||||||
mca_watch_disable
|
mca_watch_disable
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in new issue
Block a user