Skip to content

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

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

feat(render): draw the cursor in screenshots#96
Ayman Bagabas (aymanbagabas) wants to merge 1 commit 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.

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
@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.

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>

Rebasing onto a main that had since gained window titles folded the
follow-up commits into this one, so it also carries: the cursor row
counted as a `usize`, since a full render is as long as the scrollback
and a profile can set that past what a `u16` holds, and the removal of a
doc line left documenting a constant.
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