# 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 [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 # 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 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' }