erosolar-cli 1.7.190 → 1.7.192

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.
@@ -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