@alchemy.run/sigil 0.0.0-alpha.4 → 0.0.0-alpha.6

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 (138) hide show
  1. package/README.md +21 -9
  2. package/THIRD_PARTY_NOTICES.md +39 -1
  3. package/dist/Text-BobFKi74.d.ts +452 -0
  4. package/dist/ansi.d.ts +122 -131
  5. package/dist/ansi.js +86 -6
  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-BMzMwV7Q.d.ts +22 -0
  11. package/dist/color-policy-SVj1pYTA.js +560 -0
  12. package/dist/color-profile-DHhQHY55.js +36 -0
  13. package/dist/color-profile-u0Nhe9Nv.d.ts +97 -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-Bh4yGP6w.d.ts +186 -0
  18. package/dist/detect-BuTXtY6e.js +373 -0
  19. package/dist/env-YVw64yZS.js +9 -0
  20. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  21. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  22. package/dist/index-DZ88EXJv.d.ts +21 -0
  23. package/dist/index.d.ts +143 -498
  24. package/dist/index.js +1026 -2244
  25. package/dist/osc-CCH7xDoS.js +71 -0
  26. package/dist/osc-Cn0fw77g.d.ts +23 -0
  27. package/dist/paint-C19minOS.d.ts +81 -0
  28. package/dist/query-vaIeGOkH.d.ts +152 -0
  29. package/dist/router.d.ts +392 -0
  30. package/dist/router.js +709 -0
  31. package/dist/sample-Cqw1bjUL.js +445 -0
  32. package/dist/screen-BOSLQ8fF.d.ts +49 -0
  33. package/dist/screen-CiPytswf.js +342 -0
  34. package/dist/screen.d.ts +5 -0
  35. package/dist/screen.js +5 -0
  36. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  37. package/dist/serialize-BTkAZgw1.js +79 -0
  38. package/dist/session-aZr9O8h3.js +665 -0
  39. package/dist/sgr-BhwaWAJB.js +246 -0
  40. package/dist/store-CgrG9K4y.d.ts +72 -0
  41. package/dist/string-width-CijQwpIk.js +69 -0
  42. package/dist/strip-BvU4toXG.js +6 -0
  43. package/dist/terminal.d.ts +118 -0
  44. package/dist/terminal.js +2 -0
  45. package/dist/tokenize-AjqbvtiT.js +1242 -0
  46. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  47. package/dist/truncate-D31fhU6i.js +562 -0
  48. package/dist/use-focus-BzqAJi0n.js +1337 -0
  49. package/package.json +41 -9
  50. package/src/ansi/chalk.ts +5 -3
  51. package/src/ansi/escapes.ts +14 -0
  52. package/src/ansi/graphemes.ts +8 -0
  53. package/src/ansi/hyperlink.ts +44 -0
  54. package/src/ansi/index.ts +3 -1
  55. package/src/ansi/osc.ts +77 -0
  56. package/src/ansi/tokenize.ts +3 -4
  57. package/src/capabilities/color-policy.ts +34 -0
  58. package/src/capabilities/detect.ts +594 -0
  59. package/src/capabilities/index.ts +37 -0
  60. package/src/capabilities/query.ts +657 -0
  61. package/src/capabilities/store.ts +379 -0
  62. package/src/color/index.ts +3 -0
  63. package/src/color/paint.ts +169 -0
  64. package/src/color/palette.ts +48 -0
  65. package/src/color/sample.ts +323 -0
  66. package/src/color.ts +1 -0
  67. package/src/components/AnsiText.tsx +42 -0
  68. package/src/components/App.tsx +98 -10
  69. package/src/components/BackgroundContext.ts +2 -3
  70. package/src/components/Box.tsx +0 -8
  71. package/src/components/CursorContext.ts +1 -1
  72. package/src/components/Hyperlink.tsx +56 -0
  73. package/src/components/TerminalOscContext.ts +25 -0
  74. package/src/components/Text.tsx +22 -45
  75. package/src/components/Transform.tsx +1 -1
  76. package/src/dom.ts +11 -2
  77. package/src/global.d.ts +3 -0
  78. package/src/hooks/use-capabilities.ts +73 -0
  79. package/src/hooks/use-cursor.ts +2 -2
  80. package/src/hooks/use-terminal-osc.ts +59 -0
  81. package/src/index.ts +45 -1
  82. package/src/ink.tsx +213 -153
  83. package/src/{render-node-to-output.ts → paint-tree.ts} +75 -48
  84. package/src/reconciler.ts +24 -3
  85. package/src/render-background.ts +36 -15
  86. package/src/render-border.ts +94 -61
  87. package/src/render-frame.ts +83 -0
  88. package/src/render-to-string.ts +21 -6
  89. package/src/render.ts +15 -6
  90. package/src/router/components.tsx +343 -0
  91. package/src/router/context.ts +41 -0
  92. package/src/router/history.ts +194 -0
  93. package/src/router/hooks.tsx +391 -0
  94. package/src/router/index.ts +34 -0
  95. package/src/router/matcher.ts +571 -0
  96. package/src/screen/ansi.ts +184 -0
  97. package/src/screen/canvas.ts +160 -0
  98. package/src/screen/cell.ts +138 -0
  99. package/src/screen/color-profile.ts +47 -0
  100. package/src/screen/geometry.ts +9 -0
  101. package/src/screen/index.ts +6 -0
  102. package/src/screen/screen.ts +305 -0
  103. package/src/screen/serialize.ts +129 -0
  104. package/src/screen.ts +1 -0
  105. package/src/semantic-text-style.ts +118 -0
  106. package/src/squash-text-nodes.ts +2 -5
  107. package/src/structured-text.ts +325 -0
  108. package/src/styles.ts +19 -14
  109. package/src/terminal/index.ts +2 -0
  110. package/src/terminal/inline-presenter.ts +120 -0
  111. package/src/terminal/input.ts +86 -0
  112. package/src/terminal/render-scheduler.ts +37 -0
  113. package/src/terminal/screen-presenter.ts +188 -0
  114. package/src/terminal/session.ts +407 -0
  115. package/src/terminal.ts +1 -0
  116. package/src/testing/browser.ts +588 -0
  117. package/src/testing/emulators.ts +205 -0
  118. package/src/testing/explorer-app/index.html +12 -0
  119. package/src/testing/explorer-app/main.ts +381 -0
  120. package/src/testing/explorer-app/style.css +194 -0
  121. package/src/testing/explorer-app/tsconfig.json +15 -0
  122. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  123. package/src/testing/index.ts +26 -0
  124. package/src/testing/keys.ts +56 -0
  125. package/src/testing/live.ts +85 -0
  126. package/src/testing/matchers.ts +70 -0
  127. package/src/testing/public.ts +94 -0
  128. package/src/testing/terminal.ts +349 -0
  129. package/src/testing/vitest.ts +157 -0
  130. package/src/transform-adapter.ts +14 -0
  131. package/src/wrap-text.ts +4 -0
  132. package/dist/sgr-CMfEpjSk.d.ts +0 -91
  133. package/dist/truncate-Cr6xVFMa.js +0 -2330
  134. package/src/ansi/supports-color.ts +0 -207
  135. package/src/colorize.ts +0 -60
  136. package/src/log-update.ts +0 -370
  137. package/src/output.ts +0 -308
  138. package/src/renderer.ts +0 -73
@@ -0,0 +1,665 @@
1
+ import { G as pasteEnd, K as pasteStart, S as ansiEscapes } from "./sgr-BhwaWAJB.js";
2
+ import { a as setPointerShape, c as setWorkingDirectory, i as setClipboard, l as tmuxPassthrough, n as notify, o as setTerminalProgress, s as setWindowTitle, u as cliCursor } from "./osc-CCH7xDoS.js";
3
+ import { i as getCapabilities, t as colorState } from "./color-policy-SVj1pYTA.js";
4
+ import { n as cellsEqual } from "./cell-_ZVhbfl0.js";
5
+ import { n as serializeScreen, t as serializeLine } from "./serialize-BTkAZgw1.js";
6
+ //#region src/cursor-position.ts
7
+ const showCursorEscape = ansiEscapes.cursorShow;
8
+ const hideCursorEscape = ansiEscapes.cursorHide;
9
+ /**
10
+ Compare two cursor positions. Returns true if they differ.
11
+ */
12
+ const cursorPositionChanged = (a, b) => a?.x !== b?.x || a?.y !== b?.y;
13
+ /**
14
+ Build escape sequence to move cursor from the bottom of the output to the target position and show it.
15
+
16
+ `bottomLine` is the row the renderer left the cursor on, counted from the top of the output.
17
+ That is always `lines.length - 1` for `lines = str.split('\n')`, whether or not the output ends
18
+ with a newline:
19
+
20
+ - With a trailing newline, `split` yields one extra empty element and the renderer stops just
21
+ past the last visible line — which is `lines.length - 1`.
22
+ - Without one, there is no extra element and the renderer deliberately stops on the last visible
23
+ line instead of moving past it — which is also `lines.length - 1`.
24
+
25
+ This is the same row basis `buildReturnToBottom` measures from, so the two stay in step.
26
+ */
27
+ const buildCursorSuffix = (bottomLine, cursorPosition) => {
28
+ if (!cursorPosition) return "";
29
+ const moveUp = bottomLine - cursorPosition.y;
30
+ return (moveUp > 0 ? ansiEscapes.cursorUp(moveUp) : "") + ansiEscapes.cursorTo(cursorPosition.x) + showCursorEscape;
31
+ };
32
+ /**
33
+ Build escape sequence to move cursor from previousCursorPosition back to the bottom of output.
34
+ This must be done before eraseLines or any operation that assumes cursor is at the bottom.
35
+ */
36
+ const buildReturnToBottom = (previousLineCount, previousCursorPosition) => {
37
+ if (!previousCursorPosition) return "";
38
+ const down = previousLineCount - 1 - previousCursorPosition.y;
39
+ return (down > 0 ? ansiEscapes.cursorDown(down) : "") + ansiEscapes.cursorTo(0);
40
+ };
41
+ /**
42
+ Build the escape sequence for cursor-only updates (output unchanged, cursor moved).
43
+ Hides cursor if it was previously shown, returns to bottom, then repositions.
44
+
45
+ `buildReturnToBottom` has just placed the cursor on row `previousLineCount - 1`, so the
46
+ suffix measures from there rather than recomputing the row from the output.
47
+ */
48
+ const buildCursorOnlySequence = (input) => {
49
+ const hidePrefix = input.cursorWasShown ? hideCursorEscape : "";
50
+ const returnToBottom = buildReturnToBottom(input.previousLineCount, input.previousCursorPosition);
51
+ const cursorSuffix = buildCursorSuffix(input.previousLineCount - 1, input.cursorPosition);
52
+ return hidePrefix + returnToBottom + cursorSuffix;
53
+ };
54
+ /**
55
+ Build the prefix that hides cursor and returns to bottom before erasing or rewriting.
56
+ Returns empty string if cursor was not shown.
57
+ */
58
+ const buildReturnToBottomPrefix = (cursorWasShown, previousLineCount, previousCursorPosition) => {
59
+ if (!cursorWasShown) return "";
60
+ return hideCursorEscape + buildReturnToBottom(previousLineCount, previousCursorPosition);
61
+ };
62
+ //#endregion
63
+ //#region src/input-parser.ts
64
+ const escape = "\x1B";
65
+ const isCsiParameterByte = (byte) => {
66
+ return byte >= 48 && byte <= 63;
67
+ };
68
+ const isCsiIntermediateByte = (byte) => {
69
+ return byte >= 32 && byte <= 47;
70
+ };
71
+ const isCsiFinalByte = (byte) => {
72
+ return byte >= 64 && byte <= 126;
73
+ };
74
+ const parseCsiSequence = (input, startIndex, prefixLength) => {
75
+ const csiPayloadStart = startIndex + prefixLength + 1;
76
+ let index = csiPayloadStart;
77
+ for (; index < input.length; index++) {
78
+ const byte = input.codePointAt(index);
79
+ if (byte === void 0) return "pending";
80
+ if (isCsiParameterByte(byte) || isCsiIntermediateByte(byte)) continue;
81
+ if (byte === 91 && index === csiPayloadStart) continue;
82
+ if (isCsiFinalByte(byte)) return {
83
+ sequence: input.slice(startIndex, index + 1),
84
+ nextIndex: index + 1
85
+ };
86
+ return;
87
+ }
88
+ return "pending";
89
+ };
90
+ const parseSs3Sequence = (input, startIndex, prefixLength) => {
91
+ const nextIndex = startIndex + prefixLength + 2;
92
+ if (nextIndex > input.length) return "pending";
93
+ const finalByte = input.codePointAt(nextIndex - 1);
94
+ if (finalByte === void 0 || !isCsiFinalByte(finalByte)) return;
95
+ return {
96
+ sequence: input.slice(startIndex, nextIndex),
97
+ nextIndex
98
+ };
99
+ };
100
+ const parseControlSequence = (input, startIndex, prefixLength) => {
101
+ const sequenceType = input[startIndex + prefixLength];
102
+ if (sequenceType === void 0) return "pending";
103
+ if (sequenceType === "[") return parseCsiSequence(input, startIndex, prefixLength);
104
+ if (sequenceType === "O") return parseSs3Sequence(input, startIndex, prefixLength);
105
+ };
106
+ const parseEscapedCodePoint = (input, escapeIndex) => {
107
+ const nextCodePoint = input.codePointAt(escapeIndex + 1);
108
+ const nextCodePointLength = nextCodePoint !== void 0 && nextCodePoint > 65535 ? 2 : 1;
109
+ const nextIndex = escapeIndex + 1 + nextCodePointLength;
110
+ return {
111
+ sequence: input.slice(escapeIndex, nextIndex),
112
+ nextIndex
113
+ };
114
+ };
115
+ const parseEscapeSequence = (input, escapeIndex) => {
116
+ if (escapeIndex === input.length - 1) return "pending";
117
+ if (input[escapeIndex + 1] === escape) {
118
+ if (escapeIndex + 2 >= input.length) return "pending";
119
+ const doubleEscapeSequence = parseControlSequence(input, escapeIndex, 2);
120
+ if (doubleEscapeSequence === "pending") return "pending";
121
+ if (doubleEscapeSequence) return doubleEscapeSequence;
122
+ return {
123
+ sequence: input.slice(escapeIndex, escapeIndex + 2),
124
+ nextIndex: escapeIndex + 2
125
+ };
126
+ }
127
+ const controlSequence = parseControlSequence(input, escapeIndex, 1);
128
+ if (controlSequence === "pending") return "pending";
129
+ if (controlSequence) return controlSequence;
130
+ return parseEscapedCodePoint(input, escapeIndex);
131
+ };
132
+ /**
133
+ Split a chunk of non-escape text so that backspace bytes (`0x7F` and `0x08`) become individual events. When a user holds the backspace key, the terminal sends repeated bytes in a single stdin chunk. Without splitting, `parseKeypress` receives the multi-byte string and fails to recognize it as a key event, corrupting the input state.
134
+
135
+ Other control characters like `\r` and `\t` are NOT split because they can legitimately appear inside pasted text.
136
+ */
137
+ const splitBackspaceBytes = (text, events) => {
138
+ let textSegmentStart = 0;
139
+ for (let index = 0; index < text.length; index++) {
140
+ const character = text[index];
141
+ if (character === "" || character === "\b") {
142
+ if (index > textSegmentStart) events.push(text.slice(textSegmentStart, index));
143
+ events.push(character);
144
+ textSegmentStart = index + 1;
145
+ }
146
+ }
147
+ if (textSegmentStart < text.length) events.push(text.slice(textSegmentStart));
148
+ };
149
+ const parseKeypresses = (input) => {
150
+ const events = [];
151
+ let index = 0;
152
+ const pendingFrom = (pendingStartIndex) => ({
153
+ events,
154
+ pending: input.slice(pendingStartIndex)
155
+ });
156
+ while (index < input.length) {
157
+ const escapeIndex = input.indexOf(escape, index);
158
+ if (escapeIndex === -1) {
159
+ splitBackspaceBytes(input.slice(index), events);
160
+ return {
161
+ events,
162
+ pending: ""
163
+ };
164
+ }
165
+ if (escapeIndex > index) splitBackspaceBytes(input.slice(index, escapeIndex), events);
166
+ const parsedEscapeSequence = parseEscapeSequence(input, escapeIndex);
167
+ if (parsedEscapeSequence === "pending") return pendingFrom(escapeIndex);
168
+ if (parsedEscapeSequence.sequence === pasteStart) {
169
+ const afterStart = parsedEscapeSequence.nextIndex;
170
+ const endIndex = input.indexOf(pasteEnd, afterStart);
171
+ if (endIndex === -1) return pendingFrom(escapeIndex);
172
+ events.push({ paste: input.slice(afterStart, endIndex) });
173
+ index = endIndex + pasteEnd.length;
174
+ continue;
175
+ }
176
+ events.push(parsedEscapeSequence.sequence);
177
+ index = parsedEscapeSequence.nextIndex;
178
+ }
179
+ return {
180
+ events,
181
+ pending: ""
182
+ };
183
+ };
184
+ const createInputParser = () => {
185
+ let pending = "";
186
+ return {
187
+ push(chunk) {
188
+ const parsedInput = parseKeypresses(pending + chunk);
189
+ pending = parsedInput.pending;
190
+ return parsedInput.events;
191
+ },
192
+ hasPendingEscape() {
193
+ return pending.startsWith(escape) && !pending.startsWith(pasteStart) && pending !== pasteStart.slice(0, -1);
194
+ },
195
+ flushPendingEscape() {
196
+ if (!pending.startsWith(escape)) return;
197
+ const pendingEscape = pending;
198
+ pending = "";
199
+ return pendingEscape;
200
+ },
201
+ reset() {
202
+ pending = "";
203
+ }
204
+ };
205
+ };
206
+ //#endregion
207
+ //#region src/terminal/input.ts
208
+ const sgrMouse = /^\u001B\[<(\d+);(\d+);(\d+)([Mm])$/;
209
+ /** Stateful stdin decoder that separates terminal reports from application input. */
210
+ var TerminalInput = class {
211
+ #parser = createInputParser();
212
+ #capabilities;
213
+ #mouseListeners = /* @__PURE__ */ new Set();
214
+ constructor(capabilities) {
215
+ this.#capabilities = capabilities;
216
+ }
217
+ push(chunk) {
218
+ return this.#parser.push(chunk).filter((event) => {
219
+ if (typeof event !== "string") return true;
220
+ if (this.#capabilities.ingest(event)) return false;
221
+ const mouse = parseMouseEvent(event);
222
+ if (!mouse) return true;
223
+ for (const listener of this.#mouseListeners) listener(mouse);
224
+ return false;
225
+ });
226
+ }
227
+ subscribeMouse(listener) {
228
+ this.#mouseListeners.add(listener);
229
+ return () => this.#mouseListeners.delete(listener);
230
+ }
231
+ hasPendingEscape() {
232
+ return this.#parser.hasPendingEscape();
233
+ }
234
+ flushPendingEscape() {
235
+ return this.#parser.flushPendingEscape();
236
+ }
237
+ reset() {
238
+ this.#parser.reset();
239
+ }
240
+ };
241
+ function parseMouseEvent(sequence) {
242
+ const match = sequence.match(sgrMouse);
243
+ if (!match) return;
244
+ const code = Number(match[1]);
245
+ const column = Number(match[2]);
246
+ const row = Number(match[3]);
247
+ if (column < 1 || row < 1) return;
248
+ const wheel = (code & 64) !== 0;
249
+ const move = (code & 32) !== 0;
250
+ const buttonCode = code & 3;
251
+ const button = wheel ? buttonCode === 0 ? "wheel-up" : "wheel-down" : [
252
+ "left",
253
+ "middle",
254
+ "right",
255
+ "none"
256
+ ][buttonCode];
257
+ return {
258
+ type: wheel ? "wheel" : move ? "move" : match[4] === "m" || buttonCode === 3 ? "release" : "press",
259
+ x: column - 1,
260
+ y: row - 1,
261
+ button,
262
+ shift: (code & 4) !== 0,
263
+ alt: (code & 8) !== 0,
264
+ ctrl: (code & 16) !== 0
265
+ };
266
+ }
267
+ //#endregion
268
+ //#region src/terminal/screen-presenter.ts
269
+ /** Presents structured frames in an inline terminal region. */
270
+ var ScreenPresenter = class {
271
+ #screen;
272
+ #lineCount = 0;
273
+ #cursor;
274
+ #cursorWasShown = false;
275
+ #fullscreen = false;
276
+ #hidden = false;
277
+ #write;
278
+ #stream;
279
+ constructor(write, stream) {
280
+ this.#write = write;
281
+ this.#stream = stream;
282
+ }
283
+ present(screen, options) {
284
+ if (!this.#hidden) {
285
+ cliCursor.hide(this.#stream);
286
+ this.#hidden = true;
287
+ }
288
+ const previousScreen = this.#screen;
289
+ const sameScreen = previousScreen !== void 0 && screensEqual(previousScreen, screen);
290
+ const cursorChanged = cursorPositionChanged(options.cursor, this.#cursor);
291
+ const fullscreen = options.fullscreen ?? false;
292
+ const presentationModeChanged = this.#fullscreen !== fullscreen;
293
+ if (!options.forceRewrite && sameScreen && !cursorChanged && !presentationModeChanged) return false;
294
+ if (!options.forceRewrite && sameScreen && !presentationModeChanged) {
295
+ this.#write(buildCursorOnlySequence({
296
+ cursorWasShown: this.#cursorWasShown,
297
+ previousLineCount: this.#lineCount,
298
+ previousCursorPosition: this.#cursor,
299
+ cursorPosition: options.cursor
300
+ }));
301
+ this.#remember(screen, options.cursor, options.fullscreen ?? false);
302
+ return true;
303
+ }
304
+ const firstChangedRow = !options.forceRewrite && previousScreen !== void 0 && previousScreen.width === screen.width && this.#fullscreen === fullscreen ? findFirstChangedRow(previousScreen, screen) : void 0;
305
+ const serializeOptions = {
306
+ colorProfile: options.colorProfile,
307
+ styles: options.colorProfile !== "none"
308
+ };
309
+ if (firstChangedRow !== void 0) {
310
+ const eraseCount = this.#lineCount - firstChangedRow;
311
+ const body = screen.toRows().slice(firstChangedRow).map((line) => serializeLine(line, serializeOptions)).join("\n");
312
+ const output = fullscreen || body === "" ? body : `${body}\n`;
313
+ this.#write(buildReturnToBottomPrefix(this.#cursorWasShown, this.#lineCount, this.#cursor) + ansiEscapes.eraseLines(eraseCount) + output + buildCursorSuffix(screen.height - (fullscreen ? 1 : 0), options.cursor));
314
+ } else {
315
+ const body = serializeScreen(screen, serializeOptions);
316
+ const output = fullscreen ? body : `${body}\n`;
317
+ this.#write(buildReturnToBottomPrefix(this.#cursorWasShown, this.#lineCount, this.#cursor) + ansiEscapes.eraseLines(this.#lineCount) + output + buildCursorSuffix(screen.height - (fullscreen ? 1 : 0), options.cursor));
318
+ }
319
+ this.#remember(screen, options.cursor, fullscreen);
320
+ return true;
321
+ }
322
+ willPresent(screen, cursor, fullscreen = false, forceRewrite = false) {
323
+ return forceRewrite || this.#screen === void 0 || !screensEqual(this.#screen, screen) || cursorPositionChanged(cursor, this.#cursor) || fullscreen !== this.#fullscreen;
324
+ }
325
+ clear() {
326
+ this.#write(buildReturnToBottomPrefix(this.#cursorWasShown, this.#lineCount, this.#cursor) + ansiEscapes.eraseLines(this.#lineCount));
327
+ this.#lineCount = 0;
328
+ this.#cursor = void 0;
329
+ this.#cursorWasShown = false;
330
+ this.#fullscreen = false;
331
+ }
332
+ reset() {
333
+ this.#screen = void 0;
334
+ this.#lineCount = 0;
335
+ this.#cursor = void 0;
336
+ this.#cursorWasShown = false;
337
+ }
338
+ done() {
339
+ this.reset();
340
+ if (this.#hidden) {
341
+ cliCursor.show(this.#stream);
342
+ this.#hidden = false;
343
+ }
344
+ }
345
+ get current() {
346
+ return this.#screen;
347
+ }
348
+ #remember(screen, cursor, fullscreen) {
349
+ this.#screen = screen;
350
+ this.#lineCount = screen.height + (fullscreen ? 0 : 1);
351
+ this.#cursor = cursor ? { ...cursor } : void 0;
352
+ this.#cursorWasShown = cursor !== void 0;
353
+ this.#fullscreen = fullscreen;
354
+ }
355
+ };
356
+ function findFirstChangedRow(previous, next) {
357
+ const height = Math.max(previous.height, next.height);
358
+ for (let y = 0; y < height; y++) if (!rowsEqual(previous, next, y)) return y;
359
+ }
360
+ function rowsEqual(left, right, y) {
361
+ const length = Math.max(left.rowLength(y), right.rowLength(y));
362
+ for (let x = 0; x < length; x++) if (!cellsEqual(left.cellAt(x, y), right.cellAt(x, y))) return false;
363
+ return true;
364
+ }
365
+ function screensEqual(left, right) {
366
+ if (left.width !== right.width || left.height !== right.height) return false;
367
+ for (let y = 0; y < left.height; y++) if (!rowsEqual(left, right, y)) return false;
368
+ return true;
369
+ }
370
+ //#endregion
371
+ //#region src/terminal/session.ts
372
+ const imperativeProgressOwner = Symbol("imperative-progress");
373
+ const imperativeTitleOwner = Symbol("imperative-title");
374
+ /** Instance-scoped terminal state and lifecycle ownership. */
375
+ var TerminalSession = class {
376
+ stdin;
377
+ stdout;
378
+ stderr;
379
+ capabilities;
380
+ input;
381
+ #presenter;
382
+ cursor = {
383
+ visible: true,
384
+ shape: "block",
385
+ blinking: true
386
+ };
387
+ suspended = false;
388
+ alternateScreen = false;
389
+ inlineScreen = true;
390
+ #policy;
391
+ #profile;
392
+ #modes = /* @__PURE__ */ new Map();
393
+ #pendingWrites = /* @__PURE__ */ new Set();
394
+ #unsubscribe;
395
+ #cleaned = false;
396
+ #progressActive = false;
397
+ #progressSequence;
398
+ #progressHeartbeat;
399
+ #progressPublishers = /* @__PURE__ */ new Map();
400
+ #titlePublishers = /* @__PURE__ */ new Map();
401
+ #titleActive = false;
402
+ constructor(options) {
403
+ this.stdin = options.stdin;
404
+ this.stdout = options.stdout;
405
+ this.stderr = options.stderr;
406
+ this.capabilities = getCapabilities(this.stdin, this.stdout);
407
+ this.input = new TerminalInput(this.capabilities);
408
+ this.#presenter = new ScreenPresenter((data) => this.write(data), this.stdout);
409
+ this.#policy = options.colorPolicy ?? "auto";
410
+ this.#profile = colorState(this.capabilities.current, this.#policy).effective;
411
+ this.#unsubscribe = this.capabilities.subscribe((capabilities) => {
412
+ const profile = colorState(capabilities, this.#policy).effective;
413
+ if (profile !== this.#profile) {
414
+ this.#profile = profile;
415
+ this.#presenter.reset();
416
+ }
417
+ options.onCapabilitiesChange?.();
418
+ }, { resizes: false });
419
+ }
420
+ get colorProfile() {
421
+ return this.#profile;
422
+ }
423
+ /** Encodes a structured frame at the terminal boundary. */
424
+ encode(screen) {
425
+ return serializeScreen(screen, {
426
+ colorProfile: this.colorProfile,
427
+ styles: this.colorProfile !== "none"
428
+ });
429
+ }
430
+ present(screen, options = {}) {
431
+ return this.#presenter.present(screen, {
432
+ colorProfile: this.colorProfile,
433
+ cursor: this.cursor.position,
434
+ ...options
435
+ });
436
+ }
437
+ willPresent(screen, options = {}) {
438
+ return this.#presenter.willPresent(screen, this.cursor.position, options.fullscreen, options.forceRewrite);
439
+ }
440
+ clearFrame() {
441
+ this.#presenter.clear();
442
+ }
443
+ resetFrame() {
444
+ this.#presenter.reset();
445
+ }
446
+ finishFrame() {
447
+ this.#presenter.done();
448
+ }
449
+ setColorPolicy(policy) {
450
+ this.#policy = policy;
451
+ const profile = colorState(this.capabilities.current, policy).effective;
452
+ if (profile !== this.#profile) {
453
+ this.#profile = profile;
454
+ this.#presenter.reset();
455
+ }
456
+ }
457
+ /** Writes through the session and tracks callback-based backpressure completion. */
458
+ write(data) {
459
+ let settle;
460
+ const completion = new Promise((resolve) => {
461
+ settle = resolve;
462
+ });
463
+ this.#pendingWrites.add(completion);
464
+ try {
465
+ return this.stdout.write(data, () => {
466
+ this.#pendingWrites.delete(completion);
467
+ settle();
468
+ });
469
+ } catch (error) {
470
+ this.#pendingWrites.delete(completion);
471
+ settle();
472
+ throw error;
473
+ }
474
+ }
475
+ async flush() {
476
+ await Promise.all(this.#pendingWrites);
477
+ }
478
+ /** Copy text through OSC 52, including tmux passthrough when required. */
479
+ copyToClipboard(text, selection = "clipboard") {
480
+ return this.write(this.#wrapOsc(setClipboard(text, selection)));
481
+ }
482
+ setTitle(title) {
483
+ return this.publishTitle(imperativeTitleOwner, title);
484
+ }
485
+ publishTitle(owner, title) {
486
+ this.#titlePublishers.delete(owner);
487
+ if (title !== void 0) this.#titlePublishers.set(owner, title);
488
+ const current = [...this.#titlePublishers.values()].at(-1);
489
+ if (current !== void 0) {
490
+ this.#titleActive = true;
491
+ return this.write(this.#wrapOsc(setWindowTitle(current)));
492
+ }
493
+ if (!this.#titleActive) return true;
494
+ this.#titleActive = false;
495
+ return this.write(this.#wrapOsc(setWindowTitle("")));
496
+ }
497
+ setWorkingDirectory(directory) {
498
+ return this.write(this.#wrapOsc(setWorkingDirectory(directory)));
499
+ }
500
+ notify(title) {
501
+ return this.write(this.#wrapOsc(notify(title)));
502
+ }
503
+ setPointerShape(shape) {
504
+ return this.write(this.#wrapOsc(setPointerShape(shape)));
505
+ }
506
+ publishProgress(owner, state, value) {
507
+ this.#progressPublishers.delete(owner);
508
+ if (state !== "inactive") this.#progressPublishers.set(owner, {
509
+ state,
510
+ value
511
+ });
512
+ return this.#applyPublishedProgress();
513
+ }
514
+ #applyPublishedProgress() {
515
+ const current = [...this.#progressPublishers.values()].at(-1);
516
+ return current === void 0 ? this.#writeProgress("inactive") : this.#writeProgress(current.state, current.value);
517
+ }
518
+ /**
519
+ * Update terminal-native progress. Active progress is reset during cleanup
520
+ * so a completed or failed process cannot leave a stale terminal indicator.
521
+ */
522
+ setProgress(state, value) {
523
+ this.#progressPublishers.delete(imperativeProgressOwner);
524
+ if (state !== "inactive") this.#progressPublishers.set(imperativeProgressOwner, {
525
+ state,
526
+ value
527
+ });
528
+ return this.#applyPublishedProgress();
529
+ }
530
+ #writeProgress(state, value) {
531
+ this.#progressActive = state !== "inactive";
532
+ const sequence = this.#wrapOsc(setTerminalProgress(state, value));
533
+ if (!this.#progressActive) this.#stopProgressHeartbeat();
534
+ else {
535
+ this.#progressSequence = sequence;
536
+ if (this.#progressHeartbeat === void 0) {
537
+ this.#progressHeartbeat = setInterval(() => {
538
+ if (this.#progressSequence !== void 0) this.write(this.#progressSequence);
539
+ }, 1e3);
540
+ this.#progressHeartbeat.unref?.();
541
+ }
542
+ }
543
+ return this.write(sequence);
544
+ }
545
+ #stopProgressHeartbeat() {
546
+ if (this.#progressHeartbeat !== void 0) {
547
+ clearInterval(this.#progressHeartbeat);
548
+ this.#progressHeartbeat = void 0;
549
+ }
550
+ this.#progressSequence = void 0;
551
+ }
552
+ enableMode(modeOrEnable, disable) {
553
+ const mode = typeof modeOrEnable === "string" ? {
554
+ id: disable ?? modeOrEnable,
555
+ enable: modeOrEnable,
556
+ disable: disable ?? ""
557
+ } : modeOrEnable;
558
+ const active = this.#modes.get(mode.id);
559
+ if (active) {
560
+ active.count++;
561
+ return;
562
+ }
563
+ this.write(mode.enable);
564
+ this.#modes.set(mode.id, {
565
+ ...mode,
566
+ count: 1
567
+ });
568
+ }
569
+ disableMode(idOrDisable) {
570
+ const entry = this.#modes.get(idOrDisable) ?? [...this.#modes.values()].find((mode) => mode.disable === idOrDisable);
571
+ if (!entry) return;
572
+ if (--entry.count > 0) return;
573
+ this.#modes.delete(entry.id);
574
+ this.write(entry.disable);
575
+ }
576
+ setCursor(position) {
577
+ this.cursor = {
578
+ ...this.cursor,
579
+ position,
580
+ visible: position !== void 0
581
+ };
582
+ }
583
+ setCursorAppearance(options) {
584
+ const next = {
585
+ ...this.cursor,
586
+ ...options.visible === void 0 ? {} : { visible: options.visible },
587
+ ...options.shape === void 0 ? {} : { shape: options.shape },
588
+ ...options.blinking === void 0 ? {} : { blinking: options.blinking },
589
+ ...options.color === void 0 ? {} : { color: options.color ?? void 0 }
590
+ };
591
+ let output = "";
592
+ if (next.shape !== this.cursor.shape || next.blinking !== this.cursor.blinking) output += ansiEscapes.cursorShape(next.shape, next.blinking);
593
+ if (options.color !== void 0) output += options.color === null ? ansiEscapes.resetCursorColor : ansiEscapes.cursorColor(options.color);
594
+ if (next.visible !== this.cursor.visible) output += next.visible ? ansiEscapes.cursorShow : ansiEscapes.cursorHide;
595
+ this.cursor = next;
596
+ if (output) this.write(output);
597
+ }
598
+ setAlternateScreen(enabled, options = {}) {
599
+ if (enabled === this.alternateScreen) return;
600
+ this.alternateScreen = enabled;
601
+ this.inlineScreen = !enabled;
602
+ if (enabled) {
603
+ this.enableMode({
604
+ id: "alternate-screen",
605
+ enable: ansiEscapes.enterAlternativeScreen,
606
+ disable: ansiEscapes.exitAlternativeScreen
607
+ });
608
+ if (options.hideCursor) this.setCursorAppearance({ visible: false });
609
+ } else this.disableMode("alternate-screen");
610
+ this.#presenter.reset();
611
+ }
612
+ beginSuspension() {
613
+ if (this.suspended) throw new Error("The terminal session is already suspended");
614
+ this.suspended = true;
615
+ }
616
+ resume() {
617
+ if (!this.suspended) return;
618
+ this.suspended = false;
619
+ this.#presenter.reset();
620
+ }
621
+ cleanup() {
622
+ if (this.#cleaned) return;
623
+ this.#cleaned = true;
624
+ this.#unsubscribe();
625
+ this.#stopProgressHeartbeat();
626
+ this.#progressPublishers.clear();
627
+ if (this.#progressActive) {
628
+ try {
629
+ this.stdout.write(this.#wrapOsc(setTerminalProgress("inactive")));
630
+ } catch {}
631
+ this.#progressActive = false;
632
+ }
633
+ this.#titlePublishers.clear();
634
+ if (this.#titleActive) {
635
+ try {
636
+ this.stdout.write(this.#wrapOsc(setWindowTitle("")));
637
+ } catch {}
638
+ this.#titleActive = false;
639
+ }
640
+ for (const mode of [...this.#modes.values()].reverse()) try {
641
+ this.stdout.write(mode.disable);
642
+ } catch {}
643
+ if (this.cursor.color !== void 0) try {
644
+ this.stdout.write(ansiEscapes.resetCursorColor);
645
+ } catch {}
646
+ if (!this.cursor.visible) try {
647
+ this.stdout.write(ansiEscapes.cursorShow);
648
+ } catch {}
649
+ this.#modes.clear();
650
+ this.alternateScreen = false;
651
+ this.inlineScreen = true;
652
+ this.suspended = false;
653
+ this.cursor = {
654
+ visible: true,
655
+ shape: "block",
656
+ blinking: true
657
+ };
658
+ this.#presenter.done();
659
+ }
660
+ #wrapOsc(sequence) {
661
+ return this.capabilities.current.terminal.multiplexer === "tmux" ? tmuxPassthrough(sequence) : sequence;
662
+ }
663
+ };
664
+ //#endregion
665
+ export { buildCursorSuffix as a, hideCursorEscape as c, buildCursorOnlySequence as i, TerminalInput as n, buildReturnToBottomPrefix as o, parseMouseEvent as r, cursorPositionChanged as s, TerminalSession as t };