Repository navigation
OSC 22 cursor support - #137
natemoo-re wants to merge 9 commits into
Conversation
Changeset suggestionThe current changeset no longer matches this PR. This review contains a corrected replacement. Why: Changeset package scope does not match the affected packages confidently. Changeset contains implementation or roadmap noise. View the proposed changeset---
'@bomb.sh/tty': minor
---
Adds a `cursor` property to `open()` so the mouse pointer changes shape (via OSC 22) while it hovers over an element.
When `term.capabilities.pointerShape` is `true`, `render()` appends the resolved shape to `output` after the frame's cell bytes. The shape resets to `default` when the pointer leaves, when a frame renders without `pointer`, or when `update()` withdraws the capability. `detectTerminal()` seeds `pointerShape: true` for terminals that set shapes without answering the query (ghostty, kitty, foot, xterm ≥ 367). For anything else, assert support with `term.update([{ type: "capability", key: "pointer-shape", value: true }])`.
```ts
term.render([open("link", { cursor: "pointer" }), text("Docs"), close()], { pointer });
```
#### Migration
`pointerShape` moves from `RuntimeCapabilities` to the base `Capabilities` interface so it is present from the first `term.capabilities` read (seeded by `detectTerminal()`, then raised by the probe reply).
```diff
-import type { RuntimeCapabilities } from "@bomb.sh/tty";
+import type { Capabilities } from "@bomb.sh/tty";
-const supported = term.capabilities.pointerShape; // was RuntimeCapabilities-only
+const supported = term.capabilities.pointerShape; // now available on Capabilities
```
Code that only reads `term.capabilities.pointerShape` will continue to compile; code that constructed a `RuntimeCapabilities` object literal must remove the `pointerShape` field from that object.Review this changeset manually If this draft is correct, react with 🚀 and Cooper will commit it to this branch. |
| @@ -0,0 +1,11 @@ | |||
| --- | |||
There was a problem hiding this comment.
Changeset needs revision.
Changeset package scope does not match the affected packages confidently. Changeset contains implementation or roadmap noise.
View the proposed replacement
---
'@bomb.sh/tty': minor
---
Adds a `cursor` property to `open()` so the mouse pointer changes shape (via OSC 22) while it hovers over an element.
When `term.capabilities.pointerShape` is `true`, `render()` appends the resolved shape to `output` after the frame's cell bytes. The shape resets to `default` when the pointer leaves, when a frame renders without `pointer`, or when `update()` withdraws the capability. `detectTerminal()` seeds `pointerShape: true` for terminals that set shapes without answering the query (ghostty, kitty, foot, xterm ≥ 367). For anything else, assert support with `term.update([{ type: "capability", key: "pointer-shape", value: true }])`.
```ts
term.render([open("link", { cursor: "pointer" }), text("Docs"), close()], { pointer });
```
#### Migration
`pointerShape` moves from `RuntimeCapabilities` to the base `Capabilities` interface so it is present from the first `term.capabilities` read (seeded by `detectTerminal()`, then raised by the probe reply).
```diff
-import type { RuntimeCapabilities } from "@bomb.sh/tty";
+import type { Capabilities } from "@bomb.sh/tty";
-const supported = term.capabilities.pointerShape; // was RuntimeCapabilities-only
+const supported = term.capabilities.pointerShape; // now available on Capabilities
```
Code that only reads `term.capabilities.pointerShape` will continue to compile; code that constructed a `RuntimeCapabilities` object literal must remove the `pointerShape` field from that object.
commit: |
|
Size Increased — +5.2 KB 127.9 KB unpacked |
50be391 to
926c307
Compare
Merging this PR will degrade performance by 22.15%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing |
af52e23 to
082e1e9
Compare
…lity Adds renderer-spec §7.9 and wires it into the existing capability sections instead of a standalone feature spec: pointer shape is one more consumer of RuntimeCapabilities, gated the same way as the rest. - §7.9: open() gains pointerShape (kitty/CSS shape names; anything else is ignored and never reaches output). While capabilities.pointerShape is true, the last declaring element in hit-test order wins (innermost; capture-mode floats hide what is beneath, passthrough floats defer to it). Changes are appended to result.output as OSC 22 ; <shape> ST, so the host keeps writing one buffer (cowboyd on #101, replacing #101's separate result.cursor). There is no createTerm opt-in: the capability is the only gate. - Restore (dreyfus92 on #101): set-only OSC 22 cannot pop, so the Term sets "default" when the pointer leaves or is omitted, and update() returns the reset when the capability is withdrawn. Resize keeps the emitted shape since the terminal pointer is unaffected. - §7.3: output "may be" a view, since a frame carrying OSC 22 is assembled in TypeScript. - §7.7, §7.8, §11.2, §12.2 and terminfo-spec §6.3/§9.3 point at §7.9 instead of a "focused feature specification". terminfo-spec §6.3 documents the escape hatch for set-only terminals that never answer the query: term.update([{ type: "capability", key: "pointer-shape", value: true }]). "pointerShape" rather than "cursor": cursor already names the hardware text cursor (§7.6), and pointerShape matches the capability key and RuntimeCapabilities field.
#101's bespoke parse_osc() drew review for unbounded digit accumulation on `ESC ] 9999...`. The rebuild drops that parser and relies on #132's parse_osc_response(), which already bails once the OSC number passes 22 and caps payloads at MAX_RESPONSE. These tests pin that behavior for OSC 22 so it cannot regress: BEL and ST terminators, the "0" empty-stack reply, byte-at-a-time delivery, a broken ST, a 64-digit OSC number, OSC 220, and an over-long payload (no event, parser recovers for the next reply).
… them (renderer-spec §7.9)
Elements declare open(id, { pointerShape }). While
term.capabilities.pointerShape is true (raised by #132/#133 from the
probe's OSC 22 query reply), each render resolves the shape under the
pointer and appends `OSC 22 ; <shape> ST` to result.output when it
changes. The host keeps writing one buffer (cowboyd on #101). The
capability is the only gate; there is no createTerm option.
Everything lives in TypeScript; the wasm module and the packed encoding
are untouched, so there is no size or startup cost (#101 regressed
createTerm ~20% and grew the bundle 8.3 KB). Without the capability the
render path pays one boolean check and never reads pointerShape (a test
pins this with a counting getter). With it, the directive walk runs only
when the pointer is over something, and only a frame whose shape
changed copies output to append the sequence.
Resolution takes the last declaring id in Clay's pointer-over order:
pre-order within the topmost layer the pointer reaches, so the innermost
element wins and capture-mode floats hide what is beneath while
passthrough floats defer to it. snapshot() records its declared shapes
beside the packed bytes so pre-packed subtrees still participate.
Values outside the kitty/CSS vocabulary are ignored (and rejected by
validate()), so arbitrary strings never reach an OSC payload.
Restore (dreyfus92 on #101): set-only OSC 22 cannot pop, so the emitted
shape is the one piece of cross-frame state. It resets to "default"
when the pointer leaves or is omitted, and update() returns the reset
when the pointer-shape capability is withdrawn. Resize keeps it, since
the terminal's pointer did not change.
…e mode AGENTS.md's "renderer MUST NOT manage terminal state" invariant listed only the caret exception. Pointer shape is the second: the renderer owns it only while the pointerShape capability is true (renderer-spec §7.9). renderer-spec §7.9 now says resolution and output are identical in line mode, settling cowboyd's open question on #101. Bytes appear only when the caller passes `pointer`, which already implies an interactive terminal with mouse reporting, so a line-mode carve-out would protect nothing. The "emits in line mode" test in the feat commit pins it.
The OSC 22 query is answered by kitty alone; every other set-supporting terminal (ghostty, foot, xterm ≥ 367) never replies, so detection could never see them and the demos silently degraded in the most popular modern terminal. terminfo-spec §6.1 now carries a known-support table of terminals with primary evidence for OSC 22 set support — ghostty (its own docs), kitty (its own spec), foot (foot-ctlseqs(7)), and xterm (ctlseqs, version floor 367 via XTERM_VERSION). detectTerminal() seeds pointerShape from the environment; the probe reply remains authoritative evidence, and the manual CapabilityEvent opt-in stays for anything the table misses. mintty is deliberately excluded: it implements OSC 22 but its accepted names are Windows cursor names (hand, ibeam), not the CSS names §7.9 emits. Rio and Konsole lack primary evidence (Konsole is contradicted by ucs-detect, which — like our probe — tests the query form). The spec records the accepted false-positive trade: nested contexts that inherit the environment unchanged (Emacs term-mode) are granted by association; muxers and IDE terminals rewrite their identity and are naturally excluded.
Walks the full OSC 22 pointer-shape chain in the user's terminal — probe round-trip with per-query breakdown, parser CapabilityEvents, renderer emission — and separates query-less set-supporting terminals from terminals without OSC 22 at all. Built to debug the ghostty detection gap; kept as a general diagnostic for the capability layer.
debug-pointer-shape.ts walked the whole capability chain already; only its last section was pointer-shape specific. Renamed and generalized: it now prints the full static Capabilities at detection, the merged RuntimeCapabilities after folding probe events, and which fields the probe raised. The pointer-shape section stays as the end-to-end emission check (the one capability render() consumes today), with the verdict distinguishing probe-confirmed support from a known-support table grant.
082e1e9 to
b9f09b4
Compare
CSS names the mouse-pointer property cursor and the insertion-point
properties caret-*, and the library already calls the insertion point
caret (text()'s caret declaration, §7.6) — so the element property now
matches CSS exactly: open(id, { cursor: "grab" }).
The capability layer keeps its names: the CapabilityEvent key stays
"pointer-shape", RuntimeCapabilities.pointerShape stays, and
pointer-shape.ts / PointerShape / POINTER_SHAPES / resolvePointerShape
stay — those name the kitty OSC 22 pointer-shape protocol, which is
what they are on the wire.
Minor changeset: we're 0.x.
Supercedes #101.
Adds support for the OSC 22 cursor shapes, which supports a subset of the CSS
cursorkeywords.The public API is an element that accepts the pointer shape:
open(id, { cursor: "pointer" }). The renderer writes the escape sequence directly intoresult.output. This works in line mode and alt screens.Review comments from #101 have been addressed and regression tests added.
OSC 22 bytes are only emitted for terminals that report support (using the probe infrastructure added in #132) or have been documented to support the escape sequence without supporting the query form (ghostty).
Demos updated in #144.