Steam's autoscroll worked, except when it didn't. Every report of that came down to the same thing: Steam started in a way that carried no -noverifyfiles, noticed the patched web helper script, put its own copy back, and spent the session without the flag. There is no way to make every possible way of starting Steam carry an argument - a game launcher, a shortcut, a terminal, another program calling `steam steam://` - so the patch stops depending on it. The check is on size and timestamp, not on content, so the patch is now written to look untouched: the bytes the flag costs come back out of the script's own comments, and the timestamp of the untouched copy is restored afterwards. The file Steam finds is exactly as long and exactly as old as the one it wrote. A client that verifies its files finds nothing to repair. -noverifyfiles stays as the fallback for a script with no comments left to pay for the flag. Two things fall out of that. A patch that keeps the size no longer has to wait for Steam to close, because there is no size mismatch for the client to chase; only the growing one still defers. And an installation patched by the earlier version is quietly redone at the original size on the next apply, so the fix arrives without anyone having to know about it. Then the launch paths that were never covered: - Shortcuts on the desktop itself. Nothing in the XDG search path looks at that folder, so nothing had ever seen them - and Steam writes one there for every game somebody asks for a shortcut to. Its name is translated, so it is read from user-dirs.dirs and handed to the watcher in a drop-in. - Steam as a Flatpak was never recognised as Steam, so its entry got nothing while its script got patched - the worst of both. The switch goes after the application id there, where flatpak passes it on rather than reading it. - steam-native and steam-jupiter, which are ordinary Steam starts under another name, and Flatpak and snap entries in ~/.config/autostart. Two things found on the way: a client update left the undo copy holding the script from before the update, so undoing would have put an old version back; and a shortcut edited in place and later deleted would have been recreated by `disable`.
282 lines
8.6 KiB
Bash
282 lines
8.6 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# Deciding what happens to each application, and doing it.
|
|
#
|
|
# The routing lives in mca_route and is used twice: once by mca_apply to patch
|
|
# and once by the applications screen to show what would happen. Keeping it in
|
|
# one function is what makes that screen trustworthy - it is not a description
|
|
# of the behaviour, it is the behaviour.
|
|
|
|
MCA_CHANGES=0
|
|
MCA_ROUTES=()
|
|
|
|
# mca_route <kind> <id> <program> [packaging]
|
|
# What happens to one application, left in MCA_ROUTE:
|
|
# flags - the launcher reads a flag file; write it there
|
|
# desktop - no flag file; shadow or edit the desktop entry
|
|
# steam - Steam's own two-part treatment
|
|
# unknown - cannot tell what engine this is (an AppImage), so nothing is done
|
|
# off - detected, but switched off
|
|
#
|
|
# Assigns rather than prints so the memo inside mca_has_flags_file survives:
|
|
# from inside a command substitution it would be filled in a subshell and
|
|
# thrown away, which is the whole point of having it.
|
|
MCA_ROUTE=''
|
|
|
|
mca_route() {
|
|
local kind="$1" id="$2" prog="$3" packaging="${4:-native}"
|
|
|
|
if [[ $kind == steam ]]; then
|
|
if mca_config_list_has Skip "$id" || [[ $CFG_STEAM != yes ]]; then
|
|
MCA_ROUTE=off
|
|
else
|
|
MCA_ROUTE=steam
|
|
fi
|
|
return
|
|
fi
|
|
|
|
if ! mca_kind_wanted "$kind" "$id" "$packaging"; then
|
|
[[ $kind == unknown ]] && MCA_ROUTE=unknown || MCA_ROUTE=off
|
|
return
|
|
fi
|
|
|
|
# A Flatpak or a snap carries its own copy of everything and sees none of
|
|
# the host's wrappers, so the desktop entry is the only way in.
|
|
if [[ $packaging != native ]]; then
|
|
MCA_ROUTE=desktop
|
|
return
|
|
fi
|
|
|
|
if mca_has_flags_file "$prog"; then
|
|
MCA_ROUTE=flags
|
|
else
|
|
MCA_ROUTE=desktop
|
|
fi
|
|
}
|
|
|
|
# mca_apply
|
|
# Brings everything into line with the current settings. Safe to run as often
|
|
# as it likes to be - it writes only what differs, which is what keeps the
|
|
# watcher from chasing its own changes.
|
|
mca_apply() {
|
|
local i id file prog kind packaging route steam_done=0
|
|
|
|
MCA_CHANGES=0
|
|
MCA_ROUTES=()
|
|
MCA_N_ON=0; MCA_N_OFF=0; MCA_N_UNKNOWN=0
|
|
|
|
mca_config_load
|
|
mca_scan
|
|
|
|
for i in "${!MCA_IDS[@]}"; do
|
|
id="${MCA_IDS[i]}"
|
|
file="${MCA_FILES[i]}"
|
|
prog="${MCA_PROGS[i]}"
|
|
kind="${MCA_KINDS[i]}"
|
|
packaging="${MCA_PACKAGING[i]}"
|
|
|
|
mca_route "$kind" "$id" "$prog" "$packaging"
|
|
route="$MCA_ROUTE"
|
|
MCA_ROUTES[i]="$route"
|
|
|
|
case "$route" in
|
|
unknown) MCA_N_UNKNOWN=$(( MCA_N_UNKNOWN + 1 )) ;;
|
|
off) MCA_N_OFF=$(( MCA_N_OFF + 1 )) ;;
|
|
*) MCA_N_ON=$(( MCA_N_ON + 1 )) ;;
|
|
esac
|
|
|
|
case "$route" in
|
|
flags)
|
|
mca_flags_apply "$prog" || mca_desktop_apply "$id" "$file"
|
|
;;
|
|
desktop)
|
|
mca_desktop_apply "$id" "$file"
|
|
;;
|
|
steam)
|
|
mca_steam_desktop_apply "$id" "$file" "$packaging"
|
|
if (( ! steam_done )); then
|
|
mca_steam_apply
|
|
steam_done=1
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Steam is worth patching even when its desktop entry is missing - a user
|
|
# who starts it from a script or a game launcher still gets the interface.
|
|
if [[ $CFG_STEAM == yes ]] && (( ! steam_done )) && mca_steam_installed; then
|
|
mca_steam_apply
|
|
fi
|
|
|
|
# The shortcuts Steam writes for single games. They are not applications
|
|
# and are not offered as ones, but starting a game with Steam closed is a
|
|
# Steam start like any other: without the switch the client finds the
|
|
# patched helper script, puts its own back, and the interface loses
|
|
# autoscroll for the rest of the session.
|
|
if [[ $CFG_STEAM == yes ]]; then
|
|
for i in "${!MCA_STEAM_LINKS[@]}"; do
|
|
mca_steam_desktop_apply "${MCA_STEAM_LINKS[i]}" \
|
|
"${MCA_STEAM_LINK_FILES[i]}" "${MCA_STEAM_LINK_PACK[i]}"
|
|
done
|
|
fi
|
|
|
|
# Steam's autostart entry carries Steam's own switch and follows the Steam
|
|
# setting, not this one - leaving it out while Steam is patched is what puts
|
|
# the client in an update loop - so both are checked inside.
|
|
if [[ $CFG_AUTOSTART == yes || $CFG_STEAM == yes ]]; then
|
|
mca_autostart_apply
|
|
fi
|
|
|
|
# Shortcuts on the desktop itself. Nothing above has seen them - the XDG
|
|
# search path does not go there - and a game started from one starts Steam
|
|
# without its switch, which is the difference between autoscroll working
|
|
# and autoscroll working most of the time. Each entry is gated on its own,
|
|
# so there is nothing to check out here.
|
|
mca_shortcuts_apply
|
|
|
|
[[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply
|
|
|
|
mca_prune_orphans
|
|
|
|
if (( MCA_CHANGES )) && mca_have update-desktop-database; then
|
|
update-desktop-database "$MCA_APPDIR" 2>/dev/null || true
|
|
fi
|
|
|
|
mca_state_write last_apply "$(date +%s)"
|
|
return 0
|
|
}
|
|
|
|
# mca_revert
|
|
# Undoes every recorded change and forgets them.
|
|
mca_revert() {
|
|
MCA_CHANGES=0
|
|
mca_revert_all
|
|
|
|
if (( MCA_CHANGES )) && mca_have update-desktop-database; then
|
|
update-desktop-database "$MCA_APPDIR" 2>/dev/null || true
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The watcher
|
|
# ---------------------------------------------------------------------------
|
|
# A systemd user path unit watching every directory a desktop entry can appear
|
|
# in. That covers a package from whatever the distribution's package manager
|
|
# is, a Flatpak, a snap, an AppImage registered by hand and a Steam client
|
|
# update, without a hook per package manager.
|
|
|
|
MCA_UNIT_PATH="middleclick-autoscroll.path"
|
|
MCA_UNIT_SERVICE="middleclick-autoscroll.service"
|
|
|
|
# The desktop folder is watched like every other directory an entry can turn up
|
|
# in, but its name is translated and the unit that ships with the package
|
|
# cannot know it. So that one path is written here, into a drop-in, from the
|
|
# name this system actually uses.
|
|
MCA_UNIT_DROPIN="${MCA_XDG_CONFIG}/systemd/user/${MCA_UNIT_PATH}.d"
|
|
|
|
mca_watch_available() {
|
|
mca_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} || -S "/run/user/$(id -u)/bus" ]]
|
|
}
|
|
|
|
mca_watch_enabled() {
|
|
mca_watch_available || return 1
|
|
systemctl --user is-enabled --quiet "$MCA_UNIT_PATH" 2>/dev/null
|
|
}
|
|
|
|
# _mca_watch_dropin_write
|
|
# Returns 0 when the drop-in changed and systemd has to be told, 1 when it was
|
|
# already right - the same contract as mca_write_if_changed, and for the same
|
|
# reason: the watcher runs after every desktop entry that appears anywhere on
|
|
# the system, and a daemon-reload each time would be absurd.
|
|
_mca_watch_dropin_write() {
|
|
local dir file="$MCA_UNIT_DROPIN/desktop.conf" content
|
|
|
|
dir="$(mca_desktop_folder)"
|
|
[[ -d $dir ]] || { _mca_watch_dropin_remove; return $?; }
|
|
|
|
# A per cent sign starts a specifier in a unit file and has to be doubled
|
|
# to mean itself. Rare in a folder name, fatal when it happens.
|
|
content="[Path]"$'\n'"PathModified=${dir//%/%%}"$'\n'
|
|
|
|
mca_write_if_changed "$file" "$content"
|
|
}
|
|
|
|
_mca_watch_dropin_remove() {
|
|
[[ -e "$MCA_UNIT_DROPIN/desktop.conf" ]] || return 1
|
|
rm -f -- "$MCA_UNIT_DROPIN/desktop.conf"
|
|
rmdir "$MCA_UNIT_DROPIN" 2>/dev/null || true
|
|
return 0
|
|
}
|
|
|
|
mca_watch_enable() {
|
|
mca_watch_available || return 1
|
|
_mca_watch_dropin_write
|
|
systemctl --user daemon-reload > /dev/null 2>&1 || true
|
|
systemctl --user enable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || return 1
|
|
systemctl --user enable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
|
return 0
|
|
}
|
|
|
|
# mca_watch_refresh
|
|
# Keeps a running watcher in step with a desktop folder that has been renamed,
|
|
# which is what a change of session language does to it.
|
|
mca_watch_refresh() {
|
|
mca_watch_available || return 0
|
|
_mca_watch_dropin_write || return 0
|
|
systemctl --user daemon-reload > /dev/null 2>&1 || true
|
|
systemctl --user restart "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
|
|
return 0
|
|
}
|
|
|
|
mca_watch_disable() {
|
|
mca_watch_available || return 0
|
|
systemctl --user disable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
|
|
systemctl --user disable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
|
_mca_watch_dropin_remove && systemctl --user daemon-reload > /dev/null 2>&1
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Reporting
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# mca_count_routes
|
|
# Fills MCA_N_ON / MCA_N_OFF / MCA_N_UNKNOWN from a scan, without writing
|
|
# anything. Used by the status block and the menu header.
|
|
MCA_N_ON=0
|
|
MCA_N_OFF=0
|
|
MCA_N_UNKNOWN=0
|
|
|
|
mca_count_routes() {
|
|
local i route
|
|
|
|
MCA_N_ON=0; MCA_N_OFF=0; MCA_N_UNKNOWN=0
|
|
MCA_ROUTES=()
|
|
|
|
for i in "${!MCA_IDS[@]}"; do
|
|
mca_route "${MCA_KINDS[i]}" "${MCA_IDS[i]}" "${MCA_PROGS[i]}" \
|
|
"${MCA_PACKAGING[i]}"
|
|
route="$MCA_ROUTE"
|
|
MCA_ROUTES[i]="$route"
|
|
case "$route" in
|
|
unknown) MCA_N_UNKNOWN=$(( MCA_N_UNKNOWN + 1 )) ;;
|
|
off) MCA_N_OFF=$(( MCA_N_OFF + 1 )) ;;
|
|
*) MCA_N_ON=$(( MCA_N_ON + 1 )) ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# mca_route_label <route>
|
|
mca_route_label() {
|
|
case "$1" in
|
|
flags) mca_msg "flag file" ;;
|
|
desktop) mca_msg "launcher" ;;
|
|
steam) mca_msg "Steam" ;;
|
|
unknown) mca_msg "cannot tell" ;;
|
|
*) mca_msg "off" ;;
|
|
esac
|
|
printf '\n'
|
|
}
|