Steam compares its installed files against its manifest at every start and restores whatever differs, which is why its launcher entry carries -noverifyfiles. The entry in ~/.config/autostart never got it: it points at /usr/bin/steam, a shell script, so it fell through the Chromium filter in mca_autostart_apply and was left alone. A Steam started at login therefore verified, found the web helper script 46 bytes larger than the manifest says, restored it, and the path unit patched it straight back - an update dialog that begins again every few seconds and never finishes. The same client started from the menu was fine, which is what made it look like a problem of Steam's own. The autostart entry now carries the switch as well. It follows the Steam setting rather than the autostart one, because leaving it out while Steam is patched is exactly what causes the loop. That covers the entry that exists; a terminal or a script still starts Steam without the switch. So the patch no longer fights back either: a script that was patched before, is not patched now, and belongs to a client that is still running has just been restored by Steam, and doing it again would only have the two of them undoing each other. It waits for the next apply with Steam closed - the helper is started once, at the start, so patching it now would not have helped that session anyway. Also: Steam does not checksum that script, it compares sizes - the log says "Verifying file sizes only" - while the comments and the documentation claimed otherwise. And the status screen now tells a patch that is waiting from one that is missing, instead of reporting both as not patched yet.
516 lines
17 KiB
Bash
516 lines
17 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# Putting the flag where the application will read it, and taking it back out.
|
|
#
|
|
# There are two ways in, and the order matters:
|
|
#
|
|
# 1. A flag file. Arch's Electron and Chromium wrappers read extra arguments
|
|
# from $XDG_CONFIG_HOME/<name>-flags.conf. This is the good one - it is the
|
|
# supported way to pass arguments, it survives package upgrades untouched,
|
|
# and it applies however the program is started, including from a terminal.
|
|
#
|
|
# 2. A desktop entry. For applications that ship their own binary with no
|
|
# wrapper - most vendor packages, every Flatpak - there is nowhere else to
|
|
# put an argument, so a copy of the entry with the flag appended goes into
|
|
# ~/.local/share/applications, where it shadows the system one.
|
|
#
|
|
# Nothing outside $HOME is ever written to.
|
|
|
|
MCA_MARK_BEGIN='# >>> middleclick-autoscroll'
|
|
MCA_MARK_END='# <<< middleclick-autoscroll'
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Command lines
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# _mca_exec_insert_at <exec line>
|
|
# Where an argument has to go: after the program and its own arguments, but
|
|
# before the first field code (%U and friends) or Flatpak's @@ markers, which
|
|
# are placeholders the launcher expands rather than arguments.
|
|
_mca_exec_insert_at() {
|
|
local rest="$1" tok idx=0
|
|
|
|
while [[ -n $rest ]]; do
|
|
while [[ $rest == ' '* ]]; do rest="${rest# }"; idx=$(( idx + 1 )); done
|
|
[[ -n $rest ]] || break
|
|
tok="${rest%% *}"
|
|
case "$tok" in
|
|
%[a-zA-Z]|@@|@@u) printf '%s\n' "$idx"; return 0 ;;
|
|
esac
|
|
rest="${rest:${#tok}}"
|
|
idx=$(( idx + ${#tok} ))
|
|
done
|
|
|
|
printf '%s\n' "$(( ${#1} ))"
|
|
}
|
|
|
|
# _mca_exec_after_program <exec line>
|
|
# The position just after the program name, for arguments that a program only
|
|
# accepts before its positional ones.
|
|
_mca_exec_after_program() {
|
|
local rest="$1" idx=0 tok
|
|
|
|
while [[ $rest == ' '* ]]; do rest="${rest# }"; idx=$(( idx + 1 )); done
|
|
tok="${rest%% *}"
|
|
printf '%s\n' "$(( idx + ${#tok} ))"
|
|
}
|
|
|
|
# mca_exec_inject <exec line> [flags] [position: before-fields|after-program]
|
|
# The same line with the given flags added, defaulting to the configured ones.
|
|
# Prints nothing and returns 1 when there was nothing to change, so callers can
|
|
# tell a real edit from a no-op without diffing.
|
|
#
|
|
# An --enable-blink-features that is already there is extended rather than
|
|
# repeated: Chromium keeps the last occurrence of the option and drops the
|
|
# rest, so a second one would silently disable whatever the first turned on.
|
|
mca_exec_inject() {
|
|
local line="$1" flagstr="${2:-}" where="${3:-before-fields}"
|
|
local flag value existing at changed=0
|
|
local -a flags
|
|
|
|
[[ -n $flagstr ]] || flagstr="$(mca_flags)"
|
|
read -r -a flags <<< "$flagstr"
|
|
|
|
for flag in "${flags[@]}"; do
|
|
if [[ $flag == --enable-blink-features=* ]]; then
|
|
value="${flag#--enable-blink-features=}"
|
|
if [[ $line =~ --enable-blink-features=([A-Za-z0-9,_-]*) ]]; then
|
|
existing="${BASH_REMATCH[1]}"
|
|
[[ ",$existing," == *",$value,"* ]] && continue
|
|
line="${line/--enable-blink-features=$existing/--enable-blink-features=${existing:+$existing,}$value}"
|
|
changed=1
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
[[ " $line " == *" $flag "* ]] && continue
|
|
|
|
if [[ $where == after-program ]]; then
|
|
at="$(_mca_exec_after_program "$line")"
|
|
line="${line:0:$at} $flag${line:$at}"
|
|
else
|
|
at="$(_mca_exec_insert_at "$line")"
|
|
if (( at >= ${#line} )); then
|
|
line="${line% } $flag"
|
|
else
|
|
line="${line:0:$at}$flag ${line:$at}"
|
|
fi
|
|
fi
|
|
changed=1
|
|
done
|
|
|
|
(( changed )) || return 1
|
|
printf '%s\n' "$line"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Flag files
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# mca_flags_apply <program>
|
|
# Writes the flags into the file the program's launcher reads. Returns 0 when
|
|
# the program has such a file (whether or not anything needed changing), 1 when
|
|
# it has none and the desktop entry route has to be used instead.
|
|
mca_flags_apply() {
|
|
local prog="$1" name target='' existing_target=''
|
|
local -a candidates=()
|
|
|
|
mapfile -t candidates < <(mca_flags_candidates "$prog" | awk '!seen[$0]++')
|
|
(( ${#candidates[@]} )) || return 1
|
|
|
|
# A wrapper that offers several files reads the first one that exists and
|
|
# ignores the rest, so an existing file is always the right one to edit.
|
|
for name in "${candidates[@]}"; do
|
|
if [[ -f "$MCA_XDG_CONFIG/$name" ]]; then
|
|
existing_target="$name"
|
|
_mca_flags_write "$MCA_XDG_CONFIG/$name"
|
|
fi
|
|
done
|
|
[[ -n $existing_target ]] && return 0
|
|
|
|
# None exists yet. The last candidate is the wrapper's own fallback - the
|
|
# generic electron-flags.conf rather than electron43-flags.conf - so
|
|
# creating that one also covers every other application using the same
|
|
# shared Electron.
|
|
target="${candidates[-1]}"
|
|
_mca_flags_write "$MCA_XDG_CONFIG/$target"
|
|
return 0
|
|
}
|
|
|
|
# _mca_flags_write <file>
|
|
# Adds the flags as a marked block, or merges into an --enable-blink-features
|
|
# line that is already in the file.
|
|
_mca_flags_write() {
|
|
local file="$1" content='' line flag value existing merged=0 block=''
|
|
local -a flags
|
|
local created=0
|
|
|
|
read -r -a flags <<< "$(mca_flags)"
|
|
|
|
[[ -f $file ]] || created=1
|
|
if (( ! created )); then
|
|
content="$(< "$file")"
|
|
# Already ours: the block is rewritten below, so drop the old one first.
|
|
if [[ $content == *"$MCA_MARK_BEGIN"* ]]; then
|
|
content="$(_mca_flags_drop_block "$file")"
|
|
fi
|
|
fi
|
|
|
|
# Merge into a blink-features line the user (or a distribution) put there.
|
|
if [[ $content == *--enable-blink-features=* ]]; then
|
|
local out=''
|
|
while IFS= read -r line; do
|
|
if [[ $line =~ ^[[:space:]]*--enable-blink-features=([A-Za-z0-9,_-]*)[[:space:]]*$ ]]; then
|
|
existing="${BASH_REMATCH[1]}"
|
|
if [[ ",$existing," != *",$MCA_FEATURE,"* ]]; then
|
|
line="--enable-blink-features=${existing:+$existing,}$MCA_FEATURE"
|
|
fi
|
|
merged=1
|
|
fi
|
|
out+="$line"$'\n'
|
|
done <<< "$content"
|
|
content="${out%$'\n'}"
|
|
fi
|
|
|
|
for flag in "${flags[@]}"; do
|
|
(( merged )) && [[ $flag == --enable-blink-features=* ]] && continue
|
|
block+="$flag"$'\n'
|
|
done
|
|
|
|
if [[ -n $block ]]; then
|
|
content="${content:+$content$'\n'}$MCA_MARK_BEGIN"$'\n'"$block$MCA_MARK_END"
|
|
fi
|
|
|
|
if mca_write_if_changed "$file" "$content"$'\n'; then
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
|
|
if (( created )); then
|
|
mca_ledger_add flags "$file" created
|
|
elif (( merged )) && [[ -z $block ]]; then
|
|
mca_ledger_add flags "$file" merged
|
|
elif (( merged )); then
|
|
mca_ledger_add flags "$file" merged+block
|
|
else
|
|
mca_ledger_add flags "$file" block
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
_mca_flags_drop_block() {
|
|
awk -v b="$MCA_MARK_BEGIN" -v e="$MCA_MARK_END" '
|
|
$0 == b { skip = 1; next }
|
|
$0 == e { skip = 0; next }
|
|
!skip
|
|
' "$1" 2>/dev/null
|
|
}
|
|
|
|
# mca_flags_revert <file> <detail>
|
|
mca_flags_revert() {
|
|
local file="$1" detail="$2" content line out='' existing new
|
|
|
|
[[ -f $file ]] || return 0
|
|
|
|
case "$detail" in
|
|
created)
|
|
# Only remove the file if nothing but our block ended up in it;
|
|
# anything else means the user started using it in the meantime.
|
|
content="$(_mca_flags_drop_block "$file")"
|
|
if [[ -z ${content//[[:space:]]/} ]]; then
|
|
rm -f -- "$file"
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
content="$(_mca_flags_drop_block "$file")"
|
|
|
|
if [[ $detail == merged* ]]; then
|
|
while IFS= read -r line; do
|
|
if [[ $line =~ ^[[:space:]]*--enable-blink-features=([A-Za-z0-9,_-]*)[[:space:]]*$ ]]; then
|
|
existing="${BASH_REMATCH[1]}"
|
|
new="${existing//$MCA_FEATURE/}"
|
|
new="${new//,,/,}"; new="${new#,}"; new="${new%,}"
|
|
[[ -z $new ]] && continue
|
|
line="--enable-blink-features=$new"
|
|
fi
|
|
out+="$line"$'\n'
|
|
done <<< "$content"
|
|
content="${out%$'\n'}"
|
|
fi
|
|
|
|
if [[ -z ${content//[[:space:]]/} ]]; then
|
|
rm -f -- "$file"
|
|
else
|
|
printf '%s\n' "$content" > "$file"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Desktop entries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# A generated entry and an entry that was edited in place look similar and must
|
|
# never be confused: the first one is ours to delete, the second one is the
|
|
# user's file with one line changed and has to be restored from its backup. They
|
|
# carry different markers so that the scan, the "already done" checks and the
|
|
# undo can all tell them apart.
|
|
MCA_MARK_SHADOW='X-MCA-Generated'
|
|
MCA_MARK_INPLACE='X-MCA-Patched'
|
|
|
|
# _mca_desktop_transform <file> <marker> [flags] [position]
|
|
# The whole file with every Exec line rewritten - the main one and the one in
|
|
# each Desktop Action, because those are the right-click menu entries and a
|
|
# user who starts Steam from "Library" expects the same behaviour there.
|
|
_mca_desktop_transform() {
|
|
local file="$1" marker="$2" flags="${3:-}" where="${4:-before-fields}"
|
|
local line rest new marked=0
|
|
|
|
while IFS= read -r line || [[ -n $line ]]; do
|
|
case "$line" in
|
|
Exec=*)
|
|
rest="${line#Exec=}"
|
|
new="$(mca_exec_inject "$rest" "$flags" "$where")" && line="Exec=$new"
|
|
;;
|
|
# A D-Bus activated entry is started through its service file and
|
|
# the Exec line is never used, so the flag would go nowhere.
|
|
DBusActivatable=true)
|
|
line='DBusActivatable=false'
|
|
;;
|
|
# A marker from an older version; the current one is written below.
|
|
X-MCA-*)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
printf '%s\n' "$line"
|
|
|
|
if (( ! marked )) && [[ $line =~ ^[[:space:]]*\[Desktop\ Entry\][[:space:]]*$ ]]; then
|
|
printf '%s=%s\n' "$marker" "$MCA_VERSION"
|
|
marked=1
|
|
fi
|
|
done < "$file"
|
|
}
|
|
|
|
# mca_desktop_apply <id> <source entry> [flags] [position]
|
|
# Puts a patched copy in ~/.local/share/applications, or patches the entry in
|
|
# place when it already lives there.
|
|
mca_desktop_apply() {
|
|
local id="$1" src="$2" flags="${3:-}" where="${4:-before-fields}"
|
|
local target="$MCA_APPDIR/$id.desktop" content backup
|
|
|
|
if [[ "$src" == "$target" ]]; then
|
|
# The user's own entry - an AppImage, a web app shortcut, something
|
|
# installed by hand. There is nowhere to shadow it from, so it is
|
|
# edited directly and the original is kept.
|
|
grep -q "^$MCA_MARK_INPLACE=" "$src" 2>/dev/null && return 0
|
|
|
|
content="$(_mca_desktop_transform "$src" "$MCA_MARK_INPLACE" "$flags" "$where")"
|
|
[[ -n $content ]] || return 1
|
|
[[ "$content" == "$(< "$src")" ]] && return 0
|
|
|
|
backup="$(mca_backup "$src")" || return 1
|
|
if mca_write_if_changed "$src" "$content"$'\n'; then
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
mca_ledger_add inplace "$src" "$backup"
|
|
return 0
|
|
fi
|
|
|
|
content="$(_mca_desktop_transform "$src" "$MCA_MARK_SHADOW" "$flags" "$where")"
|
|
[[ -n $content ]] || return 1
|
|
|
|
mkdir -p "$MCA_APPDIR" 2>/dev/null || return 1
|
|
|
|
# Something is already shadowing this entry. Unless it is a shadow of ours,
|
|
# it is the user's own file - possibly one we edited in place earlier - and
|
|
# overwriting it here would lose it.
|
|
if [[ -e $target ]] && ! grep -q "^$MCA_MARK_SHADOW=" "$target" 2>/dev/null; then
|
|
return 1
|
|
fi
|
|
|
|
if mca_write_if_changed "$target" "$content"$'\n'; then
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
mca_ledger_add shadow "$target" "$src"
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Autostart entries
|
|
# ---------------------------------------------------------------------------
|
|
# Applications that start themselves at login write their own entry into
|
|
# ~/.config/autostart, pointing straight at their binary. Those bypass the
|
|
# desktop entry in the menu completely, which is why Discord launched at login
|
|
# used to behave differently from Discord launched from the menu.
|
|
#
|
|
# Steam is here too, even though it is not a Chromium process itself: its entry
|
|
# needs -noverifyfiles exactly like the menu one, and it is the entry most
|
|
# likely to exist, because Steam writes it as soon as "run at startup" is
|
|
# ticked. Without it, a Steam started at login restores the patched web helper
|
|
# script and the watcher patches it back, over and over.
|
|
|
|
mca_autostart_apply() {
|
|
local dir="$MCA_XDG_CONFIG/autostart" file exec_line prog content backup
|
|
|
|
[[ -d $dir ]] || return 0
|
|
|
|
for file in "$dir"/*.desktop; do
|
|
[[ -f $file ]] || continue
|
|
grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null && continue
|
|
|
|
exec_line="$(mca_desktop_get "$file" Exec)"
|
|
[[ -n $exec_line ]] || continue
|
|
|
|
mca_exec_program "$exec_line" || continue
|
|
prog="$MCA_PROG"
|
|
|
|
if [[ ${prog##*/} == steam || ${prog##*/} == steam-runtime ]]; then
|
|
[[ $CFG_STEAM == yes ]] || continue
|
|
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \
|
|
"$MCA_STEAM_LAUNCH_FLAG" after-program)"
|
|
else
|
|
[[ $CFG_AUTOSTART == yes ]] || continue
|
|
mca_is_chromium "$prog" || continue
|
|
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE")"
|
|
fi
|
|
[[ -n $content ]] || continue
|
|
[[ "$content" == "$(< "$file")" ]] && continue
|
|
|
|
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
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Spotify
|
|
# ---------------------------------------------------------------------------
|
|
# The official client is CEF rather than Electron and, when installed through
|
|
# spotify-launcher, is started by a program that builds its own command line.
|
|
# That launcher has a configuration file with a slot for exactly this.
|
|
|
|
mca_spotify_apply() {
|
|
local file="$MCA_XDG_CONFIG/spotify-launcher.conf" content line out='' backup
|
|
local found_section=0 found_args=0
|
|
|
|
mca_have spotify-launcher || return 0
|
|
|
|
if [[ -f $file ]]; then
|
|
content="$(< "$file")"
|
|
while IFS= read -r line; do
|
|
if [[ $line =~ ^[[:space:]]*\[spotify\][[:space:]]*$ ]]; then
|
|
found_section=1
|
|
elif [[ $line =~ ^[[:space:]]*extra_arguments[[:space:]]*=[[:space:]]*\[(.*)\][[:space:]]*$ ]]; then
|
|
local args="${BASH_REMATCH[1]}"
|
|
found_args=1
|
|
if [[ $args != *"$MCA_FEATURE"* ]]; then
|
|
line="extra_arguments = [${args:+$args, }\"$MCA_FLAG\"]"
|
|
fi
|
|
fi
|
|
out+="$line"$'\n'
|
|
done <<< "$content"
|
|
content="${out%$'\n'}"
|
|
fi
|
|
|
|
if (( ! found_args )); then
|
|
(( found_section )) || content="${content:+$content$'\n'}[spotify]"
|
|
content="${content:+$content$'\n'}extra_arguments = [\"$MCA_FLAG\"]"
|
|
fi
|
|
|
|
[[ -f $file ]] && backup="$(mca_backup "$file")"
|
|
|
|
if mca_write_if_changed "$file" "$content"$'\n'; then
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
mca_ledger_add inplace "$file" "${backup:-}"
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Leftovers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# mca_prune_orphans
|
|
# Removes generated entries whose application is gone.
|
|
#
|
|
# Uninstalling something deletes its entry from /usr/share/applications, but the
|
|
# copy shadowing it is in the user's home and pacman knows nothing about it. It
|
|
# would sit in the menu forever, offering to start a program that is no longer
|
|
# installed - and the watcher would not notice, because a plain apply only ever
|
|
# looks at what is there now.
|
|
mca_prune_orphans() {
|
|
local kind path source line
|
|
local -a lines=()
|
|
|
|
[[ -f $MCA_LEDGER ]] || return 0
|
|
mapfile -t lines < "$MCA_LEDGER"
|
|
|
|
for line in "${lines[@]}"; do
|
|
IFS=$'\t' read -r kind path source <<< "$line"
|
|
[[ $kind == shadow && -n $source ]] || continue
|
|
[[ -e $source ]] && continue
|
|
|
|
# Only ever remove something still recognisably ours.
|
|
if [[ -f $path ]] && grep -q "^$MCA_MARK_SHADOW=" "$path" 2>/dev/null; then
|
|
rm -f -- "$path"
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
mca_ledger_forget "$path"
|
|
done
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Undo
|
|
# ---------------------------------------------------------------------------
|
|
|
|
mca_revert_all() {
|
|
local kind path detail
|
|
|
|
[[ -f $MCA_LEDGER ]] || return 0
|
|
|
|
# Backwards, so a file that was recorded twice ends up at its oldest state.
|
|
while IFS=$'\t' read -r kind path detail; do
|
|
[[ -n $path ]] || continue
|
|
case "$kind" in
|
|
shadow)
|
|
# Only remove what is still recognisably ours.
|
|
if [[ -f $path ]] && grep -q "^$MCA_MARK_SHADOW=" "$path" 2>/dev/null; then
|
|
rm -f -- "$path"
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
;;
|
|
inplace)
|
|
if [[ -n $detail && -f "$MCA_BACKUPDIR/$detail" ]]; then
|
|
if [[ -e $path ]] || [[ -d "$(dirname -- "$path")" ]]; then
|
|
cp -p -- "$MCA_BACKUPDIR/$detail" "$path" 2>/dev/null \
|
|
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
rm -f -- "$MCA_BACKUPDIR/$detail"
|
|
elif [[ -f $path ]]; then
|
|
# No backup: the file did not exist before we wrote it.
|
|
rm -f -- "$path"
|
|
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
fi
|
|
;;
|
|
flags)
|
|
mca_flags_revert "$path" "$detail" \
|
|
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
;;
|
|
steam)
|
|
mca_steam_revert "$path" "$detail" \
|
|
&& MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
|
;;
|
|
esac
|
|
done < <(tac "$MCA_LEDGER" 2>/dev/null)
|
|
|
|
rm -f -- "$MCA_LEDGER"
|
|
rmdir "$MCA_BACKUPDIR" 2>/dev/null || true
|
|
return 0
|
|
}
|