Skip to content

feat: expose inverse, strikethrough, faint, invisible and cursorVisible in snapshots - #17

Open
ThomasK33 wants to merge 1 commit into
mainfrom
feat/snapshot-sgr-attrs-cursor-visible
Open

ThomasK33 wants to merge 1 commit into
mainfrom
feat/snapshot-sgr-attrs-cursor-visible

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Closes #15

Summary

Snapshots now expose the remaining SGR cell attributes and DECTCEM cursor visibility, so coder/agent-tty (coder/agent-tty#171) can render reverse video, strikethrough, dim and hidden text, and hide the cursor.

  • SnapshotCell gains optional faint (SGR 2), inverse (SGR 7), invisible (SGR 8) and strikethrough (SGR 9).
    • Same convention as bold/italic/underline: the key is present with true only when set, and omitted otherwise.
    • inverse is the raw attribute. foreground/background are not swapped.
  • TerminalSnapshot gains cursorVisible?: boolean. The native binding always sets it (true/false); the type is optional for compatibility.
  • Values come from libghostty-vt's C API at the pinned commit: GhosttyStyle.{faint,inverse,invisible,strikethrough} via the existing ghostty_grid_ref_style call, and ghostty_terminal_get(GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE). No new libghostty entry points.
  • README documents the snapshot fields. CHANGELOG.md is not hand-edited: the release-changelog workflow generates it with Communique on release/v* PRs.

Existing fields are unchanged.

Tests (written first, red before the native change)

  • Per attribute (inverse 7/27, strikethrough 9/29, faint 2/22, invisible 8/28): unstyled cell, set, attribute-specific reset, set again, SGR 0 reset.
  • All attributes combined on one cell (ESC[1;2;3;4;7;8;9m).
  • Inverse keeps the unswapped ESC[31;42m colors.
  • cursorVisible: default true, ESC[?25l gives false (with and without includeCells), ESC[?25h gives true.

Validation

Gate Result
npm run build:libghostty (pinned Ghostty 48ccec18, Zig 0.15.2) pass
npm run build:native pass, no warnings
npm run build pass
npm run typecheck (and an ad hoc tsc run including test/) pass
npm test 13/13 pass (7 new)
npm run smoke pass

The repo has no lint script.

Dogfood

A standalone Node ESM script imports @coder/libghostty-vt-node (linked to this branch's build), feeds ESC[?25l, then ESC[31;42;7mI ESC[27mi ESC[0m ESC[9mS ESC[29ms ESC[2mF ESC[22mf ESC[8mH ESC[28mh ESC[1;2;7;8;9mX ESC[0mx, then ESC[?25h, and prints snapshot({ includeCells: true }):

Script
import { createTerminal, getNativeInfo } from "@coder/libghostty-vt-node";

const term = createTerminal({ cols: 40, rows: 4 });
const steps = [];
const snap = (label) => {
  const s = term.snapshot({ includeCells: true });
  steps.push({ label, cursorVisible: s.cursorVisible, cursorRow: s.cursorRow, cursorCol: s.cursorCol });
  return s;
};
snap("initial");
term.feed("\x1b[?25l");
snap("after ESC[?25l");
term.feed("\x1b[31;42;7mI\x1b[27mi\x1b[0m \x1b[9mS\x1b[29ms \x1b[2mF\x1b[22mf \x1b[8mH\x1b[28mh \x1b[1;2;7;8;9mX\x1b[0mx");
term.feed("\x1b[?25h");
const s = snap("after ESC[?25h");
console.log(JSON.stringify({ ghostty: getNativeInfo().ghosttyCommit, cursorSteps: steps, line0: s.visibleLines[0].text, cells: s.cells }, null, 1));
term.dispose();

Output (one cell per line):

{"cursorSteps":[{"label":"initial","cursorVisible":true,"cursorRow":0,"cursorCol":0},{"label":"after ESC[?25l","cursorVisible":false,"cursorRow":0,"cursorCol":0},{"label":"after ESC[?25h","cursorVisible":true,"cursorRow":0,"cursorCol":14}],"line0":"Ii Ss Ff Hh Xx"}
{"row":0,"col":0,"text":"I","width":1,"inverse":true,"foreground":"#cc6666","background":"#b5bd68"}
{"row":0,"col":1,"text":"i","width":1,"foreground":"#cc6666","background":"#b5bd68"}
{"row":0,"col":2,"text":" ","width":1}
{"row":0,"col":3,"text":"S","width":1,"strikethrough":true}
{"row":0,"col":4,"text":"s","width":1}
{"row":0,"col":5,"text":" ","width":1}
{"row":0,"col":6,"text":"F","width":1,"faint":true}
{"row":0,"col":7,"text":"f","width":1}
{"row":0,"col":8,"text":" ","width":1}
{"row":0,"col":9,"text":"H","width":1,"invisible":true}
{"row":0,"col":10,"text":"h","width":1}
{"row":0,"col":11,"text":" ","width":1}
{"row":0,"col":12,"text":"X","width":1,"bold":true,"faint":true,"inverse":true,"invisible":true,"strikethrough":true}
{"row":0,"col":13,"text":"x","width":1}

The uppercase cells carry the attribute, and the lowercase cells after each reset do not. I keeps #cc6666 on #b5bd68 (palette 1 on palette 2), the same as unswapped i.

Notes for agent-tty

  • Apply reverse video by swapping foreground/background yourself, substituting your default colors when either one is absent.
  • invisible cells still report their text, and visibleLines/getVisibleText() still include hidden text (unchanged). Skip drawing glyphs for invisible cells.
  • Unchanged limitation: cells only contains cells with text. Erased cells that have only a background color (for example, an ESC[K status bar fill) are not emitted.

Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

…le in snapshots

Read the remaining SGR flags from GhosttyStyle and DECTCEM from
GHOSTTY_TERMINAL_DATA_CURSOR_VISIBLE. Cell flags are omitted when unset,
matching bold/italic/underline. inverse is the raw attribute; colors are
not swapped.

Closes #15

Change-Id: I4e10cde9ffd9e8b79171b1945692e9560664ffb2
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review
@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-24T12:06:46.439329Z f19f8e5 Manual request
🔒 Security Review ✅ Completed 2026-09-24T12:11:42.671596Z f19f8e5 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: f19f8e5924

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: f19f8e5924

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

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.

Expose inverse, strikethrough, faint, invisible and cursor visibility in snapshot cells

1 participant