fix(config-toggle): read keys via raw tty, fixing arrows/Tab/redraw/prompt

fish's `read` invokes its interactive line editor on a TTY, which (1)
prints a `read> ` prompt below the panel, (2) intercepts Tab and arrow
keys for its own line editing so they never reach the switch, and (3)
shifts the cursor down a line, throwing off the `\e[14A` redraw so top
borders stacked on every keypress. Plain keys (j/k/q/space) passed
through, masking the problem.

Add __config_toggle_read_key: puts the terminal in raw, no-echo mode
(`stty raw -echo min 1 time 1`), reads one keypress from /dev/tty, and
returns a normalized token (up/down/left/right/tab/space/escape/quit or
the literal char) by decoding the bytes via od. Arrow keys and Tab now
work, there is no stray prompt, and the redraw stays aligned. Ctrl-C in
raw mode arrives as byte 3 and maps to quit; bare Esc exits after the
0.1s inter-byte timer.

Rewrite the event loop to consume these tokens and restore Esc to the
help text.
This commit is contained in:
2026-06-11 03:03:32 -04:00
parent 5a018fe975
commit 608b0227cb
2 changed files with 103 additions and 33 deletions
+14 -33
View File
@@ -46,7 +46,7 @@ function config-toggle --description 'Interactive TUI for toggling opinionated c
echo " $c_flag↑ ↓$c_reset or $c_flag""j k$c_reset Move cursor up / down"
echo " $c_flag""Space$c_reset Cycle: ON → OFF → DEFAULT → ON"
echo " $c_flag""Tab$c_reset Switch scope (Universal ↔ Session)"
echo " $c_flag""q$c_reset / $c_flag""Q$c_reset Exit"
echo " $c_flag""q$c_reset / $c_flag""Esc$c_reset Exit"
echo
echo "$c_head""Scopes:$c_reset"
echo " $c_flag""Universal$c_reset Persistent across all sessions ($c_dim""set -U$c_reset)"
@@ -81,52 +81,33 @@ function config-toggle --description 'Interactive TUI for toggling opinionated c
__config_toggle_draw $cur_row $cur_scope $vars
# ── Event loop ────────────────────────────────────────
# read -s -n 1 reads exactly one char without echoing it. Arrow keys send
# ESC+[+letter as a 3-byte burst — after reading the ESC we immediately
# read 2 more bytes which are already in the terminal buffer. Single-char
# keys ('j','k',' ','q', Tab) are returned right away with no extra
# blocking. NOTE: the nchars flag is -n; -k is not a valid read option.
# __config_toggle_read_key reads a single keypress from /dev/tty in raw
# mode and returns a normalized token (up/down/tab/space/escape/quit or a
# literal char). It bypasses fish's `read`, whose line editor swallows Tab
# and arrow keys and prints a `read> ` prompt — unusable for a TUI.
while true
# Check for Ctrl-C signal (trap sets this flag)
# Check for Ctrl-C signal (trap sets this flag during redraw, when the
# terminal is briefly back in cooked mode and SIGINT can fire).
if set -q __config_toggle_exit
set -eg __config_toggle_exit
break
end
# -n 1 reads exactly one char; -s suppresses echo so keystrokes do
# not garble the panel. If read fails (EOF / non-tty stdin) bail out
# instead of spinning the redraw loop.
read -s -n 1 -l key
or break
# Detect CSI escape sequences (arrow keys: ESC [ A/B)
if test "$key" = \e
read -s -n 1 -l key2
if test "$key2" = "["
read -s -n 1 -l key3
set key "$key$key2$key3" # \e[A or \e[B
else
# ESC + non-bracket (ALT+key or plain ESC): treat key2 as key
set key $key2
end
end
set -l key (__config_toggle_read_key)
or break # not a TTY — exit instead of spinning
switch $key
case \e\[A # Up arrow
case up k
set cur_row (math "max(0, $cur_row - 1)")
case \e\[B # Down arrow
case down j
set cur_row (math "min(6, $cur_row + 1)")
case k
set cur_row (math "max(0, $cur_row - 1)")
case j
set cur_row (math "min(6, $cur_row + 1)")
case \t # Tab — switch scope
case tab # Tab — switch scope
if test $cur_scope = universal
set cur_scope session
else
set cur_scope universal
end
case ' ' # Space — cycle and apply immediately
case space # Space — cycle and apply immediately
set -l varname $vars[(math $cur_row + 1)]
set -l cur_val (__config_toggle_get_val $varname $cur_scope)
set -l next_val ""
@@ -162,7 +143,7 @@ function config-toggle --description 'Interactive TUI for toggling opinionated c
set -eg $varname
end
end
case q Q
case q Q quit escape
break
end