middleclick-autoscroll 1.0.0
Middle-click autoscroll for every Chromium-based application on the system: Electron and CEF applications, Chromium-based browsers, Flatpaks, Spotify and Steam, plus anything installed later. The flag goes into an Arch wrapper's <name>-flags.conf where one exists, and into a shadowing desktop entry where it does not. Steam gets its web helper script patched and -noverifyfiles on its launcher, because it restores its own files otherwise. Every change is recorded so it can be taken back exactly.
This commit is contained in:
commit
d3c82fdff8
18 files changed
+4208
No files matched your search
+523
@@ -0,0 +1,523 @@
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# The interactive front end.
|
||||
#
|
||||
# This is the whole configuration interface. There is a file behind it, but no
|
||||
# part of the program ever asks anyone to open it: one switch on the front
|
||||
# screen, a settings list for the categories, and a list of the applications
|
||||
# themselves for the cases where the automatic answer is not the wanted one.
|
||||
|
||||
# Terminal mode.
|
||||
#
|
||||
# bash flips the terminal into non-canonical mode for each `read -sn1` and back
|
||||
# out again in between. That gap matters: in canonical mode DEL is the ERASE
|
||||
# character, so the line discipline eats it instead of delivering it, and a
|
||||
# backspace typed while the interface was between reads simply vanishes.
|
||||
# Holding non-canonical mode for the whole interface removes the gap.
|
||||
MCA_TERM_SAVED=''
|
||||
|
||||
mca_ui_term_raw() {
|
||||
mca_have stty || return 0
|
||||
[[ -t 0 ]] || return 0
|
||||
[[ -n $MCA_TERM_SAVED ]] && return 0
|
||||
|
||||
MCA_TERM_SAVED="$(stty -g 2>/dev/null)" || { MCA_TERM_SAVED=''; return 0; }
|
||||
stty -icanon -echo min 1 time 0 2>/dev/null || true
|
||||
}
|
||||
|
||||
mca_ui_term_restore() {
|
||||
[[ -n $MCA_TERM_SAVED ]] || return 0
|
||||
stty "$MCA_TERM_SAVED" 2>/dev/null || true
|
||||
MCA_TERM_SAVED=''
|
||||
}
|
||||
|
||||
# Runs an action with the terminal handed back to normal line mode, so anything
|
||||
# it prints - or prompts for - behaves the way a program expects.
|
||||
mca_ui_cooked() {
|
||||
mca_ui_term_restore
|
||||
"$@"
|
||||
local rc=$?
|
||||
mca_ui_term_raw
|
||||
return $rc
|
||||
}
|
||||
|
||||
# mca_read_key
|
||||
# One keypress, resolved to a symbolic name. Arrow keys arrive as ESC [ A, so
|
||||
# the tail of the sequence is consumed here rather than being mistaken for
|
||||
# three separate presses.
|
||||
mca_read_key() {
|
||||
local k rest
|
||||
|
||||
IFS= read -rsn1 k || return 1
|
||||
|
||||
case "$k" in
|
||||
$'\e')
|
||||
if IFS= read -rsn2 -t 0.05 rest; then
|
||||
case "$rest" in
|
||||
'[A') printf 'up\n' ;;
|
||||
'[B') printf 'down\n' ;;
|
||||
'[C') printf 'right\n' ;;
|
||||
'[D') printf 'left\n' ;;
|
||||
*) printf 'escape\n' ;;
|
||||
esac
|
||||
else
|
||||
printf 'escape\n'
|
||||
fi
|
||||
;;
|
||||
''|$'\r') printf 'enter\n' ;;
|
||||
$'\x7f'|$'\b') printf 'backspace\n' ;;
|
||||
' ') printf 'space\n' ;;
|
||||
*) printf '%s\n' "$k" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# mca_ui_read_line <initial>
|
||||
# A minimal line editor built on mca_read_key, with the result in
|
||||
# MCA_LINE_RESULT. This exists instead of bash's own `read -r` because mixing
|
||||
# line mode into a single-key interface breaks it: after one cooked-mode read
|
||||
# the following `read -sn1` stops receiving keystrokes entirely.
|
||||
MCA_LINE_RESULT=''
|
||||
|
||||
mca_ui_read_line() {
|
||||
local buf="${1:-}" key
|
||||
|
||||
MCA_LINE_RESULT=''
|
||||
printf '%s' "$buf"
|
||||
|
||||
while true; do
|
||||
key="$(mca_read_key)" || { printf '\n'; return 1; }
|
||||
|
||||
case "$key" in
|
||||
enter)
|
||||
printf '\n'
|
||||
MCA_LINE_RESULT="$buf"
|
||||
return 0
|
||||
;;
|
||||
escape)
|
||||
printf '\n'
|
||||
return 1
|
||||
;;
|
||||
backspace)
|
||||
if [[ -n $buf ]]; then
|
||||
buf="${buf%?}"
|
||||
printf '\b \b'
|
||||
fi
|
||||
;;
|
||||
space)
|
||||
buf+=' '
|
||||
printf ' '
|
||||
;;
|
||||
up|down|left|right) ;;
|
||||
*)
|
||||
[[ ${#key} -eq 1 ]] || continue
|
||||
buf+="$key"
|
||||
printf '%s' "$key"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
mca_pause() {
|
||||
printf '\n %s' "$(mca_msg "Press any key to continue...")"
|
||||
read -rsn1 _ || true
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# _mca_row <label> <value>
|
||||
# printf's %-28s pads by bytes, so a label containing "ü" comes out one column
|
||||
# short. ${#s} counts characters in a UTF-8 locale, so the padding is computed
|
||||
# here instead - and applied inline, because command substitution would eat the
|
||||
# trailing spaces again.
|
||||
_mca_row() {
|
||||
local label="$1" value="$2" pad
|
||||
pad=$(( 30 - ${#label} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
printf ' %s%*s %s\n' "$label" "$pad" '' "$value"
|
||||
}
|
||||
|
||||
_mca_onoff() {
|
||||
if [[ $1 == yes ]]; then
|
||||
printf '%s%s%s' "$MCA_C_GREEN" "$(mca_msg "ON")" "$MCA_C_RESET"
|
||||
else
|
||||
printf '%s%s%s' "$MCA_C_DIM" "$(mca_msg "OFF")" "$MCA_C_RESET"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Status
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# mca_ui_status
|
||||
# Shared by the `status` subcommand and the menu header. Scans, so the numbers
|
||||
# describe what is on disk right now rather than what was true at the last run.
|
||||
mca_ui_status() {
|
||||
local last covered
|
||||
|
||||
mca_scan_once
|
||||
mca_count_routes
|
||||
|
||||
last="$(mca_state_read last_apply '')"
|
||||
|
||||
_mca_row "$(mca_msg "Autoscroll")" "$(_mca_onoff "$CFG_ENABLED")"
|
||||
printf '\n'
|
||||
|
||||
if [[ $CFG_ENABLED == yes ]]; then
|
||||
covered="$MCA_N_ON"
|
||||
else
|
||||
covered=0
|
||||
fi
|
||||
|
||||
_mca_row "$(mca_msg "Applications covered")" \
|
||||
"$(mca_msg "%d of %d" "$covered" "$(( MCA_N_ON + MCA_N_OFF + MCA_N_UNKNOWN ))")"
|
||||
|
||||
if (( MCA_N_UNKNOWN )); then
|
||||
_mca_row "$(mca_msg "Not identified")" \
|
||||
"${MCA_C_DIM}$(mca_msg "%d - see the applications list" "$MCA_N_UNKNOWN")${MCA_C_RESET}"
|
||||
fi
|
||||
|
||||
if mca_steam_installed; then
|
||||
if mca_steam_patched; then
|
||||
_mca_row "$(mca_msg "Steam")" "$(_mca_onoff yes)"
|
||||
elif [[ $CFG_STEAM == yes && $CFG_ENABLED == yes ]]; then
|
||||
_mca_row "$(mca_msg "Steam")" \
|
||||
"${MCA_C_YELLOW}$(mca_msg "not patched yet")${MCA_C_RESET}"
|
||||
else
|
||||
_mca_row "$(mca_msg "Steam")" "$(_mca_onoff no)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if mca_watch_available; then
|
||||
if mca_watch_enabled; then
|
||||
_mca_row "$(mca_msg "New applications")" "$(_mca_onoff yes)"
|
||||
else
|
||||
_mca_row "$(mca_msg "New applications")" "$(_mca_onoff no)"
|
||||
fi
|
||||
fi
|
||||
|
||||
_mca_row "$(mca_msg "Last applied")" "$(mca_time_ago "$last")"
|
||||
|
||||
if [[ $CFG_ENABLED == yes ]]; then
|
||||
printf '\n %s%s%s\n' "$MCA_C_DIM" \
|
||||
"$(mca_msg "Applications pick this up the next time they are started.")" \
|
||||
"$MCA_C_RESET"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Settings
|
||||
# ---------------------------------------------------------------------------
|
||||
# Format: Key|type|default|label-msgid
|
||||
# type is bool or text.
|
||||
MCA_SETTINGS=(
|
||||
"PatchApps|bool|yes|Electron and CEF applications"
|
||||
"PatchBrowsers|bool|yes|Chromium-based browsers"
|
||||
"PatchFlatpak|bool|yes|Flatpak applications"
|
||||
"PatchAutostart|bool|yes|Programs that start themselves at login"
|
||||
"PatchSteam|bool|yes|Steam"
|
||||
"PatchSpotify|bool|yes|Spotify"
|
||||
"WatchNewApps|bool|yes|Apply to newly installed applications"
|
||||
"ExtraFlags|text||Additional Chromium arguments"
|
||||
)
|
||||
|
||||
_mca_is_true() {
|
||||
case "${1,,}" in
|
||||
yes|y|true|1|on|enabled) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
MCA_SETTING_SHOWN=''
|
||||
MCA_LBL_ON=''
|
||||
MCA_LBL_OFF=''
|
||||
MCA_LBL_NONE=''
|
||||
|
||||
_mca_setting_display() {
|
||||
local type="$1" value="$2"
|
||||
|
||||
case "$type" in
|
||||
bool)
|
||||
if _mca_is_true "$value"; then
|
||||
MCA_SETTING_SHOWN="${MCA_C_GREEN}${MCA_LBL_ON}${MCA_C_RESET}"
|
||||
else
|
||||
MCA_SETTING_SHOWN="${MCA_C_DIM}${MCA_LBL_OFF}${MCA_C_RESET}"
|
||||
fi
|
||||
;;
|
||||
text)
|
||||
if [[ -n $value ]]; then
|
||||
MCA_SETTING_SHOWN="$value"
|
||||
else
|
||||
MCA_SETTING_SHOWN="${MCA_C_DIM}${MCA_LBL_NONE}${MCA_C_RESET}"
|
||||
fi
|
||||
;;
|
||||
*) MCA_SETTING_SHOWN="$value" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# mca_ui_settings
|
||||
# A cursor list rather than a numbered menu. The frame is assembled in memory
|
||||
# and written once, and everything constant is resolved before the loop:
|
||||
# drawing the naive way costs a command substitution per label per frame, which
|
||||
# is slow enough that arrow keys feel like the console is reloading.
|
||||
#
|
||||
# Returns 0 when something was changed, so the caller knows to re-apply.
|
||||
mca_ui_settings() {
|
||||
local count=${#MCA_SETTINGS[@]}
|
||||
local -a names=() types=() defaults=() labels=() values=()
|
||||
local spec name type default label locale i key frame row pad dirty=1 cursor=0
|
||||
local touched=0
|
||||
|
||||
locale="$(mca_ui_locale)"
|
||||
mca_msg_into "$locale" "ON"; MCA_LBL_ON="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "OFF"; MCA_LBL_OFF="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "(none)"; MCA_LBL_NONE="$MCA_MSG_RESULT"
|
||||
|
||||
for spec in "${MCA_SETTINGS[@]}"; do
|
||||
IFS='|' read -r name type default label <<< "$spec"
|
||||
names+=("$name"); types+=("$type"); defaults+=("$default")
|
||||
mca_msg_into "$locale" "$label"
|
||||
labels+=("$MCA_MSG_RESULT")
|
||||
done
|
||||
|
||||
local title hint
|
||||
mca_msg_into "$locale" "Settings"; title="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "Up/Down select - Space or Right changes - q goes back"
|
||||
hint="$MCA_MSG_RESULT"
|
||||
|
||||
local clearseq
|
||||
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
|
||||
|
||||
while true; do
|
||||
if (( dirty )); then
|
||||
for i in "${!names[@]}"; do
|
||||
_mca_config_lookup "${names[i]}" "${defaults[i]}"
|
||||
values[i]="$MCA_CONFIG_VALUE"
|
||||
done
|
||||
dirty=0
|
||||
fi
|
||||
|
||||
frame="$clearseq"$'\n'"${MCA_C_BOLD}${MCA_C_BLUE} ${title}${MCA_C_RESET}"$'\n\n'
|
||||
|
||||
local marker selected="${MCA_C_BLUE}▸${MCA_C_RESET} "
|
||||
for i in "${!names[@]}"; do
|
||||
_mca_setting_display "${types[i]}" "${values[i]}"
|
||||
pad=$(( 46 - ${#labels[i]} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
if (( i == cursor )); then marker="$selected"; else marker=' '; fi
|
||||
printf -v row ' %s%s%*s %s' \
|
||||
"$marker" "${labels[i]}" "$pad" '' "$MCA_SETTING_SHOWN"
|
||||
frame+="$row"$'\n'
|
||||
done
|
||||
|
||||
frame+=$'\n'" ${MCA_C_DIM}${hint}${MCA_C_RESET}"$'\n'
|
||||
printf '%s' "$frame"
|
||||
|
||||
key="$(mca_read_key)" || return "$(( ! touched ))"
|
||||
|
||||
type="${types[cursor]}"
|
||||
name="${names[cursor]}"
|
||||
|
||||
case "$key" in
|
||||
up|k) cursor=$(( (cursor - 1 + count) % count )) ;;
|
||||
down|j) cursor=$(( (cursor + 1) % count )) ;;
|
||||
space|enter|right|l|left|h)
|
||||
if [[ $type == text ]]; then
|
||||
[[ $key == left || $key == h ]] && continue
|
||||
mca_ui_edit_text "$name" "${values[cursor]}" && touched=1
|
||||
else
|
||||
if _mca_is_true "${values[cursor]}"; then
|
||||
mca_config_set "$name" no
|
||||
else
|
||||
mca_config_set "$name" yes
|
||||
fi || { mca_bad "$(mca_msg "Could not save the setting.")"; mca_pause; }
|
||||
touched=1
|
||||
fi
|
||||
dirty=1
|
||||
;;
|
||||
q|Q|escape) return "$(( ! touched ))" ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# mca_ui_edit_text <key> <current>
|
||||
mca_ui_edit_text() {
|
||||
local name="$1" current="$2"
|
||||
|
||||
printf '\n %s\n' "$(mca_msg "Arguments separated by spaces, empty for none:")"
|
||||
printf ' > '
|
||||
|
||||
if mca_ui_read_line "$current"; then
|
||||
mca_config_set "$name" "$MCA_LINE_RESULT" \
|
||||
|| { mca_bad "$(mca_msg "Could not save the setting.")"; mca_pause; }
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Applications
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# mca_ui_apps
|
||||
# Every application that was found, what will happen to it, and a key to
|
||||
# change that. This is where an AppImage that could not be identified gets
|
||||
# turned on, and where a single application gets left out without having to
|
||||
# switch off its whole category.
|
||||
#
|
||||
# Returns 0 when something was changed.
|
||||
mca_ui_apps() {
|
||||
local count key frame row pad i cursor=0 dirty=1 touched=0 locale
|
||||
local -a labels=() states=()
|
||||
|
||||
locale="$(mca_ui_locale)"
|
||||
|
||||
local title hint legend
|
||||
mca_msg_into "$locale" "Applications"; title="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "Up/Down select - Space turns one on or off - q goes back"
|
||||
hint="$MCA_MSG_RESULT"
|
||||
mca_msg_into "$locale" "Anything not identified is left alone until it is turned on here."
|
||||
legend="$MCA_MSG_RESULT"
|
||||
|
||||
local clearseq
|
||||
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
|
||||
|
||||
printf '%s\n %s\n' "$clearseq" "$(mca_msg "Looking at the installed applications...")"
|
||||
mca_config_load
|
||||
mca_scan_once
|
||||
count=${#MCA_IDS[@]}
|
||||
|
||||
if (( count == 0 )); then
|
||||
printf '\n %s\n' "$(mca_msg "No Chromium-based applications found.")"
|
||||
mca_pause
|
||||
return 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
if (( dirty )); then
|
||||
mca_config_load
|
||||
mca_count_routes
|
||||
labels=(); states=()
|
||||
for i in "${!MCA_IDS[@]}"; do
|
||||
labels+=("${MCA_NAMES[i]}")
|
||||
states+=("${MCA_ROUTES[i]}")
|
||||
done
|
||||
dirty=0
|
||||
fi
|
||||
|
||||
frame="$clearseq"$'\n'"${MCA_C_BOLD}${MCA_C_BLUE} ${title}${MCA_C_RESET}"$'\n\n'
|
||||
|
||||
local marker selected="${MCA_C_BLUE}▸${MCA_C_RESET} " shown
|
||||
for i in "${!labels[@]}"; do
|
||||
case "${states[i]}" in
|
||||
off) shown="${MCA_C_DIM}$(mca_msg "off")${MCA_C_RESET}" ;;
|
||||
unknown) shown="${MCA_C_DIM}$(mca_msg "cannot tell")${MCA_C_RESET}" ;;
|
||||
steam) shown="${MCA_C_GREEN}$(mca_msg "on")${MCA_C_RESET} ${MCA_C_DIM}($(mca_msg "Steam"))${MCA_C_RESET}" ;;
|
||||
flags) shown="${MCA_C_GREEN}$(mca_msg "on")${MCA_C_RESET} ${MCA_C_DIM}($(mca_msg "flag file"))${MCA_C_RESET}" ;;
|
||||
*) shown="${MCA_C_GREEN}$(mca_msg "on")${MCA_C_RESET} ${MCA_C_DIM}($(mca_msg "launcher"))${MCA_C_RESET}" ;;
|
||||
esac
|
||||
pad=$(( 34 - ${#labels[i]} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
if (( i == cursor )); then marker="$selected"; else marker=' '; fi
|
||||
printf -v row ' %s%s%*s %s' "$marker" "${labels[i]}" "$pad" '' "$shown"
|
||||
frame+="$row"$'\n'
|
||||
done
|
||||
|
||||
frame+=$'\n'
|
||||
# Without this the list reads as a list of what is switched on, which
|
||||
# it is not while the whole thing is off.
|
||||
if [[ $CFG_ENABLED != yes ]]; then
|
||||
frame+=" ${MCA_C_YELLOW}$(mca_msg "Autoscroll is off - this is what would be covered.")${MCA_C_RESET}"$'\n'
|
||||
fi
|
||||
frame+=" ${MCA_C_DIM}${legend}${MCA_C_RESET}"$'\n'
|
||||
frame+=" ${MCA_C_DIM}${hint}${MCA_C_RESET}"$'\n'
|
||||
printf '%s' "$frame"
|
||||
|
||||
key="$(mca_read_key)" || return "$(( ! touched ))"
|
||||
|
||||
case "$key" in
|
||||
up|k) cursor=$(( (cursor - 1 + count) % count )) ;;
|
||||
down|j) cursor=$(( (cursor + 1) % count )) ;;
|
||||
space|enter|right|left|l|h)
|
||||
case "${states[cursor]}" in
|
||||
off|unknown)
|
||||
mca_config_list_del Skip "${MCA_IDS[cursor]}"
|
||||
mca_config_list_add Include "${MCA_IDS[cursor]}"
|
||||
;;
|
||||
*)
|
||||
mca_config_list_del Include "${MCA_IDS[cursor]}"
|
||||
mca_config_list_add Skip "${MCA_IDS[cursor]}"
|
||||
;;
|
||||
esac
|
||||
touched=1
|
||||
dirty=1
|
||||
;;
|
||||
q|Q|escape) return "$(( ! touched ))" ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The menu
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
mca_ui_menu() {
|
||||
local choice
|
||||
|
||||
mca_ui_term_raw
|
||||
trap 'mca_ui_term_restore' EXIT INT TERM
|
||||
|
||||
while true; do
|
||||
mca_config_load
|
||||
|
||||
clear 2>/dev/null || true
|
||||
mca_head " $MCA_PRETTY"
|
||||
mca_ui_status
|
||||
printf '\n'
|
||||
printf ' [1] %s\n' "$(mca_msg "Turn autoscroll on or off")"
|
||||
printf ' [2] %s\n' "$(mca_msg "Apply now")"
|
||||
printf ' [3] %s\n' "$(mca_msg "Applications")"
|
||||
printf ' [4] %s\n' "$(mca_msg "Settings")"
|
||||
printf ' [q] %s\n' "$(mca_msg "Quit")"
|
||||
printf '\n > '
|
||||
|
||||
choice="$(mca_read_key)" || {
|
||||
printf '\n'; mca_ui_term_restore; trap - EXIT INT TERM; return 0
|
||||
}
|
||||
case "$choice" in
|
||||
enter|space|up|down|left|right|escape) choice='' ;;
|
||||
esac
|
||||
printf '%s\n' "$choice"
|
||||
|
||||
case "$choice" in
|
||||
1)
|
||||
MCA_UI_NEEDS_ACK=''
|
||||
if [[ $CFG_ENABLED == yes ]]; then
|
||||
mca_ui_cooked mca_do_disable
|
||||
else
|
||||
mca_ui_cooked mca_do_enable
|
||||
fi
|
||||
[[ -n $MCA_UI_NEEDS_ACK ]] && mca_pause
|
||||
;;
|
||||
2)
|
||||
mca_ui_cooked mca_do_apply
|
||||
mca_pause
|
||||
;;
|
||||
3)
|
||||
if mca_ui_apps; then
|
||||
[[ $CFG_ENABLED == yes ]] && mca_ui_cooked mca_do_apply --changed
|
||||
fi
|
||||
;;
|
||||
4)
|
||||
if mca_ui_settings; then
|
||||
[[ $CFG_ENABLED == yes ]] && mca_ui_cooked mca_do_apply --changed
|
||||
fi
|
||||
;;
|
||||
q|Q) mca_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
|
||||
# Anything else - Enter, arrow keys, stray characters - just
|
||||
# redraws. Escape is deliberately not a quit key, so a mistyped
|
||||
# arrow key cannot close the menu.
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
Reference in new issue
Block a user