maistro 1.0.390
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/LICENSE +15 -0
- package/README.md +107 -0
- package/dist/app.d.ts +247 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +4971 -0
- package/dist/app.js.map +1 -0
- package/dist/buildInfo.d.ts +5 -0
- package/dist/buildInfo.d.ts.map +1 -0
- package/dist/buildInfo.js +2 -0
- package/dist/buildInfo.js.map +1 -0
- package/dist/caffeinate.d.ts +72 -0
- package/dist/caffeinate.d.ts.map +1 -0
- package/dist/caffeinate.js +258 -0
- package/dist/caffeinate.js.map +1 -0
- package/dist/claudePath.d.ts +10 -0
- package/dist/claudePath.d.ts.map +1 -0
- package/dist/claudePath.js +34 -0
- package/dist/claudePath.js.map +1 -0
- package/dist/clipboard.d.ts +44 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +442 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/config.d.ts +211 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +933 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +50 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +81 -0
- package/dist/constants.js.map +1 -0
- package/dist/contextBuilder.d.ts +38 -0
- package/dist/contextBuilder.d.ts.map +1 -0
- package/dist/contextBuilder.js +113 -0
- package/dist/contextBuilder.js.map +1 -0
- package/dist/dependencyDetector.d.ts +57 -0
- package/dist/dependencyDetector.d.ts.map +1 -0
- package/dist/dependencyDetector.js +505 -0
- package/dist/dependencyDetector.js.map +1 -0
- package/dist/executor.d.ts +83 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +583 -0
- package/dist/executor.js.map +1 -0
- package/dist/git.d.ts +85 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +283 -0
- package/dist/git.js.map +1 -0
- package/dist/imageManager.d.ts +161 -0
- package/dist/imageManager.d.ts.map +1 -0
- package/dist/imageManager.js +674 -0
- package/dist/imageManager.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +437 -0
- package/dist/index.js.map +1 -0
- package/dist/input-visual-test.d.ts +9 -0
- package/dist/input-visual-test.d.ts.map +1 -0
- package/dist/input-visual-test.js +108 -0
- package/dist/input-visual-test.js.map +1 -0
- package/dist/inputBox.d.ts +228 -0
- package/dist/inputBox.d.ts.map +1 -0
- package/dist/inputBox.js +966 -0
- package/dist/inputBox.js.map +1 -0
- package/dist/logger.d.ts +136 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +347 -0
- package/dist/logger.js.map +1 -0
- package/dist/orchestrator.d.ts +149 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +821 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/planner.d.ts +86 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +830 -0
- package/dist/planner.js.map +1 -0
- package/dist/pty-test-runner.d.ts +87 -0
- package/dist/pty-test-runner.d.ts.map +1 -0
- package/dist/pty-test-runner.js +721 -0
- package/dist/pty-test-runner.js.map +1 -0
- package/dist/screen.d.ts +44 -0
- package/dist/screen.d.ts.map +1 -0
- package/dist/screen.js +152 -0
- package/dist/screen.js.map +1 -0
- package/dist/taskQueue.d.ts +70 -0
- package/dist/taskQueue.d.ts.map +1 -0
- package/dist/taskQueue.js +282 -0
- package/dist/taskQueue.js.map +1 -0
- package/dist/tui-test-harness.d.ts +216 -0
- package/dist/tui-test-harness.d.ts.map +1 -0
- package/dist/tui-test-harness.js +527 -0
- package/dist/tui-test-harness.js.map +1 -0
- package/dist/types.d.ts +257 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +46 -0
- package/dist/types.js.map +1 -0
- package/dist/ui-visual-test.d.ts +15 -0
- package/dist/ui-visual-test.d.ts.map +1 -0
- package/dist/ui-visual-test.js +141 -0
- package/dist/ui-visual-test.js.map +1 -0
- package/dist/ui.d.ts +272 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +1531 -0
- package/dist/ui.js.map +1 -0
- package/dist/validator.d.ts +53 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +491 -0
- package/dist/validator.js.map +1 -0
- package/dist/versionCheck.d.ts +63 -0
- package/dist/versionCheck.d.ts.map +1 -0
- package/dist/versionCheck.js +261 -0
- package/dist/versionCheck.js.map +1 -0
- package/package.json +62 -0
package/dist/inputBox.js
ADDED
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
import { mightBePastedImage, detectPastedImage } from './imageManager.js';
|
|
2
|
+
import { getNewlineHint } from './config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Get the display width of a string (accounts for wide characters like CJK, emoji).
|
|
5
|
+
* Returns the number of terminal columns the string occupies.
|
|
6
|
+
*/
|
|
7
|
+
export function getDisplayWidth(str) {
|
|
8
|
+
let width = 0;
|
|
9
|
+
for (const char of str) {
|
|
10
|
+
width += getCharWidth(char);
|
|
11
|
+
}
|
|
12
|
+
return width;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get the display width of a single character.
|
|
16
|
+
* - East Asian Wide characters (CJK, fullwidth forms): 2
|
|
17
|
+
* - Most emoji: 2
|
|
18
|
+
* - Regular ASCII and other characters: 1
|
|
19
|
+
* - Control characters: 0
|
|
20
|
+
*/
|
|
21
|
+
function getCharWidth(char) {
|
|
22
|
+
const code = char.codePointAt(0);
|
|
23
|
+
if (code === undefined)
|
|
24
|
+
return 0;
|
|
25
|
+
// Control characters
|
|
26
|
+
if (code < 0x20 || (code >= 0x7F && code < 0xA0)) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
// Check for wide characters
|
|
30
|
+
if (isWideCharacter(code)) {
|
|
31
|
+
return 2;
|
|
32
|
+
}
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if a Unicode code point is a wide (2-column) character.
|
|
37
|
+
*/
|
|
38
|
+
function isWideCharacter(code) {
|
|
39
|
+
// Hangul Jamo
|
|
40
|
+
if (code >= 0x1100 && code <= 0x115F)
|
|
41
|
+
return true;
|
|
42
|
+
// Hangul Compatibility Jamo
|
|
43
|
+
if (code >= 0x3130 && code <= 0x318F)
|
|
44
|
+
return true;
|
|
45
|
+
// CJK Radicals, Kangxi Radicals, CJK Symbols
|
|
46
|
+
if (code >= 0x2E80 && code <= 0x303F)
|
|
47
|
+
return true;
|
|
48
|
+
// Hiragana, Katakana
|
|
49
|
+
if (code >= 0x3040 && code <= 0x30FF)
|
|
50
|
+
return true;
|
|
51
|
+
// Bopomofo, Hangul Compatibility Jamo, Kanbun
|
|
52
|
+
if (code >= 0x3100 && code <= 0x319F)
|
|
53
|
+
return true;
|
|
54
|
+
// Enclosed CJK Letters
|
|
55
|
+
if (code >= 0x3200 && code <= 0x32FF)
|
|
56
|
+
return true;
|
|
57
|
+
// CJK Compatibility
|
|
58
|
+
if (code >= 0x3300 && code <= 0x33FF)
|
|
59
|
+
return true;
|
|
60
|
+
// CJK Unified Ideographs Extension A
|
|
61
|
+
if (code >= 0x3400 && code <= 0x4DBF)
|
|
62
|
+
return true;
|
|
63
|
+
// CJK Unified Ideographs
|
|
64
|
+
if (code >= 0x4E00 && code <= 0x9FFF)
|
|
65
|
+
return true;
|
|
66
|
+
// Yi Syllables and Radicals
|
|
67
|
+
if (code >= 0xA000 && code <= 0xA4CF)
|
|
68
|
+
return true;
|
|
69
|
+
// Hangul Syllables
|
|
70
|
+
if (code >= 0xAC00 && code <= 0xD7A3)
|
|
71
|
+
return true;
|
|
72
|
+
// CJK Compatibility Ideographs
|
|
73
|
+
if (code >= 0xF900 && code <= 0xFAFF)
|
|
74
|
+
return true;
|
|
75
|
+
// Vertical Forms
|
|
76
|
+
if (code >= 0xFE10 && code <= 0xFE1F)
|
|
77
|
+
return true;
|
|
78
|
+
// CJK Compatibility Forms
|
|
79
|
+
if (code >= 0xFE30 && code <= 0xFE4F)
|
|
80
|
+
return true;
|
|
81
|
+
// Halfwidth and Fullwidth Forms (fullwidth range)
|
|
82
|
+
if (code >= 0xFF00 && code <= 0xFF60)
|
|
83
|
+
return true;
|
|
84
|
+
if (code >= 0xFFE0 && code <= 0xFFE6)
|
|
85
|
+
return true;
|
|
86
|
+
// Emoji (various ranges)
|
|
87
|
+
if (code >= 0x1F300 && code <= 0x1F9FF)
|
|
88
|
+
return true;
|
|
89
|
+
if (code >= 0x1FA00 && code <= 0x1FAFF)
|
|
90
|
+
return true;
|
|
91
|
+
// Supplementary Ideographic Plane
|
|
92
|
+
if (code >= 0x20000 && code <= 0x2FFFF)
|
|
93
|
+
return true;
|
|
94
|
+
// Tertiary Ideographic Plane
|
|
95
|
+
if (code >= 0x30000 && code <= 0x3FFFF)
|
|
96
|
+
return true;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get the character index that corresponds to a given display column.
|
|
101
|
+
* Returns the index of the character at or before the column.
|
|
102
|
+
*/
|
|
103
|
+
export function charIndexAtDisplayColumn(str, column) {
|
|
104
|
+
let width = 0;
|
|
105
|
+
let index = 0;
|
|
106
|
+
for (const char of str) {
|
|
107
|
+
if (width >= column)
|
|
108
|
+
break;
|
|
109
|
+
width += getCharWidth(char);
|
|
110
|
+
index++;
|
|
111
|
+
}
|
|
112
|
+
return Math.min(index, [...str].length);
|
|
113
|
+
}
|
|
114
|
+
export class InputBox {
|
|
115
|
+
inputBuffer;
|
|
116
|
+
cursorIndex;
|
|
117
|
+
isFirstRender;
|
|
118
|
+
escPendingClear;
|
|
119
|
+
escTimeout;
|
|
120
|
+
escapeBuffer;
|
|
121
|
+
escapeTimeout;
|
|
122
|
+
resolved;
|
|
123
|
+
options;
|
|
124
|
+
stdout;
|
|
125
|
+
stdin;
|
|
126
|
+
dataHandler;
|
|
127
|
+
rawModeWasEnabled;
|
|
128
|
+
// Track preferred column for vertical navigation (like VS Code)
|
|
129
|
+
preferredVisualCol;
|
|
130
|
+
// History navigation state
|
|
131
|
+
historyIndex;
|
|
132
|
+
savedInput;
|
|
133
|
+
// Track rendered line count for self-clearing (fixes duplication when onRedraw is empty)
|
|
134
|
+
lastRenderedLineCount;
|
|
135
|
+
constructor(options) {
|
|
136
|
+
this.options = {
|
|
137
|
+
placeholder: options.placeholder || '',
|
|
138
|
+
hint: options.hint || 'Enter send',
|
|
139
|
+
mode: options.mode || 'multi-line',
|
|
140
|
+
...options,
|
|
141
|
+
};
|
|
142
|
+
this.stdout = options.stdout || process.stdout;
|
|
143
|
+
this.stdin = options.stdin || process.stdin;
|
|
144
|
+
this.inputBuffer = options.initialValue || '';
|
|
145
|
+
this.cursorIndex = this.inputBuffer.length;
|
|
146
|
+
this.isFirstRender = true;
|
|
147
|
+
this.escPendingClear = false;
|
|
148
|
+
this.escTimeout = null;
|
|
149
|
+
this.escapeBuffer = '';
|
|
150
|
+
this.escapeTimeout = null;
|
|
151
|
+
this.resolved = false;
|
|
152
|
+
this.dataHandler = null;
|
|
153
|
+
this.rawModeWasEnabled = false;
|
|
154
|
+
this.preferredVisualCol = null;
|
|
155
|
+
// History state: -1 means not browsing history (editing current input)
|
|
156
|
+
this.historyIndex = -1;
|
|
157
|
+
this.savedInput = '';
|
|
158
|
+
// Track rendered lines for self-clearing
|
|
159
|
+
this.lastRenderedLineCount = 0;
|
|
160
|
+
}
|
|
161
|
+
/** Get current input value */
|
|
162
|
+
getValue() {
|
|
163
|
+
return this.inputBuffer;
|
|
164
|
+
}
|
|
165
|
+
/** Set input value programmatically */
|
|
166
|
+
setValue(value) {
|
|
167
|
+
this.inputBuffer = value;
|
|
168
|
+
this.cursorIndex = value.length;
|
|
169
|
+
this.render();
|
|
170
|
+
}
|
|
171
|
+
/** Clear input */
|
|
172
|
+
clear() {
|
|
173
|
+
this.inputBuffer = '';
|
|
174
|
+
this.cursorIndex = 0;
|
|
175
|
+
this.escPendingClear = false;
|
|
176
|
+
this.resetHistoryState();
|
|
177
|
+
this.render();
|
|
178
|
+
}
|
|
179
|
+
/** Update history array */
|
|
180
|
+
setHistory(history) {
|
|
181
|
+
this.options.history = history;
|
|
182
|
+
}
|
|
183
|
+
/** Mark as first render (call after external screen redraw) */
|
|
184
|
+
markFirstRender() {
|
|
185
|
+
this.isFirstRender = true;
|
|
186
|
+
}
|
|
187
|
+
/** Get current history index (-1 means not browsing history) */
|
|
188
|
+
getHistoryIndex() {
|
|
189
|
+
return this.historyIndex;
|
|
190
|
+
}
|
|
191
|
+
/** Reset history navigation state (call when user modifies input) */
|
|
192
|
+
resetHistoryState() {
|
|
193
|
+
this.historyIndex = -1;
|
|
194
|
+
this.savedInput = '';
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Navigate history in the given direction.
|
|
198
|
+
* @returns true if navigation occurred, false if at boundary
|
|
199
|
+
*/
|
|
200
|
+
navigateHistory(direction) {
|
|
201
|
+
const history = this.options.history;
|
|
202
|
+
if (!history || history.length === 0)
|
|
203
|
+
return false;
|
|
204
|
+
// If custom handler provided, use it
|
|
205
|
+
if (this.options.onHistoryNavigate) {
|
|
206
|
+
const newValue = this.options.onHistoryNavigate(direction, this.inputBuffer, this.historyIndex);
|
|
207
|
+
if (newValue !== null) {
|
|
208
|
+
// Save current input if just starting history navigation
|
|
209
|
+
if (this.historyIndex === -1 && direction === 'up') {
|
|
210
|
+
this.savedInput = this.inputBuffer;
|
|
211
|
+
}
|
|
212
|
+
this.inputBuffer = newValue;
|
|
213
|
+
this.cursorIndex = newValue.length;
|
|
214
|
+
// Update history index (managed by callback, but we track for state)
|
|
215
|
+
if (direction === 'up') {
|
|
216
|
+
this.historyIndex = this.historyIndex === -1 ? history.length - 1 : Math.max(0, this.historyIndex - 1);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.historyIndex = Math.min(history.length, this.historyIndex + 1);
|
|
220
|
+
if (this.historyIndex >= history.length) {
|
|
221
|
+
this.historyIndex = -1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.render();
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
// Default history navigation logic
|
|
230
|
+
if (direction === 'up') {
|
|
231
|
+
if (this.historyIndex === -1) {
|
|
232
|
+
// Save current input and go to most recent history
|
|
233
|
+
this.savedInput = this.inputBuffer;
|
|
234
|
+
this.historyIndex = history.length - 1;
|
|
235
|
+
}
|
|
236
|
+
else if (this.historyIndex > 0) {
|
|
237
|
+
// Go to older history
|
|
238
|
+
this.historyIndex--;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// Already at oldest, do nothing
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
this.inputBuffer = history[this.historyIndex];
|
|
245
|
+
this.cursorIndex = this.inputBuffer.length;
|
|
246
|
+
this.render();
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// direction === 'down'
|
|
251
|
+
if (this.historyIndex === -1) {
|
|
252
|
+
// Not in history, do nothing
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
else if (this.historyIndex < history.length - 1) {
|
|
256
|
+
// Go to newer history
|
|
257
|
+
this.historyIndex++;
|
|
258
|
+
this.inputBuffer = history[this.historyIndex];
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
// At most recent history, go back to saved input
|
|
262
|
+
this.historyIndex = -1;
|
|
263
|
+
this.inputBuffer = this.savedInput;
|
|
264
|
+
}
|
|
265
|
+
this.cursorIndex = this.inputBuffer.length;
|
|
266
|
+
this.render();
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Check if cursor is at the first visual line of input
|
|
272
|
+
*/
|
|
273
|
+
isAtFirstLine() {
|
|
274
|
+
const { lineNum, visualRow } = this.getVisualLineInfo();
|
|
275
|
+
return lineNum === 0 && visualRow === 0;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Check if cursor is at the last visual line of input
|
|
279
|
+
*/
|
|
280
|
+
isAtLastLine() {
|
|
281
|
+
const { lineNum, lines, visualRow, totalVisualRows } = this.getVisualLineInfo();
|
|
282
|
+
return lineNum === lines.length - 1 && visualRow === totalVisualRows - 1;
|
|
283
|
+
}
|
|
284
|
+
safeWrite(data) {
|
|
285
|
+
try {
|
|
286
|
+
if (this.stdout.writable) {
|
|
287
|
+
this.stdout.write(data);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// Ignore write errors
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
getTermWidth() {
|
|
295
|
+
return this.stdout.columns || 80;
|
|
296
|
+
}
|
|
297
|
+
getTermHeight() {
|
|
298
|
+
return this.stdout.rows || 24;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get cursor position info within multi-line input.
|
|
302
|
+
* Returns the logical line number and column within that line.
|
|
303
|
+
*/
|
|
304
|
+
getLineInfo() {
|
|
305
|
+
const lines = this.inputBuffer.split('\n');
|
|
306
|
+
let pos = 0;
|
|
307
|
+
for (let i = 0; i < lines.length; i++) {
|
|
308
|
+
const lineLen = lines[i].length;
|
|
309
|
+
if (this.cursorIndex <= pos + lineLen) {
|
|
310
|
+
return {
|
|
311
|
+
lineNum: i,
|
|
312
|
+
col: this.cursorIndex - pos,
|
|
313
|
+
lines,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
pos += lineLen + 1; // +1 for newline
|
|
317
|
+
}
|
|
318
|
+
// Cursor at end
|
|
319
|
+
return {
|
|
320
|
+
lineNum: lines.length - 1,
|
|
321
|
+
col: lines[lines.length - 1].length,
|
|
322
|
+
lines,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get the content width available for text (terminal width minus prefix).
|
|
327
|
+
* Prefix is "> " for first line and " " for continuation lines = 2 chars.
|
|
328
|
+
*/
|
|
329
|
+
getContentWidth() {
|
|
330
|
+
return Math.max(10, this.getTermWidth() - 2);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Get visual line info for navigation.
|
|
334
|
+
* Visual lines are what you see on screen after wrapping.
|
|
335
|
+
* Returns info about which visual row the cursor is on within the current logical line.
|
|
336
|
+
* Uses display width to account for wide characters (CJK, emoji, etc.).
|
|
337
|
+
*/
|
|
338
|
+
getVisualLineInfo() {
|
|
339
|
+
const { lineNum, col, lines } = this.getLineInfo();
|
|
340
|
+
const contentWidth = this.getContentWidth();
|
|
341
|
+
const lineContent = lines[lineNum];
|
|
342
|
+
// Calculate total display width of the line (plus 1 for cursor at end)
|
|
343
|
+
const lineDisplayWidth = getDisplayWidth(lineContent) + 1;
|
|
344
|
+
// Calculate how many visual rows this logical line spans
|
|
345
|
+
const totalVisualRows = Math.max(1, Math.ceil(lineDisplayWidth / contentWidth));
|
|
346
|
+
// Calculate display column of cursor (width of characters before cursor)
|
|
347
|
+
const textBeforeCursor = [...lineContent].slice(0, col).join('');
|
|
348
|
+
const cursorDisplayCol = getDisplayWidth(textBeforeCursor);
|
|
349
|
+
// Which visual row is the cursor on?
|
|
350
|
+
const visualRow = Math.floor(cursorDisplayCol / contentWidth);
|
|
351
|
+
// Display column within this visual row
|
|
352
|
+
const visualCol = cursorDisplayCol % contentWidth;
|
|
353
|
+
return {
|
|
354
|
+
lineNum,
|
|
355
|
+
col,
|
|
356
|
+
lines,
|
|
357
|
+
visualRow,
|
|
358
|
+
visualCol,
|
|
359
|
+
totalVisualRows,
|
|
360
|
+
contentWidth,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Calculate cursor index from logical line number and column.
|
|
365
|
+
*/
|
|
366
|
+
cursorIndexFromLineCol(lineNum, col, lines) {
|
|
367
|
+
let pos = 0;
|
|
368
|
+
for (let i = 0; i < lineNum; i++) {
|
|
369
|
+
pos += lines[i].length + 1; // +1 for newline
|
|
370
|
+
}
|
|
371
|
+
return pos + Math.min(col, lines[lineNum].length);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Render the input box.
|
|
375
|
+
* Uses full-screen redraw approach for reliability.
|
|
376
|
+
* Supports multi-line input with proper cursor positioning.
|
|
377
|
+
*
|
|
378
|
+
* Self-clears previous output to prevent duplication when onRedraw is empty.
|
|
379
|
+
*/
|
|
380
|
+
render() {
|
|
381
|
+
const termWidth = this.getTermWidth();
|
|
382
|
+
const border = '─'.repeat(termWidth);
|
|
383
|
+
const { placeholder, hint, onRedraw } = this.options;
|
|
384
|
+
// Calculate input line count for onRedraw callback
|
|
385
|
+
const inputLineCount = this.inputBuffer ? this.inputBuffer.split('\n').length : 1;
|
|
386
|
+
// Self-clear previous content before calling onRedraw
|
|
387
|
+
// This prevents duplicate input boxes when navigating history or re-rendering
|
|
388
|
+
// Only clear if we have previous content (lastRenderedLineCount > 0)
|
|
389
|
+
if (this.lastRenderedLineCount > 0) {
|
|
390
|
+
this.safeWrite(`\x1b[${this.lastRenderedLineCount}A`); // Move cursor up
|
|
391
|
+
this.safeWrite('\x1b[J'); // Clear from cursor to end of screen
|
|
392
|
+
}
|
|
393
|
+
// Always call onRedraw to ensure content is drawn
|
|
394
|
+
// On first render with hideWhen, we especially need this since the InputBox won't draw anything visible
|
|
395
|
+
onRedraw(inputLineCount);
|
|
396
|
+
this.isFirstRender = false;
|
|
397
|
+
// Check if we should hide the input box visually
|
|
398
|
+
if (this.options.hideWhen?.()) {
|
|
399
|
+
// Clear any residual content from previous render
|
|
400
|
+
this.safeWrite('\x1b[J');
|
|
401
|
+
this.lastRenderedLineCount = 0;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// Track VISUAL line count for this render (terminal rows, not logical lines)
|
|
405
|
+
// This accounts for line wrapping when content exceeds terminal width
|
|
406
|
+
let visualLineCount = 0;
|
|
407
|
+
const contentWidth = this.getContentWidth(); // Width available for content after prefix
|
|
408
|
+
// Top border (always 1 visual line - it exactly fits terminal width)
|
|
409
|
+
this.safeWrite(`\x1b[90m${border}\x1b[0m\n`);
|
|
410
|
+
visualLineCount++;
|
|
411
|
+
// Input line(s) with cursor - supports multi-line
|
|
412
|
+
if (this.inputBuffer) {
|
|
413
|
+
// Split input into lines for multi-line display
|
|
414
|
+
const { lineNum: cursorLine, col: cursorCol, lines } = this.getLineInfo();
|
|
415
|
+
// Calculate available rows for input (terminal height minus borders, hint, some buffer for content above)
|
|
416
|
+
// Reserve: 1 top border + 1 bottom border + 1 hint + 2 safety buffer = 5 lines
|
|
417
|
+
const maxInputLines = Math.max(3, this.getTermHeight() - 5);
|
|
418
|
+
const totalLines = lines.length;
|
|
419
|
+
const needsScrolling = totalLines > maxInputLines;
|
|
420
|
+
// Determine which lines to show (keep cursor line visible)
|
|
421
|
+
let startLine = 0;
|
|
422
|
+
let endLine = totalLines;
|
|
423
|
+
if (needsScrolling) {
|
|
424
|
+
// Center the cursor line in the visible area, with preference to show context
|
|
425
|
+
const halfWindow = Math.floor(maxInputLines / 2);
|
|
426
|
+
startLine = Math.max(0, cursorLine - halfWindow);
|
|
427
|
+
endLine = Math.min(totalLines, startLine + maxInputLines);
|
|
428
|
+
// Adjust start if we're near the end
|
|
429
|
+
if (endLine === totalLines) {
|
|
430
|
+
startLine = Math.max(0, totalLines - maxInputLines);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// Show scroll indicator at top if there are hidden lines above
|
|
434
|
+
if (startLine > 0) {
|
|
435
|
+
this.safeWrite(`\x1b[90m ↑ ${startLine} more line${startLine > 1 ? 's' : ''} above\x1b[0m\n`);
|
|
436
|
+
visualLineCount++;
|
|
437
|
+
}
|
|
438
|
+
// Render visible lines
|
|
439
|
+
for (let idx = startLine; idx < endLine; idx++) {
|
|
440
|
+
const line = lines[idx];
|
|
441
|
+
// First line gets "> ", continuation lines get " " (same width for alignment)
|
|
442
|
+
const prefix = idx === 0 ? '\x1b[90m>\x1b[0m ' : ' ';
|
|
443
|
+
// Show line break indicator (↵) at end of line if not the last line
|
|
444
|
+
const lineBreakIndicator = idx < totalLines - 1 ? '\x1b[90m↵\x1b[0m' : '';
|
|
445
|
+
if (idx === cursorLine) {
|
|
446
|
+
// This line has the cursor
|
|
447
|
+
const beforeCursor = line.slice(0, cursorCol);
|
|
448
|
+
const atCursor = line[cursorCol] || ' ';
|
|
449
|
+
const afterCursor = line.slice(cursorCol + 1);
|
|
450
|
+
this.safeWrite(`${prefix}${beforeCursor}\x1b[7m${atCursor}\x1b[0m${afterCursor}${lineBreakIndicator}\n`);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
this.safeWrite(`${prefix}${line}${lineBreakIndicator}\n`);
|
|
454
|
+
}
|
|
455
|
+
// Calculate visual lines for this content line (accounting for terminal wrapping)
|
|
456
|
+
// Line content width = display width of line + prefix (2) + line break indicator (~1 if present)
|
|
457
|
+
const lineDisplayWidth = getDisplayWidth(line) + 2 + (idx < totalLines - 1 ? 1 : 0);
|
|
458
|
+
const termWidth = this.getTermWidth();
|
|
459
|
+
const visualLinesForThisLine = Math.max(1, Math.ceil(lineDisplayWidth / termWidth));
|
|
460
|
+
visualLineCount += visualLinesForThisLine;
|
|
461
|
+
}
|
|
462
|
+
// Show scroll indicator at bottom if there are hidden lines below
|
|
463
|
+
if (endLine < totalLines) {
|
|
464
|
+
const hiddenBelow = totalLines - endLine;
|
|
465
|
+
this.safeWrite(`\x1b[90m ↓ ${hiddenBelow} more line${hiddenBelow > 1 ? 's' : ''} below\x1b[0m\n`);
|
|
466
|
+
visualLineCount++;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
else if (placeholder) {
|
|
470
|
+
this.safeWrite(`\x1b[90m>\x1b[0m \x1b[7m \x1b[0m\x1b[90m${placeholder}\x1b[0m\n`);
|
|
471
|
+
visualLineCount++;
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
this.safeWrite(`\x1b[90m>\x1b[0m \x1b[7m \x1b[0m\n`);
|
|
475
|
+
visualLineCount++;
|
|
476
|
+
}
|
|
477
|
+
// Bottom border (always 1 visual line)
|
|
478
|
+
this.safeWrite(`\x1b[90m${border}\x1b[0m\n`);
|
|
479
|
+
visualLineCount++;
|
|
480
|
+
// Custom footer or default hint
|
|
481
|
+
if (this.options.customFooter) {
|
|
482
|
+
// Let caller render custom footer (e.g., image list + hint)
|
|
483
|
+
// Note: customFooter is responsible for its own line tracking
|
|
484
|
+
this.options.customFooter(this.escPendingClear, termWidth);
|
|
485
|
+
visualLineCount++; // Assume at least 1 line for custom footer
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
// Hint OUTSIDE the input box, right-aligned
|
|
489
|
+
// Show escape confirmation hint if pending, otherwise normal hint
|
|
490
|
+
const displayHint = this.escPendingClear ? 'Escape again to clear' : (hint || `Enter send · ${getNewlineHint()}`);
|
|
491
|
+
const hintColor = this.escPendingClear ? '\x1b[33m' : '\x1b[90m'; // Yellow for warning
|
|
492
|
+
// Use ANSI cursor positioning to right-align hint reliably (move cursor to column)
|
|
493
|
+
// Use display width for proper alignment with non-ASCII characters
|
|
494
|
+
const hintCol = Math.max(1, termWidth - getDisplayWidth(displayHint) + 1);
|
|
495
|
+
this.safeWrite(`\x1b[${hintCol}G${hintColor}${displayHint}\x1b[0m\n`);
|
|
496
|
+
visualLineCount++;
|
|
497
|
+
}
|
|
498
|
+
// Clear any residual content below (from previous longer input/wrapping)
|
|
499
|
+
this.safeWrite('\x1b[J');
|
|
500
|
+
// Save visual line count for next render's self-clear
|
|
501
|
+
this.lastRenderedLineCount = visualLineCount;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Handle a key press
|
|
505
|
+
*/
|
|
506
|
+
handleKey(key) {
|
|
507
|
+
if (this.resolved) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Let custom handler try first
|
|
511
|
+
if (this.options.onKey && this.options.onKey(key, this)) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Handle escape sequence buffering
|
|
515
|
+
if (this.escapeBuffer) {
|
|
516
|
+
key = this.escapeBuffer + key;
|
|
517
|
+
this.escapeBuffer = '';
|
|
518
|
+
if (this.escapeTimeout) {
|
|
519
|
+
clearTimeout(this.escapeTimeout);
|
|
520
|
+
this.escapeTimeout = null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Bare escape - wait briefly for more input (could be start of sequence)
|
|
524
|
+
if (key === '\x1b') {
|
|
525
|
+
this.escapeBuffer = key;
|
|
526
|
+
this.escapeTimeout = setTimeout(() => {
|
|
527
|
+
this.escapeBuffer = '';
|
|
528
|
+
this.handleBareEscape();
|
|
529
|
+
}, 50);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// Clear escape pending state on any other input
|
|
533
|
+
if (this.escPendingClear && key !== '\x1b') {
|
|
534
|
+
if (this.escTimeout) {
|
|
535
|
+
clearTimeout(this.escTimeout);
|
|
536
|
+
this.escTimeout = null;
|
|
537
|
+
}
|
|
538
|
+
this.escPendingClear = false;
|
|
539
|
+
// Will re-render below with normal hint
|
|
540
|
+
}
|
|
541
|
+
// Ctrl+C - cancel
|
|
542
|
+
if (key === '\x03') {
|
|
543
|
+
this.cleanup();
|
|
544
|
+
this.options.onCancel?.();
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
// Clipboard paste - check system clipboard for images
|
|
548
|
+
// Supported sequences:
|
|
549
|
+
// - \x16 (Ctrl+V) - standard control character
|
|
550
|
+
// - \x1b[118;9u (Kitty protocol Cmd+V) - 118='v', 9=Super modifier
|
|
551
|
+
// - \x1bOV (configurable in iTerm2 for Cmd+V)
|
|
552
|
+
const isPasteKey = key === '\x16' || key === '\x1b[118;9u' || key === '\x1bOV';
|
|
553
|
+
if (isPasteKey && this.options.onClipboardPaste) {
|
|
554
|
+
this.options.onClipboardPaste().then(() => {
|
|
555
|
+
// Re-render after paste completes (callback may have redrawn screen)
|
|
556
|
+
this.isFirstRender = true;
|
|
557
|
+
this.render();
|
|
558
|
+
}).catch(() => {
|
|
559
|
+
// Re-render even on error
|
|
560
|
+
this.isFirstRender = true;
|
|
561
|
+
this.render();
|
|
562
|
+
});
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// Shift+Up/Down - history navigation (if shiftArrowHistory enabled or no multi-line)
|
|
566
|
+
// Kitty protocol: \x1b[1;2A (Shift+Up), \x1b[1;2B (Shift+Down)
|
|
567
|
+
if (key === '\x1b[1;2A') { // Shift+Up
|
|
568
|
+
if (this.options.shiftArrowHistory || this.options.mode === 'single-line') {
|
|
569
|
+
if (this.navigateHistory('up'))
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
// Fall through to normal up handling if history nav didn't happen
|
|
573
|
+
}
|
|
574
|
+
if (key === '\x1b[1;2B') { // Shift+Down
|
|
575
|
+
if (this.options.shiftArrowHistory || this.options.mode === 'single-line') {
|
|
576
|
+
if (this.navigateHistory('down'))
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
// Fall through to normal down handling if history nav didn't happen
|
|
580
|
+
}
|
|
581
|
+
// Shift+Enter - insert newline (multi-line input only)
|
|
582
|
+
// Common sequences: \x1b[13;2u (kitty/modern), \x1bOM (some terminals), or \x0a (Ctrl+J / some Shift+Enter)
|
|
583
|
+
if (key === '\x1b[13;2u' || key === '\x1bOM' || key === '\x0a') {
|
|
584
|
+
// Only allow newlines in multi-line mode
|
|
585
|
+
if (this.options.mode === 'single-line') {
|
|
586
|
+
// In single-line mode, treat as submit (like Enter)
|
|
587
|
+
this.cleanup();
|
|
588
|
+
this.options.onSubmit?.(this.inputBuffer.trim());
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.inputBuffer = this.inputBuffer.slice(0, this.cursorIndex) + '\n' + this.inputBuffer.slice(this.cursorIndex);
|
|
592
|
+
this.cursorIndex++;
|
|
593
|
+
this.resetHistoryState(); // User modified input, reset history
|
|
594
|
+
this.render();
|
|
595
|
+
this.options.onChange?.(this.inputBuffer);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// Enter - submit (only plain Enter \r, not \n which may be Shift+Enter)
|
|
599
|
+
// Also handle batched input like "run\r" or "run\r\n" when typing quickly or pasting
|
|
600
|
+
const endsWithEnter = key === '\r' || key.endsWith('\r') || key.endsWith('\r\n');
|
|
601
|
+
if (endsWithEnter) {
|
|
602
|
+
// If batched input, add the content before Enter to the buffer first
|
|
603
|
+
const enterSuffix = key.endsWith('\r\n') ? '\r\n' : (key.endsWith('\r') ? '\r' : '');
|
|
604
|
+
if (enterSuffix && key.length > enterSuffix.length) {
|
|
605
|
+
const contentBeforeEnter = key.slice(0, -enterSuffix.length);
|
|
606
|
+
// Only insert printable characters
|
|
607
|
+
let charsToInsert = '';
|
|
608
|
+
for (const c of contentBeforeEnter) {
|
|
609
|
+
const code = c.charCodeAt(0);
|
|
610
|
+
if ((code >= 32 && code < 127) || code >= 128) {
|
|
611
|
+
charsToInsert += c;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (charsToInsert.length > 0) {
|
|
615
|
+
this.inputBuffer = this.inputBuffer.slice(0, this.cursorIndex) + charsToInsert + this.inputBuffer.slice(this.cursorIndex);
|
|
616
|
+
this.cursorIndex += charsToInsert.length;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
this.cleanup();
|
|
620
|
+
this.options.onSubmit?.(this.inputBuffer.trim());
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
// Arrow keys
|
|
624
|
+
if (key === '\x1b[D') { // Left
|
|
625
|
+
if (this.cursorIndex > 0) {
|
|
626
|
+
this.cursorIndex--;
|
|
627
|
+
this.preferredVisualCol = null; // Reset preferred column on horizontal movement
|
|
628
|
+
this.render();
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (key === '\x1b[C') { // Right
|
|
633
|
+
if (this.cursorIndex < this.inputBuffer.length) {
|
|
634
|
+
this.cursorIndex++;
|
|
635
|
+
this.preferredVisualCol = null; // Reset preferred column on horizontal movement
|
|
636
|
+
this.render();
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (key === '\x1b[A') { // Up - move to previous visual line
|
|
641
|
+
const { lineNum, lines, visualRow, visualCol, totalVisualRows, contentWidth } = this.getVisualLineInfo();
|
|
642
|
+
const atFirstLine = lineNum === 0 && visualRow === 0;
|
|
643
|
+
// At first line: check for boundary navigation or history
|
|
644
|
+
if (atFirstLine) {
|
|
645
|
+
// Try boundary navigation callback first
|
|
646
|
+
if (this.options.onBoundaryNavigation?.('up')) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
// Try history navigation (if not using shiftArrowHistory mode)
|
|
650
|
+
if (!this.options.shiftArrowHistory && this.options.history) {
|
|
651
|
+
if (this.navigateHistory('up'))
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
// Nothing to do, already at top
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
// Set preferred column if not already set (for maintaining horizontal position)
|
|
658
|
+
// preferredVisualCol is a display column, not character index
|
|
659
|
+
if (this.preferredVisualCol === null) {
|
|
660
|
+
this.preferredVisualCol = visualCol;
|
|
661
|
+
}
|
|
662
|
+
if (visualRow > 0) {
|
|
663
|
+
// Move up within the same logical line (to previous visual row)
|
|
664
|
+
// Calculate target display column on the previous visual row
|
|
665
|
+
const targetDisplayCol = (visualRow - 1) * contentWidth + Math.min(this.preferredVisualCol, contentWidth - 1);
|
|
666
|
+
// Convert display column to character index
|
|
667
|
+
const newCol = charIndexAtDisplayColumn(lines[lineNum], targetDisplayCol);
|
|
668
|
+
this.cursorIndex = this.cursorIndexFromLineCol(lineNum, newCol, lines);
|
|
669
|
+
}
|
|
670
|
+
else if (lineNum > 0) {
|
|
671
|
+
// Move to the previous logical line's last visual row
|
|
672
|
+
const prevLine = lines[lineNum - 1];
|
|
673
|
+
const prevLineDisplayWidth = getDisplayWidth(prevLine) + 1;
|
|
674
|
+
const prevTotalVisualRows = Math.max(1, Math.ceil(prevLineDisplayWidth / contentWidth));
|
|
675
|
+
// Target the last visual row of the previous line, at preferred column
|
|
676
|
+
const targetDisplayCol = (prevTotalVisualRows - 1) * contentWidth + Math.min(this.preferredVisualCol, contentWidth - 1);
|
|
677
|
+
const newCol = charIndexAtDisplayColumn(prevLine, targetDisplayCol);
|
|
678
|
+
this.cursorIndex = this.cursorIndexFromLineCol(lineNum - 1, newCol, lines);
|
|
679
|
+
}
|
|
680
|
+
this.render();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (key === '\x1b[B') { // Down - move to next visual line
|
|
684
|
+
const { lineNum, lines, visualRow, visualCol, totalVisualRows, contentWidth } = this.getVisualLineInfo();
|
|
685
|
+
const atLastLine = lineNum === lines.length - 1 && visualRow === totalVisualRows - 1;
|
|
686
|
+
// At last line: check for boundary navigation or history
|
|
687
|
+
if (atLastLine) {
|
|
688
|
+
// Try boundary navigation callback first
|
|
689
|
+
if (this.options.onBoundaryNavigation?.('down')) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
// Try history navigation (if not using shiftArrowHistory mode)
|
|
693
|
+
if (!this.options.shiftArrowHistory && this.options.history) {
|
|
694
|
+
if (this.navigateHistory('down'))
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
// Nothing to do, already at bottom
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
// Set preferred column if not already set
|
|
701
|
+
// preferredVisualCol is a display column, not character index
|
|
702
|
+
if (this.preferredVisualCol === null) {
|
|
703
|
+
this.preferredVisualCol = visualCol;
|
|
704
|
+
}
|
|
705
|
+
if (visualRow < totalVisualRows - 1) {
|
|
706
|
+
// Move down within the same logical line (to next visual row)
|
|
707
|
+
// Calculate target display column on the next visual row
|
|
708
|
+
const targetDisplayCol = (visualRow + 1) * contentWidth + Math.min(this.preferredVisualCol, contentWidth - 1);
|
|
709
|
+
// Convert display column to character index
|
|
710
|
+
const newCol = charIndexAtDisplayColumn(lines[lineNum], targetDisplayCol);
|
|
711
|
+
this.cursorIndex = this.cursorIndexFromLineCol(lineNum, newCol, lines);
|
|
712
|
+
}
|
|
713
|
+
else if (lineNum < lines.length - 1) {
|
|
714
|
+
// Move to the next logical line's first visual row
|
|
715
|
+
const nextLine = lines[lineNum + 1];
|
|
716
|
+
// Target the first visual row of the next line, at preferred column
|
|
717
|
+
const newCol = charIndexAtDisplayColumn(nextLine, this.preferredVisualCol);
|
|
718
|
+
this.cursorIndex = this.cursorIndexFromLineCol(lineNum + 1, newCol, lines);
|
|
719
|
+
}
|
|
720
|
+
this.render();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
// Home key or Ctrl+A - handle various terminal sequences
|
|
724
|
+
// CSI H, CSI 1~, SS3 H, CSI 7~ (rxvt), Ctrl+A
|
|
725
|
+
if (key === '\x1b[H' || key === '\x1b[1~' || key === '\x1bOH' || key === '\x1b[7~' || key === '\x01') {
|
|
726
|
+
this.cursorIndex = 0;
|
|
727
|
+
this.preferredVisualCol = null; // Reset on horizontal movement
|
|
728
|
+
this.render();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
// End key or Ctrl+E - handle various terminal sequences
|
|
732
|
+
// CSI F, CSI 4~, SS3 F, CSI 8~ (rxvt), Ctrl+E
|
|
733
|
+
if (key === '\x1b[F' || key === '\x1b[4~' || key === '\x1bOF' || key === '\x1b[8~' || key === '\x05') {
|
|
734
|
+
this.cursorIndex = this.inputBuffer.length;
|
|
735
|
+
this.preferredVisualCol = null; // Reset on horizontal movement
|
|
736
|
+
this.render();
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
// Backspace
|
|
740
|
+
if (key === '\x7f' || key === '\b') {
|
|
741
|
+
if (this.cursorIndex > 0) {
|
|
742
|
+
this.inputBuffer = this.inputBuffer.slice(0, this.cursorIndex - 1) + this.inputBuffer.slice(this.cursorIndex);
|
|
743
|
+
this.cursorIndex--;
|
|
744
|
+
this.preferredVisualCol = null; // Reset on text modification
|
|
745
|
+
this.resetHistoryState(); // User modified input, reset history
|
|
746
|
+
this.render();
|
|
747
|
+
this.options.onChange?.(this.inputBuffer);
|
|
748
|
+
}
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
// Delete
|
|
752
|
+
if (key === '\x1b[3~') {
|
|
753
|
+
if (this.cursorIndex < this.inputBuffer.length) {
|
|
754
|
+
this.inputBuffer = this.inputBuffer.slice(0, this.cursorIndex) + this.inputBuffer.slice(this.cursorIndex + 1);
|
|
755
|
+
this.preferredVisualCol = null; // Reset on text modification
|
|
756
|
+
this.resetHistoryState(); // User modified input, reset history
|
|
757
|
+
this.render();
|
|
758
|
+
this.options.onChange?.(this.inputBuffer);
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
// Ignore other escape sequences
|
|
763
|
+
if (key.startsWith('\x1b[') || key.startsWith('\x1bO')) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Regular printable characters (and newlines for pasted multi-line text)
|
|
767
|
+
// Handle different line ending styles: \n (Unix), \r (old Mac), \r\n (Windows)
|
|
768
|
+
let charsToInsert = '';
|
|
769
|
+
for (let i = 0; i < key.length; i++) {
|
|
770
|
+
const c = key[i];
|
|
771
|
+
if (c >= ' ' && c !== '\x7f') {
|
|
772
|
+
// Regular printable character
|
|
773
|
+
charsToInsert += c;
|
|
774
|
+
}
|
|
775
|
+
else if (c === '\n') {
|
|
776
|
+
// Unix newline
|
|
777
|
+
charsToInsert += '\n';
|
|
778
|
+
}
|
|
779
|
+
else if (c === '\r') {
|
|
780
|
+
// Carriage return - convert to newline
|
|
781
|
+
// Skip following \n if this is \r\n (Windows line ending)
|
|
782
|
+
if (key[i + 1] === '\n') {
|
|
783
|
+
i++; // Skip the \n
|
|
784
|
+
}
|
|
785
|
+
charsToInsert += '\n';
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (charsToInsert.length > 0) {
|
|
789
|
+
// Check for pasted image data before inserting as text
|
|
790
|
+
if (this.options.onImagePaste && mightBePastedImage(charsToInsert)) {
|
|
791
|
+
const detection = detectPastedImage(charsToInsert);
|
|
792
|
+
if (detection.detected) {
|
|
793
|
+
// Call the handler - it may be async
|
|
794
|
+
const result = this.options.onImagePaste(charsToInsert, detection);
|
|
795
|
+
if (result instanceof Promise) {
|
|
796
|
+
// Handle async callback
|
|
797
|
+
result.then((handled) => {
|
|
798
|
+
if (!handled) {
|
|
799
|
+
// If not handled, insert the text anyway
|
|
800
|
+
this.insertText(charsToInsert);
|
|
801
|
+
}
|
|
802
|
+
}).catch(() => {
|
|
803
|
+
// On error, insert the text
|
|
804
|
+
this.insertText(charsToInsert);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
else if (result) {
|
|
808
|
+
// Synchronously handled - don't insert text
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
// Not handled - insert the text
|
|
813
|
+
this.insertText(charsToInsert);
|
|
814
|
+
}
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
this.insertText(charsToInsert);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Insert text at current cursor position
|
|
823
|
+
*/
|
|
824
|
+
insertText(text) {
|
|
825
|
+
// In single-line mode, strip newlines from inserted text
|
|
826
|
+
let textToInsert = text;
|
|
827
|
+
if (this.options.mode === 'single-line') {
|
|
828
|
+
textToInsert = text.replace(/\n/g, ' ');
|
|
829
|
+
}
|
|
830
|
+
this.inputBuffer = this.inputBuffer.slice(0, this.cursorIndex) + textToInsert + this.inputBuffer.slice(this.cursorIndex);
|
|
831
|
+
this.cursorIndex += textToInsert.length;
|
|
832
|
+
this.preferredVisualCol = null; // Reset on text insertion
|
|
833
|
+
this.resetHistoryState(); // User modified input, reset history
|
|
834
|
+
this.render();
|
|
835
|
+
this.options.onChange?.(this.inputBuffer);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Handle bare escape key (no sequence following)
|
|
839
|
+
*/
|
|
840
|
+
handleBareEscape() {
|
|
841
|
+
if (this.resolved)
|
|
842
|
+
return;
|
|
843
|
+
// If input is empty, just cancel
|
|
844
|
+
if (!this.inputBuffer) {
|
|
845
|
+
this.cleanup();
|
|
846
|
+
this.options.onCancel?.();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
// If already pending clear, this is the second escape - clear the input
|
|
850
|
+
if (this.escPendingClear) {
|
|
851
|
+
if (this.escTimeout) {
|
|
852
|
+
clearTimeout(this.escTimeout);
|
|
853
|
+
this.escTimeout = null;
|
|
854
|
+
}
|
|
855
|
+
this.escPendingClear = false;
|
|
856
|
+
this.clear();
|
|
857
|
+
this.options.onChange?.('');
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
// First escape with text - show confirmation hint
|
|
861
|
+
this.escPendingClear = true;
|
|
862
|
+
this.render();
|
|
863
|
+
// Auto-reset after 1 second
|
|
864
|
+
this.escTimeout = setTimeout(() => {
|
|
865
|
+
this.escPendingClear = false;
|
|
866
|
+
this.escTimeout = null;
|
|
867
|
+
if (!this.resolved) {
|
|
868
|
+
this.render();
|
|
869
|
+
}
|
|
870
|
+
}, 1000);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Clean up and stop listening
|
|
874
|
+
*/
|
|
875
|
+
cleanup() {
|
|
876
|
+
if (this.resolved)
|
|
877
|
+
return;
|
|
878
|
+
this.resolved = true;
|
|
879
|
+
if (this.escTimeout) {
|
|
880
|
+
clearTimeout(this.escTimeout);
|
|
881
|
+
this.escTimeout = null;
|
|
882
|
+
}
|
|
883
|
+
if (this.escapeTimeout) {
|
|
884
|
+
clearTimeout(this.escapeTimeout);
|
|
885
|
+
this.escapeTimeout = null;
|
|
886
|
+
}
|
|
887
|
+
if (this.dataHandler) {
|
|
888
|
+
this.stdin.removeListener('data', this.dataHandler);
|
|
889
|
+
this.dataHandler = null;
|
|
890
|
+
}
|
|
891
|
+
// Restore raw mode state
|
|
892
|
+
if (this.stdin.setRawMode && !this.rawModeWasEnabled) {
|
|
893
|
+
this.stdin.setRawMode(false);
|
|
894
|
+
}
|
|
895
|
+
this.safeWrite('\n');
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Start listening for input
|
|
899
|
+
*/
|
|
900
|
+
start() {
|
|
901
|
+
this.resolved = false;
|
|
902
|
+
this.isFirstRender = true;
|
|
903
|
+
// Enable raw mode
|
|
904
|
+
this.rawModeWasEnabled = !!this.stdin.isRaw;
|
|
905
|
+
if (this.stdin.setRawMode) {
|
|
906
|
+
this.stdin.setRawMode(true);
|
|
907
|
+
}
|
|
908
|
+
this.stdin.resume?.();
|
|
909
|
+
// Set up key handler
|
|
910
|
+
this.dataHandler = (key) => {
|
|
911
|
+
this.handleKey(key.toString());
|
|
912
|
+
};
|
|
913
|
+
this.stdin.on('data', this.dataHandler);
|
|
914
|
+
// Initial render
|
|
915
|
+
this.render();
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Run as a promise (convenience method)
|
|
919
|
+
*/
|
|
920
|
+
prompt() {
|
|
921
|
+
return new Promise((resolve) => {
|
|
922
|
+
const originalOnSubmit = this.options.onSubmit;
|
|
923
|
+
const originalOnCancel = this.options.onCancel;
|
|
924
|
+
this.options.onSubmit = (value) => {
|
|
925
|
+
originalOnSubmit?.(value);
|
|
926
|
+
resolve(value);
|
|
927
|
+
};
|
|
928
|
+
this.options.onCancel = () => {
|
|
929
|
+
originalOnCancel?.();
|
|
930
|
+
resolve(null);
|
|
931
|
+
};
|
|
932
|
+
this.start();
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Create and run an InputBox as a promise
|
|
938
|
+
*/
|
|
939
|
+
export async function inputBox(options) {
|
|
940
|
+
const box = new InputBox(options);
|
|
941
|
+
return box.prompt();
|
|
942
|
+
}
|
|
943
|
+
// === Factory methods for common use cases ===
|
|
944
|
+
/**
|
|
945
|
+
* Create a single-line InputBox (no multi-line support)
|
|
946
|
+
*/
|
|
947
|
+
export function singleLineInput(options) {
|
|
948
|
+
return new InputBox({ ...options, mode: 'single-line' });
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Create an InputBox with history navigation support
|
|
952
|
+
*/
|
|
953
|
+
export function inputWithHistory(options) {
|
|
954
|
+
return new InputBox({ ...options, mode: 'multi-line' });
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Create a single-line InputBox with history navigation
|
|
958
|
+
*/
|
|
959
|
+
export function singleLineWithHistory(options) {
|
|
960
|
+
return new InputBox({
|
|
961
|
+
...options,
|
|
962
|
+
mode: 'single-line',
|
|
963
|
+
shiftArrowHistory: true, // Use Shift+Up/Down for history in single-line mode
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
//# sourceMappingURL=inputBox.js.map
|