erosolar-cli 1.7.189 → 1.7.191
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/shell/interactiveShell.d.ts +29 -126
- package/dist/shell/interactiveShell.d.ts.map +1 -1
- package/dist/shell/interactiveShell.js +163 -1438
- package/dist/shell/interactiveShell.js.map +1 -1
- package/dist/shell/terminalInput.d.ts +186 -0
- package/dist/shell/terminalInput.d.ts.map +1 -0
- package/dist/shell/terminalInput.js +855 -0
- package/dist/shell/terminalInput.js.map +1 -0
- package/dist/shell/terminalInputAdapter.d.ts +94 -0
- package/dist/shell/terminalInputAdapter.d.ts.map +1 -0
- package/dist/shell/terminalInputAdapter.js +187 -0
- package/dist/shell/terminalInputAdapter.js.map +1 -0
- package/dist/ui/keyboardShortcuts.js +1 -1
- package/dist/ui/keyboardShortcuts.js.map +1 -1
- package/dist/ui/persistentPrompt.d.ts +11 -13
- package/dist/ui/persistentPrompt.d.ts.map +1 -1
- package/dist/ui/persistentPrompt.js +22 -57
- package/dist/ui/persistentPrompt.js.map +1 -1
- package/dist/ui/shortcutsHelp.js +1 -1
- package/dist/ui/shortcutsHelp.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TerminalInput - Clean, unified terminal input handling
|
|
3
|
+
*
|
|
4
|
+
* Design principles:
|
|
5
|
+
* - Single source of truth for input state
|
|
6
|
+
* - Native bracketed paste support (no heuristics)
|
|
7
|
+
* - Clean cursor model with render-time wrapping
|
|
8
|
+
* - State machine for different input modes
|
|
9
|
+
* - No readline dependency for display
|
|
10
|
+
*/
|
|
11
|
+
import { EventEmitter } from 'node:events';
|
|
12
|
+
// ANSI escape codes
|
|
13
|
+
const ESC = {
|
|
14
|
+
// Cursor control
|
|
15
|
+
SAVE: '\x1b7',
|
|
16
|
+
RESTORE: '\x1b8',
|
|
17
|
+
HIDE: '\x1b[?25l',
|
|
18
|
+
SHOW: '\x1b[?25h',
|
|
19
|
+
TO: (row, col) => `\x1b[${row};${col}H`,
|
|
20
|
+
TO_COL: (col) => `\x1b[${col}G`,
|
|
21
|
+
// Line control
|
|
22
|
+
CLEAR_LINE: '\x1b[2K',
|
|
23
|
+
CLEAR_TO_END: '\x1b[0J',
|
|
24
|
+
// Scroll region
|
|
25
|
+
SET_SCROLL: (top, bottom) => `\x1b[${top};${bottom}r`,
|
|
26
|
+
RESET_SCROLL: '\x1b[r',
|
|
27
|
+
// Style
|
|
28
|
+
RESET: '\x1b[0m',
|
|
29
|
+
DIM: '\x1b[2m',
|
|
30
|
+
REVERSE: '\x1b[7m',
|
|
31
|
+
BOLD: '\x1b[1m',
|
|
32
|
+
BG_DARK: '\x1b[48;5;236m', // Dark gray background
|
|
33
|
+
// Bracketed paste mode
|
|
34
|
+
PASTE_ENABLE: '\x1b[?2004h',
|
|
35
|
+
PASTE_DISABLE: '\x1b[?2004l',
|
|
36
|
+
PASTE_START: '\x1b[200~',
|
|
37
|
+
PASTE_END: '\x1b[201~',
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Unified terminal input handler
|
|
41
|
+
*/
|
|
42
|
+
export class TerminalInput extends EventEmitter {
|
|
43
|
+
out;
|
|
44
|
+
config;
|
|
45
|
+
// Core state
|
|
46
|
+
buffer = '';
|
|
47
|
+
cursor = 0;
|
|
48
|
+
mode = 'idle';
|
|
49
|
+
// Paste accumulation
|
|
50
|
+
pasteBuffer = '';
|
|
51
|
+
isPasting = false;
|
|
52
|
+
// History
|
|
53
|
+
history = [];
|
|
54
|
+
historyIndex = -1;
|
|
55
|
+
tempInput = '';
|
|
56
|
+
maxHistory = 100;
|
|
57
|
+
// Queue for streaming mode
|
|
58
|
+
queue = [];
|
|
59
|
+
queueIdCounter = 0;
|
|
60
|
+
// Display state
|
|
61
|
+
reservedLines = 2;
|
|
62
|
+
scrollRegionActive = false;
|
|
63
|
+
lastRenderContent = '';
|
|
64
|
+
lastRenderCursor = -1;
|
|
65
|
+
isRendering = false;
|
|
66
|
+
// Lifecycle
|
|
67
|
+
disposed = false;
|
|
68
|
+
enabled = true;
|
|
69
|
+
constructor(writeStream = process.stdout, config = {}) {
|
|
70
|
+
super();
|
|
71
|
+
this.out = writeStream;
|
|
72
|
+
this.config = {
|
|
73
|
+
maxLines: config.maxLines ?? 15,
|
|
74
|
+
maxLength: config.maxLength ?? 10000,
|
|
75
|
+
maxQueueSize: config.maxQueueSize ?? 100,
|
|
76
|
+
promptChar: config.promptChar ?? '> ',
|
|
77
|
+
continuationChar: config.continuationChar ?? '│ ',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// ===========================================================================
|
|
81
|
+
// PUBLIC API
|
|
82
|
+
// ===========================================================================
|
|
83
|
+
/**
|
|
84
|
+
* Enable bracketed paste mode in terminal
|
|
85
|
+
*/
|
|
86
|
+
enableBracketedPaste() {
|
|
87
|
+
if (this.isTTY()) {
|
|
88
|
+
this.write(ESC.PASTE_ENABLE);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Disable bracketed paste mode
|
|
93
|
+
*/
|
|
94
|
+
disableBracketedPaste() {
|
|
95
|
+
if (this.isTTY()) {
|
|
96
|
+
this.write(ESC.PASTE_DISABLE);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Process raw terminal data (handles bracketed paste sequences)
|
|
101
|
+
* Returns true if the data was consumed (paste sequence)
|
|
102
|
+
*/
|
|
103
|
+
processRawData(data) {
|
|
104
|
+
// Check for paste start
|
|
105
|
+
if (data.includes(ESC.PASTE_START)) {
|
|
106
|
+
this.isPasting = true;
|
|
107
|
+
this.pasteBuffer = '';
|
|
108
|
+
// Extract content after paste start
|
|
109
|
+
const startIdx = data.indexOf(ESC.PASTE_START) + ESC.PASTE_START.length;
|
|
110
|
+
const remaining = data.slice(startIdx);
|
|
111
|
+
// Check if paste end is also in this chunk
|
|
112
|
+
if (remaining.includes(ESC.PASTE_END)) {
|
|
113
|
+
const endIdx = remaining.indexOf(ESC.PASTE_END);
|
|
114
|
+
this.pasteBuffer = remaining.slice(0, endIdx);
|
|
115
|
+
this.finishPaste();
|
|
116
|
+
return { consumed: true, passthrough: remaining.slice(endIdx + ESC.PASTE_END.length) };
|
|
117
|
+
}
|
|
118
|
+
this.pasteBuffer = remaining;
|
|
119
|
+
return { consumed: true, passthrough: '' };
|
|
120
|
+
}
|
|
121
|
+
// Accumulating paste
|
|
122
|
+
if (this.isPasting) {
|
|
123
|
+
if (data.includes(ESC.PASTE_END)) {
|
|
124
|
+
const endIdx = data.indexOf(ESC.PASTE_END);
|
|
125
|
+
this.pasteBuffer += data.slice(0, endIdx);
|
|
126
|
+
this.finishPaste();
|
|
127
|
+
return { consumed: true, passthrough: data.slice(endIdx + ESC.PASTE_END.length) };
|
|
128
|
+
}
|
|
129
|
+
this.pasteBuffer += data;
|
|
130
|
+
return { consumed: true, passthrough: '' };
|
|
131
|
+
}
|
|
132
|
+
return { consumed: false, passthrough: data };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Handle a keypress event
|
|
136
|
+
*/
|
|
137
|
+
handleKeypress(str, key) {
|
|
138
|
+
if (this.disposed || !this.enabled)
|
|
139
|
+
return;
|
|
140
|
+
// Handle control keys
|
|
141
|
+
if (key?.ctrl) {
|
|
142
|
+
this.handleCtrlKey(key);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Handle meta/alt keys
|
|
146
|
+
if (key?.meta) {
|
|
147
|
+
this.handleMetaKey(key);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Handle special keys
|
|
151
|
+
if (key?.name) {
|
|
152
|
+
const handled = this.handleSpecialKey(str, key);
|
|
153
|
+
if (handled)
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Insert printable characters
|
|
157
|
+
if (str && !key?.ctrl && !key?.meta) {
|
|
158
|
+
this.insertText(str);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Set the input mode
|
|
163
|
+
*/
|
|
164
|
+
setMode(mode) {
|
|
165
|
+
const prevMode = this.mode;
|
|
166
|
+
this.mode = mode;
|
|
167
|
+
if (mode === 'streaming' && prevMode !== 'streaming') {
|
|
168
|
+
this.enableScrollRegion();
|
|
169
|
+
}
|
|
170
|
+
else if (mode !== 'streaming' && prevMode === 'streaming') {
|
|
171
|
+
this.disableScrollRegion();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get current mode
|
|
176
|
+
*/
|
|
177
|
+
getMode() {
|
|
178
|
+
return this.mode;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get the current buffer content
|
|
182
|
+
*/
|
|
183
|
+
getBuffer() {
|
|
184
|
+
return this.buffer;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Set buffer content
|
|
188
|
+
*/
|
|
189
|
+
setBuffer(text, cursorPos) {
|
|
190
|
+
this.buffer = this.sanitize(text).slice(0, this.config.maxLength);
|
|
191
|
+
this.cursor = cursorPos ?? this.buffer.length;
|
|
192
|
+
this.clampCursor();
|
|
193
|
+
this.scheduleRender();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Clear the buffer
|
|
197
|
+
*/
|
|
198
|
+
clear() {
|
|
199
|
+
this.buffer = '';
|
|
200
|
+
this.cursor = 0;
|
|
201
|
+
this.historyIndex = -1;
|
|
202
|
+
this.tempInput = '';
|
|
203
|
+
this.scheduleRender();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get queued inputs
|
|
207
|
+
*/
|
|
208
|
+
getQueue() {
|
|
209
|
+
return [...this.queue];
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Dequeue next input
|
|
213
|
+
*/
|
|
214
|
+
dequeue() {
|
|
215
|
+
const item = this.queue.shift();
|
|
216
|
+
this.scheduleRender();
|
|
217
|
+
return item;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Clear the queue
|
|
221
|
+
*/
|
|
222
|
+
clearQueue() {
|
|
223
|
+
this.queue = [];
|
|
224
|
+
this.scheduleRender();
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Render the input area
|
|
228
|
+
*/
|
|
229
|
+
render() {
|
|
230
|
+
if (!this.canRender())
|
|
231
|
+
return;
|
|
232
|
+
if (this.isRendering)
|
|
233
|
+
return;
|
|
234
|
+
// Skip if nothing changed
|
|
235
|
+
if (this.buffer === this.lastRenderContent && this.cursor === this.lastRenderCursor) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.isRendering = true;
|
|
239
|
+
try {
|
|
240
|
+
const { rows, cols } = this.getSize();
|
|
241
|
+
const maxWidth = Math.max(8, cols - 4);
|
|
242
|
+
// Wrap buffer into display lines
|
|
243
|
+
const { lines, cursorLine, cursorCol } = this.wrapBuffer(maxWidth);
|
|
244
|
+
const displayLines = Math.min(lines.length, this.config.maxLines);
|
|
245
|
+
// Update reserved lines if needed
|
|
246
|
+
this.updateReservedLines(displayLines);
|
|
247
|
+
// Calculate display window (keep cursor visible)
|
|
248
|
+
let startLine = 0;
|
|
249
|
+
if (lines.length > displayLines) {
|
|
250
|
+
startLine = Math.max(0, cursorLine - displayLines + 1);
|
|
251
|
+
startLine = Math.min(startLine, lines.length - displayLines);
|
|
252
|
+
}
|
|
253
|
+
const visibleLines = lines.slice(startLine, startLine + displayLines);
|
|
254
|
+
const adjustedCursorLine = cursorLine - startLine;
|
|
255
|
+
// Render
|
|
256
|
+
this.write(ESC.HIDE);
|
|
257
|
+
this.write(ESC.RESET);
|
|
258
|
+
const startRow = rows - this.reservedLines + 1;
|
|
259
|
+
this.write(ESC.TO(startRow, 1));
|
|
260
|
+
this.write(ESC.CLEAR_LINE);
|
|
261
|
+
// Separator line
|
|
262
|
+
const sepWidth = Math.min(cols - 2, 55);
|
|
263
|
+
this.write(ESC.DIM);
|
|
264
|
+
this.write('─'.repeat(sepWidth));
|
|
265
|
+
// Status hints
|
|
266
|
+
const hints = [];
|
|
267
|
+
if (lines.length > 1)
|
|
268
|
+
hints.push(`${lines.length} lines`);
|
|
269
|
+
if (this.mode === 'streaming' && this.queue.length > 0) {
|
|
270
|
+
hints.push(`${this.queue.length} queued`);
|
|
271
|
+
}
|
|
272
|
+
if (hints.length > 0) {
|
|
273
|
+
this.write(' ' + hints.join(' • '));
|
|
274
|
+
}
|
|
275
|
+
this.write(ESC.RESET);
|
|
276
|
+
// Render input lines
|
|
277
|
+
let finalRow = startRow + 1;
|
|
278
|
+
let finalCol = 3;
|
|
279
|
+
for (let i = 0; i < visibleLines.length; i++) {
|
|
280
|
+
this.write('\n');
|
|
281
|
+
this.write(ESC.CLEAR_LINE);
|
|
282
|
+
const line = visibleLines[i] ?? '';
|
|
283
|
+
const absoluteLineIdx = startLine + i;
|
|
284
|
+
const isFirstLine = absoluteLineIdx === 0;
|
|
285
|
+
const isCursorLine = i === adjustedCursorLine;
|
|
286
|
+
// Background
|
|
287
|
+
this.write(ESC.BG_DARK);
|
|
288
|
+
// Prompt prefix
|
|
289
|
+
this.write(ESC.DIM);
|
|
290
|
+
this.write(isFirstLine ? this.config.promptChar : this.config.continuationChar);
|
|
291
|
+
this.write(ESC.RESET);
|
|
292
|
+
this.write(ESC.BG_DARK);
|
|
293
|
+
if (isCursorLine) {
|
|
294
|
+
// Render with block cursor
|
|
295
|
+
const col = Math.min(cursorCol, line.length);
|
|
296
|
+
const before = line.slice(0, col);
|
|
297
|
+
const at = col < line.length ? line[col] : ' ';
|
|
298
|
+
const after = col < line.length ? line.slice(col + 1) : '';
|
|
299
|
+
this.write(before);
|
|
300
|
+
this.write(ESC.REVERSE + ESC.BOLD);
|
|
301
|
+
this.write(at);
|
|
302
|
+
this.write(ESC.RESET + ESC.BG_DARK);
|
|
303
|
+
this.write(after);
|
|
304
|
+
finalRow = startRow + 1 + i;
|
|
305
|
+
finalCol = this.config.promptChar.length + col + 1;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
this.write(line);
|
|
309
|
+
}
|
|
310
|
+
// Pad to edge
|
|
311
|
+
const lineLen = this.config.promptChar.length + line.length + (isCursorLine && cursorCol >= line.length ? 1 : 0);
|
|
312
|
+
const padding = Math.max(0, cols - lineLen - 1);
|
|
313
|
+
if (padding > 0)
|
|
314
|
+
this.write(' '.repeat(padding));
|
|
315
|
+
this.write(ESC.RESET);
|
|
316
|
+
}
|
|
317
|
+
// Position cursor
|
|
318
|
+
this.write(ESC.TO(finalRow, Math.min(finalCol, cols)));
|
|
319
|
+
this.write(ESC.SHOW);
|
|
320
|
+
// Update state
|
|
321
|
+
this.lastRenderContent = this.buffer;
|
|
322
|
+
this.lastRenderCursor = this.cursor;
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
this.isRendering = false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Force a re-render
|
|
330
|
+
*/
|
|
331
|
+
forceRender() {
|
|
332
|
+
this.lastRenderContent = '';
|
|
333
|
+
this.lastRenderCursor = -1;
|
|
334
|
+
this.render();
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Enable/disable input
|
|
338
|
+
*/
|
|
339
|
+
setEnabled(enabled) {
|
|
340
|
+
this.enabled = enabled;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Handle terminal resize
|
|
344
|
+
*/
|
|
345
|
+
handleResize() {
|
|
346
|
+
this.lastRenderContent = '';
|
|
347
|
+
this.lastRenderCursor = -1;
|
|
348
|
+
if (this.scrollRegionActive) {
|
|
349
|
+
this.disableScrollRegion();
|
|
350
|
+
this.enableScrollRegion();
|
|
351
|
+
}
|
|
352
|
+
this.scheduleRender();
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Dispose and clean up
|
|
356
|
+
*/
|
|
357
|
+
dispose() {
|
|
358
|
+
if (this.disposed)
|
|
359
|
+
return;
|
|
360
|
+
this.disposed = true;
|
|
361
|
+
this.enabled = false;
|
|
362
|
+
this.disableScrollRegion();
|
|
363
|
+
this.disableBracketedPaste();
|
|
364
|
+
this.buffer = '';
|
|
365
|
+
this.queue = [];
|
|
366
|
+
this.removeAllListeners();
|
|
367
|
+
}
|
|
368
|
+
// ===========================================================================
|
|
369
|
+
// INPUT HANDLING
|
|
370
|
+
// ===========================================================================
|
|
371
|
+
handleCtrlKey(key) {
|
|
372
|
+
switch (key.name) {
|
|
373
|
+
case 'c':
|
|
374
|
+
if (this.buffer.length > 0) {
|
|
375
|
+
this.clear();
|
|
376
|
+
this.write('^C\n');
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
this.emit('interrupt');
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
case 'a': // Home
|
|
383
|
+
this.moveCursorToLineStart();
|
|
384
|
+
break;
|
|
385
|
+
case 'e': // End
|
|
386
|
+
this.moveCursorToLineEnd();
|
|
387
|
+
break;
|
|
388
|
+
case 'u': // Delete to start
|
|
389
|
+
this.deleteToStart();
|
|
390
|
+
break;
|
|
391
|
+
case 'k': // Delete to end
|
|
392
|
+
this.deleteToEnd();
|
|
393
|
+
break;
|
|
394
|
+
case 'w': // Delete word
|
|
395
|
+
this.deleteWord();
|
|
396
|
+
break;
|
|
397
|
+
case 'left': // Word left
|
|
398
|
+
this.moveCursorWordLeft();
|
|
399
|
+
break;
|
|
400
|
+
case 'right': // Word right
|
|
401
|
+
this.moveCursorWordRight();
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
handleMetaKey(key) {
|
|
406
|
+
switch (key.name) {
|
|
407
|
+
case 'left':
|
|
408
|
+
case 'b':
|
|
409
|
+
this.moveCursorWordLeft();
|
|
410
|
+
break;
|
|
411
|
+
case 'right':
|
|
412
|
+
case 'f':
|
|
413
|
+
this.moveCursorWordRight();
|
|
414
|
+
break;
|
|
415
|
+
case 'backspace':
|
|
416
|
+
this.deleteWord();
|
|
417
|
+
break;
|
|
418
|
+
case 'return':
|
|
419
|
+
this.insertNewline();
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
handleSpecialKey(_str, key) {
|
|
424
|
+
switch (key.name) {
|
|
425
|
+
case 'return':
|
|
426
|
+
if (key.shift || key.meta) {
|
|
427
|
+
this.insertNewline();
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
this.submit();
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
433
|
+
case 'backspace':
|
|
434
|
+
this.deleteBackward();
|
|
435
|
+
return true;
|
|
436
|
+
case 'delete':
|
|
437
|
+
this.deleteForward();
|
|
438
|
+
return true;
|
|
439
|
+
case 'left':
|
|
440
|
+
this.moveCursorLeft();
|
|
441
|
+
return true;
|
|
442
|
+
case 'right':
|
|
443
|
+
this.moveCursorRight();
|
|
444
|
+
return true;
|
|
445
|
+
case 'up':
|
|
446
|
+
this.handleUp();
|
|
447
|
+
return true;
|
|
448
|
+
case 'down':
|
|
449
|
+
this.handleDown();
|
|
450
|
+
return true;
|
|
451
|
+
case 'home':
|
|
452
|
+
this.moveCursorToLineStart();
|
|
453
|
+
return true;
|
|
454
|
+
case 'end':
|
|
455
|
+
this.moveCursorToLineEnd();
|
|
456
|
+
return true;
|
|
457
|
+
case 'tab':
|
|
458
|
+
this.insertText(' ');
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
insertText(text) {
|
|
464
|
+
const clean = this.sanitize(text);
|
|
465
|
+
if (!clean)
|
|
466
|
+
return;
|
|
467
|
+
const available = this.config.maxLength - this.buffer.length;
|
|
468
|
+
if (available <= 0)
|
|
469
|
+
return;
|
|
470
|
+
const chunk = clean.slice(0, available);
|
|
471
|
+
this.buffer = this.buffer.slice(0, this.cursor) + chunk + this.buffer.slice(this.cursor);
|
|
472
|
+
this.cursor += chunk.length;
|
|
473
|
+
this.emit('change', this.buffer);
|
|
474
|
+
this.scheduleRender();
|
|
475
|
+
}
|
|
476
|
+
insertNewline() {
|
|
477
|
+
if (this.buffer.length >= this.config.maxLength)
|
|
478
|
+
return;
|
|
479
|
+
this.buffer = this.buffer.slice(0, this.cursor) + '\n' + this.buffer.slice(this.cursor);
|
|
480
|
+
this.cursor++;
|
|
481
|
+
this.emit('change', this.buffer);
|
|
482
|
+
this.scheduleRender();
|
|
483
|
+
}
|
|
484
|
+
deleteBackward() {
|
|
485
|
+
if (this.cursor === 0)
|
|
486
|
+
return;
|
|
487
|
+
this.buffer = this.buffer.slice(0, this.cursor - 1) + this.buffer.slice(this.cursor);
|
|
488
|
+
this.cursor--;
|
|
489
|
+
this.emit('change', this.buffer);
|
|
490
|
+
this.scheduleRender();
|
|
491
|
+
}
|
|
492
|
+
deleteForward() {
|
|
493
|
+
if (this.cursor >= this.buffer.length)
|
|
494
|
+
return;
|
|
495
|
+
this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
|
|
496
|
+
this.emit('change', this.buffer);
|
|
497
|
+
this.scheduleRender();
|
|
498
|
+
}
|
|
499
|
+
deleteToStart() {
|
|
500
|
+
if (this.cursor === 0)
|
|
501
|
+
return;
|
|
502
|
+
this.buffer = this.buffer.slice(this.cursor);
|
|
503
|
+
this.cursor = 0;
|
|
504
|
+
this.emit('change', this.buffer);
|
|
505
|
+
this.scheduleRender();
|
|
506
|
+
}
|
|
507
|
+
deleteToEnd() {
|
|
508
|
+
if (this.cursor >= this.buffer.length)
|
|
509
|
+
return;
|
|
510
|
+
this.buffer = this.buffer.slice(0, this.cursor);
|
|
511
|
+
this.emit('change', this.buffer);
|
|
512
|
+
this.scheduleRender();
|
|
513
|
+
}
|
|
514
|
+
deleteWord() {
|
|
515
|
+
if (this.cursor === 0)
|
|
516
|
+
return;
|
|
517
|
+
let pos = this.cursor;
|
|
518
|
+
// Skip whitespace
|
|
519
|
+
while (pos > 0 && this.isWhitespace(this.buffer[pos - 1]))
|
|
520
|
+
pos--;
|
|
521
|
+
// Skip word
|
|
522
|
+
while (pos > 0 && !this.isWhitespace(this.buffer[pos - 1]))
|
|
523
|
+
pos--;
|
|
524
|
+
this.buffer = this.buffer.slice(0, pos) + this.buffer.slice(this.cursor);
|
|
525
|
+
this.cursor = pos;
|
|
526
|
+
this.emit('change', this.buffer);
|
|
527
|
+
this.scheduleRender();
|
|
528
|
+
}
|
|
529
|
+
moveCursorLeft() {
|
|
530
|
+
if (this.cursor > 0) {
|
|
531
|
+
this.cursor--;
|
|
532
|
+
this.scheduleRender();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
moveCursorRight() {
|
|
536
|
+
if (this.cursor < this.buffer.length) {
|
|
537
|
+
this.cursor++;
|
|
538
|
+
this.scheduleRender();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
moveCursorWordLeft() {
|
|
542
|
+
let pos = this.cursor;
|
|
543
|
+
while (pos > 0 && this.isWhitespace(this.buffer[pos - 1]))
|
|
544
|
+
pos--;
|
|
545
|
+
while (pos > 0 && !this.isWhitespace(this.buffer[pos - 1]))
|
|
546
|
+
pos--;
|
|
547
|
+
this.cursor = pos;
|
|
548
|
+
this.scheduleRender();
|
|
549
|
+
}
|
|
550
|
+
moveCursorWordRight() {
|
|
551
|
+
let pos = this.cursor;
|
|
552
|
+
while (pos < this.buffer.length && !this.isWhitespace(this.buffer[pos]))
|
|
553
|
+
pos++;
|
|
554
|
+
while (pos < this.buffer.length && this.isWhitespace(this.buffer[pos]))
|
|
555
|
+
pos++;
|
|
556
|
+
this.cursor = pos;
|
|
557
|
+
this.scheduleRender();
|
|
558
|
+
}
|
|
559
|
+
moveCursorToLineStart() {
|
|
560
|
+
// Find start of current line
|
|
561
|
+
let pos = this.cursor;
|
|
562
|
+
while (pos > 0 && this.buffer[pos - 1] !== '\n')
|
|
563
|
+
pos--;
|
|
564
|
+
this.cursor = pos;
|
|
565
|
+
this.scheduleRender();
|
|
566
|
+
}
|
|
567
|
+
moveCursorToLineEnd() {
|
|
568
|
+
// Find end of current line
|
|
569
|
+
let pos = this.cursor;
|
|
570
|
+
while (pos < this.buffer.length && this.buffer[pos] !== '\n')
|
|
571
|
+
pos++;
|
|
572
|
+
this.cursor = pos;
|
|
573
|
+
this.scheduleRender();
|
|
574
|
+
}
|
|
575
|
+
handleUp() {
|
|
576
|
+
// Check if we can move up within buffer (multi-line)
|
|
577
|
+
const { cursorLine } = this.getCursorPosition();
|
|
578
|
+
if (cursorLine > 0) {
|
|
579
|
+
this.moveCursorUp();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// Otherwise navigate history
|
|
583
|
+
if (this.history.length === 0)
|
|
584
|
+
return;
|
|
585
|
+
if (this.historyIndex === -1) {
|
|
586
|
+
this.tempInput = this.buffer;
|
|
587
|
+
}
|
|
588
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
589
|
+
this.historyIndex++;
|
|
590
|
+
this.buffer = this.history[this.history.length - 1 - this.historyIndex] ?? '';
|
|
591
|
+
this.cursor = this.buffer.length;
|
|
592
|
+
this.scheduleRender();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
handleDown() {
|
|
596
|
+
// Check if we can move down within buffer
|
|
597
|
+
const { cursorLine, totalLines } = this.getCursorPosition();
|
|
598
|
+
if (cursorLine < totalLines - 1) {
|
|
599
|
+
this.moveCursorDown();
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
// Otherwise navigate history
|
|
603
|
+
if (this.historyIndex > 0) {
|
|
604
|
+
this.historyIndex--;
|
|
605
|
+
this.buffer = this.history[this.history.length - 1 - this.historyIndex] ?? '';
|
|
606
|
+
this.cursor = this.buffer.length;
|
|
607
|
+
this.scheduleRender();
|
|
608
|
+
}
|
|
609
|
+
else if (this.historyIndex === 0) {
|
|
610
|
+
this.historyIndex = -1;
|
|
611
|
+
this.buffer = this.tempInput;
|
|
612
|
+
this.cursor = this.buffer.length;
|
|
613
|
+
this.scheduleRender();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
moveCursorUp() {
|
|
617
|
+
const lines = this.buffer.split('\n');
|
|
618
|
+
let lineStart = 0;
|
|
619
|
+
let lineIdx = 0;
|
|
620
|
+
// Find current line
|
|
621
|
+
for (let i = 0; i < lines.length; i++) {
|
|
622
|
+
const lineEnd = lineStart + (lines[i]?.length ?? 0);
|
|
623
|
+
if (this.cursor <= lineEnd) {
|
|
624
|
+
lineIdx = i;
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
lineStart = lineEnd + 1; // +1 for newline
|
|
628
|
+
}
|
|
629
|
+
if (lineIdx === 0)
|
|
630
|
+
return;
|
|
631
|
+
// Calculate column in current line
|
|
632
|
+
const colInLine = this.cursor - lineStart;
|
|
633
|
+
// Move to previous line
|
|
634
|
+
const prevLineStart = lineStart - 1 - (lines[lineIdx - 1]?.length ?? 0);
|
|
635
|
+
const prevLineLen = lines[lineIdx - 1]?.length ?? 0;
|
|
636
|
+
this.cursor = prevLineStart + Math.min(colInLine, prevLineLen);
|
|
637
|
+
this.scheduleRender();
|
|
638
|
+
}
|
|
639
|
+
moveCursorDown() {
|
|
640
|
+
const lines = this.buffer.split('\n');
|
|
641
|
+
let lineStart = 0;
|
|
642
|
+
let lineIdx = 0;
|
|
643
|
+
// Find current line
|
|
644
|
+
for (let i = 0; i < lines.length; i++) {
|
|
645
|
+
const lineEnd = lineStart + (lines[i]?.length ?? 0);
|
|
646
|
+
if (this.cursor <= lineEnd) {
|
|
647
|
+
lineIdx = i;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
lineStart = lineEnd + 1;
|
|
651
|
+
}
|
|
652
|
+
if (lineIdx >= lines.length - 1)
|
|
653
|
+
return;
|
|
654
|
+
const colInLine = this.cursor - lineStart;
|
|
655
|
+
const nextLineStart = lineStart + (lines[lineIdx]?.length ?? 0) + 1;
|
|
656
|
+
const nextLineLen = lines[lineIdx + 1]?.length ?? 0;
|
|
657
|
+
this.cursor = nextLineStart + Math.min(colInLine, nextLineLen);
|
|
658
|
+
this.scheduleRender();
|
|
659
|
+
}
|
|
660
|
+
getCursorPosition() {
|
|
661
|
+
const lines = this.buffer.split('\n');
|
|
662
|
+
let pos = 0;
|
|
663
|
+
for (let i = 0; i < lines.length; i++) {
|
|
664
|
+
const lineLen = lines[i]?.length ?? 0;
|
|
665
|
+
if (this.cursor <= pos + lineLen) {
|
|
666
|
+
return {
|
|
667
|
+
cursorLine: i,
|
|
668
|
+
cursorCol: this.cursor - pos,
|
|
669
|
+
totalLines: lines.length,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
pos += lineLen + 1;
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
cursorLine: lines.length - 1,
|
|
676
|
+
cursorCol: lines[lines.length - 1]?.length ?? 0,
|
|
677
|
+
totalLines: lines.length,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
submit() {
|
|
681
|
+
const text = this.buffer.trim();
|
|
682
|
+
if (!text)
|
|
683
|
+
return;
|
|
684
|
+
// Add to history
|
|
685
|
+
if (this.history[this.history.length - 1] !== text) {
|
|
686
|
+
this.history.push(text);
|
|
687
|
+
if (this.history.length > this.maxHistory) {
|
|
688
|
+
this.history.shift();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
this.historyIndex = -1;
|
|
692
|
+
this.tempInput = '';
|
|
693
|
+
// Queue or submit based on mode
|
|
694
|
+
if (this.mode === 'streaming') {
|
|
695
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
696
|
+
return; // Queue full
|
|
697
|
+
}
|
|
698
|
+
this.queue.push({
|
|
699
|
+
id: ++this.queueIdCounter,
|
|
700
|
+
text,
|
|
701
|
+
timestamp: Date.now(),
|
|
702
|
+
});
|
|
703
|
+
this.emit('queue', text);
|
|
704
|
+
this.clear();
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
this.emit('submit', text);
|
|
708
|
+
this.clear();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
finishPaste() {
|
|
712
|
+
const content = this.pasteBuffer;
|
|
713
|
+
this.pasteBuffer = '';
|
|
714
|
+
this.isPasting = false;
|
|
715
|
+
if (!content)
|
|
716
|
+
return;
|
|
717
|
+
// Insert paste content at cursor
|
|
718
|
+
const clean = this.sanitize(content);
|
|
719
|
+
const available = this.config.maxLength - this.buffer.length;
|
|
720
|
+
const chunk = clean.slice(0, available);
|
|
721
|
+
this.buffer = this.buffer.slice(0, this.cursor) + chunk + this.buffer.slice(this.cursor);
|
|
722
|
+
this.cursor += chunk.length;
|
|
723
|
+
this.emit('change', this.buffer);
|
|
724
|
+
this.scheduleRender();
|
|
725
|
+
}
|
|
726
|
+
// ===========================================================================
|
|
727
|
+
// SCROLL REGION
|
|
728
|
+
// ===========================================================================
|
|
729
|
+
enableScrollRegion() {
|
|
730
|
+
if (this.scrollRegionActive || !this.isTTY())
|
|
731
|
+
return;
|
|
732
|
+
const { rows } = this.getSize();
|
|
733
|
+
const scrollBottom = Math.max(1, rows - this.reservedLines);
|
|
734
|
+
this.write(ESC.SET_SCROLL(1, scrollBottom));
|
|
735
|
+
this.scrollRegionActive = true;
|
|
736
|
+
this.forceRender();
|
|
737
|
+
}
|
|
738
|
+
disableScrollRegion() {
|
|
739
|
+
if (!this.scrollRegionActive)
|
|
740
|
+
return;
|
|
741
|
+
this.write(ESC.RESET_SCROLL);
|
|
742
|
+
this.scrollRegionActive = false;
|
|
743
|
+
}
|
|
744
|
+
updateReservedLines(contentLines) {
|
|
745
|
+
const { rows } = this.getSize();
|
|
746
|
+
const needed = 1 + contentLines; // separator + content
|
|
747
|
+
const maxAllowed = Math.max(1, rows - 1);
|
|
748
|
+
const newReserved = Math.min(Math.max(2, needed), maxAllowed);
|
|
749
|
+
if (newReserved !== this.reservedLines) {
|
|
750
|
+
this.reservedLines = newReserved;
|
|
751
|
+
if (this.scrollRegionActive) {
|
|
752
|
+
const scrollBottom = Math.max(1, rows - this.reservedLines);
|
|
753
|
+
this.write(ESC.SET_SCROLL(1, scrollBottom));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// ===========================================================================
|
|
758
|
+
// BUFFER WRAPPING
|
|
759
|
+
// ===========================================================================
|
|
760
|
+
wrapBuffer(maxWidth) {
|
|
761
|
+
const width = Math.max(1, maxWidth);
|
|
762
|
+
const lines = [];
|
|
763
|
+
let cursorLine = 0;
|
|
764
|
+
let cursorCol = 0;
|
|
765
|
+
let charIndex = 0;
|
|
766
|
+
if (this.buffer.length === 0) {
|
|
767
|
+
return { lines: [''], cursorLine: 0, cursorCol: 0 };
|
|
768
|
+
}
|
|
769
|
+
const rawLines = this.buffer.split('\n');
|
|
770
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
771
|
+
const raw = rawLines[i] ?? '';
|
|
772
|
+
if (raw.length === 0) {
|
|
773
|
+
// Empty line from newline
|
|
774
|
+
if (this.cursor === charIndex) {
|
|
775
|
+
cursorLine = lines.length;
|
|
776
|
+
cursorCol = 0;
|
|
777
|
+
}
|
|
778
|
+
lines.push('');
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
// Wrap long lines
|
|
782
|
+
for (let start = 0; start < raw.length; start += width) {
|
|
783
|
+
const segment = raw.slice(start, start + width);
|
|
784
|
+
const segmentStart = charIndex + start;
|
|
785
|
+
const segmentEnd = segmentStart + segment.length;
|
|
786
|
+
if (this.cursor >= segmentStart && this.cursor <= segmentEnd) {
|
|
787
|
+
cursorLine = lines.length;
|
|
788
|
+
cursorCol = this.cursor - segmentStart;
|
|
789
|
+
}
|
|
790
|
+
lines.push(segment);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
charIndex += raw.length;
|
|
794
|
+
// Account for newline between raw lines
|
|
795
|
+
if (i < rawLines.length - 1) {
|
|
796
|
+
if (this.cursor === charIndex) {
|
|
797
|
+
cursorLine = lines.length;
|
|
798
|
+
cursorCol = 0;
|
|
799
|
+
}
|
|
800
|
+
charIndex++;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// Safety: clamp values
|
|
804
|
+
cursorLine = Math.max(0, Math.min(cursorLine, lines.length - 1));
|
|
805
|
+
cursorCol = Math.max(0, Math.min(cursorCol, lines[cursorLine]?.length ?? 0));
|
|
806
|
+
return { lines, cursorLine, cursorCol };
|
|
807
|
+
}
|
|
808
|
+
// ===========================================================================
|
|
809
|
+
// UTILITIES
|
|
810
|
+
// ===========================================================================
|
|
811
|
+
scheduleRender() {
|
|
812
|
+
if (!this.canRender())
|
|
813
|
+
return;
|
|
814
|
+
queueMicrotask(() => this.render());
|
|
815
|
+
}
|
|
816
|
+
canRender() {
|
|
817
|
+
return !this.disposed && this.enabled && this.isTTY();
|
|
818
|
+
}
|
|
819
|
+
isTTY() {
|
|
820
|
+
return !!this.out.isTTY && this.out.writable !== false;
|
|
821
|
+
}
|
|
822
|
+
getSize() {
|
|
823
|
+
return {
|
|
824
|
+
rows: this.out.rows ?? 24,
|
|
825
|
+
cols: this.out.columns ?? 80,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
write(data) {
|
|
829
|
+
try {
|
|
830
|
+
if (this.out.writable) {
|
|
831
|
+
this.out.write(data);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
// Ignore write errors
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
sanitize(text) {
|
|
839
|
+
if (!text)
|
|
840
|
+
return '';
|
|
841
|
+
// Remove ANSI codes and control chars (except newlines)
|
|
842
|
+
return text
|
|
843
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '') // OSC sequences
|
|
844
|
+
.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '') // CSI sequences
|
|
845
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '') // Control chars except \n and \r
|
|
846
|
+
.replace(/\r\n?/g, '\n'); // Normalize line endings
|
|
847
|
+
}
|
|
848
|
+
isWhitespace(char) {
|
|
849
|
+
return char === ' ' || char === '\t' || char === '\n';
|
|
850
|
+
}
|
|
851
|
+
clampCursor() {
|
|
852
|
+
this.cursor = Math.max(0, Math.min(this.cursor, this.buffer.length));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
//# sourceMappingURL=terminalInput.js.map
|