564 lines
16 KiB
Bash
564 lines
16 KiB
Bash
# 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. It is 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
|
|
|
|
# Not an ON/OFF like the rows around it: what is being reported is the state
|
|
# of the paste, not of a switch, and off is the state this program is after.
|
|
if mca_kde_available; then
|
|
if [[ "$(mca_kde_read)" == false ]]; then
|
|
_mca_row "$(mca_msg "Middle-click paste")" \
|
|
"${MCA_C_GREEN}$(mca_msg "off")${MCA_C_RESET}"
|
|
else
|
|
_mca_row "$(mca_msg "Middle-click paste")" \
|
|
"${MCA_C_DIM}$(mca_msg "on")${MCA_C_RESET}"
|
|
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"
|
|
"PatchSnap|bool|yes|Snap applications"
|
|
"PatchAutostart|bool|yes|Programs that start themselves at login"
|
|
"PatchSteam|bool|yes|Steam"
|
|
"PatchSpotify|bool|yes|Spotify"
|
|
"DisablePaste|bool|yes|Turn off KDE's middle-click paste"
|
|
"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: change, q: 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 warn
|
|
mca_msg_into "$locale" "Applications"; title="$MCA_MSG_RESULT"
|
|
mca_msg_into "$locale" "Up/Down: select, Space: turn on or off, q: 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"
|
|
mca_msg_into "$locale" "Autoscroll is off. This is what would be covered."
|
|
warn="$MCA_MSG_RESULT"
|
|
|
|
# The per-row labels are resolved once, here. Looking them up inside the
|
|
# drawing loop is a fork per row per keypress, and that is enough to make
|
|
# the arrow keys feel like the screen is reloading.
|
|
local l_off l_cannot l_on l_steam l_flagfile l_launcher
|
|
mca_msg_into "$locale" "off"; l_off="$MCA_MSG_RESULT"
|
|
mca_msg_into "$locale" "cannot tell"; l_cannot="$MCA_MSG_RESULT"
|
|
mca_msg_into "$locale" "on"; l_on="$MCA_MSG_RESULT"
|
|
mca_msg_into "$locale" "Steam"; l_steam="$MCA_MSG_RESULT"
|
|
mca_msg_into "$locale" "flag file"; l_flagfile="$MCA_MSG_RESULT"
|
|
mca_msg_into "$locale" "launcher"; l_launcher="$MCA_MSG_RESULT"
|
|
|
|
local s_off="${MCA_C_DIM}${l_off}${MCA_C_RESET}"
|
|
local s_cannot="${MCA_C_DIM}${l_cannot}${MCA_C_RESET}"
|
|
local s_on="${MCA_C_GREEN}${l_on}${MCA_C_RESET}"
|
|
local s_steam="${s_on} ${MCA_C_DIM}(${l_steam})${MCA_C_RESET}"
|
|
local s_flags="${s_on} ${MCA_C_DIM}(${l_flagfile})${MCA_C_RESET}"
|
|
local s_desktop="${s_on} ${MCA_C_DIM}(${l_launcher})${MCA_C_RESET}"
|
|
|
|
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="$s_off" ;;
|
|
unknown) shown="$s_cannot" ;;
|
|
steam) shown="$s_steam" ;;
|
|
flags) shown="$s_flags" ;;
|
|
*) shown="$s_desktop" ;;
|
|
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}${warn}${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 "Re-apply everything")"
|
|
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
|
|
;;
|
|
# Not a plain apply: with the watcher running there is never
|
|
# anything left for one to do, and a menu entry that answers
|
|
# "already done" every time is not an action. This is the repair -
|
|
# everything is taken back and written again from scratch, which is
|
|
# what fixes an application that drifted, a flag file somebody
|
|
# edited, or Steam after it restored its own script.
|
|
2)
|
|
mca_ui_cooked mca_do_apply --rebuild
|
|
mca_pause
|
|
;;
|
|
3)
|
|
if mca_ui_apps; then
|
|
[[ $CFG_ENABLED == yes ]] && mca_ui_cooked mca_do_apply --rebuild
|
|
fi
|
|
;;
|
|
4)
|
|
if mca_ui_settings; then
|
|
[[ $CFG_ENABLED == yes ]] && mca_ui_cooked mca_do_apply --rebuild
|
|
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
|
|
}
|