Compare commits

...
3 Commits
Author SHA1 Message Date
Felitendo a17aed4b3d Lower the battery threshold to 30% and default the cachy-update prompt to yes
The prompt offering to silence cachy-update's own update notification now
defaults to yes: once updates install themselves that notification is nothing
but noise, so the common answer should be the one you get by pressing Enter.

It also accepts "j" now. The prompt is translated, so a German user reads
"[J/n]" and types the German letter - which the old check for "y" alone
silently read as a refusal.
2026-08-08 16:30:20 +02:00
Felitendo 2fa830284e Make the settings screen redraw 50x faster
Arrow-key navigation took 435 ms per keypress, which reads as the whole console
reloading on every press - because it effectively was. The cost was forks: each
frame ran a command substitution per label for the value, the translation and
the rendered state, 54 subshells for eighteen rows.

The frame is now assembled in memory and written once. Translations, the split
specs and the terminfo clear string are resolved before the loop; values are
re-read only after something actually changes, not on cursor movement. Helpers
on that path assign to a variable instead of printing, since printing is what
forced the substitution.

Measured on the same machine: 435 ms -> 7.5 ms per keypress.
2026-08-08 16:25:09 +02:00
Felitendo c308b7e286 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.
2026-08-08 16:18:59 +02:00
10 changed files with 583 additions and 42 deletions

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
# Overridable so a packager can pass the version it is actually building # Overridable so a packager can pass the version it is actually building
# (`make VERSION=$pkgver`). The literal below is the fallback for builds # (`make VERSION=$pkgver`). The literal below is the fallback for builds
# straight from a checkout, and is what a release tag has to carry. # straight from a checkout, and is what a release tag has to carry.
VERSION ?= 1.0.9 VERSION ?= 1.1.2
PREFIX ?= /usr PREFIX ?= /usr
DESTDIR ?= DESTDIR ?=
+6 -1
View File
@@ -30,9 +30,14 @@ That opens a menu with the two switches there are:
[3] Update now [3] Update now
[4] Show log [4] Show log
[5] Show current conditions [5] Show current conditions
[6] Settings
[q] Quit [q] Quit
``` ```
Everything is configurable from **[6] Settings** — a cursor list covering all
eighteen options, so nothing needs a text editor. Arrow keys select, Space or
Right changes a value, `q` goes back; changes are written immediately.
The interface is fully translated; on a German system everything above appears The interface is fully translated; on a German system everything above appears
in German. in German.
@@ -69,7 +74,7 @@ deliberately.
A run is postponed — and retried an hour later — when: A run is postponed — and retried an hour later — when:
- the battery is below 40 % (ignored on mains power; desktops without a battery - the battery is below 30 % (ignored on mains power; desktops without a battery
are never affected), are never affected),
- a game is running: GameMode, a known game process, or anything holding a - a game is running: GameMode, a known game process, or anything holding a
blocking idle inhibitor, blocking idle inhibitor,
+4 -1
View File
@@ -16,7 +16,10 @@ updated in the background, with no password prompt and no terminal.
Run without a command it opens a small interactive menu with the two switches Run without a command it opens a small interactive menu with the two switches
that matter - automatic updates on/off and notifications on/off - plus the that matter - automatic updates on/off and notifications on/off - plus the
current status. current status, and a settings screen covering every remaining option so the
configuration file never has to be edited by hand. In the settings screen the
arrow keys select, Space or Right changes a value, and _q_ goes back; every
change is written out immediately.
The actual work is done by a systemd system service. The timer ticks hourly; The actual work is done by a systemd system service. The timer ticks hourly;
whether a tick does anything is decided by _UpdateInterval_ (daily by default). whether a tick does anything is decided by _UpdateInterval_ (daily by default).
+67 -1
View File
@@ -168,7 +168,7 @@ msgstr ""
msgid "base-devel is missing - AUR packages cannot be built without it." msgid "base-devel is missing - AUR packages cannot be built without it."
msgstr "" msgstr ""
msgid "cachy-update also notifies about available updates. Turn its notifications off? [y/N]" msgid "cachy-update also notifies about available updates. Turn its notifications off? [Y/n]"
msgstr "" msgstr ""
msgid "cachy-update's update check has been disabled." msgid "cachy-update's update check has been disabled."
@@ -269,3 +269,69 @@ msgstr ""
msgid "%d packages are being updated. Please leave the computer switched on until this is done." msgid "%d packages are being updated. Please leave the computer switched on until this is done."
msgstr "" msgstr ""
msgid "Settings"
msgstr ""
msgid "(none)"
msgstr ""
msgid "Up/Down select - Space or Right changes - q goes back"
msgstr ""
msgid "Package names separated by spaces, empty to clear:"
msgstr ""
msgid "Notify when an update starts"
msgstr ""
msgid "Notify after a successful update"
msgstr ""
msgid "Notify when something goes wrong"
msgstr ""
msgid "Notify when a restart is needed"
msgstr ""
msgid "Time between update runs"
msgstr ""
msgid "Postpone while a game is running"
msgstr ""
msgid "Only update on mains power"
msgstr ""
msgid "Minimum battery level (%)"
msgstr ""
msgid "Update AUR packages"
msgstr ""
msgid "Update Flatpaks"
msgstr ""
msgid "Update AppImages"
msgstr ""
msgid "Also rebuild -git packages"
msgstr ""
msgid "AUR helper"
msgstr ""
msgid "Resolve package conflicts automatically"
msgstr ""
msgid "Trim the package cache"
msgstr ""
msgid "Cached versions to keep"
msgstr ""
msgid "Remove packages nothing needs any more"
msgstr ""
msgid "Never update these packages"
msgstr ""
+68 -2
View File
@@ -169,8 +169,8 @@ msgstr "Kein AUR-Helper gefunden – für AUR-Updates paru oder yay installieren
msgid "base-devel is missing - AUR packages cannot be built without it." msgid "base-devel is missing - AUR packages cannot be built without it."
msgstr "base-devel fehlt – ohne das lassen sich AUR-Pakete nicht bauen." msgstr "base-devel fehlt – ohne das lassen sich AUR-Pakete nicht bauen."
msgid "cachy-update also notifies about available updates. Turn its notifications off? [y/N]" msgid "cachy-update also notifies about available updates. Turn its notifications off? [Y/n]"
msgstr "cachy-update benachrichtigt ebenfalls über verfügbare Updates. Dessen Benachrichtigungen abschalten? [j/N]" msgstr "cachy-update benachrichtigt ebenfalls über verfügbare Updates. Dessen Benachrichtigungen abschalten? [J/n]"
msgid "cachy-update's update check has been disabled." msgid "cachy-update's update check has been disabled."
msgstr "Die Update-Prüfung von cachy-update wurde abgeschaltet." msgstr "Die Update-Prüfung von cachy-update wurde abgeschaltet."
@@ -271,3 +271,69 @@ msgstr "Updates werden installiert"
#, c-format #, c-format
msgid "%d packages are being updated. Please leave the computer switched on until this is done." msgid "%d packages are being updated. Please leave the computer switched on until this is done."
msgstr "%d Pakete werden gerade aktualisiert. Bitte den Rechner so lange eingeschaltet lassen." msgstr "%d Pakete werden gerade aktualisiert. Bitte den Rechner so lange eingeschaltet lassen."
msgid "Settings"
msgstr "Einstellungen"
msgid "(none)"
msgstr "(keine)"
msgid "Up/Down select - Space or Right changes - q goes back"
msgstr "Hoch/Runter wählen – Leertaste oder Rechts ändert – q zurück"
msgid "Package names separated by spaces, empty to clear:"
msgstr "Paketnamen mit Leerzeichen getrennt, leer zum Löschen:"
msgid "Notify when an update starts"
msgstr "Melden, wenn ein Update beginnt"
msgid "Notify after a successful update"
msgstr "Melden nach erfolgreichem Update"
msgid "Notify when something goes wrong"
msgstr "Melden, wenn etwas schiefgeht"
msgid "Notify when a restart is needed"
msgstr "Melden, wenn ein Neustart nötig ist"
msgid "Time between update runs"
msgstr "Abstand zwischen Update-Läufen"
msgid "Postpone while a game is running"
msgstr "Verschieben, solange ein Spiel läuft"
msgid "Only update on mains power"
msgstr "Nur am Stromnetz aktualisieren"
msgid "Minimum battery level (%)"
msgstr "Mindest-Akkustand (%)"
msgid "Update AUR packages"
msgstr "AUR-Pakete aktualisieren"
msgid "Update Flatpaks"
msgstr "Flatpaks aktualisieren"
msgid "Update AppImages"
msgstr "AppImages aktualisieren"
msgid "Also rebuild -git packages"
msgstr "Auch -git-Pakete neu bauen"
msgid "AUR helper"
msgstr "AUR-Helfer"
msgid "Resolve package conflicts automatically"
msgstr "Paketkonflikte automatisch auflösen"
msgid "Trim the package cache"
msgstr "Paket-Cache beschneiden"
msgid "Cached versions to keep"
msgstr "Behaltene Versionen im Cache"
msgid "Remove packages nothing needs any more"
msgstr "Nicht mehr benötigte Pakete entfernen"
msgid "Never update these packages"
msgstr "Diese Pakete nie aktualisieren"
+1 -1
View File
@@ -31,7 +31,7 @@ UpdateInterval=1d
# Never update while the battery is below this percentage. Ignored on mains # Never update while the battery is below this percentage. Ignored on mains
# power and on machines without a battery. # power and on machines without a battery.
MinBatteryPercent=40 MinBatteryPercent=30
# Update only while plugged in. Stricter than MinBatteryPercent. # Update only while plugged in. Stricter than MinBatteryPercent.
RequireAC=no RequireAC=no
+10 -2
View File
@@ -117,9 +117,17 @@ cau_offer_disable_cachy_update() {
(( found )) || return 0 (( found )) || return 0
printf '\n %s\n ' \ printf '\n %s\n ' \
"$(cau_msg "cachy-update also notifies about available updates. Turn its notifications off? [y/N]")" "$(cau_msg "cachy-update also notifies about available updates. Turn its notifications off? [Y/n]")"
read -r answer || return 0 read -r answer || return 0
[[ ${answer,,} == y ]] || return 0
# Defaults to yes: once updates install themselves, cachy-update's "N
# updates available" is purely noise. "j" is accepted too - the prompt is
# translated, so a German user types the German letter and used to have
# that silently read as "no".
case "${answer,,}" in
''|y|yes|j|ja) ;;
*) return 0 ;;
esac
while read -r user uid; do while read -r user uid; do
[[ -n $user ]] || continue [[ -n $user ]] || continue
+45 -3
View File
@@ -75,13 +75,55 @@ cau_msg() {
cau_msg_in "$(cau_ui_locale)" "$@" cau_msg_in "$(cau_ui_locale)" "$@"
} }
# cau_msg_into <locale> <msgid>
# Plain lookup with the result in CAU_MSG_RESULT and no printf formatting.
# For callers that redraw many labels per keypress, where wrapping cau_msg in a
# command substitution would cost a fork per label.
CAU_MSG_RESULT=''
cau_msg_into() {
local locale="$1" msgid="$2" cachekey
cachekey="${locale}"$'\x1f'"${msgid}"
if [[ -n ${CAU_MSG_CACHE[$cachekey]+set} ]]; then
CAU_MSG_RESULT="${CAU_MSG_CACHE[$cachekey]}"
return 0
fi
CAU_MSG_RESULT="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)"
[[ -n $CAU_MSG_RESULT ]] || CAU_MSG_RESULT="$msgid"
CAU_MSG_CACHE[$cachekey]="$CAU_MSG_RESULT"
return 0
}
# 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 <locale> <msgid> [printf args...]
cau_msg_in() { cau_msg_in() {
local locale="$1" msgid="$2" translated local locale="$1" msgid="$2" translated cachekey
shift 2 shift 2
translated="$(LC_ALL="$locale" LANGUAGE="${locale%%.*}" gettext -- "$msgid" 2>/dev/null)" cachekey="${locale}"$'\x1f'"${msgid}"
[[ -n $translated ]] || translated="$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 # shellcheck disable=SC2059 # the format string is the translated message
printf -- "$translated" "$@" printf -- "$translated" "$@"
+41 -12
View File
@@ -6,14 +6,39 @@
# root-run daemon, and sourcing it would turn a stray line into arbitrary code # root-run daemon, and sourcing it would turn a stray line into arbitrary code
# execution. The format is one "Key=Value" per line, '#' starts a comment. # execution. The format is one "Key=Value" per line, '#' starts a comment.
# cau_config_get <Key> [default] # The file is cached and parsed in-process rather than shelled out to sed on
cau_config_get() { # every lookup. The settings screen reads every key on every redraw, and a fork
local key="$1" default="${2:-}" val # per key made the redraw slow enough to swallow keystrokes.
CAU_CONFIG_CACHE=''
CAU_CONFIG_CACHED=0
[[ -r $CAU_CONFIG ]] || { printf '%s\n' "$default"; return; } _cau_config_slurp() {
(( CAU_CONFIG_CACHED )) && return 0
CAU_CONFIG_CACHE=''
[[ -r $CAU_CONFIG ]] && CAU_CONFIG_CACHE="$(< "$CAU_CONFIG")"
CAU_CONFIG_CACHED=1
return 0
}
val="$(sed -nE "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*(.*)$/\\1/p" \ # _cau_config_lookup <Key> [default]
"$CAU_CONFIG" 2>/dev/null | tail -n1)" # Result in CAU_CONFIG_VALUE. Assigning rather than printing matters on the
# settings screen, which reads every key on every frame: a command substitution
# there is a fork, and forks were the entire cost of a redraw.
CAU_CONFIG_VALUE=''
_cau_config_lookup() {
local key="$1" default="${2:-}" val='' line
CAU_CONFIG_VALUE="$default"
[[ -r $CAU_CONFIG ]] || return 0
_cau_config_slurp
# last assignment wins, matching the previous sed|tail behaviour
while IFS= read -r line; do
[[ $line == *"$key"* ]] || continue
[[ $line =~ ^[[:space:]]*"$key"[[:space:]]*=(.*)$ ]] || continue
val="${BASH_REMATCH[1]}"
done <<< "$CAU_CONFIG_CACHE"
# strip a trailing comment and surrounding whitespace/quotes # strip a trailing comment and surrounding whitespace/quotes
val="${val%%#*}" val="${val%%#*}"
@@ -22,11 +47,14 @@ cau_config_get() {
val="${val%\"}" val="${val%\"}"
val="${val#\"}" val="${val#\"}"
if [[ -n $val ]]; then [[ -n $val ]] && CAU_CONFIG_VALUE="$val"
printf '%s\n' "$val" return 0
else }
printf '%s\n' "$default"
fi # cau_config_get <Key> [default]
cau_config_get() {
_cau_config_lookup "$@"
printf '%s\n' "$CAU_CONFIG_VALUE"
} }
# cau_config_bool <Key> <default: yes|no> # cau_config_bool <Key> <default: yes|no>
@@ -78,6 +106,7 @@ cau_config_set() {
fi fi
mv -f "$tmp" "$CAU_CONFIG" mv -f "$tmp" "$CAU_CONFIG"
CAU_CONFIG_CACHED=0
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -105,7 +134,7 @@ cau_config_load() {
CFG_INTERVAL="$(cau_config_get UpdateInterval 1d)" CFG_INTERVAL="$(cau_config_get UpdateInterval 1d)"
CFG_INTERVAL_SECONDS="$(cau_duration_to_seconds "$CFG_INTERVAL" 86400)" CFG_INTERVAL_SECONDS="$(cau_duration_to_seconds "$CFG_INTERVAL" 86400)"
CFG_MIN_BATTERY="$(cau_config_int MinBatteryPercent 40)" CFG_MIN_BATTERY="$(cau_config_int MinBatteryPercent 30)"
CFG_KEEP_OLD="$(cau_config_int KeepOldPackages 3)" CFG_KEEP_OLD="$(cau_config_int KeepOldPackages 3)"
CFG_AUR_HELPER="$(cau_config_get AURHelper auto)" CFG_AUR_HELPER="$(cau_config_get AURHelper auto)"
CFG_IGNORE_PKG="$(cau_config_get IgnorePkg '')" CFG_IGNORE_PKG="$(cau_config_get IgnorePkg '')"
+340 -18
View File
@@ -7,6 +7,317 @@
CAU_UNIT="cachy-auto-update.timer" CAU_UNIT="cachy-auto-update.timer"
# 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 vanished.
# Holding non-canonical mode for the whole interface removes the gap.
CAU_TERM_SAVED=''
cau_ui_term_raw() {
cau_have stty || return 0
[[ -t 0 ]] || return 0
[[ -n $CAU_TERM_SAVED ]] && return 0
CAU_TERM_SAVED="$(stty -g 2>/dev/null)" || { CAU_TERM_SAVED=''; return 0; }
stty -icanon -echo min 1 time 0 2>/dev/null || true
}
cau_ui_term_restore() {
[[ -n $CAU_TERM_SAVED ]] || return 0
stty "$CAU_TERM_SAVED" 2>/dev/null || true
CAU_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.
cau_ui_cooked() {
cau_ui_term_restore
"$@"
local rc=$?
cau_ui_term_raw
return $rc
}
# cau_read_key
# One keypress, resolved to a symbolic name: a literal character, or one of
# up/down/left/right/enter/space/escape. Arrow keys arrive as ESC [ A, so the
# tail of the sequence is consumed here rather than being mistaken for three
# separate presses.
cau_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
;;
# Enter is an empty read in cooked mode and a carriage return in raw
# mode, depending on whether the terminal is translating it.
''|$'\r') printf 'enter\n' ;;
$'\x7f'|$'\b') printf 'backspace\n' ;;
' ') printf 'space\n' ;;
*) printf '%s\n' "$k" ;;
esac
}
# cau_ui_read_line <initial>
# A minimal line editor built on cau_read_key, with the result in
# CAU_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, reproducibly, on a real pty.
# Never leaving single-character mode side-steps that completely.
CAU_LINE_RESULT=''
cau_ui_read_line() {
local buf="${1:-}" key
CAU_LINE_RESULT=''
printf '%s' "$buf"
while true; do
key="$(cau_read_key)" || { printf '\n'; return 1; }
case "$key" in
enter)
printf '\n'
CAU_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) ;; # no cursor movement in this editor
*)
# a single printable character; control keys arrive as names
[[ ${#key} -eq 1 ]] || continue
buf+="$key"
printf '%s' "$key"
;;
esac
done
}
# Everything that can be changed without opening a text editor.
# Format: Key|type|default|label-msgid
# type is bool, choice:<space separated values>, or text.
CAU_SETTINGS=(
"NotifyOnStart|bool|yes|Notify when an update starts"
"NotifyOnSuccess|bool|yes|Notify after a successful update"
"NotifyOnError|bool|yes|Notify when something goes wrong"
"NotifyReboot|bool|yes|Notify when a restart is needed"
"UpdateInterval|choice:6h 12h 1d 2d 1w|1d|Time between update runs"
"SkipWhenGaming|bool|yes|Postpone while a game is running"
"RequireAC|bool|no|Only update on mains power"
"MinBatteryPercent|choice:0 20 30 40 50 60 70 80|30|Minimum battery level (%)"
"UpdateAUR|bool|yes|Update AUR packages"
"UpdateFlatpak|bool|yes|Update Flatpaks"
"UpdateAppImages|bool|yes|Update AppImages"
"UpdateDevel|bool|no|Also rebuild -git packages"
"AURHelper|choice:auto paru yay pikaur|auto|AUR helper"
"AutoResolveConflicts|bool|yes|Resolve package conflicts automatically"
"CleanCache|bool|yes|Trim the package cache"
"KeepOldPackages|choice:0 1 2 3 5|3|Cached versions to keep"
"RemoveOrphans|bool|no|Remove packages nothing needs any more"
"IgnorePkg|text||Never update these packages"
)
_cau_is_true() {
case "${1,,}" in
yes|y|true|1|on|enabled) return 0 ;;
*) return 1 ;;
esac
}
# _cau_setting_display <type> <value>
# Result in CAU_SETTING_SHOWN. The three constant strings are resolved once by
# the caller into CAU_LBL_*; looking them up here would put a translation call
# on the per-line path.
CAU_SETTING_SHOWN=''
CAU_LBL_ON=''
CAU_LBL_OFF=''
CAU_LBL_NONE=''
_cau_setting_display() {
local type="$1" value="$2"
case "$type" in
bool)
if _cau_is_true "$value"; then
CAU_SETTING_SHOWN="${CAU_C_GREEN}${CAU_LBL_ON}${CAU_C_RESET}"
else
CAU_SETTING_SHOWN="${CAU_C_DIM}${CAU_LBL_OFF}${CAU_C_RESET}"
fi
;;
text)
if [[ -n $value ]]; then
CAU_SETTING_SHOWN="$value"
else
CAU_SETTING_SHOWN="${CAU_C_DIM}${CAU_LBL_NONE}${CAU_C_RESET}"
fi
;;
*) CAU_SETTING_SHOWN="$value" ;;
esac
}
# _cau_setting_cycle <type> <value> <direction: 1|-1>
# The next value for this setting. Choices wrap around, so one key is enough to
# reach everything without needing a second one for the other direction.
_cau_setting_cycle() {
local type="$1" value="$2" dir="$3"
if [[ $type == bool ]]; then
_cau_is_true "$value" && printf 'no\n' || printf 'yes\n'
return
fi
local -a choices
read -r -a choices <<< "${type#choice:}"
(( ${#choices[@]} )) || { printf '%s\n' "$value"; return; }
local i idx=0
for i in "${!choices[@]}"; do
[[ ${choices[i]} == "$value" ]] && { idx=$i; break; }
done
idx=$(( (idx + dir + ${#choices[@]}) % ${#choices[@]} ))
printf '%s\n' "${choices[idx]}"
}
# cau_ui_settings
# A cursor list rather than a numbered menu: there are eighteen settings, and
# numbering them would run out of digits and force paging.
#
# The frame is assembled in memory and written once. Everything constant - the
# specs, the translated labels, the clear sequence - is resolved before the
# loop, and the values are re-read only after something actually changes.
# Drawing the naive way cost a command substitution per label per frame, which
# measured 435 ms per keypress: arrow keys felt like the console was reloading,
# because in effect it was.
cau_ui_settings() {
local count=${#CAU_SETTINGS[@]}
local -a names=() types=() defaults=() labels=()
local -a values=()
local spec name type default label locale i key frame row pad dirty=1 cursor=0
locale="$(cau_ui_locale)"
cau_msg_into "$locale" "ON"; CAU_LBL_ON="$CAU_MSG_RESULT"
cau_msg_into "$locale" "OFF"; CAU_LBL_OFF="$CAU_MSG_RESULT"
cau_msg_into "$locale" "(none)"; CAU_LBL_NONE="$CAU_MSG_RESULT"
for spec in "${CAU_SETTINGS[@]}"; do
IFS='|' read -r name type default label <<< "$spec"
names+=("$name"); types+=("$type"); defaults+=("$default")
cau_msg_into "$locale" "$label"
labels+=("$CAU_MSG_RESULT")
done
local title hint
cau_msg_into "$locale" "Settings"; title="$CAU_MSG_RESULT"
cau_msg_into "$locale" "Up/Down select - Space or Right changes - q goes back"
hint="$CAU_MSG_RESULT"
# the terminfo clear string, fetched once instead of forking per frame
local clearseq
clearseq="$(clear 2>/dev/null)" || clearseq=$'\033[H\033[2J'
while true; do
if (( dirty )); then
for i in "${!names[@]}"; do
_cau_config_lookup "${names[i]}" "${defaults[i]}"
values[i]="$CAU_CONFIG_VALUE"
done
dirty=0
fi
frame="$clearseq"$'\n'"${CAU_C_BOLD}${CAU_C_BLUE} ${title}${CAU_C_RESET}"$'\n\n'
local marker selected="${CAU_C_BLUE}▸${CAU_C_RESET} "
for i in "${!names[@]}"; do
_cau_setting_display "${types[i]}" "${values[i]}"
pad=$(( 42 - ${#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" '' "$CAU_SETTING_SHOWN"
frame+="$row"$'\n'
done
frame+=$'\n'" ${CAU_C_DIM}${hint}${CAU_C_RESET}"$'\n'
printf '%s' "$frame"
key="$(cau_read_key)" || return 0
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)
if [[ $type == text ]]; then
cau_ui_edit_text "$name" "${values[cursor]}"
else
cau_config_set "$name" "$(_cau_setting_cycle "$type" "${values[cursor]}" 1)" \
|| { cau_bad "$(cau_msg "Could not write the configuration file.")"; cau_pause; }
fi
dirty=1
;;
left|h)
if [[ $type != text ]]; then
cau_config_set "$name" "$(_cau_setting_cycle "$type" "${values[cursor]}" -1)" \
|| { cau_bad "$(cau_msg "Could not write the configuration file.")"; cau_pause; }
dirty=1
fi
;;
q|Q|escape) return 0 ;;
*) ;;
esac
done
}
# cau_ui_edit_text <key> <current>
# The one setting that is a free-text list rather than a choice.
cau_ui_edit_text() {
local name="$1" current="$2"
printf '\n %s\n' "$(cau_msg "Package names separated by spaces, empty to clear:")"
printf ' > '
# pre-filled with the current value so it can be corrected rather than
# retyped; Escape leaves it unchanged
if cau_ui_read_line "$current"; then
cau_config_set "$name" "$CAU_LINE_RESULT" \
|| { cau_bad "$(cau_msg "Could not write the configuration file.")"; cau_pause; }
fi
}
# _cau_row <label> <value> # _cau_row <label> <value>
# printf's %-28s pads by bytes, so a label containing "ü" comes out one column # 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 # short. ${#s} counts characters in a UTF-8 locale, so the padding is computed
@@ -141,6 +452,10 @@ cau_ui_status_conditions() {
cau_ui_menu() { cau_ui_menu() {
local choice local choice
cau_ui_term_raw
# restore the terminal even if this exits through Ctrl-C or an error
trap 'cau_ui_term_restore' EXIT INT TERM
while true; do while true; do
cau_config_load cau_config_load
@@ -153,45 +468,52 @@ cau_ui_menu() {
printf ' [3] %s\n' "$(cau_msg "Update now")" printf ' [3] %s\n' "$(cau_msg "Update now")"
printf ' [4] %s\n' "$(cau_msg "Show log")" printf ' [4] %s\n' "$(cau_msg "Show log")"
printf ' [5] %s\n' "$(cau_msg "Show current conditions")" printf ' [5] %s\n' "$(cau_msg "Show current conditions")"
printf ' [6] %s\n' "$(cau_msg "Settings")"
printf ' [q] %s\n' "$(cau_msg "Quit")" printf ' [q] %s\n' "$(cau_msg "Quit")"
printf '\n > ' printf '\n > '
# One keypress, no Enter. -s keeps the raw character out of the # One keypress, no Enter. A failing read means EOF (Ctrl-D, or a
# display so the echo below is the only thing printed, and a failing # script piping input), which quits.
# read means EOF (Ctrl-D, or a script piping input) - that quits. choice="$(cau_read_key)" || {
read -rsn1 choice || { printf '\n'; return 0; } printf '\n'; cau_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" printf '%s\n' "$choice"
# Actions run in normal line mode: they print program output and some
# of them prompt, neither of which behaves in raw mode.
case "$choice" in case "$choice" in
# The two switches flip and return straight to the menu, where the # The two switches flip and return straight to the menu, where the
# status block shows the result. Only a warning or an error holds # status block shows the result. Only a warning or an error holds
# the screen (see CAU_UI_NEEDS_ACK). # the screen (see CAU_UI_NEEDS_ACK).
1) 1)
CAU_UI_NEEDS_ACK='' CAU_UI_NEEDS_ACK=''
if [[ $CFG_ENABLED == yes ]]; then cau_do_disable; else cau_do_enable; fi if [[ $CFG_ENABLED == yes ]]; then
cau_ui_cooked cau_do_disable
else
cau_ui_cooked cau_do_enable
fi
if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi
;; ;;
2) 2)
CAU_UI_NEEDS_ACK='' CAU_UI_NEEDS_ACK=''
if [[ $CFG_NOTIFICATIONS == yes ]]; then if [[ $CFG_NOTIFICATIONS == yes ]]; then
cau_do_notifications off cau_ui_cooked cau_do_notifications off
else else
cau_do_notifications on cau_ui_cooked cau_do_notifications on
fi fi
if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi if [[ -n $CAU_UI_NEEDS_ACK ]]; then cau_pause; fi
;; ;;
3) cau_do_run --force; cau_pause ;; 3) cau_ui_cooked cau_do_run --force; cau_pause ;;
4) cau_do_log; cau_pause ;; 4) cau_ui_cooked cau_do_log; cau_pause ;;
5) cau_ui_status_conditions; cau_pause ;; 5) cau_ui_status_conditions; cau_pause ;;
q|Q) return 0 ;; 6) cau_ui_settings ;;
$'\e') q|Q) cau_ui_term_restore; trap - EXIT INT TERM; return 0 ;;
# Arrow keys and friends arrive as ESC [ X. Swallow the rest so # Anything else - Enter, arrow keys, stray characters - just
# one keypress does not redraw the menu three times. Escape is # redraws. Escape is deliberately not a quit key, so a mistyped
# deliberately not a quit key: that would make a stray arrow # arrow key cannot close the menu.
# key close the menu.
read -rsn2 -t 0.05 _ 2>/dev/null || true
;;
# Anything else, Enter included, just redraws.
*) ;; *) ;;
esac esac
done done