@aitty/browser 0.1.0
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/dist/browser.d.ts +6 -0
- package/dist/browser.js +5 -0
- package/dist/frontend/ansi-sequences.d.ts +5 -0
- package/dist/frontend/ansi-sequences.js +17 -0
- package/dist/frontend/ansi-style-tracker.d.ts +37 -0
- package/dist/frontend/ansi-style-tracker.js +435 -0
- package/dist/frontend/browser-terminal-renderer.d.ts +86 -0
- package/dist/frontend/browser-terminal-renderer.js +546 -0
- package/dist/frontend/cell-width.d.ts +5 -0
- package/dist/frontend/cell-width.js +9 -0
- package/dist/frontend/terminal-app.d.ts +156 -0
- package/dist/frontend/terminal-app.js +1957 -0
- package/dist/frontend/terminal-input-policies.d.ts +24 -0
- package/dist/frontend/terminal-input-policies.js +462 -0
- package/dist/frontend/terminal-theme-protocol.d.ts +22 -0
- package/dist/frontend/terminal-theme-protocol.js +270 -0
- package/dist/frontend/terminal.css +268 -0
- package/dist/theme-source.d.ts +2 -0
- package/dist/theme-source.js +2 -0
- package/package.json +54 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { BrowserTerminalBridge, BrowserTerminalRenderer, BrowserTerminalStyleOverride, BrowserTerminalStyleTracker } from "./frontend/browser-terminal-renderer.js";
|
|
2
|
+
import { TerminalInputCallbacks, TerminalKeyResolver, installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey } from "./frontend/terminal-input-policies.js";
|
|
3
|
+
import { BinaryWriteTarget, BufferedTerminalWriter, FrameScheduler, MountAittyContainerOptions, MountAittyOptions, MountTerminalDependencies, MountTerminalElements, MountedTerminalApp, TerminalConnectionState, TerminalLoadingPresentation, TerminalOutputState, TerminalScrollAdapter, TerminalScrollAdapterContext, TerminalScrollAdapterFactory, TerminalScrollMetrics, TerminalScrollOptions, TerminalSessionPresentationState, TerminalStatusListener, TerminalStatusSnapshot, TerminalTheme, TerminalTransport, TransportCloseEvent, TransportHandlers, createBufferedTerminalWriter, defineAittyTerminalElement, mountAitty, mountTerminalApp } from "./frontend/terminal-app.js";
|
|
4
|
+
import { AittyTheme, AittyThemeSource, AittyThemeSubscription } from "./theme-source.js";
|
|
5
|
+
import { AnsiStyleTracker, createAnsiStyleTracker } from "./frontend/ansi-style-tracker.js";
|
|
6
|
+
export { type AittyTheme, type AittyThemeSource, type AittyThemeSubscription, type AnsiStyleTracker, type BinaryWriteTarget, type BrowserTerminalBridge, BrowserTerminalRenderer, type BrowserTerminalStyleOverride, type BrowserTerminalStyleTracker, type BufferedTerminalWriter, type FrameScheduler, type MountAittyContainerOptions, type MountAittyOptions, type MountTerminalDependencies, type MountTerminalElements, type MountedTerminalApp, type TerminalConnectionState, type TerminalInputCallbacks, type TerminalKeyResolver, type TerminalLoadingPresentation, type TerminalOutputState, type TerminalScrollAdapter, type TerminalScrollAdapterContext, type TerminalScrollAdapterFactory, type TerminalScrollMetrics, type TerminalScrollOptions, type TerminalSessionPresentationState, type TerminalStatusListener, type TerminalStatusSnapshot, type TerminalTheme, type TerminalTransport, type TransportCloseEvent, type TransportHandlers, createAnsiStyleTracker, createBufferedTerminalWriter, defineAittyTerminalElement, installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey, mountAitty, mountTerminalApp };
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createAnsiStyleTracker } from "./frontend/ansi-style-tracker.js";
|
|
2
|
+
import { BrowserTerminalRenderer } from "./frontend/browser-terminal-renderer.js";
|
|
3
|
+
import { installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey } from "./frontend/terminal-input-policies.js";
|
|
4
|
+
import { createBufferedTerminalWriter, defineAittyTerminalElement, mountAitty, mountTerminalApp } from "./frontend/terminal-app.js";
|
|
5
|
+
export { BrowserTerminalRenderer, createAnsiStyleTracker, createBufferedTerminalWriter, defineAittyTerminalElement, installTerminalInputPolicies, isBrowserSafeShortcut, isCompositionKeyDown, isContainedTerminalNavigationKey, mountAitty, mountTerminalApp };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
//#region src/frontend/ansi-sequences.d.ts
|
|
2
|
+
declare function parseCsiParams(paramBuffer: string): number[];
|
|
3
|
+
declare function nextAltScreenState(currentState: boolean, privateMarker: string, paramBuffer: string, finalByte: string): boolean;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { nextAltScreenState, parseCsiParams };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/frontend/ansi-sequences.ts
|
|
2
|
+
const ALT_SCREEN_PARAMS = new Set([
|
|
3
|
+
47,
|
|
4
|
+
1047,
|
|
5
|
+
1049
|
|
6
|
+
]);
|
|
7
|
+
function parseCsiParams(paramBuffer) {
|
|
8
|
+
if (!paramBuffer) return [];
|
|
9
|
+
return paramBuffer.split(/[;:]/).map((value) => Number(value)).filter((value) => !Number.isNaN(value));
|
|
10
|
+
}
|
|
11
|
+
function nextAltScreenState(currentState, privateMarker, paramBuffer, finalByte) {
|
|
12
|
+
if (privateMarker !== "?" || finalByte !== "h" && finalByte !== "l") return currentState;
|
|
13
|
+
if (!parseCsiParams(paramBuffer).some((value) => ALT_SCREEN_PARAMS.has(value))) return currentState;
|
|
14
|
+
return finalByte === "h";
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { nextAltScreenState, parseCsiParams };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/frontend/ansi-style-tracker.d.ts
|
|
2
|
+
type NullableStyle = {
|
|
3
|
+
bg: string | null;
|
|
4
|
+
fg: string | null;
|
|
5
|
+
};
|
|
6
|
+
type StyleCell = (NullableStyle & {
|
|
7
|
+
displayStart?: number;
|
|
8
|
+
hidden?: boolean;
|
|
9
|
+
width?: number;
|
|
10
|
+
}) | null;
|
|
11
|
+
type CursorPosition = {
|
|
12
|
+
col: number;
|
|
13
|
+
row: number;
|
|
14
|
+
};
|
|
15
|
+
type AnsiStyleBridge = {
|
|
16
|
+
getCursor(): CursorPosition;
|
|
17
|
+
usingAltScreen(): boolean;
|
|
18
|
+
};
|
|
19
|
+
type AnsiStyleTracker = {
|
|
20
|
+
append(chunk: Uint8Array): void;
|
|
21
|
+
consumeRenderHints(): {
|
|
22
|
+
clearScreen: boolean;
|
|
23
|
+
styleChanged: boolean;
|
|
24
|
+
};
|
|
25
|
+
getGridOverride(row: number, col: number): StyleCell;
|
|
26
|
+
getScrollbackOverride(offset: number, col: number): StyleCell;
|
|
27
|
+
isAltScreenActive(): boolean;
|
|
28
|
+
reset(nextCols?: number, nextRows?: number): void;
|
|
29
|
+
resize(nextCols: number, nextRows: number): void;
|
|
30
|
+
syncFromBridge(bridge: AnsiStyleBridge | null | undefined): void;
|
|
31
|
+
};
|
|
32
|
+
declare function createAnsiStyleTracker(options?: {
|
|
33
|
+
cols?: number;
|
|
34
|
+
rows?: number;
|
|
35
|
+
}): AnsiStyleTracker;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { AnsiStyleBridge, AnsiStyleTracker, createAnsiStyleTracker };
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { nextAltScreenState, parseCsiParams } from "./ansi-sequences.js";
|
|
2
|
+
import { cellWidthForCodePoint } from "./cell-width.js";
|
|
3
|
+
//#region src/frontend/ansi-style-tracker.ts
|
|
4
|
+
const ESC = "\x1B";
|
|
5
|
+
function createBlankRow(cols) {
|
|
6
|
+
return Array.from({ length: cols }, () => null);
|
|
7
|
+
}
|
|
8
|
+
function cloneRow(row) {
|
|
9
|
+
return row.map((cell) => {
|
|
10
|
+
if (!cell) return null;
|
|
11
|
+
return {
|
|
12
|
+
bg: cell.bg ?? null,
|
|
13
|
+
displayStart: cell.displayStart,
|
|
14
|
+
fg: cell.fg ?? null,
|
|
15
|
+
hidden: cell.hidden,
|
|
16
|
+
width: cell.width
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function clamp(value, min, max) {
|
|
21
|
+
return Math.min(max, Math.max(min, value));
|
|
22
|
+
}
|
|
23
|
+
function createBuffer(cols, rows) {
|
|
24
|
+
return {
|
|
25
|
+
cursor: {
|
|
26
|
+
col: 0,
|
|
27
|
+
displayCol: 0,
|
|
28
|
+
row: 0
|
|
29
|
+
},
|
|
30
|
+
rows: Array.from({ length: rows }, () => createBlankRow(cols)),
|
|
31
|
+
savedCursor: {
|
|
32
|
+
col: 0,
|
|
33
|
+
displayCol: 0,
|
|
34
|
+
row: 0
|
|
35
|
+
},
|
|
36
|
+
scrollback: []
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function makeRgbCss(red, green, blue) {
|
|
40
|
+
return `rgb(${clamp(red, 0, 255)}, ${clamp(green, 0, 255)}, ${clamp(blue, 0, 255)})`;
|
|
41
|
+
}
|
|
42
|
+
function isPlainCharacter(character) {
|
|
43
|
+
return character >= " " && character !== "";
|
|
44
|
+
}
|
|
45
|
+
function createAnsiStyleTracker(options = {}) {
|
|
46
|
+
let cols = Math.max(1, options.cols ?? 80);
|
|
47
|
+
let rows = Math.max(1, options.rows ?? 24);
|
|
48
|
+
let mainBuffer = createBuffer(cols, rows);
|
|
49
|
+
let altBuffer = createBuffer(cols, rows);
|
|
50
|
+
let currentStyle = {
|
|
51
|
+
bg: null,
|
|
52
|
+
fg: null
|
|
53
|
+
};
|
|
54
|
+
let parserState = "text";
|
|
55
|
+
let csiPrivate = "";
|
|
56
|
+
let csiParams = "";
|
|
57
|
+
let decoder = new TextDecoder();
|
|
58
|
+
let useAltBuffer = false;
|
|
59
|
+
let renderHints = {
|
|
60
|
+
clearScreen: false,
|
|
61
|
+
styleChanged: false
|
|
62
|
+
};
|
|
63
|
+
const activeBuffer = () => useAltBuffer ? altBuffer : mainBuffer;
|
|
64
|
+
const clampCursor = (cursor) => {
|
|
65
|
+
cursor.row = clamp(cursor.row, 0, rows - 1);
|
|
66
|
+
cursor.col = clamp(cursor.col, 0, cols - 1);
|
|
67
|
+
cursor.displayCol = clamp(cursor.displayCol, 0, cols - 1);
|
|
68
|
+
};
|
|
69
|
+
const cellDisplayStart = (row, col) => row[col]?.displayStart ?? col;
|
|
70
|
+
const cellWidth = (row, col) => row[col]?.width ?? 1;
|
|
71
|
+
const makeCellState = (style, sourceCol, displayStart, width, hidden = false) => {
|
|
72
|
+
if (!hidden && style.fg === null && style.bg === null && displayStart === sourceCol && width === 1) return null;
|
|
73
|
+
return {
|
|
74
|
+
bg: style.bg,
|
|
75
|
+
displayStart,
|
|
76
|
+
fg: style.fg,
|
|
77
|
+
hidden,
|
|
78
|
+
width
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
const markCellHidden = (row, col) => {
|
|
82
|
+
const cell = row[col];
|
|
83
|
+
row[col] = {
|
|
84
|
+
bg: cell?.bg ?? null,
|
|
85
|
+
displayStart: cellDisplayStart(row, col),
|
|
86
|
+
fg: cell?.fg ?? null,
|
|
87
|
+
hidden: true,
|
|
88
|
+
width: cellWidth(row, col)
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
const hideCellsCoveredByDisplayRange = (row, displayStart, width, sourceCol) => {
|
|
92
|
+
const displayEnd = displayStart + width;
|
|
93
|
+
for (let col = 0; col < cols; col += 1) {
|
|
94
|
+
if (col === sourceCol) continue;
|
|
95
|
+
const cellStart = cellDisplayStart(row, col);
|
|
96
|
+
const cellEnd = cellStart + cellWidth(row, col);
|
|
97
|
+
if (cellStart < displayEnd && cellEnd > displayStart) markCellHidden(row, col);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const storeCellAtCursor = (width) => {
|
|
101
|
+
const buffer = activeBuffer();
|
|
102
|
+
const row = buffer.rows[buffer.cursor.row];
|
|
103
|
+
if (!row) return;
|
|
104
|
+
const displayStart = buffer.cursor.displayCol;
|
|
105
|
+
hideCellsCoveredByDisplayRange(row, displayStart, width, buffer.cursor.col);
|
|
106
|
+
row[buffer.cursor.col] = makeCellState(currentStyle, buffer.cursor.col, displayStart, width);
|
|
107
|
+
};
|
|
108
|
+
const scrollActiveBuffer = () => {
|
|
109
|
+
const buffer = activeBuffer();
|
|
110
|
+
const droppedRow = buffer.rows.shift() ?? createBlankRow(cols);
|
|
111
|
+
buffer.rows.push(createBlankRow(cols));
|
|
112
|
+
if (!useAltBuffer) mainBuffer.scrollback.unshift(cloneRow(droppedRow));
|
|
113
|
+
};
|
|
114
|
+
const moveCursorTo = (buffer, row, col) => {
|
|
115
|
+
buffer.cursor.row = clamp(row, 0, rows - 1);
|
|
116
|
+
buffer.cursor.col = clamp(col, 0, cols - 1);
|
|
117
|
+
buffer.cursor.displayCol = cellDisplayStart(buffer.rows[buffer.cursor.row] ?? [], buffer.cursor.col);
|
|
118
|
+
};
|
|
119
|
+
const advanceCursor = (width) => {
|
|
120
|
+
const buffer = activeBuffer();
|
|
121
|
+
if (buffer.cursor.col < cols - 1) {
|
|
122
|
+
buffer.cursor.col += 1;
|
|
123
|
+
buffer.cursor.displayCol = clamp(buffer.cursor.displayCol + width, 0, cols - 1);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
buffer.cursor.col = 0;
|
|
127
|
+
buffer.cursor.displayCol = 0;
|
|
128
|
+
if (buffer.cursor.row < rows - 1) {
|
|
129
|
+
buffer.cursor.row += 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
scrollActiveBuffer();
|
|
133
|
+
};
|
|
134
|
+
const clearRowRange = (rowIndex, startCol, endCol) => {
|
|
135
|
+
const row = activeBuffer().rows[rowIndex];
|
|
136
|
+
if (!row) return;
|
|
137
|
+
for (let index = startCol; index <= endCol; index += 1) row[index] = null;
|
|
138
|
+
};
|
|
139
|
+
const clearScreen = () => {
|
|
140
|
+
const buffer = activeBuffer();
|
|
141
|
+
buffer.rows = Array.from({ length: rows }, () => createBlankRow(cols));
|
|
142
|
+
renderHints.clearScreen = true;
|
|
143
|
+
};
|
|
144
|
+
const resetTerminalState = () => {
|
|
145
|
+
mainBuffer = createBuffer(cols, rows);
|
|
146
|
+
altBuffer = createBuffer(cols, rows);
|
|
147
|
+
currentStyle = {
|
|
148
|
+
bg: null,
|
|
149
|
+
fg: null
|
|
150
|
+
};
|
|
151
|
+
useAltBuffer = false;
|
|
152
|
+
renderHints.clearScreen = true;
|
|
153
|
+
};
|
|
154
|
+
const resizeRows = (buffer, nextRows) => {
|
|
155
|
+
if (nextRows === buffer.rows.length) return;
|
|
156
|
+
if (nextRows > buffer.rows.length) {
|
|
157
|
+
const missing = nextRows - buffer.rows.length;
|
|
158
|
+
buffer.rows = [...buffer.rows, ...Array.from({ length: missing }, () => createBlankRow(cols))];
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const removeCount = buffer.rows.length - nextRows;
|
|
162
|
+
const removedRows = buffer.rows.splice(0, removeCount);
|
|
163
|
+
if (buffer === mainBuffer) for (const row of removedRows) mainBuffer.scrollback.unshift(cloneRow(row));
|
|
164
|
+
moveCursorTo(buffer, Math.max(0, buffer.cursor.row - removeCount), buffer.cursor.col);
|
|
165
|
+
};
|
|
166
|
+
const resizeCols = (buffer, nextCols) => {
|
|
167
|
+
buffer.rows = buffer.rows.map((row) => {
|
|
168
|
+
if (nextCols === row.length) return row;
|
|
169
|
+
if (nextCols > row.length) return [...row, ...Array.from({ length: nextCols - row.length }, () => null)];
|
|
170
|
+
return row.slice(0, nextCols);
|
|
171
|
+
});
|
|
172
|
+
buffer.cursor.col = clamp(buffer.cursor.col, 0, nextCols - 1);
|
|
173
|
+
buffer.cursor.displayCol = clamp(buffer.cursor.displayCol, 0, nextCols - 1);
|
|
174
|
+
buffer.savedCursor.col = clamp(buffer.savedCursor.col, 0, nextCols - 1);
|
|
175
|
+
buffer.savedCursor.displayCol = clamp(buffer.savedCursor.displayCol, 0, nextCols - 1);
|
|
176
|
+
};
|
|
177
|
+
const setAltBuffer = (nextState) => {
|
|
178
|
+
if (nextState === useAltBuffer) return;
|
|
179
|
+
if (nextState) {
|
|
180
|
+
mainBuffer.savedCursor = { ...mainBuffer.cursor };
|
|
181
|
+
altBuffer = createBuffer(cols, rows);
|
|
182
|
+
useAltBuffer = true;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
useAltBuffer = false;
|
|
186
|
+
mainBuffer.cursor = { ...mainBuffer.savedCursor };
|
|
187
|
+
};
|
|
188
|
+
const handleSgr = (params) => {
|
|
189
|
+
const values = params.length === 0 ? [0] : params;
|
|
190
|
+
renderHints.styleChanged = true;
|
|
191
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
192
|
+
const value = values[index] ?? 0;
|
|
193
|
+
if (value === 0) {
|
|
194
|
+
currentStyle = {
|
|
195
|
+
bg: null,
|
|
196
|
+
fg: null
|
|
197
|
+
};
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (value === 39 || value >= 30 && value <= 37 || value >= 90 && value <= 97) {
|
|
201
|
+
currentStyle.fg = null;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (value === 49 || value >= 40 && value <= 47 || value >= 100 && value <= 107) {
|
|
205
|
+
currentStyle.bg = null;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (value === 38 || value === 48) {
|
|
209
|
+
const target = value === 38 ? "fg" : "bg";
|
|
210
|
+
const mode = values[index + 1];
|
|
211
|
+
if (mode === 2 && index + 4 < values.length) {
|
|
212
|
+
currentStyle[target] = makeRgbCss(values[index + 2], values[index + 3], values[index + 4]);
|
|
213
|
+
index += 4;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (mode === 5 && index + 2 < values.length) {
|
|
217
|
+
currentStyle[target] = null;
|
|
218
|
+
index += 2;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const handleEraseDisplay = (mode) => {
|
|
224
|
+
const buffer = activeBuffer();
|
|
225
|
+
if (mode === 2) {
|
|
226
|
+
clearScreen();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (mode === 0) {
|
|
230
|
+
clearRowRange(buffer.cursor.row, buffer.cursor.col, cols - 1);
|
|
231
|
+
for (let row = buffer.cursor.row + 1; row < rows; row += 1) clearRowRange(row, 0, cols - 1);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (mode === 1) {
|
|
235
|
+
for (let row = 0; row < buffer.cursor.row; row += 1) clearRowRange(row, 0, cols - 1);
|
|
236
|
+
clearRowRange(buffer.cursor.row, 0, buffer.cursor.col);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const handleEraseLine = (mode) => {
|
|
240
|
+
const cursor = activeBuffer().cursor;
|
|
241
|
+
if (mode === 2) {
|
|
242
|
+
clearRowRange(cursor.row, 0, cols - 1);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (mode === 1) {
|
|
246
|
+
clearRowRange(cursor.row, 0, cursor.col);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
clearRowRange(cursor.row, cursor.col, cols - 1);
|
|
250
|
+
};
|
|
251
|
+
const handleCsi = (finalByte) => {
|
|
252
|
+
const params = parseCsiParams(csiParams);
|
|
253
|
+
const buffer = activeBuffer();
|
|
254
|
+
setAltBuffer(nextAltScreenState(useAltBuffer, csiPrivate, csiParams, finalByte));
|
|
255
|
+
switch (finalByte) {
|
|
256
|
+
case "A":
|
|
257
|
+
moveCursorTo(buffer, buffer.cursor.row - Math.max(1, params[0] ?? 1), buffer.cursor.col);
|
|
258
|
+
break;
|
|
259
|
+
case "B":
|
|
260
|
+
moveCursorTo(buffer, buffer.cursor.row + Math.max(1, params[0] ?? 1), buffer.cursor.col);
|
|
261
|
+
break;
|
|
262
|
+
case "C":
|
|
263
|
+
moveCursorTo(buffer, buffer.cursor.row, buffer.cursor.col + Math.max(1, params[0] ?? 1));
|
|
264
|
+
break;
|
|
265
|
+
case "D":
|
|
266
|
+
moveCursorTo(buffer, buffer.cursor.row, buffer.cursor.col - Math.max(1, params[0] ?? 1));
|
|
267
|
+
break;
|
|
268
|
+
case "G":
|
|
269
|
+
moveCursorTo(buffer, buffer.cursor.row, (params[0] ?? 1) - 1);
|
|
270
|
+
break;
|
|
271
|
+
case "H":
|
|
272
|
+
case "f":
|
|
273
|
+
moveCursorTo(buffer, (params[0] ?? 1) - 1, (params[1] ?? 1) - 1);
|
|
274
|
+
break;
|
|
275
|
+
case "J":
|
|
276
|
+
handleEraseDisplay(params[0] ?? 0);
|
|
277
|
+
break;
|
|
278
|
+
case "K":
|
|
279
|
+
handleEraseLine(params[0] ?? 0);
|
|
280
|
+
break;
|
|
281
|
+
case "m":
|
|
282
|
+
handleSgr(params);
|
|
283
|
+
break;
|
|
284
|
+
case "s":
|
|
285
|
+
buffer.savedCursor = { ...buffer.cursor };
|
|
286
|
+
break;
|
|
287
|
+
case "u":
|
|
288
|
+
buffer.cursor = { ...buffer.savedCursor };
|
|
289
|
+
break;
|
|
290
|
+
default: break;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const writeCharacter = (character) => {
|
|
294
|
+
const width = cellWidthForCodePoint(character.codePointAt(0) ?? 32);
|
|
295
|
+
storeCellAtCursor(width);
|
|
296
|
+
advanceCursor(width);
|
|
297
|
+
};
|
|
298
|
+
const processTextCharacter = (character) => {
|
|
299
|
+
const buffer = activeBuffer();
|
|
300
|
+
if (character === ESC) {
|
|
301
|
+
parserState = "escape";
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (character === "\b") {
|
|
305
|
+
moveCursorTo(buffer, buffer.cursor.row, buffer.cursor.col - 1);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (character === "\n") {
|
|
309
|
+
if (buffer.cursor.row < rows - 1) buffer.cursor.row += 1;
|
|
310
|
+
else scrollActiveBuffer();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (character === "\r") {
|
|
314
|
+
buffer.cursor.col = 0;
|
|
315
|
+
buffer.cursor.displayCol = 0;
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (character === " ") {
|
|
319
|
+
const nextTabStop = (Math.floor(buffer.cursor.displayCol / 8) + 1) * 8;
|
|
320
|
+
moveCursorTo(buffer, buffer.cursor.row, nextTabStop);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!isPlainCharacter(character)) return;
|
|
324
|
+
writeCharacter(character);
|
|
325
|
+
};
|
|
326
|
+
const appendText = (text) => {
|
|
327
|
+
for (const character of text) {
|
|
328
|
+
if (parserState === "escape") {
|
|
329
|
+
if (character === "[") {
|
|
330
|
+
parserState = "csi";
|
|
331
|
+
csiPrivate = "";
|
|
332
|
+
csiParams = "";
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (character === "]") {
|
|
336
|
+
parserState = "osc";
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (character === "7") activeBuffer().savedCursor = { ...activeBuffer().cursor };
|
|
340
|
+
else if (character === "8") activeBuffer().cursor = { ...activeBuffer().savedCursor };
|
|
341
|
+
else if (character === "c") resetTerminalState();
|
|
342
|
+
parserState = "text";
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (parserState === "csi") {
|
|
346
|
+
if ((character === "?" || character === ">" || character === "!") && csiParams.length === 0) {
|
|
347
|
+
csiPrivate = character;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (character >= "0" && character <= "9" || character === ";" || character === ":") {
|
|
351
|
+
csiParams += character;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (character >= "@" && character <= "~") {
|
|
355
|
+
handleCsi(character);
|
|
356
|
+
parserState = "text";
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (parserState === "osc") {
|
|
361
|
+
if (character === "\x07") {
|
|
362
|
+
parserState = "text";
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (character === ESC) parserState = "osc_escape";
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (parserState === "osc_escape") {
|
|
369
|
+
parserState = character === "\\" ? "text" : "osc";
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
processTextCharacter(character);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
return {
|
|
376
|
+
append(chunk) {
|
|
377
|
+
appendText(decoder.decode(chunk, { stream: true }));
|
|
378
|
+
},
|
|
379
|
+
getGridOverride(row, col) {
|
|
380
|
+
return activeBuffer().rows[row]?.[col] ?? null;
|
|
381
|
+
},
|
|
382
|
+
getScrollbackOverride(offset, col) {
|
|
383
|
+
return mainBuffer.scrollback[offset]?.[col] ?? null;
|
|
384
|
+
},
|
|
385
|
+
isAltScreenActive() {
|
|
386
|
+
return useAltBuffer;
|
|
387
|
+
},
|
|
388
|
+
reset(nextCols = cols, nextRows = rows) {
|
|
389
|
+
cols = Math.max(1, nextCols);
|
|
390
|
+
rows = Math.max(1, nextRows);
|
|
391
|
+
mainBuffer = createBuffer(cols, rows);
|
|
392
|
+
altBuffer = createBuffer(cols, rows);
|
|
393
|
+
currentStyle = {
|
|
394
|
+
bg: null,
|
|
395
|
+
fg: null
|
|
396
|
+
};
|
|
397
|
+
parserState = "text";
|
|
398
|
+
renderHints.clearScreen = false;
|
|
399
|
+
csiPrivate = "";
|
|
400
|
+
csiParams = "";
|
|
401
|
+
decoder = new TextDecoder();
|
|
402
|
+
useAltBuffer = false;
|
|
403
|
+
},
|
|
404
|
+
resize(nextCols, nextRows) {
|
|
405
|
+
cols = Math.max(1, nextCols);
|
|
406
|
+
rows = Math.max(1, nextRows);
|
|
407
|
+
resizeCols(mainBuffer, cols);
|
|
408
|
+
resizeCols(altBuffer, cols);
|
|
409
|
+
resizeRows(mainBuffer, rows);
|
|
410
|
+
resizeRows(altBuffer, rows);
|
|
411
|
+
clampCursor(mainBuffer.cursor);
|
|
412
|
+
clampCursor(mainBuffer.savedCursor);
|
|
413
|
+
clampCursor(altBuffer.cursor);
|
|
414
|
+
clampCursor(altBuffer.savedCursor);
|
|
415
|
+
},
|
|
416
|
+
syncFromBridge(bridge) {
|
|
417
|
+
if (!bridge) return;
|
|
418
|
+
useAltBuffer = bridge.usingAltScreen();
|
|
419
|
+
const cursor = bridge.getCursor();
|
|
420
|
+
const buffer = activeBuffer();
|
|
421
|
+
buffer.cursor.row = clamp(cursor.row, 0, rows - 1);
|
|
422
|
+
buffer.cursor.col = clamp(cursor.col, 0, cols - 1);
|
|
423
|
+
},
|
|
424
|
+
consumeRenderHints() {
|
|
425
|
+
const nextHints = renderHints;
|
|
426
|
+
renderHints = {
|
|
427
|
+
clearScreen: false,
|
|
428
|
+
styleChanged: false
|
|
429
|
+
};
|
|
430
|
+
return nextHints;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
//#endregion
|
|
435
|
+
export { createAnsiStyleTracker };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//#region src/frontend/browser-terminal-renderer.d.ts
|
|
2
|
+
type TerminalCell = {
|
|
3
|
+
bg: number;
|
|
4
|
+
char: number;
|
|
5
|
+
fg: number;
|
|
6
|
+
flags: number;
|
|
7
|
+
};
|
|
8
|
+
type TerminalCursor = {
|
|
9
|
+
col: number;
|
|
10
|
+
row: number;
|
|
11
|
+
visible: boolean;
|
|
12
|
+
};
|
|
13
|
+
type BrowserTerminalBridge = {
|
|
14
|
+
bracketedPaste?(): boolean;
|
|
15
|
+
clearDirty(): void;
|
|
16
|
+
cursorKeysApp?(): boolean;
|
|
17
|
+
getCell(row: number, col: number): TerminalCell;
|
|
18
|
+
getCols(): number;
|
|
19
|
+
getCursor(): TerminalCursor;
|
|
20
|
+
getRows(): number;
|
|
21
|
+
getScrollbackCell(offset: number, col: number): TerminalCell;
|
|
22
|
+
getScrollbackCount(): number;
|
|
23
|
+
getScrollbackLineLen(offset: number): number;
|
|
24
|
+
init?(cols: number, rows: number): void;
|
|
25
|
+
isDirtyRow(row: number): boolean;
|
|
26
|
+
usingAltScreen(): boolean;
|
|
27
|
+
};
|
|
28
|
+
type BrowserTerminalStyleOverride = {
|
|
29
|
+
bg?: string | null;
|
|
30
|
+
fg?: string | null;
|
|
31
|
+
hidden?: boolean;
|
|
32
|
+
} | null;
|
|
33
|
+
type BrowserTerminalStyleTracker = {
|
|
34
|
+
getGridOverride(row: number, col: number): BrowserTerminalStyleOverride;
|
|
35
|
+
getScrollbackOverride(offset: number, col: number): BrowserTerminalStyleOverride;
|
|
36
|
+
};
|
|
37
|
+
type BrowserTerminalRenderOptions = {
|
|
38
|
+
force?: boolean;
|
|
39
|
+
};
|
|
40
|
+
type SnapshotRow = {
|
|
41
|
+
element: HTMLElement;
|
|
42
|
+
text: string;
|
|
43
|
+
};
|
|
44
|
+
type GetCell = (col: number) => TerminalCell;
|
|
45
|
+
type GetOverride = (col: number) => BrowserTerminalStyleOverride;
|
|
46
|
+
declare class BrowserTerminalRenderer {
|
|
47
|
+
private _altScreenMeaningfulScrollbackCount;
|
|
48
|
+
private _altScreenVisibleRowSnapshot;
|
|
49
|
+
private _lastAltScreenState;
|
|
50
|
+
private _renderedScrollbackCount;
|
|
51
|
+
private _restoreSnapshotCount;
|
|
52
|
+
private _scrollbackRowEls;
|
|
53
|
+
cols: number;
|
|
54
|
+
container: HTMLElement;
|
|
55
|
+
prevContainerBg: string;
|
|
56
|
+
prevCursorCol: number;
|
|
57
|
+
prevCursorRow: number;
|
|
58
|
+
prevCursorVisible: boolean;
|
|
59
|
+
prevRowBg: string[];
|
|
60
|
+
rowEls: HTMLElement[];
|
|
61
|
+
rows: number;
|
|
62
|
+
styleTracker: BrowserTerminalStyleTracker;
|
|
63
|
+
constructor(container: HTMLElement, styleTracker: BrowserTerminalStyleTracker);
|
|
64
|
+
_resolveFirstLiveGridRow(): HTMLElement | null;
|
|
65
|
+
_insertScrollbackFragment(fragment: DocumentFragment): void;
|
|
66
|
+
setup(cols: number, rows: number): void;
|
|
67
|
+
_buildRowContent(rowEl: HTMLElement, getCell: GetCell, getOverride: GetOverride, lineLen: number, cursorCol: number, rowIndex: number): void;
|
|
68
|
+
_buildScrollbackRowEl(bridge: BrowserTerminalBridge, offset: number): HTMLElement;
|
|
69
|
+
_captureVisibleRowSnapshot(): void;
|
|
70
|
+
_clearAltScreenRestoreSnapshot(): void;
|
|
71
|
+
_readGridRowText(bridge: BrowserTerminalBridge, row: number): string;
|
|
72
|
+
getVisibleRowTexts(bridge: BrowserTerminalBridge, {
|
|
73
|
+
trimPadding
|
|
74
|
+
}?: {
|
|
75
|
+
trimPadding?: boolean;
|
|
76
|
+
}): string[];
|
|
77
|
+
_readScrollbackRowText(bridge: BrowserTerminalBridge, offset: number): string;
|
|
78
|
+
_readLiveTranscriptRows(bridge: BrowserTerminalBridge, currentVisibleRows: string[]): string[];
|
|
79
|
+
_liveTranscriptContainsSnapshots(bridge: BrowserTerminalBridge, snapshots: SnapshotRow[], currentVisibleRows: string[]): boolean;
|
|
80
|
+
_getRestoredScrollbackSnapshots(bridge: BrowserTerminalBridge): SnapshotRow[];
|
|
81
|
+
_rebuildScrollback(bridge: BrowserTerminalBridge, restoredSnapshots: SnapshotRow[]): void;
|
|
82
|
+
syncScrollback(bridge: BrowserTerminalBridge, restoredSnapshots?: SnapshotRow[]): void;
|
|
83
|
+
render(bridge: BrowserTerminalBridge, options?: BrowserTerminalRenderOptions): void;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { BrowserTerminalBridge, BrowserTerminalRenderOptions, BrowserTerminalRenderer, BrowserTerminalStyleOverride, BrowserTerminalStyleTracker };
|