Files
middleclick-autoscroll/src/lib/apply.sh
T

335 lines
10 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
# spotify - the flag goes into spotify-launcher's configuration file
# gecko - a setting in each of the application's profiles
# 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 || $kind == spotify || $kind == gecko ]]; then
if mca_config_list_has Skip "$id"; then
MCA_ROUTE=off
else
MCA_ROUTE="$kind"
fi
return
fi
if ! mca_kind_wanted "$kind" "$id"; 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 spotify_done=0
MCA_CHANGES=0
MCA_ROUTES=()
MCA_N_ON=0; MCA_N_OFF=0; MCA_N_UNKNOWN=0
MCA_GECKO_SEEN=(); MCA_GECKO_WATCH=()
mca_config_load
# An installation that was set up by a version whose answer was a different
# flag still has that flag where it put it, and nothing below would touch it
# again: a file that is already marked as patched is left alone. Taking
# everything back once, here, is what carries such an installation over.
if [[ "$(mca_state_read flag_scheme 1)" != "$MCA_FLAG_SCHEME" ]]; then
mca_revert_all
fi
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" "$kind" \
|| mca_desktop_apply "$id" "$file" "$(mca_flags "$kind")"
;;
desktop)
mca_desktop_apply "$id" "$file" "$(mca_flags "$kind")"
;;
steam)
# Nothing is done to the entry itself: what Steam needs is
# in its own installation, and the same patch serves every
# way of starting the client.
if (( ! steam_done )); then
mca_steam_apply
steam_done=1
fi
;;
spotify)
(( spotify_done )) || mca_spotify_apply
spotify_done=1
;;
gecko)
mca_gecko_apply "${MCA_PROFILES[i]}" "$packaging" "$prog"
;;
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 (( ! steam_done )) && mca_steam_wanted && mca_steam_installed; then
mca_steam_apply
fi
# Autostart entries. A Chromium application that starts itself at login
# points straight at its binary and never reads the entry in the menu, so
# Discord at login used to behave differently from Discord from the menu.
mca_autostart_apply
# Shortcuts on the desktop itself, which nothing above has seen: the XDG
# search path does not go there. Each entry is gated on its own, so there is
# nothing to check out here.
mca_shortcuts_apply
# Not about any one application: middle click goes on pasting everywhere
# else on the desktop, and KDE is the one desktop that can be told not to.
# See kde.sh.
[[ $CFG_PASTE == yes ]] && mca_kde_paste_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)"
mca_state_write flag_scheme "$MCA_FLAG_SCHEME"
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. The same goes for where Firefox and its forks
# keep their profiles, which depends on what is installed.
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. That is the same contract as mca_write_if_changed, 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 path content='' changed=1
# 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.
dir="$(mca_desktop_folder)"
[[ -d $dir ]] && content="[Path]"$'\n'"PathModified=${dir//%/%%}"$'\n'
_mca_watch_conf desktop.conf "$content" && changed=0
# Filled by the apply that ran just before. A new profile is added there.
content=''
for path in "${MCA_GECKO_WATCH[@]}"; do
content+="PathChanged=${path//%/%%}"$'\n'
done
[[ -n $content ]] && content="[Path]"$'\n'"$content"
_mca_watch_conf profiles.conf "$content" && changed=0
return "$changed"
}
# _mca_watch_conf <name> <content>
# Writes one drop-in, or removes it when the content is empty. Returns 0 when
# it changed.
_mca_watch_conf() {
local file="$MCA_UNIT_DROPIN/$1"
if [[ -n $2 ]]; then
mca_write_if_changed "$file" "$2"
return
fi
[[ -e $file ]] || return 1
rm -f -- "$file"
rmdir "$MCA_UNIT_DROPIN" 2>/dev/null || true
return 0
}
_mca_watch_dropin_remove() {
local changed=1
_mca_watch_conf desktop.conf '' && changed=0
_mca_watch_conf profiles.conf '' && changed=0
return "$changed"
}
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, and with the Firefox
# profiles there are now.
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_steam_wanted
# Whether Steam gets patched, from the routes of the last scan: yes unless it
# was turned off in the applications list. Also yes when it has no entry there
# at all, so there is nothing to turn off.
mca_steam_wanted() {
local i seen=0
for i in "${!MCA_KINDS[@]}"; do
[[ ${MCA_KINDS[i]} == steam ]] || continue
[[ ${MCA_ROUTES[i]} == steam ]] && return 0
seen=1
done
(( ! seen ))
}
# mca_route_label <route>
mca_route_label() {
case "$1" in
flags) mca_msg "flag file" ;;
desktop) mca_msg "launcher" ;;
steam) mca_msg "Steam" ;;
spotify) printf 'spotify-launcher' ;;
gecko) mca_msg "profile" ;;
unknown) mca_msg "cannot tell" ;;
*) mca_msg "off" ;;
esac
printf '\n'
}