@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.
- package/README.md +21 -9
- package/THIRD_PARTY_NOTICES.md +39 -1
- package/dist/Text-BobFKi74.d.ts +452 -0
- package/dist/ansi.d.ts +122 -131
- package/dist/ansi.js +86 -6
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BMzMwV7Q.d.ts +22 -0
- package/dist/color-policy-SVj1pYTA.js +560 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color-profile-u0Nhe9Nv.d.ts +97 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-Bh4yGP6w.d.ts +186 -0
- package/dist/detect-BuTXtY6e.js +373 -0
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-DZ88EXJv.d.ts +21 -0
- package/dist/index.d.ts +143 -498
- package/dist/index.js +1026 -2244
- package/dist/osc-CCH7xDoS.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-C19minOS.d.ts +81 -0
- package/dist/query-vaIeGOkH.d.ts +152 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +709 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-BOSLQ8fF.d.ts +49 -0
- package/dist/screen-CiPytswf.js +342 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-aZr9O8h3.js +665 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-CgrG9K4y.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +118 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-BzqAJi0n.js +1337 -0
- package/package.json +41 -9
- package/src/ansi/chalk.ts +5 -3
- package/src/ansi/escapes.ts +14 -0
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +3 -1
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/tokenize.ts +3 -4
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +594 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +657 -0
- package/src/capabilities/store.ts +379 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +98 -10
- package/src/components/BackgroundContext.ts +2 -3
- package/src/components/Box.tsx +0 -8
- package/src/components/CursorContext.ts +1 -1
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +22 -45
- package/src/components/Transform.tsx +1 -1
- package/src/dom.ts +11 -2
- package/src/global.d.ts +3 -0
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +2 -2
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/index.ts +45 -1
- package/src/ink.tsx +213 -153
- package/src/{render-node-to-output.ts → paint-tree.ts} +75 -48
- package/src/reconciler.ts +24 -3
- package/src/render-background.ts +36 -15
- package/src/render-border.ts +94 -61
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +21 -6
- package/src/render.ts +15 -6
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +160 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +305 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/squash-text-nodes.ts +2 -5
- package/src/structured-text.ts +325 -0
- package/src/styles.ts +19 -14
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +86 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +188 -0
- package/src/terminal/session.ts +407 -0
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +349 -0
- package/src/testing/vitest.ts +157 -0
- package/src/transform-adapter.ts +14 -0
- package/src/wrap-text.ts +4 -0
- package/dist/sgr-CMfEpjSk.d.ts +0 -91
- package/dist/truncate-Cr6xVFMa.js +0 -2330
- package/src/ansi/supports-color.ts +0 -207
- package/src/colorize.ts +0 -60
- package/src/log-update.ts +0 -370
- package/src/output.ts +0 -308
- 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 };
|