···3131- The renderer MUST NOT perform IO. It produces bytes; the caller writes them.
32323333- The renderer MUST NOT manage terminal state (alternate buffer, mouse
3434- reporting, keyboard protocol modes). Cursor visibility and positioning
3535- are renderer-managed only when a `caret` declaration is present on
3636- a `text()` directive (see renderer-spec.md §7.6).
3434+ reporting, keyboard protocol modes). Cursor visibility and positioning are
3535+ renderer-managed only when a `caret` declaration is present on a `text()`
3636+ directive (see renderer-spec.md §7.6).
37373838- Each frame is a complete snapshot. The renderer carries no UI tree state
3939 between frames — only cell buffers for diffing.
+39-45
specs/renderer-spec.md
···337337### 7.6 Hardware cursor visibility and positioning
338338339339A `text()` directive MAY declare a `caret` property whose value is a
340340-non-negative integer code-point offset into the directive's `content`.
341341-`0` means "before the first code point"; `[...content].length` means
342342-"after the last." Offsets in between sit immediately before the cell
343343-where the corresponding code point would render.
340340+non-negative integer code-point offset into the directive's `content`. `0` means
341341+"before the first code point"; `[...content].length` means "after the last."
342342+Offsets in between sit immediately before the cell where the corresponding code
343343+point would render.
344344345345-The cell where a declared caret sits is the cell at which the code
346346-point at the caret's offset would be drawn given the layout engine's
347347-text wrapping. For an offset `N`:
345345+The cell where a declared caret sits is the cell at which the code point at the
346346+caret's offset would be drawn given the layout engine's text wrapping. For an
347347+offset `N`:
348348349349-- If `N < [...content].length`, the caret's cell is the display
350350- position of the `N`-th code point (zero-indexed) within the rendered
351351- text.
352352-- If `N == [...content].length`, the caret's cell is one display
353353- position past the last rendered code point: on the same wrapped line
354354- if there is room, or at the start of the next line if the layout
355355- wraps at the end.
356356-- If `N > [...content].length` or `N < 0`, behavior is unspecified;
357357- callers must keep offsets within bounds.
349349+- If `N < [...content].length`, the caret's cell is the display position of the
350350+ `N`-th code point (zero-indexed) within the rendered text.
351351+- If `N == [...content].length`, the caret's cell is one display position past
352352+ the last rendered code point: on the same wrapped line if there is room, or at
353353+ the start of the next line if the layout wraps at the end.
354354+- If `N > [...content].length` or `N < 0`, behavior is unspecified; callers must
355355+ keep offsets within bounds.
358356359359-Display position accounts for code points wider than one cell (CJK,
360360-fullwidth forms, some emoji): such code points occupy two cells, and
361361-the caret sits at the first cell of the pair. Cell widths are
362362-determined by the same measurement the renderer uses to lay out the
363363-text itself.
357357+Display position accounts for code points wider than one cell (CJK, fullwidth
358358+forms, some emoji): such code points occupy two cells, and the caret sits at the
359359+first cell of the pair. Cell widths are determined by the same measurement the
360360+renderer uses to lay out the text itself.
364361365365-The renderer manages the terminal's hardware cursor based on the
366366-presence of `caret:` declarations:
362362+The renderer manages the terminal's hardware cursor based on the presence of
363363+`caret:` declarations:
367364368368-- When the current frame contains one or more `caret:` declarations,
369369- the rendered `output` MUST, when written to the terminal, leave the
370370- hardware cursor visible at the cell where the first declared caret
371371- (in directive order) sits.
365365+- When the current frame contains one or more `caret:` declarations, the
366366+ rendered `output` MUST, when written to the terminal, leave the hardware
367367+ cursor visible at the cell where the first declared caret (in directive order)
368368+ sits.
372369373370- When the current frame contains no `caret:` declarations:
374374- - If the previous frame contained one, the rendered `output` MUST
375375- leave the hardware cursor hidden.
376376- - Otherwise, the rendered `output` MUST NOT include
377377- cursor-positioning or cursor-visibility bytes; the caller's
378378- cursor state is preserved.
371371+ - If the previous frame contained one, the rendered `output` MUST leave the
372372+ hardware cursor hidden.
373373+ - Otherwise, the rendered `output` MUST NOT include cursor-positioning or
374374+ cursor-visibility bytes; the caller's cursor state is preserved.
379375380376The byte-level path the renderer takes to satisfy these outcomes is
381381-implementation-defined. The renderer MAY hide the cursor before cell
382382-writes to prevent flicker, MAY omit such a hide for in-place edits
383383-where flicker is acceptable, MAY emit only a CUP when the caret has
384384-moved within an already-visible state, MAY use whatever escape-sequence
385385-path it judges appropriate. Only the post-frame cursor state above is
386386-normative.
377377+implementation-defined. The renderer MAY hide the cursor before cell writes to
378378+prevent flicker, MAY omit such a hide for in-place edits where flicker is
379379+acceptable, MAY emit only a CUP when the caret has moved within an
380380+already-visible state, MAY use whatever escape-sequence path it judges
381381+appropriate. Only the post-frame cursor state above is normative.
387382388388-If more than one `caret:` declaration is present in a frame, the
389389-renderer SHOULD use the first in directive order for the hardware
390390-cursor. Behavior for additional declarations is intentionally
391391-unspecified, leaving room for a future multi-cursor extension without
392392-breaking this contract.
383383+If more than one `caret:` declaration is present in a frame, the renderer SHOULD
384384+use the first in directive order for the hardware cursor. Behavior for
385385+additional declarations is intentionally unspecified, leaving room for a future
386386+multi-cursor extension without breaking this contract.
393387394394-This responsibility is limited to the hardware cursor's position and
395395-visibility. Cursor shape and blink rate remain caller-managed.
388388+This responsibility is limited to the hardware cursor's position and visibility.
389389+Cursor shape and blink rate remain caller-managed.
396390397391---
398392
+8-6
src/clayterm.c
···8888 * first text node carrying a caret declaration per frame, then locates
8989 * the corresponding cell after Clay layout. had_caret_last_frame is the
9090 * only cross-frame bit retained. */
9191- const char *caret_text_chars; /* start of caret-bearing text node's bytes, or NULL */
9292- int caret_text_length; /* byte length of that text node */
9393- uint32_t caret_offset; /* code-point offset within that text node */
9494- int caret_x, caret_y; /* resolved cell (column, row), valid only when caret_text_chars != NULL */
9595- int has_caret; /* 1 when the current frame placed a caret */
9696- int had_caret_last_frame; /* 1 when the previous frame placed a caret */
9191+ const char
9292+ *caret_text_chars; /* start of caret-bearing text node's bytes, or NULL */
9393+ int caret_text_length; /* byte length of that text node */
9494+ uint32_t caret_offset; /* code-point offset within that text node */
9595+ int caret_x, caret_y; /* resolved cell (column, row), valid only when
9696+ caret_text_chars != NULL */
9797+ int has_caret; /* 1 when the current frame placed a caret */
9898+ int had_caret_last_frame; /* 1 when the previous frame placed a caret */
9799};
9810099101/* Memory layout inside the arena provided by the host:
+2-1
test/term.test.ts
···11+// deno-lint-ignore-file no-control-regex
12import { beforeEach, describe, expect, it } from "./suite.ts";
23import { createTerm, type Term } from "../term.ts";
34import {
···130131 let out = decode(
131132 term.render(box("hello world"), { mode: "line" }).output,
132133 );
133133- // deno-lint-ignore no-control-regex
134134+134135 expect(out).not.toMatch(/\x1b\[\d+;\d+H/);
135136 expect(out.split("\n").length).toBe(5);
136137 expect(trim(print(out, 20, 5))).toEqual(`