--enable-blink-features is on the list of flags Chromium warns about, so every browser the flag was applied to put a yellow "unsupported command-line flag" bar above each page. Browsers are given --enable-features=MiddleClickAutoscroll instead. It asks for the same thing - Blink generates a feature of the same name for each of its runtime flags - and is not on that list. That spelling only works from Chromium 124 onwards, so everything else keeps the flag that works everywhere: an application embedding an older Chromium, Steam's CEF among them, has no such bar to show anyway. Helium knows the feature under a name of its own and ignores the Chromium one, so browsers are asked for HeliumMiddleClickAutoscroll as well; autoscroll never worked there before. A name a browser does not know is ignored, which is what makes one list safe for all of them. An installation set up by an earlier version is taken back and written again once, because a file that is already marked as patched would otherwise be left alone with the old flag in it.
346 lines
12 KiB
Bash
346 lines
12 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# Paths, translations, output helpers and the change ledger.
|
|
#
|
|
# Everything here runs unprivileged. This tool only ever writes inside the
|
|
# user's own home - flag files, desktop entries, Steam's own scripts - so there
|
|
# is no system state to guard and nothing that needs root.
|
|
|
|
MCA_VERSION="@VERSION@"
|
|
MCA_NAME="middleclick-autoscroll"
|
|
MCA_PRETTY="Middle-Click Autoscroll"
|
|
|
|
# The whole point of the package. Blink implements Windows-style autoscroll
|
|
# behind a runtime flag that Chromium does not turn on for Linux, because
|
|
# middle click is taken by primary-selection paste there.
|
|
#
|
|
# There are two ways to ask for that flag, and which one is right depends on
|
|
# what is being started:
|
|
#
|
|
# --enable-blink-features=MiddleClickAutoscroll is the one that works
|
|
# everywhere, and the only one that works in the Chromium versions embedded
|
|
# applications ship - Steam's CEF, an Electron a few years old, anything
|
|
# before Chromium 124. A browser started with it puts a yellow bar above the
|
|
# page saying an unsupported command line flag is in use, because Chromium
|
|
# keeps a list of flags worth warning about and this is on it.
|
|
#
|
|
# --enable-features=MiddleClickAutoscroll is not on that list, so a browser
|
|
# started with it says nothing. Blink generates a feature of the same name
|
|
# for every one of its runtime flags, which is what makes the two the same
|
|
# request - but only since Chromium 124. Before that the name means nothing
|
|
# and is ignored without a word.
|
|
#
|
|
# So a browser, which is the only thing that shows the bar and the one thing
|
|
# that keeps itself up to date, is given the quiet one; everything else keeps
|
|
# the one that always works.
|
|
MCA_FLAG="--enable-blink-features=MiddleClickAutoscroll"
|
|
|
|
# Helium ships the feature under its own name and does not answer to the
|
|
# Chromium one, so browsers are asked for both. A name a browser does not know
|
|
# is ignored, which is what makes one list safe to hand to all of them.
|
|
MCA_BROWSER_FLAG="--enable-features=MiddleClickAutoscroll,HeliumMiddleClickAutoscroll"
|
|
|
|
# The feature names on their own, for merging into a list that an application
|
|
# (or the user) already carries, and for taking them back out again.
|
|
MCA_FEATURE="MiddleClickAutoscroll"
|
|
MCA_BROWSER_FEATURES="MiddleClickAutoscroll,HeliumMiddleClickAutoscroll"
|
|
|
|
# Bumped when the flags above change. An installation that was set up by a
|
|
# version with a different answer is taken back and done again once, because a
|
|
# file that is already patched is otherwise left alone. See mca_apply.
|
|
MCA_FLAG_SCHEME=2
|
|
|
|
MCA_LIBDIR="${MCA_LIBDIR:-@LIBDIR@}"
|
|
MCA_LOCALEDIR="${MCA_LOCALEDIR:-@LOCALEDIR@}"
|
|
|
|
MCA_XDG_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
MCA_XDG_DATA="${XDG_DATA_HOME:-$HOME/.local/share}"
|
|
MCA_XDG_STATE="${XDG_STATE_HOME:-$HOME/.local/state}"
|
|
MCA_XDG_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}"
|
|
|
|
MCA_CONFDIR="${MCA_CONFDIR:-${MCA_XDG_CONFIG}/${MCA_NAME}}"
|
|
MCA_CONFIG="${MCA_CONFIG:-${MCA_CONFDIR}/config}"
|
|
MCA_STATEDIR="${MCA_STATEDIR:-${MCA_XDG_STATE}/${MCA_NAME}}"
|
|
MCA_CACHEDIR="${MCA_CACHEDIR:-${MCA_XDG_CACHE}/${MCA_NAME}}"
|
|
|
|
# Where generated desktop entries go. A file here shadows the one with the same
|
|
# name in /usr/share/applications, which is how an application gets extra
|
|
# command line arguments without touching a file the package manager owns.
|
|
MCA_APPDIR="${MCA_XDG_DATA}/applications"
|
|
|
|
# Copies of every file that is edited in place rather than shadowed.
|
|
MCA_BACKUPDIR="${MCA_STATEDIR}/backup"
|
|
|
|
# One line per change, so `revert` can undo exactly what was done and nothing
|
|
# else. See mca_ledger_add.
|
|
MCA_LEDGER="${MCA_STATEDIR}/ledger"
|
|
|
|
# The folder the desktop itself shows. Nothing in the XDG search path looks at
|
|
# it, so an entry that lives only there is invisible to a scan - and a game
|
|
# shortcut dragged onto the desktop is exactly that.
|
|
#
|
|
# Its name is translated: Schreibtisch, Bureau, Escritorio. The name in use is
|
|
# in the file xdg-user-dirs writes, so it is read rather than guessed, and
|
|
# ~/Desktop is only the fallback for a system that has no such file.
|
|
mca_desktop_folder() {
|
|
local file="$MCA_XDG_CONFIG/user-dirs.dirs" line dir=''
|
|
|
|
if [[ -r $file ]]; then
|
|
while IFS= read -r line; do
|
|
[[ $line == XDG_DESKTOP_DIR=* ]] || continue
|
|
dir="${line#XDG_DESKTOP_DIR=}"
|
|
dir="${dir%\"}"; dir="${dir#\"}"
|
|
done < "$file"
|
|
dir="${dir/#\$HOME/$HOME}"
|
|
fi
|
|
|
|
[[ $dir == /* ]] || dir="$HOME/Desktop"
|
|
printf '%s\n' "${dir%/}"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Translations
|
|
# ---------------------------------------------------------------------------
|
|
|
|
export TEXTDOMAIN="middleclick-autoscroll"
|
|
export TEXTDOMAINDIR="${MCA_LOCALEDIR}"
|
|
|
|
mca_ui_locale() {
|
|
local l="${MCA_UI_LOCALE:-}"
|
|
|
|
if [[ -z $l ]]; then
|
|
l="${LC_ALL:-}"
|
|
[[ -z $l ]] && l="${LC_MESSAGES:-}"
|
|
[[ -z $l ]] && l="${LANG:-}"
|
|
fi
|
|
|
|
# systemd writes /etc/locale.conf and most distributions use it; Debian and
|
|
# Ubuntu keep the same LANG= line in /etc/default/locale instead.
|
|
if [[ -z $l ]]; then
|
|
local f
|
|
for f in /etc/locale.conf /etc/default/locale; do
|
|
[[ -r $f ]] || continue
|
|
l="$(sed -n 's/^LANG=//p' "$f" | tr -d '"' | head -n1)"
|
|
[[ -n $l ]] && break
|
|
done
|
|
fi
|
|
|
|
printf '%s\n' "${l:-C}"
|
|
}
|
|
|
|
# Every gettext lookup is a fork and the settings screen redraws a screenful of
|
|
# labels per keypress, so results are memoized.
|
|
declare -A MCA_MSG_CACHE=()
|
|
|
|
MCA_MSG_RESULT=''
|
|
|
|
# mca_msg_into <locale> <msgid>
|
|
# Plain lookup with the result in MCA_MSG_RESULT and no printf formatting, for
|
|
# callers that would otherwise pay a fork per label per frame.
|
|
mca_msg_into() {
|
|
local locale="$1" msgid="$2" cachekey
|
|
cachekey="${locale}"$'\x1f'"${msgid}"
|
|
|
|
if [[ -n ${MCA_MSG_CACHE[$cachekey]+set} ]]; then
|
|
MCA_MSG_RESULT="${MCA_MSG_CACHE[$cachekey]}"
|
|
return 0
|
|
fi
|
|
|
|
MCA_MSG_RESULT="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
|
[[ -n $MCA_MSG_RESULT ]] || MCA_MSG_RESULT="$msgid"
|
|
MCA_MSG_CACHE[$cachekey]="$MCA_MSG_RESULT"
|
|
return 0
|
|
}
|
|
|
|
# mca_msg_in <locale> <msgid> [printf args...]
|
|
mca_msg_in() {
|
|
local locale="$1" msgid="$2" translated cachekey
|
|
shift 2
|
|
|
|
cachekey="${locale}"$'\x1f'"${msgid}"
|
|
if [[ -n ${MCA_MSG_CACHE[$cachekey]+set} ]]; then
|
|
translated="${MCA_MSG_CACHE[$cachekey]}"
|
|
else
|
|
translated="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
|
|
[[ -n $translated ]] || translated="$msgid"
|
|
MCA_MSG_CACHE[$cachekey]="$translated"
|
|
fi
|
|
|
|
# With no arguments the message is plain text, not a format string. Feeding
|
|
# it to printf anyway would turn a literal percent sign in a translation
|
|
# into an invalid conversion.
|
|
if (( $# == 0 )); then
|
|
printf '%s' "$translated"
|
|
return
|
|
fi
|
|
|
|
# shellcheck disable=SC2059 # the format string is the translated message
|
|
printf -- "$translated" "$@"
|
|
}
|
|
|
|
# mca_msg <msgid> [printf args...]
|
|
mca_msg() { mca_msg_in "$(mca_ui_locale)" "$@"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Output
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Decided once, while stdout is still whatever the process was started with:
|
|
# testing -t 1 at the point of use is wrong for anything called through $(...),
|
|
# which sees a pipe and would conclude nobody is watching.
|
|
MCA_INTERACTIVE=''
|
|
[[ -t 1 ]] && MCA_INTERACTIVE=1
|
|
|
|
if [[ -n $MCA_INTERACTIVE && -z ${NO_COLOR:-} ]]; then
|
|
MCA_C_RESET=$'\033[0m'
|
|
MCA_C_BOLD=$'\033[1m'
|
|
MCA_C_DIM=$'\033[2m'
|
|
MCA_C_BLUE=$'\033[38;2;23;147;209m'
|
|
MCA_C_GREEN=$'\033[32m'
|
|
MCA_C_YELLOW=$'\033[33m'
|
|
MCA_C_RED=$'\033[31m'
|
|
else
|
|
MCA_C_RESET='' MCA_C_BOLD='' MCA_C_DIM='' MCA_C_BLUE=''
|
|
MCA_C_GREEN='' MCA_C_YELLOW='' MCA_C_RED=''
|
|
fi
|
|
|
|
# Set by mca_bad and mca_note. The menu redraws straight after an action, which
|
|
# would wipe the screen; this marks that something was printed the user still
|
|
# has to read. A plain success needs no acknowledgement - the status block at
|
|
# the top of the menu already shows the new state.
|
|
MCA_UI_NEEDS_ACK=''
|
|
|
|
# --quiet silences progress chatter; errors still go to stderr. Used by the
|
|
# systemd unit, which has no terminal and logs to the journal anyway.
|
|
MCA_QUIET=''
|
|
|
|
mca_say() { [[ -n $MCA_QUIET ]] || printf '%s\n' "$*"; }
|
|
mca_head() { printf '\n%s%s%s\n\n' "$MCA_C_BOLD$MCA_C_BLUE" "$*" "$MCA_C_RESET"; }
|
|
mca_ok() { [[ -n $MCA_QUIET ]] || printf '%s✔%s %s\n' "$MCA_C_GREEN" "$MCA_C_RESET" "$*"; }
|
|
mca_bad() { MCA_UI_NEEDS_ACK=1; printf '%s✘%s %s\n' "$MCA_C_RED" "$MCA_C_RESET" "$*" >&2; }
|
|
mca_note() { MCA_UI_NEEDS_ACK=1; [[ -n $MCA_QUIET ]] || printf '%s•%s %s\n' "$MCA_C_DIM" "$MCA_C_RESET" "$*"; }
|
|
|
|
mca_have() { command -v "$1" > /dev/null 2>&1; }
|
|
|
|
# Human-readable "x minutes ago" for a unix timestamp. Empty input yields the
|
|
# translated "never".
|
|
mca_time_ago() {
|
|
local ts="$1" now delta
|
|
|
|
[[ $ts =~ ^[0-9]+$ ]] || { mca_msg "never"; printf '\n'; return; }
|
|
|
|
now="$(date +%s)"
|
|
delta=$(( now - ts ))
|
|
(( delta < 0 )) && delta=0
|
|
|
|
if (( delta < 60 )); then mca_msg "just now"
|
|
elif (( delta < 3600 )); then mca_msg "%d minutes ago" "$(( delta / 60 ))"
|
|
elif (( delta < 86400 )); then mca_msg "%d hours ago" "$(( delta / 3600 ))"
|
|
else mca_msg "%d days ago" "$(( delta / 86400 ))"
|
|
fi
|
|
printf '\n'
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# State
|
|
# ---------------------------------------------------------------------------
|
|
|
|
mca_state_read() {
|
|
local key="$1" default="${2:-}"
|
|
if [[ -r "$MCA_STATEDIR/$key" ]]; then
|
|
cat "$MCA_STATEDIR/$key"
|
|
else
|
|
printf '%s\n' "$default"
|
|
fi
|
|
}
|
|
|
|
mca_state_write() {
|
|
local key="$1"
|
|
shift
|
|
mkdir -p "$MCA_STATEDIR" 2>/dev/null || return 1
|
|
printf '%s\n' "$*" > "$MCA_STATEDIR/$key"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The change ledger
|
|
# ---------------------------------------------------------------------------
|
|
# Reverting by pattern - "delete every desktop entry that mentions the flag" -
|
|
# would also delete entries the user wrote by hand. So every change is recorded
|
|
# instead, and `revert` replays the ledger backwards.
|
|
#
|
|
# Format, tab separated:
|
|
# <kind> <path> <detail>
|
|
#
|
|
# shadow <generated desktop entry> <source entry it was generated from>
|
|
# inplace <edited file> <basename of its backup copy>
|
|
# flags <flag file> created | appended | merged
|
|
#
|
|
# `inplace` and `flags` differ in how they are undone: a backup is restored
|
|
# wholesale, a flag file only loses the one line that was added to it.
|
|
|
|
mca_ledger_add() {
|
|
local kind="$1" path="$2" detail="${3:-}"
|
|
mkdir -p "$MCA_STATEDIR" 2>/dev/null || return 1
|
|
|
|
# Never record the same path twice: applying repeatedly is normal (the path
|
|
# unit fires on every desktop file that appears) and the ledger has to stay
|
|
# a set, not a log.
|
|
mca_ledger_forget "$path"
|
|
printf '%s\t%s\t%s\n' "$kind" "$path" "$detail" >> "$MCA_LEDGER"
|
|
}
|
|
|
|
mca_ledger_forget() {
|
|
local path="$1" tmp
|
|
[[ -f $MCA_LEDGER ]] || return 0
|
|
|
|
tmp="$(mktemp "${MCA_LEDGER}.XXXXXX")" || return 1
|
|
awk -F'\t' -v p="$path" '$2 != p' "$MCA_LEDGER" > "$tmp" 2>/dev/null \
|
|
&& mv -f "$tmp" "$MCA_LEDGER" || rm -f "$tmp"
|
|
return 0
|
|
}
|
|
|
|
|
|
# mca_backup_name <file>
|
|
# The name a backup copy of that file is stored under. Derived from the path
|
|
# rather than remembered, so a caller can ask whether a file has been backed up
|
|
# before without reading the ledger.
|
|
mca_backup_name() {
|
|
printf '%s' "$1" | sed 's|/|%|g'
|
|
}
|
|
|
|
# mca_backup <file>
|
|
# Copies a file aside before it is edited in place, and prints the name the
|
|
# copy was stored under. Existing backups are never overwritten: the first copy
|
|
# is the pristine one, and a second apply must not replace it with an already
|
|
# patched version.
|
|
mca_backup() {
|
|
local file="$1" name
|
|
name="$(mca_backup_name "$file")"
|
|
|
|
mkdir -p "$MCA_BACKUPDIR" 2>/dev/null || return 1
|
|
if [[ ! -e "$MCA_BACKUPDIR/$name" ]]; then
|
|
cp -p -- "$file" "$MCA_BACKUPDIR/$name" 2>/dev/null || return 1
|
|
fi
|
|
printf '%s\n' "$name"
|
|
}
|
|
|
|
# mca_write_if_changed <path> <content>
|
|
# Writing a file that is already correct would touch its mtime, and the systemd
|
|
# path unit watches these directories: an unconditional write would retrigger
|
|
# the service, which would write again, forever.
|
|
mca_write_if_changed() {
|
|
local path="$1" content="$2" current=''
|
|
|
|
[[ -r $path ]] && current="$(< "$path")"
|
|
|
|
# $(< file) drops trailing newlines and callers pass content that ends in
|
|
# one, so both sides are trimmed before comparing. Getting this wrong makes
|
|
# every file look changed on every run, which is exactly the feedback loop
|
|
# this function exists to prevent.
|
|
[[ "${current%"${current##*[!$'\n']}"}" == "${content%"${content##*[!$'\n']}"}" ]] && return 1
|
|
|
|
mkdir -p "$(dirname "$path")" 2>/dev/null || return 2
|
|
printf '%s' "$content" > "$path" || return 2
|
|
return 0
|
|
}
|