Add modular menu framework, migrate Sites & Page Timing onto it
Start of pulling the menu system out of the 12k-line single-file installer so individual menus can change without risking the rest of the script (network, VNC, addons, etc). This is groundwork for the planned web-based management UI, which will share the same lib/config.sh read/write layer instead of duplicating it. - lib/menu.sh: generic numbered-menu framework (auto-numbered entries, "0" always exits/returns) plus the validated input helpers menus need. - lib/config.sh: single load/save for config.json. Fixes a latent bug where the old Sites menu wrote config.json without first loading swipe/navigation/lockout settings, silently resetting them to defaults on save. - menus/sites.sh: Sites & Page Timing fully migrated - add/edit/delete/ reorder pages, set duration (auto-rotate/manual/hidden) and home page. Also fixes an off-by-one in the ported reorder logic (moving an item landed one slot short of the requested position) caught by testing. - install.sh: new entry point for managing an already-installed kiosk via `git clone` + `./install.sh`, wired to the Sites menu. Does not yet replace first-time provisioning, which still uses the existing single-file installer. All new site CRUD/reorder/home-page paths were exercised against a scratch config.json (add with/without basic auth, edit duration, set home + timeout, 2- and 3-item reorders in both directions, delete) to confirm the resulting config.json matches expectations.
This commit is contained in:
+240
@@ -0,0 +1,240 @@
|
||||
#!/bin/bash
|
||||
################################################################################
|
||||
# lib/menu.sh - Reusable numbered-menu framework + validated input helpers.
|
||||
#
|
||||
# Goal: menu *behavior* (numbering, "0 to exit/return", input validation)
|
||||
# lives here once. Individual menus/*.sh files only supply their content
|
||||
# (labels + handler functions) and never re-implement the loop/echo/case
|
||||
# boilerplate that made the old single-file installer hard to change safely.
|
||||
#
|
||||
# Usage:
|
||||
# my_menu_builder() {
|
||||
# MENU_LABELS=("Do thing A" "Do thing B")
|
||||
# MENU_HANDLERS=(action_a action_b)
|
||||
# }
|
||||
# run_menu "MY MENU TITLE" my_menu_builder [my_status_func]
|
||||
#
|
||||
# The builder runs fresh on every redraw, so labels/handlers can change
|
||||
# based on current state (e.g. "no sites yet" vs "5 sites configured").
|
||||
################################################################################
|
||||
|
||||
################################################################################
|
||||
# Logging
|
||||
################################################################################
|
||||
|
||||
log_info() {
|
||||
echo "[INFO] $*"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "[ERROR] $*" >&2
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo "✓ $*"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo "⚠ $*"
|
||||
}
|
||||
|
||||
pause() {
|
||||
read -r -p "Press Enter to continue..."
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Validated input helpers
|
||||
################################################################################
|
||||
|
||||
validate_yes_no() {
|
||||
local answer="$1"
|
||||
case "${answer,,}" in
|
||||
y|yes|yeah|yep|yup|sure|ok|okay) return 0 ;;
|
||||
n|no|nope|nah) return 1 ;;
|
||||
*) return 2 ;; # invalid
|
||||
esac
|
||||
}
|
||||
|
||||
ask_yes_no() {
|
||||
local prompt="$1"
|
||||
local default="${2:-n}"
|
||||
local answer
|
||||
|
||||
while true; do
|
||||
read -r -p "$prompt (y/n) [$default]: " answer
|
||||
answer="${answer:-$default}"
|
||||
|
||||
validate_yes_no "$answer"
|
||||
local result=$?
|
||||
|
||||
if [[ $result -eq 0 ]]; then
|
||||
return 0
|
||||
elif [[ $result -eq 1 ]]; then
|
||||
return 1
|
||||
else
|
||||
echo "❌ Invalid input. Please enter 'y' for yes or 'n' for no"
|
||||
echo
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
validate_integer() {
|
||||
local value="$1"
|
||||
local min="${2:--2147483648}"
|
||||
local max="${3:-2147483647}"
|
||||
|
||||
if [[ $value =~ ^-?[0-9]+$ ]]; then
|
||||
if [[ $value -ge $min && $value -le $max ]]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
ask_integer() {
|
||||
local prompt="$1"
|
||||
local default="$2"
|
||||
local min="${3:--2147483648}"
|
||||
local max="${4:-2147483647}"
|
||||
local value
|
||||
|
||||
while true; do
|
||||
read -r -p "$prompt [$default]: " value
|
||||
value="${value:-$default}"
|
||||
|
||||
if validate_integer "$value" "$min" "$max"; then
|
||||
echo "$value"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Invalid number. Please enter an integer between $min and $max" >&2
|
||||
echo >&2
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
validate_url() {
|
||||
local url="$1"
|
||||
if [[ $url =~ ^(https?|file|data)://.*$ ]] || [[ $url =~ ^about: ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
ask_url() {
|
||||
local prompt="$1"
|
||||
local default="$2"
|
||||
local url
|
||||
|
||||
while true; do
|
||||
read -r -p "$prompt [$default]: " url
|
||||
url="${url:-$default}"
|
||||
|
||||
if validate_url "$url"; then
|
||||
echo "$url"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Invalid URL. Must start with http://, https://, file://, data:, or about:" >&2
|
||||
echo >&2
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
ask_text() {
|
||||
local prompt="$1"
|
||||
local default="${2:-}"
|
||||
local value
|
||||
|
||||
read -r -p "$prompt [$default]: " value
|
||||
echo "${value:-$default}"
|
||||
}
|
||||
|
||||
validate_menu_choice() {
|
||||
local choice="$1"
|
||||
local max="$2"
|
||||
validate_integer "$choice" 0 "$max"
|
||||
}
|
||||
|
||||
ask_menu_choice() {
|
||||
local max="$1"
|
||||
local choice
|
||||
|
||||
while true; do
|
||||
read -r -p "Choose [0-$max]: " choice
|
||||
|
||||
if validate_menu_choice "$choice" "$max"; then
|
||||
echo "$choice"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Invalid choice. Please enter a number between 0 and $max" >&2
|
||||
echo >&2
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Menu framework
|
||||
################################################################################
|
||||
|
||||
print_menu_header() {
|
||||
local title="$1"
|
||||
echo "══════════════════════════════════════════════════════════"
|
||||
printf " %s\n" "$title"
|
||||
echo "══════════════════════════════════════════════════════════"
|
||||
echo
|
||||
}
|
||||
|
||||
# run_menu TITLE BUILDER_FUNC [STATUS_FUNC] [EXIT_LABEL]
|
||||
#
|
||||
# BUILDER_FUNC must set the globals MENU_LABELS and MENU_HANDLERS (parallel
|
||||
# indexed arrays). It is called once per redraw, so it can reflect current
|
||||
# state. STATUS_FUNC, if given, is called right after the header to print
|
||||
# read-only context (current settings, current list, etc).
|
||||
#
|
||||
# Entries are auto-numbered 1..N. "0" always returns from run_menu - no
|
||||
# menu file needs to hand-roll its own exit case.
|
||||
run_menu() {
|
||||
local title="$1"
|
||||
local builder="$2"
|
||||
local status_func="${3:-}"
|
||||
local exit_label="${4:-Return}"
|
||||
|
||||
while true; do
|
||||
clear
|
||||
print_menu_header "$title"
|
||||
|
||||
if [[ -n "$status_func" ]]; then
|
||||
"$status_func"
|
||||
echo
|
||||
fi
|
||||
|
||||
local -a MENU_LABELS=()
|
||||
local -a MENU_HANDLERS=()
|
||||
"$builder"
|
||||
|
||||
if [[ "${#MENU_LABELS[@]}" -eq 0 ]]; then
|
||||
log_warning "Nothing to do here yet."
|
||||
echo " 0. $exit_label"
|
||||
echo
|
||||
ask_menu_choice 0 >/dev/null
|
||||
return 0
|
||||
fi
|
||||
|
||||
local i=1
|
||||
for label in "${MENU_LABELS[@]}"; do
|
||||
printf " %2d. %s\n" "$i" "$label"
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo " 0. $exit_label"
|
||||
echo
|
||||
|
||||
local choice
|
||||
choice=$(ask_menu_choice "${#MENU_LABELS[@]}")
|
||||
|
||||
if [[ "$choice" == "0" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
"${MENU_HANDLERS[$((choice - 1))]}"
|
||||
done
|
||||
}
|
||||
Reference in New Issue
Block a user