Files
middleclick-autoscroll/src/lib/apply.sh
T
Felitendo 004094ef78 feat: turn off KDE's middle-click paste
Inside a Chromium application the flag settles this on its own - with
autoscroll on, Blink stops pasting the primary selection. Everywhere else
on the desktop middle click goes on pasting, which is the thing this whole
program exists to get away from.

KWin has a switch for it, on Wayland, where the paste is a protocol the
compositor either offers or does not: EnablePrimarySelection in kwinrc.
Written with kwriteconfig, the way System Settings would, because kwinrc
has a cascade behind it. What the key said before goes in the ledger, so
disable puts it back - and a key that already said false is left alone and
not recorded, so a paste the user switched off themselves is never
switched back on.

X11 has no such switch and nothing is written there.
2026-09-08 13:30:43 +02:00

286 lines
8.7 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
# 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
;;
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
# 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.
if [[ $CFG_AUTOSTART == yes ]]; then
mca_autostart_apply
fi
# 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
[[ $CFG_SPOTIFY == yes ]] && mca_spotify_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.
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'
}