From 608b0227cb0dc9169e78eb4bd1ee9131687e3fa5 Mon Sep 17 00:00:00 2001 From: rootiest Date: Thu, 11 Jun 2026 03:03:32 -0400 Subject: [PATCH] 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. --- functions/__config_toggle_read_key.fish | 89 +++++++++++++++++++++++++ functions/config-toggle.fish | 47 ++++--------- 2 files changed, 103 insertions(+), 33 deletions(-) create mode 100644 functions/__config_toggle_read_key.fish diff --git a/functions/__config_toggle_read_key.fish b/functions/__config_toggle_read_key.fish new file mode 100644 index 0000000..1929836 --- /dev/null +++ b/functions/__config_toggle_read_key.fish @@ -0,0 +1,89 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# __config_toggle_read_key +# +# DESCRIPTION +# Reads a single keypress directly from the controlling terminal in raw +# mode and echoes a normalized token naming the key. Bypasses fish's +# `read` builtin, whose interactive line editor swallows Tab and arrow +# keys (and prints a `read> ` prompt) — none of which is usable for a TUI. +# +# The terminal is put into raw, no-echo mode with a 0.1s inter-byte timer +# (`stty raw -echo min 1 time 1`) so a multi-byte escape sequence (e.g. +# an arrow key, ESC [ A) is captured in one read while a lone key returns +# promptly. Original terminal settings are always restored before return. +# +# In raw mode Ctrl-C does not raise SIGINT; it arrives as byte 3 (ETX), +# which is reported as the token "quit". +# +# ARGUMENTS +# (none) +# +# RETURNS +# 0 A key was read; one of these tokens is printed to stdout: +# up down left right arrow keys +# space tab enter escape +# quit Ctrl-C (byte 3) in raw mode +# any other single printable character +# "" nothing decodable was read +# 1 The terminal could not be put into raw mode (stdin is not a TTY) +# +# EXAMPLE +# set -l key (__config_toggle_read_key) +# or return # not a TTY — bail +# switch $key +# case up; echo "moved up" +# case space; echo "toggled" +# end +function __config_toggle_read_key + # Snapshot current terminal settings; failure means stdin is not a TTY. + set -l saved (stty -g /dev/null) + or return 1 + + # Raw, no-echo. min 1 / time 1: block for the first byte, then wait up to + # 0.1s for the rest of a burst (escape sequence) before returning. + stty raw -echo min 1 time 1 /dev/null + + # One read() of up to 3 bytes — covers ESC [ A style sequences. od emits + # the bytes as space-separated decimal codes. + set -l codes (dd if=/dev/tty bs=3 count=1 2>/dev/null \ + | od -An -tu1 2>/dev/null | string trim | string split -n ' ') + + # Restore the terminal before doing anything else. + stty $saved /dev/null + + switch (string join ' ' $codes) + case '27 91 65' + echo up + case '27 91 66' + echo down + case '27 91 67' + echo right + case '27 91 68' + echo left + case 27 + echo escape + case 9 + echo tab + case 32 + echo space + case 10 13 + echo enter + case 3 + echo quit + case '' + echo '' + case '*' + # Single printable byte → emit its character; ignore stray + # multi-byte sequences we do not recognise. The two-step octal + # form avoids fish mangling a one-shot '\\%03o' format string. + if test (count $codes) -eq 1 + set -l oct (printf '%03o' $codes[1]) + printf '%b\n' "\\$oct" + else + echo '' + end + end +end diff --git a/functions/config-toggle.fish b/functions/config-toggle.fish index 28908e9..8d845bd 100644 --- a/functions/config-toggle.fish +++ b/functions/config-toggle.fish @@ -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