Add a settings screen so nothing needs a text editor

All eighteen options are now reachable from the menu as a cursor list: arrows
select, Space or Right cycles a value, q goes back, changes are written
immediately. A numbered menu would have run out of digits.

Three real bugs surfaced while building it, each found by testing against an
actual pty rather than a pipe:

- Labels went through printf as format strings, so the percent sign in
  "Minimum battery level (%)" was an invalid conversion. cau_msg_in now only
  treats a message as a format string when arguments were actually passed -
  otherwise any literal % a translator writes is a trap.

- Mixing bash's line-mode read into a single-key interface left the following
  read -sn1 receiving nothing at all, reproducibly, so the screen froze after
  editing the package list. Replaced with a small line editor built on the same
  single-character reader.

- Backspace was being swallowed: in canonical mode DEL is the ERASE character
  and the line discipline consumes it, and bash returns to canonical mode
  between each read -sn1. The interface now holds non-canonical mode for its
  whole lifetime and hands the terminal back only around actions that print or
  prompt, with a trap restoring it on Ctrl-C.

Translations and config reads are memoized; the screen redraws every label on
every keypress and a fork per lookup was the reason the redraw was slow enough
to matter.
This commit is contained in:
Felitendo committed 2026-08-08 16:18:59 +02:00
1 parent 2eff62f9fd
commit c308b7e286
8 files changed
+491 -26

No files matched your search

+24 -3
View File
@@ -75,13 +75,34 @@ cau_msg() {
cau_msg_in "$(cau_ui_locale)" "$@"
}
# Translations are memoized. Every gettext lookup is a fork, and the settings
# screen redraws forty-odd labels per keypress; without this the redraw takes
# long enough that a keystroke arriving during it is lost when the terminal
# switches back to single-character mode.
declare -A CAU_MSG_CACHE=()
# cau_msg_in <locale> <msgid> [printf args...]
cau_msg_in() {
local locale="$1" msgid="$2" translated
local locale="$1" msgid="$2" translated cachekey
shift 2
translated="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
[[ -n $translated ]] || translated="$msgid"
cachekey="${locale}"$'\x1f'"${msgid}"
if [[ -n ${CAU_MSG_CACHE[$cachekey]+set} ]]; then
translated="${CAU_MSG_CACHE[$cachekey]}"
else
translated="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
[[ -n $translated ]] || translated="$msgid"
CAU_MSG_CACHE[$cachekey]="$translated"
fi
# With no arguments the message is plain text, not a format string. Feeding
# it to printf anyway turns any literal percent sign in it - "Battery (%)",
# "100 % done" - into an invalid conversion, and that is a trap every
# translator would eventually walk into.
if (( $# == 0 )); then
printf '%s' "$translated"
return
fi
# shellcheck disable=SC2059 # the format string is the translated message
printf -- "$translated" "$@"