-
Notifications
You must be signed in to change notification settings - Fork 5.7k
fix(web): fully hide terminal cursor edges during blink off phase #11620
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
base: main
Are you sure you want to change the base?
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 |
|---|---|---|
|
|
@@ -244,27 +244,43 @@ export function renderGhosttySnapshot(options: { | |
| } | ||
| } | ||
|
|
||
| if (cursorOn && snapshot.cursorVisible && snapshot.cursorX >= 0 && snapshot.cursorY >= 0) { | ||
| if (snapshot.cursorVisible && snapshot.cursorX >= 0 && snapshot.cursorY >= 0) { | ||
| const left = padding + snapshot.cursorX * metrics.width; | ||
| const top = originY + snapshot.cursorY * metrics.height; | ||
| context.fillStyle = cssColor(snapshot.cursor); | ||
| if (!focused) { | ||
| // An unfocused terminal draws a hollow cursor so the active pane is obvious. | ||
| context.strokeStyle = cssColor(snapshot.cursor); | ||
| context.strokeRect(left + 0.5, top + 0.5, metrics.width - 1, metrics.height - 1); | ||
| } else if (snapshot.cursorStyle === 0) { | ||
| context.fillRect(left, top, 2, metrics.height); | ||
| } else if (snapshot.cursorStyle === 2) { | ||
| context.fillRect(left, top + metrics.height - 2, metrics.width, 2); | ||
| } else if (snapshot.cursorStyle === 3) { | ||
| context.strokeStyle = cssColor(snapshot.cursor); | ||
| context.strokeRect(left + 0.5, top + 0.5, metrics.width - 1, metrics.height - 1); | ||
| if (cursorOn) { | ||
| context.fillStyle = cssColor(snapshot.cursor); | ||
| if (!focused) { | ||
| // An unfocused terminal draws a hollow cursor so the active pane is obvious. | ||
| context.strokeStyle = cssColor(snapshot.cursor); | ||
| context.strokeRect(left + 0.5, top + 0.5, metrics.width - 1, metrics.height - 1); | ||
| } else if (snapshot.cursorStyle === 0) { | ||
| context.fillRect(left, top, 2, metrics.height); | ||
| } else if (snapshot.cursorStyle === 2) { | ||
| context.fillRect(left, top + metrics.height - 2, metrics.width, 2); | ||
| } else if (snapshot.cursorStyle === 3) { | ||
| context.strokeStyle = cssColor(snapshot.cursor); | ||
| context.strokeRect(left + 0.5, top + 0.5, metrics.width - 1, metrics.height - 1); | ||
| } else { | ||
| context.fillRect(left, top, metrics.width, metrics.height); | ||
| const cell = snapshot.rowData[snapshot.cursorY]?.cells[snapshot.cursorX]; | ||
| if (cell?.text) { | ||
| context.font = fontForCell(cell, fontSize, fontFamily); | ||
| context.fillStyle = cssColor(snapshot.background); | ||
| context.fillText(cell.text, left, top + metrics.baseline, metrics.width); | ||
| } | ||
| } | ||
| } else { | ||
| // During the blink off phase, explicitly clear the full cursor cell to | ||
| // erase any subpixel edge remnants left by bar, underline, or stroke | ||
| // cursors whose thin geometry may not be fully covered by the row | ||
| // background fill alone. | ||
| context.fillStyle = cssColor(snapshot.background); | ||
|
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Restore the cursor cell paint layers after the blink-off clear.
🤖 Prompt for AI Agents |
||
| context.fillRect(left, top, metrics.width, metrics.height); | ||
|
Comment on lines
+277
to
278
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.
When the blinking cursor rests on a cell with a non-default background, selection tint, underline, strikethrough, overline, or hovered-link underline, this post-pass overwrites the already-correct row rendering with the terminal-wide background and only redraws the glyph. Consequently, the cell visibly loses its background and decorations throughout every blink-off phase; the clear path must restore the cell's actual background, selection overlay, and decorations. Useful? React with 👍 / 👎. |
||
| // Redraw the cell text so the glyph remains visible under the cleared cursor. | ||
| const cell = snapshot.rowData[snapshot.cursorY]?.cells[snapshot.cursorX]; | ||
| if (cell?.text) { | ||
| if (cell && !cell.invisible && cell.text.length > 0) { | ||
| context.font = fontForCell(cell, fontSize, fontFamily); | ||
| context.fillStyle = cssColor(snapshot.background); | ||
| context.fillStyle = cssColor(cell.foreground); | ||
| context.fillText(cell.text, left, top + metrics.baseline, metrics.width); | ||
|
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.
When the cursor is on the leading cell of a double-width glyph, the normal row renderer includes its spacer tail and renders across two cells, but this blink-off redraw constrains the same glyph to Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
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.
🟡 Medium
ghostty/renderer.ts:273During the cursor blink-off phase, a non-default or selected cursor cell is painted with
snapshot.background, so its effective background is lost and previously rendered underline, strikethrough, and overline pixels disappear. The post-row clear must repaint the cell background (includingcell.selected/selectionBackground) and restore the cell's text decorations before returning.🤖 Copy this AI Prompt to have your agent fix this: