Skip to content

Land raw-fd keystroke reading on main (follow-up to #15) - #18

Merged
VIVAAN-DHAWAN merged 2 commits into
mainfrom
fix/raw-fd-keystroke-reading
Aug 23, 2026
Merged

VIVAAN-DHAWAN merged 2 commits into
mainfrom
fix/raw-fd-keystroke-reading

Conversation

@VIVAAN-DHAWAN

@VIVAAN-DHAWAN VIVAAN-DHAWAN commented Aug 23, 2026 •

Copy link
Copy Markdown
Owner

Why

Follow-up to #15. That PR was stacked on #14, so when it was merged, GitHub landed its commits into the stacked base branch (fix/interactive-arrow-nameerror) instead of main — main still reads menu keys through buffered sys.stdin.read(1) (cli.py:423), so the bug this fix addresses is live on main today:

The menu read single keys through the buffered text layer while _read_escape_sequence() drains bytes straight from the file descriptor. When two keystrokes arrive in one packet — exactly what fast arrow tapping produces — the buffered layer pulls the entire packet into its internal buffer, select() on the descriptor then reports nothing ready, and both taps are dropped; leftover bytes ([, B) are later misread as ordinary keys and can swallow Enter, leaving the menu stuck. A closed stdin also spins the clear-and-redraw loop forever.

What users will see / Surface area

  • src/diffly_cli/cli.py::interactive_view: every keystroke is read with os.read(fd, 1) so both consumers share a single raw byte stream.
  • EOF or Ctrl-D exits the menu cleanly instead of spinning.
  • Key semantics unchanged: q/Q quit, Space toggles, arrows move, Enter renders.

Validation

  • Cherry-pick of 0d893e1 (the exact commit merged as Read interactive keystrokes at the raw fd layer #15) onto current main.
  • Regression tests: test_interactive_menu_keeps_up_with_rapid_arrow_taps (↓↓↑ delivered in one packet + Enter) and test_menu_exits_when_stdin_reaches_eof; both hang/fail without the fix.
  • Full suite: pytest -q → 58 passed.

Follow-up hardening (second commit)

Once the pty tests were made to exercise the real loop (see below), three more defects surfaced:

  • tty.setcbreak discards typed keys on newer Pythons — it defaults to TCSAFLUSH, flushing everything queued while the menu rendered. Now pinned to TCSANOW.
  • Escape reader over-consumed bursts — it drained up to 16 bytes per call, swallowing a following Space/Enter. It now reads one byte at a time and stops at the CSI final byte; test_escape_sequence_reader_consumes_exactly_one_sequence proves [A\x1b[B consumes exactly one sequence and leaves the next Escape untouched.
  • Restore blocked on unread ptys — TCSADRAIN can wait forever when nothing drains echoed output (CI harnesses); restore now uses TCSANOW.

The pty regression tests previously passed vacuously: pytest's captured stdout failed the isatty guard, so they fell back to the plain report without ever running the menu. They now patch both guards and drive real cursor movement (visible in -s output), and the suite runs them for real.

The menu read single keys through buffered sys.stdin.read(1) while
_read_escape_sequence drains bytes straight from the descriptor. When
two keystrokes arrive in one packet (fast arrow taps), the buffered
layer pulls the whole packet into its internal buffer, the fd-level
reader finds nothing ready, and both taps are silently dropped; the
leftover bytes then masquerade as ordinary keys and can swallow Enter,
leaving the menu stuck.

Read every key with os.read(fd, 1) so both consumers share one byte
stream, and exit cleanly on EOF or Ctrl-D instead of spinning the
redraw loop forever on a closed stdin.
@github-actions

Copy link
Copy Markdown

Diffly verdict: QUARANTINE

#18 · 2 files · 60 lines changed · checks: PENDING

Risk flags: NO_TEST_COVERAGE (low), CHECKS_PENDING (medium)

Risk flags and reasoning
  • NO_TEST_COVERAGE (low): Changed production files have no obvious neighboring or repository test coverage; this is a review hint, not a verdict gate. — src/diffly_cli/cli.py
  • CHECKS_PENDING (medium): Required status checks are still pending. — combined commit status; diffly; test (3.10); test (3.12); test (3.13)
  • QUARANTINE because required checks are still running.
Blast-radius summary
File Change Symbols Direct callers Tests
src/diffly_cli/cli.py +12/-2 <top-level changes> — —
tests/test_regressions.py +46/-0 <top-level changes> — —

Deterministic triage is authoritative; any optional LLM explanation cannot change the verdict.

Three defects surfaced once the pty tests actually exercised the menu
loop (their stdout-isatty guard previously short-circuited to the
fallback report):

- tty.setcbreak defaults to TCSAFLUSH on newer Pythons and silently
  discarded keys typed while the menu rendered; pin TCSANOW.
- _read_escape_sequence drained up to 16 bytes per call, swallowing
  neighboring keystrokes in a burst; read one byte at a time and stop
  at the CSI final byte so trailing keys stay queued.
- Restoring termios with TCSADRAIN can block forever when nothing
  reads our echoed output (CI ptys); restore with TCSANOW instead.

Tests now patch both isatty guards, drive real cursor movement over a
pty, and assert exact sequence consumption.
@VIVAAN-DHAWAN
VIVAAN-DHAWAN merged commit 2762778 into main Aug 23, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant