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:
@@ -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
|
||||||
|
# <char> 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/tty 2>/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/tty 2>/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/tty 2>/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
|
||||||
@@ -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↑ ↓$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""Space$c_reset Cycle: ON → OFF → DEFAULT → ON"
|
||||||
echo " $c_flag""Tab$c_reset Switch scope (Universal ↔ Session)"
|
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
|
||||||
echo "$c_head""Scopes:$c_reset"
|
echo "$c_head""Scopes:$c_reset"
|
||||||
echo " $c_flag""Universal$c_reset Persistent across all sessions ($c_dim""set -U$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
|
__config_toggle_draw $cur_row $cur_scope $vars
|
||||||
|
|
||||||
# ── Event loop ────────────────────────────────────────
|
# ── Event loop ────────────────────────────────────────
|
||||||
# read -s -n 1 reads exactly one char without echoing it. Arrow keys send
|
# __config_toggle_read_key reads a single keypress from /dev/tty in raw
|
||||||
# ESC+[+letter as a 3-byte burst — after reading the ESC we immediately
|
# mode and returns a normalized token (up/down/tab/space/escape/quit or a
|
||||||
# read 2 more bytes which are already in the terminal buffer. Single-char
|
# literal char). It bypasses fish's `read`, whose line editor swallows Tab
|
||||||
# keys ('j','k',' ','q', Tab) are returned right away with no extra
|
# and arrow keys and prints a `read> ` prompt — unusable for a TUI.
|
||||||
# blocking. NOTE: the nchars flag is -n; -k is not a valid read option.
|
|
||||||
while true
|
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
|
if set -q __config_toggle_exit
|
||||||
set -eg __config_toggle_exit
|
set -eg __config_toggle_exit
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
# -n 1 reads exactly one char; -s suppresses echo so keystrokes do
|
set -l key (__config_toggle_read_key)
|
||||||
# not garble the panel. If read fails (EOF / non-tty stdin) bail out
|
or break # not a TTY — exit instead of spinning
|
||||||
# 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
|
|
||||||
|
|
||||||
switch $key
|
switch $key
|
||||||
case \e\[A # Up arrow
|
case up k
|
||||||
set cur_row (math "max(0, $cur_row - 1)")
|
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)")
|
set cur_row (math "min(6, $cur_row + 1)")
|
||||||
case k
|
case tab # Tab — switch scope
|
||||||
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
|
|
||||||
if test $cur_scope = universal
|
if test $cur_scope = universal
|
||||||
set cur_scope session
|
set cur_scope session
|
||||||
else
|
else
|
||||||
set cur_scope universal
|
set cur_scope universal
|
||||||
end
|
end
|
||||||
case ' ' # Space — cycle and apply immediately
|
case space # Space — cycle and apply immediately
|
||||||
set -l varname $vars[(math $cur_row + 1)]
|
set -l varname $vars[(math $cur_row + 1)]
|
||||||
set -l cur_val (__config_toggle_get_val $varname $cur_scope)
|
set -l cur_val (__config_toggle_get_val $varname $cur_scope)
|
||||||
set -l next_val ""
|
set -l next_val ""
|
||||||
@@ -162,7 +143,7 @@ function config-toggle --description 'Interactive TUI for toggling opinionated c
|
|||||||
set -eg $varname
|
set -eg $varname
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
case q Q
|
case q Q quit escape
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user