forked from vhaudiquet/weekly-activity
fix(wizard): read keypresses from the raw fd so arrow keys work
sys.stdin.read(1) on the buffered TextIOWrapper pulls the whole escape sequence into its Python-level buffer, so the follow-up select() sees an empty fd and every arrow/Home/End press degrades to bare Esc; the unread '[B' tail then leaks into the next keypress. Read raw bytes via os.read() and select() on the file descriptor instead, keeping the same key tokens and classifications. Add pty-driven regression tests over a real cbreak terminal covering CSI sequences, single-byte keys, bare Esc, and tail-leak.
This commit is contained in:
@@ -343,23 +343,26 @@ def _read_keypress() -> str:
|
||||
"""Read one keystroke from stdin (cbreak mode); classify it."""
|
||||
import select
|
||||
|
||||
char = sys.stdin.read(1)
|
||||
if char == "\x1b":
|
||||
fd = sys.stdin.fileno()
|
||||
# Read raw bytes from the fd: a buffered text read would pull the whole
|
||||
# escape sequence into its own buffer, leaving nothing for select to see.
|
||||
first = os.read(fd, 1)
|
||||
if first == b"\x1b":
|
||||
# A bare Esc is not followed by more bytes; CSI sequences are. Peek briefly.
|
||||
readable, _, _ = select.select([sys.stdin], [], [], 0.05)
|
||||
readable, _, _ = select.select([fd], [], [], 0.05)
|
||||
if not readable:
|
||||
return _KEY_ESCAPE
|
||||
tail = sys.stdin.read(2)
|
||||
return _CSI_KEYS.get(tail, _KEY_OTHER)
|
||||
if char in ("\r", "\n"):
|
||||
tail = os.read(fd, 2)
|
||||
return _CSI_KEYS.get(tail.decode("ascii", "replace"), _KEY_OTHER)
|
||||
if first in (b"\r", b"\n"):
|
||||
return _KEY_ENTER
|
||||
if char == "\x01": # Ctrl-A
|
||||
if first == b"\x01": # Ctrl-A
|
||||
return _KEY_HOME
|
||||
if char == "\x05": # Ctrl-E
|
||||
if first == b"\x05": # Ctrl-E
|
||||
return _KEY_END
|
||||
if char == "k":
|
||||
if first == b"k":
|
||||
return _KEY_UP
|
||||
if char == "j":
|
||||
if first == b"j":
|
||||
return _KEY_DOWN
|
||||
return _KEY_OTHER
|
||||
|
||||
|
||||
Reference in New Issue
Block a user