Skip to content

feat(render): draw the cursor in screenshots - #96

Open
Ayman Bagabas (aymanbagabas) wants to merge 6 commits into
feat/osc-colorsfrom
feat/cursor-render
Open

feat(render): draw the cursor in screenshots#96
Ayman Bagabas (aymanbagabas) wants to merge 6 commits into
feat/osc-colorsfrom
feat/cursor-render

Conversation

@aymanbagabas

@aymanbagabas Ayman Bagabas (aymanbagabas) commented Aug 5, 2026

Copy link
Copy Markdown
Member

Third of a three PR stack: #94#95#96, based on #95 so the diff here is only this layer.

A screenshot showed the grid but never showed where the terminal was about to write. You could not tell an editor's caret position, whether a program had hidden the cursor, or which mode it was in.

Before / after

The same session, screenshotted by the same command. The cursor is parked mid-word on The:

before after
before after

The emulator now reports visibility (DECTCEM, CSI ?25 h and l) and shape (DECSCUSR, CSI Ps SP q). The renderer draws the cursor after the text pass: a block fills the cell, an underline sits on its bottom edge, a bar on its left.

Visibility matters as much as position. Full-screen programs hide the cursor while repainting, so a screenshot that ignored DECTCEM would show one parked wherever the last write happened to land.

The cursor never hides content

A block redraws the character beneath it in the cell background, the way a terminal does. The redraw goes through the same path as the text pass, which keeps right two cases a naive redraw gets wrong:

case naive redraw here
double-width () block covers the left half, glyph squashed into one cell block spans both cells, glyph drawn at full width
nerd font glyph emitted as text the font has no glyph for, so the block swallows it redrawn as the same vector glyph
double-width, covered across both cells nerd font glyph, redrawn as a glyph
wide nerd

The cursor follows a color a program sets

OSC 12 sets the cursor color and OSC 112 resets it, both from #95. The cursor had nothing to paint until this PR, so this is where they become visible. One session, screenshotted after each step, cursor parked on the q:

default OSC 12 sets #ff8700 OSC 112 resets
default orange reset

The default cursor color follows the profile foreground, so the block is the same grey as the text. The q stays legible in all three.

Notes for review

  • Blink is deliberately not represented. A screenshot is a single moment, and a blinking cursor is drawn in the half of the cycle where it is visible.
  • Emulator::cursor is relative to the visible screen, so screenshot --full offsets it past the scrollback above.
  • Alacritty's HollowBlock (what it draws for an unfocused window) maps to Block, since a headless terminal has no notion of focus. Its Hidden shape means the same as the mode being off.
  • 2 conformance cases, which run against every backend, cover hide/show and the three shapes. 6 renderer tests cover each shape's rectangle, the readable character, the double-width span, the vector glyph redraw, a hidden or out-of-range cursor, and the runtime color.

A screenshot showed the grid but never where the terminal was about to
write, so a reader could not tell an editor's caret position, whether a
program had hidden the cursor, or which mode it was in.

The emulator now reports visibility (`DECTCEM`) and shape (`DECSCUSR`),
and the renderer draws the cursor after the text pass: a block fills the
cell, an underline sits on its bottom edge, and a bar on its left.

A block redraws the character beneath it in the cell's background color,
so it stays readable rather than being swallowed. The redraw goes
through the same path as the text pass, which keeps two cases right that
a naive redraw gets wrong: a double-width character is covered across
both of its cells instead of being clipped and squashed into one, and a
nerd font glyph comes back as a vector glyph rather than as a character
the text font has no glyph for.

Blink is deliberately not represented; a screenshot is a single moment,
and a blinking cursor is drawn in the half of the cycle where it shows.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
Brings main's move of the request types out of the core crate down the
stack. The screenshot keeps main's signature and error handling, and holds
the state guard across both the emulator borrow and the cursor lookup so a
single lock covers the whole render.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds cursor rendering to SVG screenshots so captures reflect cursor visibility, position, shape, and color, aligning screenshots more closely with real terminal behavior and improving debugging/visual verification.

Changes:

  • Extend the Emulator contract with cursor visibility and shape reporting (DECTCEM / DECSCUSR).
  • Render the cursor (block/underline/bar) as an overlay pass in the SVG renderer, including block “redraw under cursor” behavior.
  • Thread cursor visibility/position through screenshot generation and add conformance + renderer tests.
Show a summary per file
File Description
crates/shell-use/src/terminal/emu.rs Adds CursorShape and new Emulator APIs for cursor visibility/shape.
crates/shell-use/src/terminal/conformance.rs Adds conformance tests for cursor hide/show and shape selection sequences.
crates/shell-use/src/terminal/alacritty.rs Implements new cursor APIs by mapping Alacritty terminal mode/style to CursorShape + visibility.
crates/shell-use/src/render/svg.rs Adds cursor drawing pass to SVG output and expands renderer test coverage for cursor behavior.
crates/shell-use/src/engine.rs Computes cursor position within full/visible row sets and passes it into SVG rendering.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread crates/shell-use/src/render/svg.rs Outdated
Comment thread crates/shell-use/src/engine.rs Outdated
A full render is as long as the scrollback, which a profile sets and does
not cap. Offsetting the cursor past the history counted that in a `u16`,
so a session with a scrollback deeper than 65535 rows wrapped: with
`scrollback = 70000` and 68004 rows the cursor drew on row 2467, which is
68000 - 65536 and looks like a real line. The row is a `usize` now, since
it indexes rows rather than the screen.

The line documenting `render_svg` was also left behind above
`CURSOR_THICKNESS` when that constant was added between them, so it
documented the constant. `render_svg` carries its own description, so the
stray line is gone.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
@aymanbagabas

Copy link
Copy Markdown
Member Author

For anyone hitting red CI on this one: the two failures seen here were both flakes, not this PR.

Both are instances of the class tracked in #98. Worth noting the check matrix has no fail-fast: false, so one flake cancels the other two platforms and reads as three failures.

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.

2 participants