-
Notifications
You must be signed in to change notification settings - Fork 1
fix: ghost cursor at (0,0) and ESC k title-set payload leak #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -553,6 +553,61 @@ | |||||
| this.writeInternal(data, callback); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Strip unimplemented escape sequences that Ghostty WASM does not consume | ||||||
| * cleanly. The current parser (5714ed07) prints the inner text of | ||||||
| * `ESC k <text> ESC \` (screen/tmux title set) onto the grid instead of | ||||||
| * silently consuming it — the same is true for any 7-bit terminator | ||||||
| * (`BEL`). We pre-filter the input so those titles don't leak as visible | ||||||
| * text. Issue: coder/ghostty-web#153. | ||||||
| * | ||||||
| * Only `ESC k …` is stripped. OSC sequences (`ESC ] …`) already work in | ||||||
| * the WASM parser and are untouched. | ||||||
| */ | ||||||
| private stripUnimplementedTitleSequences(data: string | Uint8Array): string | Uint8Array { | ||||||
|
Check failure on line 567 in lib/terminal.ts
|
||||||
| if (typeof data === 'string') { | ||||||
| // ESC = \x1b, ST = \x1b\x5c (ESC followed by backslash), BEL = \x07 | ||||||
| return data.replace(/\x1bk[^\x1b\x07]*(?:\x1b\\|\x07)/g, ''); | ||||||
|
Check warning on line 570 in lib/terminal.ts
|
||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regex used for string input (
Suggested change
|
||||||
| } | ||||||
| // Byte-level scan for Uint8Array. We only allocate a copy when we | ||||||
| // actually find a sequence to strip. | ||||||
| let i = 0; | ||||||
| let writeIdx = -1; | ||||||
| let out: Uint8Array | null = null; | ||||||
| while (i < data.length) { | ||||||
| if (data[i] === 0x1b && i + 1 < data.length && data[i + 1] === 0x6b) { | ||||||
| // Found ESC k — scan forward to ESC \ or BEL | ||||||
| let j = i + 2; | ||||||
| while (j < data.length) { | ||||||
| if (data[j] === 0x07) { | ||||||
| j++; | ||||||
| break; | ||||||
| } | ||||||
| if (data[j] === 0x1b && j + 1 < data.length && data[j + 1] === 0x5c) { | ||||||
| j += 2; | ||||||
| break; | ||||||
| } | ||||||
|
Comment on lines
+586
to
+589
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check for the |
||||||
| // No terminator yet — keep scanning (handles split writes if WASM | ||||||
| // ever assembles them; defensively bail if we hit another ESC k). | ||||||
| j++; | ||||||
| } | ||||||
| if (out === null) { | ||||||
| out = new Uint8Array(data.length); | ||||||
| out.set(data.subarray(0, i)); | ||||||
| writeIdx = i; | ||||||
| } | ||||||
| i = j; | ||||||
| continue; | ||||||
| } | ||||||
| if (out !== null) { | ||||||
| out[writeIdx++] = data[i]; | ||||||
| } | ||||||
| i++; | ||||||
| } | ||||||
| if (out === null) return data; | ||||||
| return out.subarray(0, writeIdx); | ||||||
| } | ||||||
|
Comment on lines
+567
to
+609
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||
|
|
||||||
| /** | ||||||
| * Internal write implementation (extracted from write()) | ||||||
| */ | ||||||
|
|
@@ -561,6 +616,10 @@ | |||||
| // preserve selection when new data arrives. Selection is cleared by user actions | ||||||
| // like clicking or typing, not by incoming data. | ||||||
|
|
||||||
| // Strip unimplemented escape sequences (e.g. ESC k …) that would | ||||||
| // otherwise leak their payload onto the grid. See issue #153. | ||||||
| const sanitized = this.stripUnimplementedTitleSequences(data); | ||||||
|
|
||||||
| // Save scroll state before writing, ONLY when preserveScrollOnWrite is | ||||||
| // active. viewportY is relative to the bottom, so if new lines push | ||||||
| // content into scrollback we need to bump viewportY by the same amount | ||||||
|
|
@@ -571,7 +630,7 @@ | |||||
| preserveScroll && savedViewportY > 0 ? this.wasmTerm!.getScrollbackLength() : 0; | ||||||
|
|
||||||
| // Write directly to WASM terminal (handles VT parsing internally) | ||||||
| this.wasmTerm!.write(data); | ||||||
| this.wasmTerm!.write(sanitized); | ||||||
|
|
||||||
| // Process any responses generated by the terminal (e.g., DSR cursor position) | ||||||
| // These need to be sent back to the PTY via onData | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current logic results in redundant rendering when the cursor moves horizontally on the same row. In this case,
cursor.yandthis.lastCursorPosition.yare identical. The preceding block (lines 309-315) already ensures the row is rendered if it is not dirty. By removing the row inequality check, this row is now rendered twice in a single frame. Adding a check to ensure the rows are different would be more efficient.