Lifecycle, reset completeness, and a public class that shipped mojibake - #100
Conversation
Perf comparison3 run(s) of each side, alternating on one machine. Allocation is a count and is gated exactly. Time is a measurement, so its gate is derived from the spread this job just observed in itself rather than fixed in advance.
Each corpus is gated at assemblies measured
Regressions
|
There was a problem hiding this comment.
🟡 Changes recommended
The OSC parsing changes currently drop all non-ASCII payload characters and include a documentation/behavior mismatch around OSC truncation vs drop semantics.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR is the final audit pass focusing on terminal lifecycle correctness (dispose/reset), parser conformance, and protocol edge cases that previously produced incorrect state or unbounded work/memory. It also removes a public API duplicate (XTerm.Charset.Charsets) that shipped with mojibake.
Changes:
- Make
TerminalimplementIDisposable, ignore writes after dispose, and restore more complete RIS/reset state (tab stops, charset designations, Sixel modes, reverse wraparound behavior). - Tighten VT parser correctness and safety (CAN/SUB abandon sequences, 8-bit ST ends OSC, saturating CSI params, OSC payload cap, DEL handling).
- Fix protocol behaviors across input/output paths (tab stop semantics via HTS/TBC, mouse/keyboard encoding fixes, Kitty/Png bounds and unsigned IDs, buffer bookkeeping fixes like trim/splice).
File summaries
| File | Description |
|---|---|
| src/XTerm.NET/Terminal.cs | Implements IDisposable, ignores writes after dispose, resets additional terminal state including tab stops and Sixel/charset state. |
| src/XTerm.NET/Selection/SelectionManager.cs | Fixes wrapped-line selection newline insertion and word selection expansion direction. |
| src/XTerm.NET/Parser/EscapeSequenceParser.cs | Improves state machine conformance and adds bounds/saturation logic for hostile inputs (OSC size, CSI params, control handling). |
| src/XTerm.NET/InputHandler.cs | Fixes multiple terminal semantics (wrap/erase/editing edge cases, charset save/restore, bounds, Kitty, etc.). |
| src/XTerm.NET/Input/MouseTracker.cs | Corrects coordinate clamping and modifier semantics for specific mouse protocols. |
| src/XTerm.NET/Input/KeyboardInputGenerator.cs | Aligns Escape/Home/End/keypad outputs with xterm behavior and application modes. |
| src/XTerm.NET/Graphics/PngDecoder.cs | Bounds decompression output to prevent allocation bombs. |
| src/XTerm.NET/Graphics/KittyCommand.cs | Parses Kitty image IDs as unsigned per protocol. |
| src/XTerm.NET/Common/CommandExtensions.cs | Adds ECMA-48 cursor motion aliases (HPA/HPR/VPR) to CSI mapping. |
| src/XTerm.NET/Common/ClusterTable.cs | Caps cluster interning and avoids expensive ConcurrentDictionary.Count on hot path. |
| src/XTerm.NET/Charset/Charsets.cs | Removes public duplicate charset table that shipped incorrect glyph mappings. |
| src/XTerm.NET/Buffer/TerminalBuffer.cs | Fixes nullable contract for GetLine, reports scrollback trims, and restores more cursor state. |
| src/XTerm.NET/Buffer/CircularList.cs | Corrects splice-at-capacity insertion semantics and releases trimmed references. |
| src/XTerm.NET/Buffer/BufferLine.cs | Adds wide-cell latch/repairs and extends erase/fill to preserve the wide-cell invariant. |
| src/XTerm.NET.Tests/* | Adds coverage for parser conformance, hostile bounds, lifecycle/reset, selection/bookkeeping, cursor/margins, and wide-cell invariants. |
Review details
- Files reviewed: 21/21 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| /// <summary> | ||
| /// Sets a cell at a specific column. | ||
| /// </summary> | ||
| /// <summary> | ||
| /// Whether this line has ever held a two-column character. A latch, not a count: it exists so | ||
| /// the print path can skip its orphan check with one field read, and the only cost of a stale | ||
| /// true is doing a check that finds nothing. Clearing it accurately would mean scanning the | ||
| /// row on every erase, which is the work it was added to avoid. | ||
| /// </summary> | ||
| public bool HasWideCells { get; private set; } |
| if (code == 0x1B || code == 0x07 || code == 0x9C) | ||
| { | ||
| DispatchOsc(); | ||
| Transition(code == 0x1B ? ParserState.Escape : ParserState.Ground); | ||
| } | ||
| else if (code >= 0x20) | ||
| else if (code >= 0x20 && code < 0x80) | ||
| { | ||
| OscPut(code); | ||
| } |
| // Past the cap the payload is dropped on the floor rather than truncated and dispatched: | ||
| // half a URL or half a base64 clipboard write is not something a handler should act on. | ||
| if (_osc.Length >= MaxOscPayloadChars) | ||
| return; |
JohnCampionJr
left a comment
There was a problem hiding this comment.
Two lifecycle/reset state paths still retain or cross state that RIS/reverse-wrap should bound.
| // And the charset designations, with the SO/SI shift state. InputHandler.ResetCharsets | ||
| // existed for exactly this and was called from nowhere, so a program that designated line | ||
| // drawing into G0 and died left the next one printing box characters for letters. | ||
| _inputHandler.ResetCharsets(); |
There was a problem hiding this comment.
Resetting only the live charset tables leaves _savedCharsetDesignations and SavedCursorState intact. If a program designates line drawing, executes DECSC, then RIS, a later DECRC restores the pre-reset line-drawing designation and undoes this fix. RIS should invalidate/reset the saved cursor context (including the saved designation snapshot), and the test should cover DECSC → RIS → DECRC before printing.
| { | ||
| _buffer.SetCursor(_buffer.X - 1, _buffer.Y); | ||
| } | ||
| else if (ReverseWraparound && _buffer.Y > 0) |
There was a problem hiding this comment.
This bounds reverse wrap at row 0 rather than at the scrolling region. With DECSTBM active, backspacing at X=0 on ScrollTop can move the cursor into the protected row above the region; it also lands at Cols-1 rather than the active right margin. Please use the same region/margin limits as the other cursor motions and add a test at ScrollTop with narrowed/full-width margins.
05815a6 to
21eac6e
Compare
Perf comparison — this change, against its base3 run(s) of each side, alternating on one machine. Allocation is a count and is gated exactly. Time is a measurement, so its gate is derived from the spread this job just observed in itself rather than fixed in advance.
Each corpus is gated at assemblies measured
Perf comparison — cumulative, everything since 2.0.0-rc0023 run(s) of each side, alternating on one machine. Allocation is a count and is gated exactly. Time is a measurement, so its gate is derived from the spread this job just observed in itself rather than fixed in advance.
Each corpus is gated at assemblies measured
|
21eac6e to
31cec01
Compare
- Terminal declares IDisposable. It always had the method, but without the interface no using statement, DI container or analyzer could see it, so handlers stayed subscribed for anyone who did not read the source. Dispose is idempotent now; writes after it are IGNORED rather than thrown, deliberately, because a host reads its pty on a background thread and disposing mid-read is ordinary -- throwing there would kill the read loop rather than the object being torn down. Two existing tests pin that. - Reset left the charset designations standing, so a program that designated line drawing into G0 and died left the next one printing box characters for letters. InputHandler.ResetCharsets existed for exactly this and was called from nowhere. - Reset also left the three Sixel modes, so DECRQM went on reporting mode 80 as set afterwards. - ReverseWraparound (DECSET 45) was stored and reported by DECRQM and nothing ever read it: a shell erasing a wrapped command line stopped at the wrap. - OSC 99 raised a notification with null title AND null body when the build FAILED -- missing braces meant only the inner if was guarded. - Kitty image ids were parsed through the signed reader, which saturates at int.MaxValue, so every id above 2^31 collapsed onto one value: two images with distinct legal ids became one, and deleting either removed both. The unsigned reader was already there. - Deleted XTerm.Charset.Charsets, a public duplicate of XTerm.Common.Charsets whose line-drawing table shipped as question marks. Nothing referenced it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…et RIS reach the state DECRC restores from Three review findings. RIS reset the live charset tables but left the SAVED cursor context alone, which put the reset one DECRC away from being undone: designate line drawing, DECSC, RIS, DECRC, and the dead program's designations are back. DECSC state is per-screen, so both buffers are cleared -- clearing only the active one leaves the other loaded and reachable the moment an application switches to it. Reverse wraparound was bounded at row 0 and landed at Cols - 1, so backspacing off the left edge on the first row of a DECSTBM region moved the cursor into the row the region exists to protect, and under DECSLRM it landed outside the pane. The motion moves next to the other cursor motions and reads the same limits they do: the left margin stops it, TopLimit bounds the row, and it lands on the right margin. The stray XML summary above _disposed is removed; it belonged to Dispose and was attaching itself to the field. Also folded in, as promised on #98: OscPut counts the width of what it is about to append, so a supplementary character cannot land a UTF-16 unit past the cap. It changed nothing observable -- DispatchOsc tests >= and dropped the payload either way -- but the branch below already knows the width, so holding the bound exactly costs nothing. The non-ASCII OSC test is a pin rather than a fix. Two reviewers have now read the annotated arm in the OSC control block as though it restricted the payload to ASCII; it cannot, because that block is entered only for C0 and C1, and everything from 0xA0 up reaches OscPut through the ordinary path. The test says so permanently.
31cec01 to
4234fac
Compare
|
Rebased onto main (post-#98) — clean, no stacking on this one. 1956 tests green. Both of your findings are real and fixedRIS leaving the saved cursor context intact. You are right that resetting only the live tables leaves the reset one DECRC away from being undone. Since #93 the designations live in Reverse wrap ignoring the region and the margins. Also right, and the fix is to stop having the logic in two places. The motion moved into Both regression tests fail against the previous commit. One Copilot finding I checked and am not acting onThe claim that OSC payload accumulation "now only accepts characters < 0x80" is a misreading. The block that arm lives in is entered only for C0 and C1 — That said, two reviewers have now read that arm the same wrong way, which is a fair signal the code invites it. So rather than only reply, there is now a test asserting a window title survives with its accents, CJK, an em dash and a check mark intact. If someone does break it later, they will be told. The related "truncated payload is still dispatched" note is stale — Folded in from #98As promised there: The stray |
Eighth and last of the audit PRs. Stacked on #98 → #97 → #94 → #93 → #92 → #89.
TerminaldeclaresIDisposableusing, DI container or analyzer could see it — handlers stayed subscribed for anyone who did not read the sourceReset()restores charset designationsInputHandler.ResetCharsetsexisted for exactly this and was called from nowhere, so a program that designated line drawing into G0 and died left the next one printing box characters for lettersReset()restores the three Sixel modesDECRQMwent on reporting mode 80 as set after a resetReverseWraparound(DECSET 45) is honouredifwas guarded byTryBuild, so a failed build raised the event anyway with null title and null bodyint.MaxValue, so every id above 2³¹ collapsed onto one value — two images with distinct legal ids became one, and deleting either removed both. The unsigned reader was already there for the frame backgroundPublic API
XTerm.Charset.Charsetsis deleted. A public duplicate ofXTerm.Common.Charsetswhose line-drawing table shipped as literal?characters. Nothing in the tree referenced it, but it was public, so this is a removal.Terminalnow implementsIDisposable— additive.Disposeare ignored, not thrown. I implementedObjectDisposedExceptionfirst and backed it out: two existing tests pin the no-op, and more importantly a host reads its pty on a background thread, so disposing mid-read is ordinary rather than a bug. Throwing there kills the read loop instead of the object being torn down. Documented in the method.Deliberately not fixed
Terminalstores the caller'sTerminalOptionsby reference, so two terminals built from one options object mutate each other (DECSCUSR from one changes the other's cursor). Copying brokeConstructor_WithOptions_UsesProvidedOptions, which asserts identity — and a host that mutates the options object it passed in expects that to reach the terminal. That is a contract decision rather than a bug fix; filed separately.Validation
6 new tests. Suite green: 1938. No perf run — none of this is on the print path.
🤖 Generated with Claude Code