@alchemy.run/sigil 0.0.0-alpha.1 → 0.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/README.md +499 -313
  2. package/THIRD_PARTY_NOTICES.md +70 -23
  3. package/dist/Text-DV9CuzAT.d.ts +452 -0
  4. package/dist/ansi.d.ts +217 -0
  5. package/dist/ansi.js +87 -0
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BAC9-TZX.js +592 -0
  11. package/dist/color-policy-CCxuHIdD.d.ts +22 -0
  12. package/dist/color-profile-CyeHnG1T.d.ts +97 -0
  13. package/dist/color-profile-DHhQHY55.js +36 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-B3dL4Q11.js +374 -0
  18. package/dist/detect-Db6GbKOm.d.ts +195 -0
  19. package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-D48vQhhe.d.ts +21 -0
  24. package/dist/index-DDVME65c.d.ts +919 -0
  25. package/dist/index.d.ts +1310 -0
  26. package/dist/index.js +3209 -0
  27. package/dist/osc-BFKKSqpg.js +71 -0
  28. package/dist/osc-Cn0fw77g.d.ts +23 -0
  29. package/dist/paint-Cx-zC_sX.d.ts +81 -0
  30. package/dist/query-BNc2B8GD.d.ts +152 -0
  31. package/dist/router.d.ts +392 -0
  32. package/dist/router.js +709 -0
  33. package/dist/sample-Cqw1bjUL.js +445 -0
  34. package/dist/screen-CgC2WlVM.d.ts +49 -0
  35. package/dist/screen-CiPytswf.js +342 -0
  36. package/dist/screen.d.ts +5 -0
  37. package/dist/screen.js +5 -0
  38. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  39. package/dist/serialize-BTkAZgw1.js +79 -0
  40. package/dist/session-DDQ5V300.js +723 -0
  41. package/dist/sgr-BhwaWAJB.js +246 -0
  42. package/dist/store-C1P5fOUi.d.ts +72 -0
  43. package/dist/string-width-CijQwpIk.js +69 -0
  44. package/dist/strip-BvU4toXG.js +6 -0
  45. package/dist/terminal.d.ts +121 -0
  46. package/dist/terminal.js +2 -0
  47. package/dist/tokenize-AjqbvtiT.js +1242 -0
  48. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  49. package/dist/truncate-D31fhU6i.js +562 -0
  50. package/dist/use-focus-CcCIdCWA.js +1337 -0
  51. package/dist/yoga-5jKhYCJC.js +3465 -0
  52. package/dist/yoga.d.ts +2 -0
  53. package/dist/yoga.js +2 -0
  54. package/package.json +70 -24
  55. package/src/ansi/chalk.ts +138 -0
  56. package/src/ansi/cursor.ts +46 -0
  57. package/src/ansi/east-asian-width.ts +259 -0
  58. package/src/ansi/escapes.ts +120 -0
  59. package/src/ansi/graphemes.ts +8 -0
  60. package/src/ansi/hyperlink.ts +44 -0
  61. package/src/ansi/index.ts +27 -0
  62. package/src/ansi/osc.ts +77 -0
  63. package/src/ansi/sgr.ts +234 -0
  64. package/src/ansi/slice.ts +43 -0
  65. package/src/ansi/string-width.ts +121 -0
  66. package/src/ansi/strip.ts +13 -0
  67. package/src/ansi/tokenize.ts +380 -0
  68. package/src/ansi/truncate.ts +196 -0
  69. package/src/ansi/wrap.ts +765 -0
  70. package/src/ansi-tokenizer.ts +510 -0
  71. package/src/capabilities/color-policy.ts +34 -0
  72. package/src/capabilities/detect.ts +608 -0
  73. package/src/capabilities/index.ts +37 -0
  74. package/src/capabilities/query.ts +679 -0
  75. package/src/capabilities/store.ts +394 -0
  76. package/src/code-excerpt.ts +39 -0
  77. package/src/color/index.ts +3 -0
  78. package/src/color/paint.ts +169 -0
  79. package/src/color/palette.ts +48 -0
  80. package/src/color/sample.ts +323 -0
  81. package/src/color.ts +1 -0
  82. package/src/components/AccessibilityContext.ts +5 -0
  83. package/src/components/AnimationContext.ts +24 -0
  84. package/src/components/AnsiText.tsx +42 -0
  85. package/src/components/App.tsx +878 -0
  86. package/src/components/AppContext.ts +111 -0
  87. package/src/components/BackgroundContext.ts +7 -0
  88. package/src/components/Box.tsx +100 -0
  89. package/src/components/CursorContext.ts +19 -0
  90. package/src/components/ErrorBoundary.tsx +39 -0
  91. package/src/components/ErrorOverview.tsx +135 -0
  92. package/src/components/FocusContext.ts +30 -0
  93. package/src/components/Hyperlink.tsx +56 -0
  94. package/src/components/Newline.tsx +16 -0
  95. package/src/components/Spacer.tsx +11 -0
  96. package/src/components/Static.tsx +60 -0
  97. package/src/components/StderrContext.ts +24 -0
  98. package/src/components/StdinContext.ts +48 -0
  99. package/src/components/StdoutContext.ts +26 -0
  100. package/src/components/TerminalOscContext.ts +25 -0
  101. package/src/components/Text.tsx +122 -0
  102. package/src/components/Transform.tsx +38 -0
  103. package/src/components/VirtualList.tsx +128 -0
  104. package/src/cursor-position.ts +103 -0
  105. package/src/devtools.ts +103 -0
  106. package/src/dom.ts +301 -0
  107. package/src/env.ts +12 -0
  108. package/src/get-max-width.ts +11 -0
  109. package/src/global.d.ts +38 -0
  110. package/src/glyphs.ts +99 -0
  111. package/src/hooks/use-animation.ts +142 -0
  112. package/src/hooks/use-app.ts +8 -0
  113. package/src/hooks/use-box-metrics.ts +134 -0
  114. package/src/hooks/use-capabilities.ts +73 -0
  115. package/src/hooks/use-cursor.ts +33 -0
  116. package/src/hooks/use-focus-manager.ts +62 -0
  117. package/src/hooks/use-focus.ts +82 -0
  118. package/src/hooks/use-input.ts +267 -0
  119. package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
  120. package/src/hooks/use-paste.ts +78 -0
  121. package/src/hooks/use-stderr.ts +8 -0
  122. package/src/hooks/use-stdin.ts +10 -0
  123. package/src/hooks/use-stdout.ts +8 -0
  124. package/src/hooks/use-terminal-osc.ts +59 -0
  125. package/src/hooks/use-virtual-scroll.ts +84 -0
  126. package/src/hooks/use-window-size.ts +37 -0
  127. package/src/index.ts +96 -0
  128. package/src/ink.tsx +1452 -0
  129. package/src/input-parser.ts +303 -0
  130. package/src/instances.ts +9 -0
  131. package/src/kitty-keyboard.ts +185 -0
  132. package/src/measure-element.ts +62 -0
  133. package/src/measure-text.ts +31 -0
  134. package/src/paint-tree.ts +220 -0
  135. package/src/parse-keypress.ts +515 -0
  136. package/src/parse-stack-line.ts +138 -0
  137. package/src/patch-console.ts +106 -0
  138. package/src/quick-lru.ts +85 -0
  139. package/src/reconciler.ts +425 -0
  140. package/src/render-background.ts +59 -0
  141. package/src/render-border.ts +167 -0
  142. package/src/render-frame.ts +83 -0
  143. package/src/render-to-string.ts +146 -0
  144. package/src/render.ts +284 -0
  145. package/src/router/components.tsx +343 -0
  146. package/src/router/context.ts +41 -0
  147. package/src/router/history.ts +194 -0
  148. package/src/router/hooks.tsx +391 -0
  149. package/src/router/index.ts +34 -0
  150. package/src/router/matcher.ts +571 -0
  151. package/src/sanitize-ansi.ts +33 -0
  152. package/src/screen/ansi.ts +184 -0
  153. package/src/screen/canvas.ts +160 -0
  154. package/src/screen/cell.ts +138 -0
  155. package/src/screen/color-profile.ts +47 -0
  156. package/src/screen/geometry.ts +9 -0
  157. package/src/screen/index.ts +6 -0
  158. package/src/screen/screen.ts +305 -0
  159. package/src/screen/serialize.ts +129 -0
  160. package/src/screen.ts +1 -0
  161. package/src/semantic-text-style.ts +118 -0
  162. package/src/signal-exit.ts +106 -0
  163. package/src/squash-text-nodes.ts +37 -0
  164. package/src/stream.ts +28 -0
  165. package/src/structured-text.ts +325 -0
  166. package/src/styles.ts +753 -0
  167. package/src/terminal/index.ts +2 -0
  168. package/src/terminal/inline-presenter.ts +120 -0
  169. package/src/terminal/input.ts +92 -0
  170. package/src/terminal/render-scheduler.ts +37 -0
  171. package/src/terminal/screen-presenter.ts +242 -0
  172. package/src/terminal/session.ts +408 -0
  173. package/src/terminal-size.ts +58 -0
  174. package/src/terminal.ts +1 -0
  175. package/src/testing/browser.ts +588 -0
  176. package/src/testing/emulators.ts +205 -0
  177. package/src/testing/explorer-app/index.html +12 -0
  178. package/src/testing/explorer-app/main.ts +381 -0
  179. package/src/testing/explorer-app/style.css +194 -0
  180. package/src/testing/explorer-app/tsconfig.json +15 -0
  181. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  182. package/src/testing/index.ts +26 -0
  183. package/src/testing/keys.ts +56 -0
  184. package/src/testing/live.ts +85 -0
  185. package/src/testing/matchers.ts +70 -0
  186. package/src/testing/public.ts +94 -0
  187. package/src/testing/terminal.ts +361 -0
  188. package/src/testing/vitest.ts +157 -0
  189. package/src/throttle.ts +73 -0
  190. package/src/transform-adapter.ts +14 -0
  191. package/src/types.ts +10 -0
  192. package/src/virtual-scroll.ts +133 -0
  193. package/src/wrap-text.ts +54 -0
  194. package/src/yoga/config.ts +57 -0
  195. package/src/yoga/core/absoluteLayout.ts +626 -0
  196. package/src/yoga/core/baseline.ts +66 -0
  197. package/src/yoga/core/cache.ts +136 -0
  198. package/src/yoga/core/calculateLayout.ts +2926 -0
  199. package/src/yoga/core/config.ts +104 -0
  200. package/src/yoga/core/flexLine.ts +177 -0
  201. package/src/yoga/core/helpers.ts +293 -0
  202. package/src/yoga/core/layoutResults.ts +167 -0
  203. package/src/yoga/core/node.ts +611 -0
  204. package/src/yoga/core/numeric.ts +44 -0
  205. package/src/yoga/core/pixelGrid.ts +151 -0
  206. package/src/yoga/core/style.ts +887 -0
  207. package/src/yoga/core/types.ts +224 -0
  208. package/src/yoga/generated/YGEnums.ts +263 -0
  209. package/src/yoga/index.ts +19 -0
  210. package/src/yoga/node.ts +1140 -0
  211. package/dist/index.d.mts +0 -2379
  212. package/dist/index.mjs +0 -10072
@@ -0,0 +1,2 @@
1
+ export * from "#/terminal/input.ts";
2
+ export * from "#/terminal/session.ts";
@@ -0,0 +1,120 @@
1
+ import { cliCursor } from "#/ansi/cursor.ts";
2
+ import { ansiEscapes } from "#/ansi/escapes.ts";
3
+ import {
4
+ buildCursorOnlySequence,
5
+ buildCursorSuffix,
6
+ buildReturnToBottomPrefix,
7
+ cursorPositionChanged,
8
+ hideCursorEscape,
9
+ type CursorPosition,
10
+ } from "#/cursor-position.ts";
11
+
12
+ export type InlinePresenter = {
13
+ clear: () => void;
14
+ done: () => void;
15
+ reset: () => void;
16
+ sync: (output: string) => void;
17
+ setCursorPosition: (position: CursorPosition | undefined) => void;
18
+ isCursorDirty: () => boolean;
19
+ willRender: (output: string) => boolean;
20
+ (output: string): boolean;
21
+ };
22
+
23
+ /** Full-rewrite fallback and cursor state for the structured inline renderer. */
24
+ export function createInlinePresenter(
25
+ stream: NodeJS.WritableStream,
26
+ options: { readonly showCursor?: boolean } = {},
27
+ ): InlinePresenter {
28
+ let previousLineCount = 0;
29
+ let previousOutput = "";
30
+ let hasHiddenCursor = false;
31
+ let cursorPosition: CursorPosition | undefined;
32
+ let cursorDirty = false;
33
+ let previousCursorPosition: CursorPosition | undefined;
34
+ let cursorWasShown = false;
35
+ const showCursor = options.showCursor ?? false;
36
+
37
+ const activeCursor = () => (cursorDirty ? cursorPosition : undefined);
38
+ const hasChanges = (output: string, cursor: CursorPosition | undefined) =>
39
+ output !== previousOutput || cursorPositionChanged(cursor, previousCursorPosition);
40
+
41
+ const present = (output: string): boolean => {
42
+ if (!showCursor && !hasHiddenCursor) {
43
+ cliCursor.hide(stream);
44
+ hasHiddenCursor = true;
45
+ }
46
+ const cursor = activeCursor();
47
+ cursorDirty = false;
48
+ const cursorChanged = cursorPositionChanged(cursor, previousCursorPosition);
49
+ if (!hasChanges(output, cursor)) return false;
50
+ const lines = output.split("\n");
51
+ const suffix = buildCursorSuffix(lines.length - 1, cursor);
52
+ if (output === previousOutput && cursorChanged) {
53
+ stream.write(
54
+ buildCursorOnlySequence({
55
+ cursorWasShown,
56
+ previousLineCount,
57
+ previousCursorPosition,
58
+ cursorPosition: cursor,
59
+ }),
60
+ );
61
+ } else {
62
+ previousOutput = output;
63
+ stream.write(
64
+ buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition) +
65
+ ansiEscapes.eraseLines(previousLineCount) +
66
+ output +
67
+ suffix,
68
+ );
69
+ previousLineCount = lines.length;
70
+ }
71
+ previousCursorPosition = cursor ? { ...cursor } : undefined;
72
+ cursorWasShown = cursor !== undefined;
73
+ return true;
74
+ };
75
+
76
+ present.clear = () => {
77
+ stream.write(
78
+ buildReturnToBottomPrefix(cursorWasShown, previousLineCount, previousCursorPosition) +
79
+ ansiEscapes.eraseLines(previousLineCount),
80
+ );
81
+ previousOutput = "";
82
+ previousLineCount = 0;
83
+ previousCursorPosition = undefined;
84
+ cursorWasShown = false;
85
+ };
86
+ present.done = () => {
87
+ previousOutput = "";
88
+ previousLineCount = 0;
89
+ previousCursorPosition = undefined;
90
+ cursorWasShown = false;
91
+ if (!showCursor) {
92
+ cliCursor.show(stream);
93
+ hasHiddenCursor = false;
94
+ }
95
+ };
96
+ present.reset = () => {
97
+ previousOutput = "";
98
+ previousLineCount = 0;
99
+ previousCursorPosition = undefined;
100
+ cursorWasShown = false;
101
+ };
102
+ present.sync = (output: string) => {
103
+ const cursor = activeCursor();
104
+ cursorDirty = false;
105
+ const lines = output.split("\n");
106
+ previousOutput = output;
107
+ previousLineCount = lines.length;
108
+ if (!cursor && cursorWasShown) stream.write(hideCursorEscape);
109
+ if (cursor) stream.write(buildCursorSuffix(lines.length - 1, cursor));
110
+ previousCursorPosition = cursor ? { ...cursor } : undefined;
111
+ cursorWasShown = cursor !== undefined;
112
+ };
113
+ present.setCursorPosition = (position: CursorPosition | undefined) => {
114
+ cursorPosition = position;
115
+ cursorDirty = true;
116
+ };
117
+ present.isCursorDirty = () => cursorDirty;
118
+ present.willRender = (output: string) => hasChanges(output, activeCursor());
119
+ return present;
120
+ }
@@ -0,0 +1,92 @@
1
+ import { isTerminalQueryResponse } from "#/capabilities/query.ts";
2
+ import type { CapabilitiesStore } from "#/capabilities/store.ts";
3
+ import { createInputParser, type InputEvent } from "#/input-parser.ts";
4
+
5
+ export type MouseButton = "left" | "middle" | "right" | "none" | "wheel-up" | "wheel-down";
6
+ export type TerminalMouseEvent = {
7
+ readonly type: "press" | "release" | "move" | "wheel";
8
+ readonly x: number;
9
+ readonly y: number;
10
+ readonly button: MouseButton;
11
+ readonly shift: boolean;
12
+ readonly alt: boolean;
13
+ readonly ctrl: boolean;
14
+ };
15
+
16
+ const sgrMouse = /^\u001B\[<(\d+);(\d+);(\d+)([Mm])$/;
17
+
18
+ /** Stateful stdin decoder that separates terminal reports from application input. */
19
+ export class TerminalInput {
20
+ readonly #parser = createInputParser();
21
+ readonly #capabilities: CapabilitiesStore;
22
+ readonly #mouseListeners = new Set<(event: TerminalMouseEvent) => void>();
23
+
24
+ constructor(capabilities: CapabilitiesStore) {
25
+ this.#capabilities = capabilities;
26
+ }
27
+
28
+ push(chunk: string): InputEvent[] {
29
+ return this.#parser.push(chunk).flatMap((event): InputEvent[] => {
30
+ if (typeof event !== "string") return [event];
31
+ // A user Escape immediately before a reply is not an Alt-modified key.
32
+ if (event.startsWith("\u001B\u001B") && isTerminalQueryResponse(event.slice(1))) {
33
+ return ["\u001B"];
34
+ }
35
+ if (this.#capabilities.ingest(event)) return [];
36
+ if (isTerminalQueryResponse(event)) return [];
37
+ const mouse = parseMouseEvent(event);
38
+ if (!mouse) return [event];
39
+ for (const listener of this.#mouseListeners) listener(mouse);
40
+ return [];
41
+ });
42
+ }
43
+
44
+ subscribeMouse(listener: (event: TerminalMouseEvent) => void): () => void {
45
+ this.#mouseListeners.add(listener);
46
+ return () => this.#mouseListeners.delete(listener);
47
+ }
48
+
49
+ hasPendingEscape(): boolean {
50
+ return this.#parser.hasPendingEscape();
51
+ }
52
+
53
+ flushPendingEscape(): string | undefined {
54
+ return this.#parser.flushPendingEscape();
55
+ }
56
+
57
+ reset(): void {
58
+ this.#parser.reset();
59
+ }
60
+ }
61
+
62
+ export function parseMouseEvent(sequence: string): TerminalMouseEvent | undefined {
63
+ const match = sequence.match(sgrMouse);
64
+ if (!match) return;
65
+ const code = Number(match[1]);
66
+ const column = Number(match[2]);
67
+ const row = Number(match[3]);
68
+ if (column < 1 || row < 1) return;
69
+ const wheel = (code & 64) !== 0;
70
+ const move = (code & 32) !== 0;
71
+ const buttonCode = code & 3;
72
+ const button: MouseButton = wheel
73
+ ? buttonCode === 0
74
+ ? "wheel-up"
75
+ : "wheel-down"
76
+ : (["left", "middle", "right", "none"] as const)[buttonCode]!;
77
+ return {
78
+ type: wheel
79
+ ? "wheel"
80
+ : move
81
+ ? "move"
82
+ : match[4] === "m" || buttonCode === 3
83
+ ? "release"
84
+ : "press",
85
+ x: column - 1,
86
+ y: row - 1,
87
+ button,
88
+ shift: (code & 4) !== 0,
89
+ alt: (code & 8) !== 0,
90
+ ctrl: (code & 16) !== 0,
91
+ };
92
+ }
@@ -0,0 +1,37 @@
1
+ import { throttle, type Throttled } from "#/throttle.ts";
2
+
3
+ export type RenderScheduler = {
4
+ readonly intervalMs: number;
5
+ readonly throttled: Throttled<never[]> | undefined;
6
+ readonly pending: boolean;
7
+ readonly schedule: () => void;
8
+ readonly immediate: () => void;
9
+ markRendered: () => void;
10
+ };
11
+
12
+ /** Owns frame cadence independently of React reconciliation and terminal presentation. */
13
+ export function createRenderScheduler(
14
+ render: () => void,
15
+ options: { readonly unthrottled: boolean; readonly maxFps: number },
16
+ ): RenderScheduler {
17
+ const frameInterval = options.maxFps > 0 ? Math.max(1, Math.ceil(1000 / options.maxFps)) : 0;
18
+ let pending = false;
19
+ const throttled = options.unthrottled ? undefined : throttle(render, frameInterval);
20
+ return {
21
+ intervalMs: options.unthrottled ? 0 : frameInterval,
22
+ throttled,
23
+ get pending() {
24
+ return pending;
25
+ },
26
+ schedule: options.unthrottled
27
+ ? render
28
+ : () => {
29
+ pending = true;
30
+ throttled!();
31
+ },
32
+ immediate: render,
33
+ markRendered() {
34
+ pending = false;
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,242 @@
1
+ import { cliCursor } from "#/ansi/cursor.ts";
2
+ import { ansiEscapes } from "#/ansi/escapes.ts";
3
+ import {
4
+ buildCursorOnlySequence,
5
+ buildCursorSuffix,
6
+ buildReturnToBottomPrefix,
7
+ cursorPositionChanged,
8
+ type CursorPosition,
9
+ } from "#/cursor-position.ts";
10
+ import { cellAttributes, cellsEqual, type Cell } from "#/screen/cell.ts";
11
+ import type { ColorProfile } from "#/screen/color-profile.ts";
12
+ import type { Line, Screen } from "#/screen/screen.ts";
13
+ import { serializeLine, serializeScreen } from "#/screen/serialize.ts";
14
+
15
+ type Write = (data: string) => boolean;
16
+
17
+ /** Presents structured frames in an inline terminal region. */
18
+ export class ScreenPresenter {
19
+ #screen: Screen | undefined;
20
+ #lineCount = 0;
21
+ #cursor: CursorPosition | undefined;
22
+ #cursorWasShown = false;
23
+ #fullscreen = false;
24
+ #hidden = false;
25
+ readonly #write: Write;
26
+ readonly #stream: NodeJS.WritableStream;
27
+
28
+ constructor(write: Write, stream: NodeJS.WritableStream) {
29
+ this.#write = write;
30
+ this.#stream = stream;
31
+ }
32
+
33
+ present(
34
+ screen: Screen,
35
+ options: {
36
+ readonly colorProfile: ColorProfile;
37
+ readonly cursor?: CursorPosition;
38
+ readonly fullscreen?: boolean;
39
+ readonly forceRewrite?: boolean;
40
+ },
41
+ ): boolean {
42
+ if (!this.#hidden) {
43
+ cliCursor.hide(this.#stream);
44
+ this.#hidden = true;
45
+ }
46
+
47
+ const previousScreen = this.#screen;
48
+ const sameScreen = previousScreen !== undefined && screensEqual(previousScreen, screen);
49
+ const cursorChanged = cursorPositionChanged(options.cursor, this.#cursor);
50
+ const fullscreen = options.fullscreen ?? false;
51
+ const presentationModeChanged = this.#fullscreen !== fullscreen;
52
+ if (!options.forceRewrite && sameScreen && !cursorChanged && !presentationModeChanged) {
53
+ return false;
54
+ }
55
+
56
+ if (!options.forceRewrite && sameScreen && !presentationModeChanged) {
57
+ this.#write(
58
+ buildCursorOnlySequence({
59
+ cursorWasShown: this.#cursorWasShown,
60
+ previousLineCount: this.#lineCount,
61
+ previousCursorPosition: this.#cursor,
62
+ cursorPosition: options.cursor,
63
+ }),
64
+ );
65
+ this.#remember(screen, options.cursor, options.fullscreen ?? false);
66
+ return true;
67
+ }
68
+
69
+ const canRewriteSuffix =
70
+ !options.forceRewrite &&
71
+ previousScreen !== undefined &&
72
+ previousScreen.width === screen.width &&
73
+ this.#fullscreen === fullscreen;
74
+ const firstChangedRow = canRewriteSuffix
75
+ ? findFirstChangedRow(previousScreen, screen)
76
+ : undefined;
77
+ const serializeOptions = {
78
+ colorProfile: options.colorProfile,
79
+ styles: options.colorProfile !== "none",
80
+ } as const;
81
+
82
+ if (firstChangedRow !== undefined) {
83
+ const eraseCount = this.#lineCount - firstChangedRow;
84
+ const body = screen
85
+ .toRows()
86
+ .slice(firstChangedRow)
87
+ .map((line) => serializeLine(line, serializeOptions))
88
+ .join("\n");
89
+ const output = fullscreen || body === "" ? body : `${body}\n`;
90
+ this.#write(
91
+ buildReturnToBottomPrefix(this.#cursorWasShown, this.#lineCount, this.#cursor) +
92
+ ansiEscapes.eraseLines(eraseCount) +
93
+ output +
94
+ buildCursorSuffix(screen.height - (fullscreen ? 1 : 0), options.cursor),
95
+ );
96
+ } else {
97
+ const body = serializeScreen(screen, serializeOptions);
98
+ const output = fullscreen ? body : `${body}\n`;
99
+ this.#write(
100
+ buildReturnToBottomPrefix(this.#cursorWasShown, this.#lineCount, this.#cursor) +
101
+ ansiEscapes.eraseLines(this.#lineCount) +
102
+ output +
103
+ buildCursorSuffix(screen.height - (fullscreen ? 1 : 0), options.cursor),
104
+ );
105
+ }
106
+
107
+ this.#remember(screen, options.cursor, fullscreen);
108
+ return true;
109
+ }
110
+
111
+ willPresent(
112
+ screen: Screen,
113
+ cursor: CursorPosition | undefined,
114
+ fullscreen = false,
115
+ forceRewrite = false,
116
+ ): boolean {
117
+ return (
118
+ forceRewrite ||
119
+ this.#screen === undefined ||
120
+ !screensEqual(this.#screen, screen) ||
121
+ cursorPositionChanged(cursor, this.#cursor) ||
122
+ fullscreen !== this.#fullscreen
123
+ );
124
+ }
125
+
126
+ /**
127
+ Erases the presented frame from the cursor upward.
128
+
129
+ `columns` is the terminal's current width. When it is narrower than the width
130
+ the frame was painted at, a reflowing emulator (xterm.js, Ghostty, kitty,
131
+ iTerm2, WezTerm, tmux, …) has already rewrapped every wider row onto several
132
+ physical rows before the resize event reaches us, so the erase has to cover
133
+ that physical footprint. Erasing only the logical row count leaves the frame's
134
+ top rows behind as ghosts, one more batch per resize event.
135
+ */
136
+ clear(options: { readonly columns?: number } = {}): void {
137
+ this.#write(
138
+ buildReturnToBottomPrefix(this.#cursorWasShown, this.#lineCount, this.#cursor) +
139
+ ansiEscapes.eraseLines(this.#physicalLineCount(options.columns)),
140
+ );
141
+ this.#lineCount = 0;
142
+ this.#cursor = undefined;
143
+ this.#cursorWasShown = false;
144
+ this.#fullscreen = false;
145
+ }
146
+
147
+ #physicalLineCount(columns: number | undefined): number {
148
+ if (
149
+ this.#screen === undefined ||
150
+ this.#lineCount === 0 ||
151
+ columns === undefined ||
152
+ columns < 1
153
+ ) {
154
+ return this.#lineCount;
155
+ }
156
+ let rows = 0;
157
+ for (const line of this.#screen.toRows()) {
158
+ rows += Math.max(1, Math.ceil(contentWidth(line) / columns));
159
+ }
160
+ return rows + (this.#fullscreen ? 0 : 1);
161
+ }
162
+
163
+ reset(): void {
164
+ this.#screen = undefined;
165
+ this.#lineCount = 0;
166
+ this.#cursor = undefined;
167
+ this.#cursorWasShown = false;
168
+ }
169
+
170
+ done(): void {
171
+ this.reset();
172
+ if (this.#hidden) {
173
+ cliCursor.show(this.#stream);
174
+ this.#hidden = false;
175
+ }
176
+ }
177
+
178
+ get current(): Screen | undefined {
179
+ return this.#screen;
180
+ }
181
+
182
+ #remember(screen: Screen, cursor: CursorPosition | undefined, fullscreen: boolean): void {
183
+ this.#screen = screen;
184
+ this.#lineCount = screen.height + (fullscreen ? 0 : 1);
185
+ this.#cursor = cursor ? { ...cursor } : undefined;
186
+ this.#cursorWasShown = cursor !== undefined;
187
+ this.#fullscreen = fullscreen;
188
+ }
189
+ }
190
+
191
+ /**
192
+ The columns a row occupies once written: everything up to its last visible
193
+ cell. Trailing unstyled blanks are trimmed by the serializer and never reach
194
+ the terminal, while styled blanks (a highlighted tab's padding) do.
195
+ */
196
+ function contentWidth(line: Line): number {
197
+ let width = 0;
198
+ let end = 0;
199
+ for (const cell of line) {
200
+ width += cell.width;
201
+ if (!isBlank(cell)) end = width;
202
+ }
203
+ return end;
204
+ }
205
+
206
+ function isBlank(cell: Cell): boolean {
207
+ const { style } = cell;
208
+ return (
209
+ (cell.grapheme === " " || cell.width === 0) &&
210
+ style.foreground === undefined &&
211
+ style.background === undefined &&
212
+ style.underlineColor === undefined &&
213
+ style.underline === "none" &&
214
+ style.attributes === cellAttributes.none &&
215
+ cell.hyperlink === undefined
216
+ );
217
+ }
218
+
219
+ function findFirstChangedRow(previous: Screen, next: Screen): number | undefined {
220
+ const height = Math.max(previous.height, next.height);
221
+ for (let y = 0; y < height; y++) {
222
+ if (!rowsEqual(previous, next, y)) return y;
223
+ }
224
+ return undefined;
225
+ }
226
+
227
+ function rowsEqual(left: Screen, right: Screen, y: number): boolean {
228
+ // Rows can extend past the nominal width when overflow content painted.
229
+ const length = Math.max(left.rowLength(y), right.rowLength(y));
230
+ for (let x = 0; x < length; x++) {
231
+ if (!cellsEqual(left.cellAt(x, y), right.cellAt(x, y))) return false;
232
+ }
233
+ return true;
234
+ }
235
+
236
+ function screensEqual(left: Screen, right: Screen): boolean {
237
+ if (left.width !== right.width || left.height !== right.height) return false;
238
+ for (let y = 0; y < left.height; y++) {
239
+ if (!rowsEqual(left, right, y)) return false;
240
+ }
241
+ return true;
242
+ }