feat: lib/menu-lib.sh — category-neutral menu primitives (guard/run/pick/ask_value); install list += new libs
This commit is contained in:
+1
-1
@@ -140,7 +140,7 @@ if should_run 2 scripts; then
|
||||
done
|
||||
lib_count=0
|
||||
lib_names=""
|
||||
for lf in common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh user-timers-lib.sh entertainment-plugin-lib.sh usb-lib.sh; do
|
||||
for lf in common.sh flags.sh notify.sh entertainment-lib.sh scheduler-lib.sh config-ui.sh user-timers-lib.sh entertainment-plugin-lib.sh usb-lib.sh share-lib.sh menu-lib.sh; do
|
||||
run sudo install -m 644 "lib/$lf" "/usr/local/bin/$lf"
|
||||
lib_count=$((lib_count + 1))
|
||||
lib_names+="$lf "
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# lib/menu-lib.sh — category-neutral interactive menu primitives.
|
||||
#
|
||||
# The generic half of the former share-lib interactive layer (Pattern B),
|
||||
# extracted so any `pos` tool can share one interaction vocabulary: a tty
|
||||
# guard, a looping boxed menu, a type-to-filter picker and a prompt with
|
||||
# optional default. Display goes to stderr, results to stdout; reads are
|
||||
# stdin-based and fail closed (EOF / no terminal → rc 1, never a hang),
|
||||
# so the functions are safe under the dispatcher's logging tee and inside
|
||||
# command substitution.
|
||||
#
|
||||
# Contracts (all of them, no exceptions):
|
||||
# * Defines ONLY `menu_*` functions — sourcing never clobbers a tool's own
|
||||
# helpers (same discipline as lib/notify.sh).
|
||||
# * Requires common.sh to be sourced by the CALLER for colored output;
|
||||
# CYAN/RESET get empty guarded fallbacks here so the lib also works
|
||||
# standalone-sourced (plain text instead of color — never an error).
|
||||
# * NEVER exits and never terminates the caller: every function returns,
|
||||
# failures are signalled through the return code.
|
||||
# * Display goes to stderr, results go to stdout — any function whose result
|
||||
# is meant to be command-substituted prints ONLY the result on stdout.
|
||||
# * Performs NO file writes of its own.
|
||||
#
|
||||
# Function index:
|
||||
# menu_guard rc 0 iff stdin is a terminal
|
||||
# menu_run <title> <item...> numbered menu loop → chosen index
|
||||
# menu_pick <prompt> <item...> type-to-filter picker → chosen index
|
||||
# menu_ask_value <label> [default] prompted value → entered text
|
||||
|
||||
# ── Colors (guarded fallbacks; a sourced common.sh wins) ──────
|
||||
CYAN="${CYAN:-}"
|
||||
RESET="${RESET:-}"
|
||||
|
||||
# ── Terminal guard ────────────────────────────────────────────
|
||||
# rc 0 iff stdin is a tty · rc 1 otherwise, with a one-line pointer to the
|
||||
# scriptable subcommands. Tools that must fail hard without a TTY call this
|
||||
# before entering the loop.
|
||||
menu_guard() {
|
||||
if [ -t 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
printf '[!] Interactive menu needs a terminal — use a subcommand instead (see --help).\n' >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Numbered menu loop (firewall-precedent style) ──────────────
|
||||
# Renders a section-box title + `%2d)` items + separator to stderr and reads
|
||||
# `Choose: `. stdout carries the chosen index ONLY.
|
||||
# rc 0 valid pick (index on stdout)
|
||||
# rc 1 quit (`0`/`q`/`Q`), EOF, or no terminal — callers treat this as a
|
||||
# clean menu exit (tools that must fail hard without a TTY call
|
||||
# menu_guard themselves before entering the loop).
|
||||
menu_run() {
|
||||
local title="$1"; shift
|
||||
local -a items=("$@")
|
||||
if ! menu_guard; then
|
||||
return 1
|
||||
fi
|
||||
local opt i
|
||||
while true; do
|
||||
{
|
||||
echo
|
||||
echo "${CYAN}════════════════════════════════════════════${RESET}"
|
||||
echo "${CYAN} ${title}${RESET}"
|
||||
echo "${CYAN}════════════════════════════════════════════${RESET}"
|
||||
for ((i = 0; i < ${#items[@]}; i++)); do
|
||||
printf ' %2d) %s\n' $((i + 1)) "${items[$i]}"
|
||||
done
|
||||
printf ' %2d) %s\n' 0 "Exit"
|
||||
echo "----------------------------------------"
|
||||
} >&2
|
||||
if ! read -rp "Choose: " opt; then
|
||||
return 1 # EOF — clean menu exit
|
||||
fi
|
||||
case "$opt" in
|
||||
"") ;; # empty input → redraw
|
||||
0 | q | Q) return 1 ;;
|
||||
*)
|
||||
if [[ "$opt" =~ ^[0-9]+$ ]] && (( opt >= 1 && opt <= ${#items[@]} )); then
|
||||
echo "$opt"
|
||||
return 0
|
||||
fi
|
||||
echo "Unknown choice." >&2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ── Type-to-filter picker ──────────────────────────────────────
|
||||
# Lists items on stderr; numeric choice → index (into the FULL item list) on
|
||||
# stdout; non-numeric input filters case-insensitively and redisplays with a
|
||||
# "-- N of M match 'text' --" banner; empty or `/` while filtered clears back
|
||||
# to the full list; zero matches warn and redisplay.
|
||||
# rc 0 picked · rc 1 back/cancel (`0`/`q`/`b`, EOF) — never out of bounds.
|
||||
menu_pick() {
|
||||
local prompt="${1:-Pick}"; shift
|
||||
local -a items=("$@")
|
||||
if [ "${#items[@]}" -eq 0 ]; then
|
||||
return 1
|
||||
fi
|
||||
if ! [ -t 0 ]; then
|
||||
printf '[!] Interactive picker needs a terminal.\n' >&2
|
||||
return 1
|
||||
fi
|
||||
local filter="" ans i n total=${#items[@]}
|
||||
local -a shown=() orig=()
|
||||
while true; do
|
||||
shown=()
|
||||
orig=()
|
||||
for ((i = 0; i < total; i++)); do
|
||||
if [ -z "$filter" ] || [[ "${items[$i],,}" == *"${filter,,}"* ]]; then
|
||||
shown+=("${items[$i]}")
|
||||
orig+=("$((i + 1))")
|
||||
fi
|
||||
done
|
||||
n=${#shown[@]}
|
||||
{
|
||||
echo
|
||||
if [ -n "$filter" ]; then
|
||||
printf -- "-- %d of %d match '%s' --\n" "$n" "$total" "$filter"
|
||||
else
|
||||
printf -- "-- %d available --\n" "$total"
|
||||
fi
|
||||
if [ "$n" -eq 0 ]; then
|
||||
printf '[!] no matches — enter nothing or / to clear the filter\n' >&2
|
||||
else
|
||||
for ((i = 0; i < n; i++)); do
|
||||
printf ' %2d) %s\n' $((i + 1)) "${shown[$i]}"
|
||||
done
|
||||
fi
|
||||
} >&2
|
||||
if ! read -rp "${prompt} [1-${n}], text=filter, 0=back " ans; then
|
||||
return 1 # EOF — cancel
|
||||
fi
|
||||
case "$ans" in
|
||||
"") [ -z "$filter" ] || filter="" ; continue ;;
|
||||
"/") filter="" ; continue ;;
|
||||
0 | q | Q | b | B) return 1 ;;
|
||||
*[!0-9]*)
|
||||
filter="$ans"
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
if (( ans >= 1 && ans <= n )); then
|
||||
echo "${orig[$((ans - 1))]}"
|
||||
return 0
|
||||
fi
|
||||
echo "Unknown choice." >&2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ── Prompted value with optional default ───────────────────────
|
||||
# Prints "<label> [<default>]: " (read -p sends prompts to stderr) and echoes
|
||||
# the entered value or the default when the answer is empty.
|
||||
# rc 0 value on stdout · rc 1 EOF, or empty answer with no default.
|
||||
menu_ask_value() {
|
||||
local label="$1" def="${2:-}" val pr="$1"
|
||||
[ -n "$def" ] && pr="$pr [$def]"
|
||||
if ! read -rp "${pr}: " val; then
|
||||
return 1 # EOF — cancel
|
||||
fi
|
||||
if [ -z "$val" ]; then
|
||||
[ -n "$def" ] || return 1
|
||||
echo "$def"
|
||||
return 0
|
||||
fi
|
||||
echo "$val"
|
||||
}
|
||||
Reference in New Issue
Block a user