terminal-pilot 0.0.3 → 0.0.5
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 +6 -4
- package/dist/terminal-buffer.d.ts +38 -0
- package/dist/terminal-buffer.js +445 -0
- package/dist/terminal-pilot.d.ts +1 -0
- package/dist/terminal-pilot.js +4 -4
- package/dist/terminal-session.d.ts +3 -0
- package/dist/terminal-session.js +15 -4
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -61,8 +61,9 @@ type NewSessionOptions = {
|
|
|
61
61
|
class TerminalPilot {
|
|
62
62
|
static launch(): Promise<TerminalPilot>;
|
|
63
63
|
newSession(options: NewSessionOptions): Promise<TerminalSession>;
|
|
64
|
-
getSession(id: string): TerminalSession; // throws if
|
|
65
|
-
|
|
64
|
+
getSession(id: string): TerminalSession; // throws if not in map; works for exited sessions until deleteSession
|
|
65
|
+
deleteSession(id: string): void; // explicit removal from session map
|
|
66
|
+
sessions(): TerminalSession[]; // running sessions only (exitCode === null)
|
|
66
67
|
close(): Promise<void>;
|
|
67
68
|
}
|
|
68
69
|
```
|
|
@@ -122,12 +123,13 @@ class TerminalSession {
|
|
|
122
123
|
readonly pid: number;
|
|
123
124
|
exitCode: number | null;
|
|
124
125
|
|
|
125
|
-
type(text: string): Promise<void>; // character-by-character
|
|
126
|
-
fill(text: string): Promise<void>; // bulk write
|
|
126
|
+
type(text: string): Promise<void>; // character-by-character with delay
|
|
127
|
+
fill(text: string): Promise<void>; // bulk write (\n → \r)
|
|
127
128
|
press(key: TerminalKey): Promise<void>;
|
|
128
129
|
send(raw: string): Promise<void>; // raw bytes / escape sequences
|
|
129
130
|
signal(signal: string): Promise<void>;
|
|
130
131
|
waitFor(pattern: string | RegExp, options?: WaitForOptions): Promise<string>;
|
|
132
|
+
waitForExit(options?: { timeout?: number }): Promise<number>; // throws on timeout
|
|
131
133
|
waitForQuiet(ms: number): Promise<void>;
|
|
132
134
|
screen(): Promise<TerminalScreen>;
|
|
133
135
|
history(options?: HistoryOptions): Promise<string[]>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type Cell = [number, string] | null;
|
|
2
|
+
type Row = Cell[];
|
|
3
|
+
export declare class TerminalBuffer {
|
|
4
|
+
private _cols;
|
|
5
|
+
private _rows;
|
|
6
|
+
private _screen;
|
|
7
|
+
private _cursorX;
|
|
8
|
+
private _cursorY;
|
|
9
|
+
private _savedCursor;
|
|
10
|
+
private _scrollTop;
|
|
11
|
+
private _scrollBottom;
|
|
12
|
+
private _state;
|
|
13
|
+
private _csiParams;
|
|
14
|
+
private _csiPrivate;
|
|
15
|
+
readonly displayBuffer: {
|
|
16
|
+
readonly cursorX: number;
|
|
17
|
+
readonly cursorY: number;
|
|
18
|
+
readonly data: Array<Row | undefined>;
|
|
19
|
+
};
|
|
20
|
+
constructor(cols: number, rows: number);
|
|
21
|
+
write(data: string): void;
|
|
22
|
+
resize(cols: number, rows: number): void;
|
|
23
|
+
private _makeScreen;
|
|
24
|
+
private _makeRow;
|
|
25
|
+
private _clamp;
|
|
26
|
+
private _setChar;
|
|
27
|
+
private _eraseLine;
|
|
28
|
+
private _scrollUp;
|
|
29
|
+
private _scrollDown;
|
|
30
|
+
private _newline;
|
|
31
|
+
private _parseCsiParams;
|
|
32
|
+
private _execCsi;
|
|
33
|
+
private _feed;
|
|
34
|
+
private _feedNormal;
|
|
35
|
+
private _feedEscape;
|
|
36
|
+
private _feedCsi;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
export class TerminalBuffer {
|
|
2
|
+
_cols;
|
|
3
|
+
_rows;
|
|
4
|
+
_screen;
|
|
5
|
+
_cursorX = 0;
|
|
6
|
+
_cursorY = 0;
|
|
7
|
+
_savedCursor = { x: 0, y: 0 };
|
|
8
|
+
_scrollTop = 0;
|
|
9
|
+
_scrollBottom;
|
|
10
|
+
_state = 0 /* State.Normal */;
|
|
11
|
+
_csiParams = "";
|
|
12
|
+
_csiPrivate = "";
|
|
13
|
+
displayBuffer;
|
|
14
|
+
constructor(cols, rows) {
|
|
15
|
+
this._cols = cols;
|
|
16
|
+
this._rows = rows;
|
|
17
|
+
this._scrollBottom = rows - 1;
|
|
18
|
+
this._screen = this._makeScreen(cols, rows);
|
|
19
|
+
this.displayBuffer = Object.defineProperties({}, {
|
|
20
|
+
cursorX: { get: () => this._cursorX, enumerable: true },
|
|
21
|
+
cursorY: { get: () => this._cursorY, enumerable: true },
|
|
22
|
+
data: { get: () => this._screen, enumerable: true },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
write(data) {
|
|
26
|
+
for (const ch of data) {
|
|
27
|
+
this._feed(ch);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
resize(cols, rows) {
|
|
31
|
+
// Adjust row count
|
|
32
|
+
while (this._screen.length < rows) {
|
|
33
|
+
this._screen.push(this._makeRow(cols));
|
|
34
|
+
}
|
|
35
|
+
this._screen.length = rows;
|
|
36
|
+
// Adjust col count for each row
|
|
37
|
+
for (let y = 0; y < rows; y++) {
|
|
38
|
+
const row = this._screen[y] ?? this._makeRow(cols);
|
|
39
|
+
while (row.length < cols)
|
|
40
|
+
row.push(null);
|
|
41
|
+
row.length = cols;
|
|
42
|
+
this._screen[y] = row;
|
|
43
|
+
}
|
|
44
|
+
this._cols = cols;
|
|
45
|
+
this._rows = rows;
|
|
46
|
+
this._scrollTop = 0;
|
|
47
|
+
this._scrollBottom = rows - 1;
|
|
48
|
+
this._cursorX = this._clamp(this._cursorX, 0, cols - 1);
|
|
49
|
+
this._cursorY = this._clamp(this._cursorY, 0, rows - 1);
|
|
50
|
+
}
|
|
51
|
+
_makeScreen(cols, rows) {
|
|
52
|
+
return Array.from({ length: rows }, () => this._makeRow(cols));
|
|
53
|
+
}
|
|
54
|
+
_makeRow(cols) {
|
|
55
|
+
return Array(cols).fill(null);
|
|
56
|
+
}
|
|
57
|
+
_clamp(value, min, max) {
|
|
58
|
+
return Math.max(min, Math.min(max, value));
|
|
59
|
+
}
|
|
60
|
+
_setChar(y, x, ch) {
|
|
61
|
+
const row = this._screen[y];
|
|
62
|
+
if (row && x >= 0 && x < this._cols) {
|
|
63
|
+
row[x] = [ch.charCodeAt(0), ch];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
_eraseLine(y, fromX, toX) {
|
|
67
|
+
const row = this._screen[y];
|
|
68
|
+
if (!row)
|
|
69
|
+
return;
|
|
70
|
+
for (let x = fromX; x <= toX && x < this._cols; x++) {
|
|
71
|
+
row[x] = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
_scrollUp(count) {
|
|
75
|
+
for (let i = 0; i < count; i++) {
|
|
76
|
+
this._screen.splice(this._scrollTop, 1);
|
|
77
|
+
this._screen.splice(this._scrollBottom, 0, this._makeRow(this._cols));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
_scrollDown(count) {
|
|
81
|
+
for (let i = 0; i < count; i++) {
|
|
82
|
+
this._screen.splice(this._scrollBottom, 1);
|
|
83
|
+
this._screen.splice(this._scrollTop, 0, this._makeRow(this._cols));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
_newline() {
|
|
87
|
+
if (this._cursorY === this._scrollBottom) {
|
|
88
|
+
this._scrollUp(1);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this._cursorY = Math.min(this._cursorY + 1, this._rows - 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
_parseCsiParams() {
|
|
95
|
+
if (!this._csiParams)
|
|
96
|
+
return [];
|
|
97
|
+
return this._csiParams.split(";").map((s) => (s === "" ? 0 : parseInt(s, 10)));
|
|
98
|
+
}
|
|
99
|
+
_execCsi(final) {
|
|
100
|
+
const params = this._parseCsiParams();
|
|
101
|
+
const p0 = params[0] ?? 0;
|
|
102
|
+
const p1 = params[1] ?? 0;
|
|
103
|
+
if (this._csiPrivate === "?") {
|
|
104
|
+
if (final === "h" || final === "l") {
|
|
105
|
+
// Only handle alt screen (1049) — ignore everything else
|
|
106
|
+
if (params.includes(1049)) {
|
|
107
|
+
if (final === "h") {
|
|
108
|
+
this._screen = this._makeScreen(this._cols, this._rows);
|
|
109
|
+
this._cursorX = 0;
|
|
110
|
+
this._cursorY = 0;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this._screen = this._makeScreen(this._cols, this._rows);
|
|
114
|
+
this._cursorX = 0;
|
|
115
|
+
this._cursorY = 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
switch (final) {
|
|
122
|
+
case "A": // cursor up
|
|
123
|
+
this._cursorY = this._clamp(this._cursorY - Math.max(1, p0), 0, this._rows - 1);
|
|
124
|
+
break;
|
|
125
|
+
case "B": // cursor down
|
|
126
|
+
this._cursorY = this._clamp(this._cursorY + Math.max(1, p0), 0, this._rows - 1);
|
|
127
|
+
break;
|
|
128
|
+
case "C": // cursor forward
|
|
129
|
+
case "a":
|
|
130
|
+
this._cursorX = this._clamp(this._cursorX + Math.max(1, p0), 0, this._cols - 1);
|
|
131
|
+
break;
|
|
132
|
+
case "D": // cursor backward
|
|
133
|
+
this._cursorX = this._clamp(this._cursorX - Math.max(1, p0), 0, this._cols - 1);
|
|
134
|
+
break;
|
|
135
|
+
case "E": // cursor next line
|
|
136
|
+
this._cursorY = this._clamp(this._cursorY + Math.max(1, p0), 0, this._rows - 1);
|
|
137
|
+
this._cursorX = 0;
|
|
138
|
+
break;
|
|
139
|
+
case "F": // cursor preceding line
|
|
140
|
+
this._cursorY = this._clamp(this._cursorY - Math.max(1, p0), 0, this._rows - 1);
|
|
141
|
+
this._cursorX = 0;
|
|
142
|
+
break;
|
|
143
|
+
case "G": // cursor horizontal absolute
|
|
144
|
+
case "`":
|
|
145
|
+
this._cursorX = this._clamp(Math.max(1, p0) - 1, 0, this._cols - 1);
|
|
146
|
+
break;
|
|
147
|
+
case "H": // cursor position
|
|
148
|
+
case "f":
|
|
149
|
+
this._cursorY = this._clamp(Math.max(1, p0) - 1, 0, this._rows - 1);
|
|
150
|
+
this._cursorX = this._clamp(Math.max(1, p1) - 1, 0, this._cols - 1);
|
|
151
|
+
break;
|
|
152
|
+
case "I": // cursor forward tabulation
|
|
153
|
+
for (let i = 0; i < Math.max(1, p0); i++) {
|
|
154
|
+
this._cursorX = Math.min(this._cols - 1, (Math.floor(this._cursorX / 8) + 1) * 8);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
case "J": // erase in display
|
|
158
|
+
if (p0 === 0) {
|
|
159
|
+
this._eraseLine(this._cursorY, this._cursorX, this._cols - 1);
|
|
160
|
+
for (let y = this._cursorY + 1; y < this._rows; y++)
|
|
161
|
+
this._eraseLine(y, 0, this._cols - 1);
|
|
162
|
+
}
|
|
163
|
+
else if (p0 === 1) {
|
|
164
|
+
for (let y = 0; y < this._cursorY; y++)
|
|
165
|
+
this._eraseLine(y, 0, this._cols - 1);
|
|
166
|
+
this._eraseLine(this._cursorY, 0, this._cursorX);
|
|
167
|
+
}
|
|
168
|
+
else if (p0 === 2 || p0 === 3) {
|
|
169
|
+
for (let y = 0; y < this._rows; y++)
|
|
170
|
+
this._eraseLine(y, 0, this._cols - 1);
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case "K": // erase in line
|
|
174
|
+
if (p0 === 0)
|
|
175
|
+
this._eraseLine(this._cursorY, this._cursorX, this._cols - 1);
|
|
176
|
+
else if (p0 === 1)
|
|
177
|
+
this._eraseLine(this._cursorY, 0, this._cursorX);
|
|
178
|
+
else if (p0 === 2)
|
|
179
|
+
this._eraseLine(this._cursorY, 0, this._cols - 1);
|
|
180
|
+
break;
|
|
181
|
+
case "X": // erase characters (ECH)
|
|
182
|
+
this._eraseLine(this._cursorY, this._cursorX, this._cursorX + Math.max(1, p0) - 1);
|
|
183
|
+
break;
|
|
184
|
+
case "L": { // insert lines
|
|
185
|
+
const n = Math.max(1, p0);
|
|
186
|
+
for (let i = 0; i < n; i++) {
|
|
187
|
+
this._screen.splice(this._scrollBottom, 1);
|
|
188
|
+
this._screen.splice(this._cursorY, 0, this._makeRow(this._cols));
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "M": { // delete lines
|
|
193
|
+
const n = Math.max(1, p0);
|
|
194
|
+
for (let i = 0; i < n; i++) {
|
|
195
|
+
this._screen.splice(this._cursorY, 1);
|
|
196
|
+
this._screen.splice(this._scrollBottom, 0, this._makeRow(this._cols));
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "P": { // delete characters
|
|
201
|
+
const row = this._screen[this._cursorY];
|
|
202
|
+
if (row) {
|
|
203
|
+
const n = Math.max(1, p0);
|
|
204
|
+
row.splice(this._cursorX, n);
|
|
205
|
+
while (row.length < this._cols)
|
|
206
|
+
row.push(null);
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case "@": { // insert blank characters
|
|
211
|
+
const row = this._screen[this._cursorY];
|
|
212
|
+
if (row) {
|
|
213
|
+
const n = Math.max(1, p0);
|
|
214
|
+
for (let i = 0; i < n; i++)
|
|
215
|
+
row.splice(this._cursorX, 0, null);
|
|
216
|
+
row.splice(this._cols);
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case "S": // scroll up
|
|
221
|
+
this._scrollUp(Math.max(1, p0));
|
|
222
|
+
break;
|
|
223
|
+
case "T": // scroll down
|
|
224
|
+
if (params.length <= 1)
|
|
225
|
+
this._scrollDown(Math.max(1, p0));
|
|
226
|
+
break;
|
|
227
|
+
case "Z": { // cursor backward tabulation
|
|
228
|
+
const n = Math.max(1, p0);
|
|
229
|
+
for (let i = 0; i < n; i++) {
|
|
230
|
+
this._cursorX = Math.max(0, (Math.ceil(this._cursorX / 8) - 1) * 8);
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "d": // line position absolute
|
|
235
|
+
this._cursorY = this._clamp(Math.max(1, p0) - 1, 0, this._rows - 1);
|
|
236
|
+
break;
|
|
237
|
+
case "e": // vertical position relative
|
|
238
|
+
this._cursorY = this._clamp(this._cursorY + Math.max(1, p0), 0, this._rows - 1);
|
|
239
|
+
break;
|
|
240
|
+
case "r": { // set scrolling region
|
|
241
|
+
const top = this._clamp(Math.max(1, p0) - 1, 0, this._rows - 1);
|
|
242
|
+
const bottom = this._clamp((p1 === 0 ? this._rows : p1) - 1, 0, this._rows - 1);
|
|
243
|
+
if (top < bottom) {
|
|
244
|
+
this._scrollTop = top;
|
|
245
|
+
this._scrollBottom = bottom;
|
|
246
|
+
}
|
|
247
|
+
this._cursorX = 0;
|
|
248
|
+
this._cursorY = 0;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "s": // save cursor
|
|
252
|
+
this._savedCursor = { x: this._cursorX, y: this._cursorY };
|
|
253
|
+
break;
|
|
254
|
+
case "u": // restore cursor
|
|
255
|
+
this._cursorX = this._clamp(this._savedCursor.x, 0, this._cols - 1);
|
|
256
|
+
this._cursorY = this._clamp(this._savedCursor.y, 0, this._rows - 1);
|
|
257
|
+
break;
|
|
258
|
+
case "m": // SGR — ignore (we don't track attributes)
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
_feed(ch) {
|
|
265
|
+
const code = ch.charCodeAt(0);
|
|
266
|
+
switch (this._state) {
|
|
267
|
+
case 0 /* State.Normal */:
|
|
268
|
+
this._feedNormal(ch, code);
|
|
269
|
+
break;
|
|
270
|
+
case 1 /* State.Escape */:
|
|
271
|
+
this._feedEscape(ch, code);
|
|
272
|
+
break;
|
|
273
|
+
case 2 /* State.Csi */:
|
|
274
|
+
this._feedCsi(ch, code);
|
|
275
|
+
break;
|
|
276
|
+
case 3 /* State.Osc */:
|
|
277
|
+
// consume until BEL or ESC (ESC \ = ST)
|
|
278
|
+
if (code === 0x07 || code === 0x9c) {
|
|
279
|
+
this._state = 0 /* State.Normal */;
|
|
280
|
+
}
|
|
281
|
+
else if (code === 0x1b) {
|
|
282
|
+
// next char should be `\` — just return to normal, it will be consumed
|
|
283
|
+
this._state = 0 /* State.Normal */;
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
case 4 /* State.Str */:
|
|
287
|
+
// consume until ST (0x9c) or BEL
|
|
288
|
+
if (code === 0x9c || code === 0x07) {
|
|
289
|
+
this._state = 0 /* State.Normal */;
|
|
290
|
+
}
|
|
291
|
+
else if (code === 0x1b) {
|
|
292
|
+
this._state = 0 /* State.Normal */;
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
case 5 /* State.EscCharset */:
|
|
296
|
+
// consume one character for charset designation
|
|
297
|
+
this._state = 0 /* State.Normal */;
|
|
298
|
+
break;
|
|
299
|
+
case 6 /* State.EscHash */:
|
|
300
|
+
// consume one character for line attributes
|
|
301
|
+
this._state = 0 /* State.Normal */;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
_feedNormal(ch, code) {
|
|
306
|
+
if (code === 0x1b) {
|
|
307
|
+
this._state = 1 /* State.Escape */;
|
|
308
|
+
}
|
|
309
|
+
else if (code === 0x9b) {
|
|
310
|
+
// C1 CSI
|
|
311
|
+
this._csiParams = "";
|
|
312
|
+
this._csiPrivate = "";
|
|
313
|
+
this._state = 2 /* State.Csi */;
|
|
314
|
+
}
|
|
315
|
+
else if (code === 0x9d) {
|
|
316
|
+
// C1 OSC
|
|
317
|
+
this._state = 3 /* State.Osc */;
|
|
318
|
+
}
|
|
319
|
+
else if (code === 0x90 || code === 0x98 || code === 0x9e || code === 0x9f) {
|
|
320
|
+
// DCS, SOS, PM, APC
|
|
321
|
+
this._state = 4 /* State.Str */;
|
|
322
|
+
}
|
|
323
|
+
else if (code === 0x07 || code === 0x05 || code === 0x06) {
|
|
324
|
+
// BEL, ENQ, ACK — ignore
|
|
325
|
+
}
|
|
326
|
+
else if (code === 0x08) {
|
|
327
|
+
// BS
|
|
328
|
+
if (this._cursorX > 0)
|
|
329
|
+
this._cursorX--;
|
|
330
|
+
}
|
|
331
|
+
else if (code === 0x7f) {
|
|
332
|
+
// DEL
|
|
333
|
+
if (this._cursorX > 0) {
|
|
334
|
+
this._cursorX--;
|
|
335
|
+
this._setChar(this._cursorY, this._cursorX, " ");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else if (code === 0x09) {
|
|
339
|
+
// HT
|
|
340
|
+
this._cursorX = Math.min(this._cols - 1, (Math.floor(this._cursorX / 8) + 1) * 8);
|
|
341
|
+
}
|
|
342
|
+
else if (code === 0x0a || code === 0x0b || code === 0x0c) {
|
|
343
|
+
// LF, VT, FF
|
|
344
|
+
this._newline();
|
|
345
|
+
}
|
|
346
|
+
else if (code === 0x0d) {
|
|
347
|
+
// CR
|
|
348
|
+
this._cursorX = 0;
|
|
349
|
+
}
|
|
350
|
+
else if (code === 0x0e || code === 0x0f) {
|
|
351
|
+
// SO, SI — charset switch, ignore
|
|
352
|
+
}
|
|
353
|
+
else if (code >= 0x20 && code !== 0x7f) {
|
|
354
|
+
// Printable character (including multi-byte Unicode via code points)
|
|
355
|
+
this._setChar(this._cursorY, this._cursorX, ch);
|
|
356
|
+
this._cursorX++;
|
|
357
|
+
if (this._cursorX >= this._cols) {
|
|
358
|
+
// Auto-wrap
|
|
359
|
+
this._cursorX = 0;
|
|
360
|
+
this._newline();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
_feedEscape(ch, code) {
|
|
365
|
+
this._state = 0 /* State.Normal */;
|
|
366
|
+
if (code === 0x5b) {
|
|
367
|
+
// ESC [ = CSI
|
|
368
|
+
this._csiParams = "";
|
|
369
|
+
this._csiPrivate = "";
|
|
370
|
+
this._state = 2 /* State.Csi */;
|
|
371
|
+
}
|
|
372
|
+
else if (code === 0x5d) {
|
|
373
|
+
// ESC ] = OSC
|
|
374
|
+
this._state = 3 /* State.Osc */;
|
|
375
|
+
}
|
|
376
|
+
else if (code === 0x50 || code === 0x58 || code === 0x5e || code === 0x5f) {
|
|
377
|
+
// DCS, SOS, PM, APC
|
|
378
|
+
this._state = 4 /* State.Str */;
|
|
379
|
+
}
|
|
380
|
+
else if (code === 0x28 || code === 0x29 || code === 0x2a || code === 0x2b || code === 0x2d || code === 0x2e) {
|
|
381
|
+
// ESC ( ) * + - . = charset designation (consume next char)
|
|
382
|
+
this._state = 5 /* State.EscCharset */;
|
|
383
|
+
}
|
|
384
|
+
else if (code === 0x23) {
|
|
385
|
+
// ESC # = line attributes (consume next char)
|
|
386
|
+
this._state = 6 /* State.EscHash */;
|
|
387
|
+
}
|
|
388
|
+
else if (code === 0x37) {
|
|
389
|
+
// ESC 7 = save cursor
|
|
390
|
+
this._savedCursor = { x: this._cursorX, y: this._cursorY };
|
|
391
|
+
}
|
|
392
|
+
else if (code === 0x38) {
|
|
393
|
+
// ESC 8 = restore cursor
|
|
394
|
+
this._cursorX = this._clamp(this._savedCursor.x, 0, this._cols - 1);
|
|
395
|
+
this._cursorY = this._clamp(this._savedCursor.y, 0, this._rows - 1);
|
|
396
|
+
}
|
|
397
|
+
else if (code === 0x44) {
|
|
398
|
+
// ESC D = index (LF)
|
|
399
|
+
this._newline();
|
|
400
|
+
}
|
|
401
|
+
else if (code === 0x45) {
|
|
402
|
+
// ESC E = next line
|
|
403
|
+
this._cursorX = 0;
|
|
404
|
+
this._newline();
|
|
405
|
+
}
|
|
406
|
+
else if (code === 0x4d) {
|
|
407
|
+
// ESC M = reverse index
|
|
408
|
+
if (this._cursorY === this._scrollTop) {
|
|
409
|
+
this._scrollDown(1);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
this._cursorY = Math.max(0, this._cursorY - 1);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
else if (code === 0x48) {
|
|
416
|
+
// ESC H = tab set (ignore)
|
|
417
|
+
}
|
|
418
|
+
else if (code === 0x63) {
|
|
419
|
+
// ESC c = full reset
|
|
420
|
+
this._screen = this._makeScreen(this._cols, this._rows);
|
|
421
|
+
this._cursorX = 0;
|
|
422
|
+
this._cursorY = 0;
|
|
423
|
+
this._savedCursor = { x: 0, y: 0 };
|
|
424
|
+
this._scrollTop = 0;
|
|
425
|
+
this._scrollBottom = this._rows - 1;
|
|
426
|
+
}
|
|
427
|
+
// All other ESC sequences: two-char, already consumed — ignore
|
|
428
|
+
}
|
|
429
|
+
_feedCsi(ch, code) {
|
|
430
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
431
|
+
// Final byte
|
|
432
|
+
this._execCsi(ch);
|
|
433
|
+
this._state = 0 /* State.Normal */;
|
|
434
|
+
}
|
|
435
|
+
else if (code === 0x3f || code === 0x21 || code === 0x3e || code === 0x20) {
|
|
436
|
+
// Private/intermediate marker (?, !, >, space)
|
|
437
|
+
this._csiPrivate = ch;
|
|
438
|
+
}
|
|
439
|
+
else if ((code >= 0x30 && code <= 0x39) || code === 0x3b) {
|
|
440
|
+
// Digit or semicolon — parameter byte
|
|
441
|
+
this._csiParams += ch;
|
|
442
|
+
}
|
|
443
|
+
// Other bytes ignored
|
|
444
|
+
}
|
|
445
|
+
}
|
package/dist/terminal-pilot.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare class TerminalPilot {
|
|
|
13
13
|
static launch(): Promise<TerminalPilot>;
|
|
14
14
|
newSession(opts: NewSessionOptions): Promise<TerminalSession>;
|
|
15
15
|
getSession(id: string): TerminalSession;
|
|
16
|
+
deleteSession(id: string): void;
|
|
16
17
|
sessions(): TerminalSession[];
|
|
17
18
|
close(): Promise<void>;
|
|
18
19
|
}
|
package/dist/terminal-pilot.js
CHANGED
|
@@ -18,9 +18,6 @@ export class TerminalPilot {
|
|
|
18
18
|
rows: opts.rows ?? DEFAULT_ROWS,
|
|
19
19
|
observe: opts.observe ?? false
|
|
20
20
|
});
|
|
21
|
-
session.on("exit", () => {
|
|
22
|
-
this.sessionMap.delete(session.id);
|
|
23
|
-
});
|
|
24
21
|
this.sessionMap.set(session.id, session);
|
|
25
22
|
return session;
|
|
26
23
|
}
|
|
@@ -31,8 +28,11 @@ export class TerminalPilot {
|
|
|
31
28
|
}
|
|
32
29
|
return session;
|
|
33
30
|
}
|
|
31
|
+
deleteSession(id) {
|
|
32
|
+
this.sessionMap.delete(id);
|
|
33
|
+
}
|
|
34
34
|
sessions() {
|
|
35
|
-
return [...this.sessionMap.values()];
|
|
35
|
+
return [...this.sessionMap.values()].filter((s) => s.exitCode === null);
|
|
36
36
|
}
|
|
37
37
|
async close() {
|
|
38
38
|
const sessions = [...this.sessionMap.values()];
|
|
@@ -42,6 +42,9 @@ export declare class TerminalSession {
|
|
|
42
42
|
screen(): Promise<TerminalScreen>;
|
|
43
43
|
history(opts?: HistoryOptions): Promise<string[]>;
|
|
44
44
|
resize(cols: number, rows: number): Promise<void>;
|
|
45
|
+
waitForExit(opts?: {
|
|
46
|
+
timeout?: number;
|
|
47
|
+
}): Promise<number>;
|
|
45
48
|
close(): Promise<number>;
|
|
46
49
|
on(event: "exit", cb: (code: number) => void): void;
|
|
47
50
|
}
|
package/dist/terminal-session.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
3
|
import * as nodePty from "node-pty";
|
|
5
4
|
import { stripAnsi } from "./ansi.js";
|
|
5
|
+
import { TerminalBuffer } from "./terminal-buffer.js";
|
|
6
6
|
import { keyToSequence } from "./keys.js";
|
|
7
7
|
import { TerminalScreen } from "./terminal-screen.js";
|
|
8
|
-
const require = createRequire(import.meta.url);
|
|
9
|
-
const HeadlessTerminal = require("headless-terminal");
|
|
10
8
|
const DEFAULT_COLS = 120;
|
|
11
9
|
const DEFAULT_ROWS = 40;
|
|
12
10
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
@@ -33,7 +31,7 @@ export class TerminalSession {
|
|
|
33
31
|
this.command = command;
|
|
34
32
|
this.currentCols = cols;
|
|
35
33
|
this.currentRows = rows;
|
|
36
|
-
this.terminal = new
|
|
34
|
+
this.terminal = new TerminalBuffer(cols, rows);
|
|
37
35
|
this.pty = createPtyProcess({ command, args, cwd, env, cols, rows });
|
|
38
36
|
this.pid = this.pty.pid;
|
|
39
37
|
const dataSubscription = this.pty.onData((chunk) => {
|
|
@@ -145,6 +143,19 @@ export class TerminalSession {
|
|
|
145
143
|
}
|
|
146
144
|
this.terminal.resize(cols, rows);
|
|
147
145
|
}
|
|
146
|
+
async waitForExit(opts) {
|
|
147
|
+
if (this.exitCode !== null) {
|
|
148
|
+
return this.exitCode;
|
|
149
|
+
}
|
|
150
|
+
if (opts?.timeout !== undefined) {
|
|
151
|
+
const result = await waitForExit(this.exitPromise, opts.timeout);
|
|
152
|
+
if (result === null) {
|
|
153
|
+
throw new Error(`Timed out waiting for process to exit after ${opts.timeout}ms`);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
return this.exitPromise;
|
|
158
|
+
}
|
|
148
159
|
async close() {
|
|
149
160
|
if (this.exitCode !== null) {
|
|
150
161
|
return this.exitCode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminal-pilot",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Playwright-like SDK for automating interactive CLI apps through a real PTY",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"headless"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"headless-terminal": "^0.4.0",
|
|
38
37
|
"node-pty": "^1.1.0"
|
|
39
38
|
},
|
|
40
39
|
"devDependencies": {}
|