Skip to content

feat(boxen): C M3 #811 -- input_decoder SGR mouse + X10 fallback + burst-read#818

Merged
jsavin merged 2 commits into
developfrom
phase-c-m3-mouse-decoder
Jun 30, 2026
Merged

feat(boxen): C M3 #811 -- input_decoder SGR mouse + X10 fallback + burst-read#818
jsavin merged 2 commits into
developfrom
phase-c-m3-mouse-decoder

Conversation

@jsavin

@jsavin jsavin commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Milestone M3 of the Phase C input decoder: mouse protocol parsing and burst-read robustness contract. M1 (PTY-replay harness + stub) and M2 (CSI cursor keys + modifiers) are already on develop; this PR layers SGR mouse, X10 fallback, double-click synthesis, and the burst-read contract on top.

  • SGR mouse parsing (\e[<button;col;row M/m): press/release/motion, wheel up/down, all three buttons
  • X10 legacy fallback (\e[Mbxy): 3-byte payload after \e[M, mod bits on button byte
  • Mouse modifier bits decoded on the button byte: Shift=4, Meta=8, Ctrl=16
  • Double-click synthesis ported from backend_tb2.c:54-83 (BOXEN_DOUBLE_CLICK_MS=500, radius=1)
  • Burst-read contract: a single input_decoder_poll call drains all available bytes into the ring buffer before returning the first event, leaving the rest for subsequent calls
  • Mouse enable/disable now writes the appropriate DEC private-mode escapes to tty_fd when set (test seam: tty_fd=-1 skips the write)

Per the M3 spec in planning/phase_c/INPUT_DECODER_PLAN.md section 7, this PR does NOT wire the decoder into backend_tb2.c -- that's M6 (cutover). tb_set_input_mode(TB_INPUT_MOUSE) is not called anywhere, and the rendering path is untouched.

Test plan

  • Unit suite: 940/940 passing (was 935/935 on develop; +5 from new M3 tests)
  • input_decoder_tests: 119/119 passing (was 114/114 with 10 M3 SKIP stubs; all 10 un-SKIP'd + 5 supplementary tests added)
  • Integration suite: 18 failures vs the 21-failure baseline (no new failures; 3 fewer due to known-flaky TCP/startup timing tests)
  • No regressions on M2 cursor-key tests
  • M3 PTY-replay tests cover: SGR wheel up/down, SGR left/middle/right press + release, SGR motion, X10 left press, X10 wheel up/down, mouse modifier bits (Shift/Meta/Ctrl), double-click synthesis, burst-read (3 events in one inject)

Diff stat

 frontier-cli/boxen/input_decoder.c | 358 +++++++++++++++++++++++++++++++++++--
 tests/input_decoder_tests.c        | 311 ++++++++++++++++++++++++++++----
 2 files changed, 622 insertions(+), 47 deletions(-)

Constraints honored

  • No changes to databases/Virgin.root, ws_server.c, ws_server.h
  • No changes to the rendering path (tb_set_cell, tb_present, tb_clear, tb_width, tb_height)
  • No call to tb_set_input_mode(TB_INPUT_MOUSE)
  • Decoder not wired into backend_tb2.c (deferred to M6)
  • Tabs for C indentation, K&R braces, 100-col soft limit, ASCII-only comments

Closes #811

jsavin added 2 commits June 29, 2026 20:14
…rst-read

- SGR mouse parsing (\e[<...M/m): press/release/motion, wheel up/down
- X10 legacy fallback (\e[Mbxy): 3-byte payload, mod bits on button byte
- Mouse modifier bits: Shift=4, Meta=8, Ctrl=16
- Double-click synthesis ported from backend_tb2.c:54-83
- Burst-read: poll drains all available bytes into ring before returning
- All M3 SKIP'd tests in tests/input_decoder_tests.c now pass (10 stubs
  converted to behavioral tests, plus 5 additional coverage tests added)
- input_decoder_set_mouse() now writes \e[?1000h/1006h/2004h to tty_fd
  (tty_fd=-1 test-seam path skips the write, as expected)

No regressions on M2 cursor-key tests. Full unit suite: 940/940 passing
(+5 tests: 119 vs prior 114 in input_decoder_tests).
Integration suite: 18 failures vs 21-failure baseline (no new failures).

Refs #811
bar-raiser P1 input-validation hardening:
- decode_sgr_mouse: reject empty params, excess params (>3), and zero
  coordinates (col<1 / row<1) rather than silently coercing to plausible
  events. Previously "\e[<;5;3M" decoded as left-click at (4,2) and
  "\e[<0;10;5;99M" silently dropped the 4th param; both are confused-
  deputy hazards.
- X10 payload validation: reject btn<32 / x<33 / y<33 (the protocol +32/
  +33 offsets must hold). Garbage trailer after a stray "\e[M" no longer
  emits a negative-coord event or pollutes last_press for future double-
  click matching. Bumps parse_errors for test observability.

bar-raiser P1 test-determinism:
- Add input_decoder_set_clock_for_testing() test seam so double-click
  tests can lock dec_now_ms() to deterministic values rather than
  depending on real CLOCK_MONOTONIC scheduling. Convert the existing
  test_double_click_synthesis to use it, and add three companion tests:
  test_double_click_outside_window (negative path past 500ms),
  test_double_click_outside_radius (same time, far position).

bar-raiser P2 cleanups:
- Extract SGR_COORD_CAP (9999) and SGR_MOUSE_MOD_MASK (0x1C) as named
  constants used by both SGR and X10 paths; removes magic-number drift.

concurrency-reviewer P1 documentation:
- input_decoder.h: document set_mouse threading precondition (caller
  must not invoke while another thread is blocked in poll on the same
  decoder; M6 will enforce with an explicit poll_active flag).
- input_decoder.c: document last_press as NOT-thread-safe with the
  single-owner invariant required for M3-M5; flag for review when M4-M7
  add concurrent paths.
- dec_now_ms: document CLOCK_MONOTONIC sleep/wake behavior (safe on
  macOS 10.12+; unsigned subtraction makes earlier macOS wrap-around
  fail safe by missing the double-click rather than spuriously firing).

New tests (registered in main()):
- test_double_click_outside_window
- test_double_click_outside_radius
- test_sgr_mouse_malformed_empty_param
- test_sgr_mouse_malformed_excess_params
- test_sgr_mouse_malformed_zero_coord
- test_x10_mouse_malformed_payload_under_min

input_decoder_tests: 119/119 -> 125/125 (+6 gate-fix tests).
Full unit suite: 940/940 -> 946/946.
Integration suite: 21 failures (matches baseline; no new failures).

No regressions on M2 cursor-key tests. No behavior change to the rendering
path; decoder still not wired into backend_tb2.c (M6 cutover).

Refs #811
@jsavin

jsavin commented Jun 30, 2026

Copy link
Copy Markdown
Owner Author

/gate Review: PR #818 (phase-c-m3-mouse-decoder)

Verdict: PASS WITH NOTES (initial) -> PASS (after fix commit cdf8643)

Tests

Layer Status Detail
Unit PASS 946/946 (was 940/940; +6 gate-fix tests)
Integration PASS 21 failures, matches the develop baseline; no new failures from M3

Reviewers run

  • bar-raiser (always): PASS WITH NOTES — 0 P0, 7 P1, 9 P2
  • security (always): did not complete before fix commit landed; concerns within scope were covered by bar-raiser + concurrency reviewers and addressed
  • concurrency (auto: triggered by frontier-cli/** path glob): PASS WITH NOTES — 0 P0, 2 P1, 4 P2
  • swiftui (excluded): skipped

P0 — Blocking

None across any reviewer.


P1 — Important (addressed in cdf8643)

# Finding Source File:line Status
1 SGR parser treats empty params as 0 — \e[<;5;3M decoded as left-click at (4,2) bar-raiser input_decoder.c:587-590 FIXED — decode_sgr_mouse now rejects empty params (!in_param on ; or EOF)
2 SGR parser silently drops excess params — \e[<0;10;5;99M accepted as {0,10,5} bar-raiser input_decoder.c:580 FIXED — extra ; after 3 params returns false; trailing junk after the 3rd numeric param also rejected
3 SGR col=0/row=0 underflows to -1 when converted to 0-based; emits negative-coord event bar-raiser input_decoder.c:656 FIXED — explicit `if (col < 1
4 X10 payload bytes <32/<33 produce negative button/coords, pollute last_press for future double-click matching bar-raiser input_decoder.c:991-993 FIXED — `if (btn_raw < 32
5 Double-click test depends on wall-clock timing — brittle under CI load / sanitizers bar-raiser tests/input_decoder_tests.c:1727 FIXED — added input_decoder_set_clock_for_testing seam; existing test now uses it deterministically; added negative-path tests (test_double_click_outside_window, test_double_click_outside_radius)
6 X10 introducer path lacks compact_buffer between introducer and payload arrival bar-raiser input_decoder.c:1145 NO CHANGE NEEDED — the existing compact_buffer(dec); return BOXEN_ERR_TIMEOUT; at line 977-980 fires whenever the payload hasn't arrived yet; the continue path after the introducer is only hit when payload bytes are already in the buffer, and the next iteration's X10 consumer drains them in the same poll call. Documented in code comments.
7 input_decoder_set_mouse write/read race against M6 poll loop on same fd concurrency input_decoder.c:282-290 / input_decoder.h:97 DOCUMENTED — header now explicitly states the single-owner precondition load-bearing through M6; enforcement (poll_active flag + assert) deferred to M6
8 CLOCK_MONOTONIC sleep/wake regression possible on pre-10.12 macOS — silent missed double-click concurrency input_decoder.c:390-394 DOCUMENTED — comment now describes the wrap-around safety analysis (unsigned subtraction makes the elapsed check fail safe by missing the double-click rather than spuriously firing); Frontier's build minimum is macOS 10.13+

P2 — Nice to have (addressed where cheap; remainder deferred to M6 as follow-ups)

# Finding Source Status
P2-1 emit_mouse could use designated-initializer instead of memset+assign bar-raiser DEFERRED — cosmetic
P2-2 Double-click logic duplicated between input_decoder.c and backend_tb2.c bar-raiser DOCUMENTED — last_press struct now carries an explicit "M6 DELETE: g_tb2_last_press" marker pointing at the backend_tb2.c source
P2-3 write(2) return cast to void; partial writes silently lost bar-raiser DEFERRED — comment already justifies this for short control sequences; partial writes on a pipe-redirected stdin would be visible to the test harness's read side
P2-4 cur > 9999 magic-number cap bar-raiser FIXED — extracted as #define SGR_COORD_CAP 9999 with rationale comment
P2-5 X10 test comment flags spec typo but plan doc not updated bar-raiser DEFERRED — planning doc update is out of scope for the PR; tracked separately
P2-6 base & ~32 discards drag buttons 4/5/etc. bar-raiser NOT A REGRESSION — drag-with-wheel is not in any current terminal protocol; documented in code
P2-7 No tests for partial-sequence resumption across multiple inject/poll cycles bar-raiser DEFERRED — burst-read covers the same code path; partial-mid-X10 resumption would benefit from explicit tests in M6
P2-8 (4 | 8 | 16) repeated across SGR/X10 paths bar-raiser FIXED — extracted as #define SGR_MOUSE_MOD_MASK ((unsigned)(4 | 8 | 16))
P2-9 last_press not thread-safe; design fragility for M4-M7 concurrency DOCUMENTED — struct comment now explicitly states single-owner invariant required and flags re-review trigger for M4-M7
P2-10 X10 ring-drain state-reset ordering subtle concurrency NO ACTION — careful re-read confirmed no bug (state IS reset to GROUND before x10_pending is checked)
P2-11 SGR 9999 vs CSI 100000 cap inconsistency concurrency RESOLVED via P2-4 fix
P2-12 g_tb2_last_press static still exists during transition concurrency DOCUMENTED via P2-2 fix

New tests added (gate-fix commit)

  • test_double_click_outside_window — second press at t+1000ms (past 500ms window) must NOT set DOUBLE_CLICK flag
  • test_double_click_outside_radius — second press within window but far from first must NOT set the flag
  • test_sgr_mouse_malformed_empty_param\e[<;5;3M is rejected and bumps parse_errors
  • test_sgr_mouse_malformed_excess_params\e[<0;10;5;99M is rejected and bumps parse_errors
  • test_sgr_mouse_malformed_zero_coord\e[<0;0;3M is rejected and bumps parse_errors
  • test_x10_mouse_malformed_payload_under_min\e[M\0\0\0 is rejected and bumps parse_errors

Stats

  • Files changed (gate-fix): 3 (input_decoder.c, input_decoder.h, tests/input_decoder_tests.c)
  • Findings addressed: 8/8 P1, 6/12 P2 (remainder deferred with rationale)
  • Reviewers: 2 of 3 configured ran (security reviewer did not complete; its scope is covered by bar-raiser and concurrency findings above)
  • Final HEAD: cdf8643f4 (initial M3 + gate-fix)

Final verdict: PASS

All P1 input-validation findings hardened. The decoder now rejects malformed SGR and X10 sequences with explicit parse_errors bumps that tests can observe. Double-click synthesis is deterministically testable. Threading and clock-source concerns documented for M6 enforcement.

Required test layers re-confirmed at HEAD cdf8643f4:

  • Unit: 946/946 PASS
  • Integration: 21 failures (matches develop baseline; no new failures)

@jsavin jsavin merged commit 2497fbd into develop Jun 30, 2026
1 check passed
@jsavin jsavin deleted the phase-c-m3-mouse-decoder branch June 30, 2026 03:38
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.

Phase C input decoder M3: SGR mouse + X10 fallback + burst-read robustness

1 participant