tuiuiu.js 1.0.43 → 1.0.44
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/atoms/text-input.d.ts +13 -0
- package/dist/atoms/text-input.d.ts.map +1 -1
- package/dist/atoms/text-input.js +247 -69
- package/dist/atoms/text-input.js.map +1 -1
- package/dist/core/events.d.ts.map +1 -1
- package/dist/core/events.js +9 -3
- package/dist/core/events.js.map +1 -1
- package/dist/core/hit-test.d.ts +4 -2
- package/dist/core/hit-test.d.ts.map +1 -1
- package/dist/core/hit-test.js +37 -35
- package/dist/core/hit-test.js.map +1 -1
- package/dist/core/layout.d.ts.map +1 -1
- package/dist/core/layout.js +25 -16
- package/dist/core/layout.js.map +1 -1
- package/dist/hooks/use-mouse.d.ts.map +1 -1
- package/dist/hooks/use-mouse.js +24 -4
- package/dist/hooks/use-mouse.js.map +1 -1
- package/dist/mcp/docs/atoms.d.ts.map +1 -1
- package/dist/mcp/docs/atoms.js +12 -7
- package/dist/mcp/docs/atoms.js.map +1 -1
- package/dist/storybook/stories/molecules/index.d.ts.map +1 -1
- package/dist/storybook/stories/molecules/index.js +15 -0
- package/dist/storybook/stories/molecules/index.js.map +1 -1
- package/dist/utils/cursor.d.ts.map +1 -1
- package/dist/utils/cursor.js +14 -3
- package/dist/utils/cursor.js.map +1 -1
- package/dist/utils/fs-storage.d.ts.map +1 -1
- package/dist/utils/fs-storage.js +8 -0
- package/dist/utils/fs-storage.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -64,6 +64,10 @@ export interface TextInputOptions {
|
|
|
64
64
|
width?: number;
|
|
65
65
|
/** Maximum number of lines to display (for textarea mode) */
|
|
66
66
|
maxLines?: number;
|
|
67
|
+
/** Auto-grow height based on visual line count (up to maxLines) */
|
|
68
|
+
autoGrow?: boolean;
|
|
69
|
+
/** Show scrollbar when content exceeds maxLines (default: true) */
|
|
70
|
+
showScrollbar?: boolean;
|
|
67
71
|
/** Show character count */
|
|
68
72
|
showCharCount?: boolean;
|
|
69
73
|
/** Enable word wrapping */
|
|
@@ -90,6 +94,15 @@ export declare function createTextInput(options?: TextInputOptions): {
|
|
|
90
94
|
viewportOffset: () => number;
|
|
91
95
|
isMultiline: () => boolean;
|
|
92
96
|
handleInput: (input: string, key: Key) => void;
|
|
97
|
+
setLayout: (layout: {
|
|
98
|
+
width?: number;
|
|
99
|
+
wordWrap?: boolean;
|
|
100
|
+
maxLines?: number;
|
|
101
|
+
autoGrow?: boolean;
|
|
102
|
+
}) => void;
|
|
103
|
+
setCursorPosition: (pos: number, options?: {
|
|
104
|
+
scroll?: boolean;
|
|
105
|
+
}) => void;
|
|
93
106
|
setValue: (v: string) => void;
|
|
94
107
|
clear: () => void;
|
|
95
108
|
focus: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/atoms/text-input.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/atoms/text-input.ts"],"names":[],"mappings":"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"}
|
package/dist/atoms/text-input.js
CHANGED
|
@@ -18,7 +18,8 @@ import { Box, Text } from '../primitives/nodes.js';
|
|
|
18
18
|
import { createSignal } from '../primitives/signal.js';
|
|
19
19
|
import { useInput } from '../hooks/index.js';
|
|
20
20
|
import { getTheme, getContrastColor } from '../core/theme.js';
|
|
21
|
-
import { getChars } from '../core/capabilities.js';
|
|
21
|
+
import { getChars, getRenderMode } from '../core/capabilities.js';
|
|
22
|
+
import { stringWidth } from '../utils/text-utils.js';
|
|
22
23
|
/**
|
|
23
24
|
* Calculate visual lines based on width and wrapping
|
|
24
25
|
* Preserves strict whitespace for editor behavior
|
|
@@ -89,12 +90,31 @@ export function getVisualLines(text, width, wrap) {
|
|
|
89
90
|
}
|
|
90
91
|
return lines;
|
|
91
92
|
}
|
|
93
|
+
function getCursorIndexFromColumn(text, column) {
|
|
94
|
+
if (column <= 0)
|
|
95
|
+
return 0;
|
|
96
|
+
let width = 0;
|
|
97
|
+
let index = 0;
|
|
98
|
+
for (const char of text) {
|
|
99
|
+
const charWidth = stringWidth(char);
|
|
100
|
+
if (width + charWidth > column)
|
|
101
|
+
break;
|
|
102
|
+
width += charWidth;
|
|
103
|
+
index += char.length;
|
|
104
|
+
}
|
|
105
|
+
return index;
|
|
106
|
+
}
|
|
92
107
|
/**
|
|
93
108
|
* Create a TextInput state manager
|
|
94
109
|
*/
|
|
95
110
|
export function createTextInput(options = {}) {
|
|
96
111
|
const { initialValue = '', placeholder = '', password = false, maskChar = '*', multiline = false, maxLength, history = [], onChange, onSubmit, onCancel, isActive: isActiveProp = true, width = 80, // Default width for wrapping
|
|
97
|
-
maxLines, wordWrap = false, enterCreatesNewline = false, } = options;
|
|
112
|
+
maxLines, autoGrow = false, wordWrap = false, enterCreatesNewline = false, } = options;
|
|
113
|
+
let wrapWidth = width;
|
|
114
|
+
let wrapWordWrap = wordWrap;
|
|
115
|
+
let wrapMaxLines = maxLines;
|
|
116
|
+
let wrapAutoGrow = autoGrow;
|
|
117
|
+
let resolvedMaxLines = wrapAutoGrow ? (wrapMaxLines ?? 5) : wrapMaxLines;
|
|
98
118
|
// Helper to check if input is currently active
|
|
99
119
|
// Supports both static boolean and reactive getter function
|
|
100
120
|
const checkIsActive = () => {
|
|
@@ -106,11 +126,61 @@ export function createTextInput(options = {}) {
|
|
|
106
126
|
const [historyIndex, setHistoryIndex] = createSignal(-1);
|
|
107
127
|
const [originalValue, setOriginalValue] = createSignal('');
|
|
108
128
|
const [viewportOffset, setViewportOffset] = createSignal(0);
|
|
129
|
+
const [preferredColumn, setPreferredColumn] = createSignal(null);
|
|
130
|
+
const setLayout = (layout) => {
|
|
131
|
+
if (typeof layout.width === 'number') {
|
|
132
|
+
wrapWidth = Math.max(1, layout.width);
|
|
133
|
+
}
|
|
134
|
+
if (typeof layout.wordWrap === 'boolean') {
|
|
135
|
+
wrapWordWrap = layout.wordWrap;
|
|
136
|
+
}
|
|
137
|
+
if (layout.maxLines !== undefined) {
|
|
138
|
+
wrapMaxLines = layout.maxLines;
|
|
139
|
+
}
|
|
140
|
+
if (typeof layout.autoGrow === 'boolean') {
|
|
141
|
+
wrapAutoGrow = layout.autoGrow;
|
|
142
|
+
}
|
|
143
|
+
resolvedMaxLines = wrapAutoGrow ? (wrapMaxLines ?? 5) : wrapMaxLines;
|
|
144
|
+
};
|
|
145
|
+
const getCursorLineInfo = (val, cursor) => {
|
|
146
|
+
const visualLines = getVisualLines(val, wrapWidth, wrapWordWrap);
|
|
147
|
+
let lineIndex = 0;
|
|
148
|
+
let lineStart = visualLines[0]?.start ?? 0;
|
|
149
|
+
let lineText = visualLines[0]?.text ?? '';
|
|
150
|
+
for (let i = 0; i < visualLines.length; i++) {
|
|
151
|
+
const line = visualLines[i];
|
|
152
|
+
if (cursor >= line.start && cursor <= line.end) {
|
|
153
|
+
lineIndex = i;
|
|
154
|
+
lineStart = line.start;
|
|
155
|
+
lineText = line.text;
|
|
156
|
+
if (cursor < line.end || i === visualLines.length - 1) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const column = stringWidth(lineText.slice(0, Math.max(0, cursor - lineStart)));
|
|
162
|
+
return { visualLines, lineIndex, lineStart, lineText, column };
|
|
163
|
+
};
|
|
164
|
+
const updatePreferredColumn = (val, cursor) => {
|
|
165
|
+
const { column } = getCursorLineInfo(val, cursor);
|
|
166
|
+
setPreferredColumn(column);
|
|
167
|
+
};
|
|
168
|
+
const setCursorPositionInternal = (pos, options) => {
|
|
169
|
+
const current = value();
|
|
170
|
+
const clamped = Math.max(0, Math.min(pos, current.length));
|
|
171
|
+
setCursorPosition(clamped);
|
|
172
|
+
if (options?.updatePreferredColumn !== false) {
|
|
173
|
+
updatePreferredColumn(current, clamped);
|
|
174
|
+
}
|
|
175
|
+
if (options?.scroll !== false) {
|
|
176
|
+
scrollToCursor(current, clamped);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
109
179
|
// Scroll to cursor helper
|
|
110
180
|
const scrollToCursor = (val, cursor) => {
|
|
111
|
-
if (!
|
|
181
|
+
if (!resolvedMaxLines)
|
|
112
182
|
return; // No scrolling needed if no max height
|
|
113
|
-
const visualLines = getVisualLines(val,
|
|
183
|
+
const visualLines = getVisualLines(val, wrapWidth, wrapWordWrap);
|
|
114
184
|
// Find visual line containing cursor
|
|
115
185
|
let cursorLineFn = 0;
|
|
116
186
|
// Fallback if cursor is at very end
|
|
@@ -148,8 +218,8 @@ export function createTextInput(options = {}) {
|
|
|
148
218
|
if (cursorLineFn < currentOffset) {
|
|
149
219
|
setViewportOffset(cursorLineFn);
|
|
150
220
|
}
|
|
151
|
-
else if (cursorLineFn >= currentOffset +
|
|
152
|
-
setViewportOffset(cursorLineFn -
|
|
221
|
+
else if (cursorLineFn >= currentOffset + resolvedMaxLines) {
|
|
222
|
+
setViewportOffset(cursorLineFn - resolvedMaxLines + 1);
|
|
153
223
|
}
|
|
154
224
|
};
|
|
155
225
|
// Word boundary detection
|
|
@@ -195,9 +265,8 @@ export function createTextInput(options = {}) {
|
|
|
195
265
|
const newValue = currentValue.slice(0, pos) + '\n' + currentValue.slice(pos);
|
|
196
266
|
setValue(newValue);
|
|
197
267
|
const newPos = pos + 1;
|
|
198
|
-
|
|
268
|
+
setCursorPositionInternal(newPos);
|
|
199
269
|
setIsMultilineMode(true);
|
|
200
|
-
scrollToCursor(newValue, newPos);
|
|
201
270
|
onChange?.(newValue);
|
|
202
271
|
return;
|
|
203
272
|
}
|
|
@@ -209,9 +278,8 @@ export function createTextInput(options = {}) {
|
|
|
209
278
|
const newValue = currentValue.slice(0, pos) + '\n' + currentValue.slice(pos);
|
|
210
279
|
setValue(newValue);
|
|
211
280
|
const newPos = pos + 1;
|
|
212
|
-
|
|
281
|
+
setCursorPositionInternal(newPos);
|
|
213
282
|
setIsMultilineMode(true);
|
|
214
|
-
scrollToCursor(newValue, newPos);
|
|
215
283
|
onChange?.(newValue);
|
|
216
284
|
}
|
|
217
285
|
else {
|
|
@@ -231,13 +299,11 @@ export function createTextInput(options = {}) {
|
|
|
231
299
|
if (key.ctrl) {
|
|
232
300
|
// Move to previous word boundary
|
|
233
301
|
const newPos = findPrevWordBoundary(currentValue, pos);
|
|
234
|
-
|
|
235
|
-
scrollToCursor(currentValue, newPos);
|
|
302
|
+
setCursorPositionInternal(newPos);
|
|
236
303
|
}
|
|
237
304
|
else {
|
|
238
305
|
const newPos = Math.max(0, pos - 1);
|
|
239
|
-
|
|
240
|
-
scrollToCursor(currentValue, newPos);
|
|
306
|
+
setCursorPositionInternal(newPos);
|
|
241
307
|
}
|
|
242
308
|
return;
|
|
243
309
|
}
|
|
@@ -245,59 +311,83 @@ export function createTextInput(options = {}) {
|
|
|
245
311
|
if (key.ctrl) {
|
|
246
312
|
// Move to next word boundary
|
|
247
313
|
const newPos = findNextWordBoundary(currentValue, pos);
|
|
248
|
-
|
|
249
|
-
scrollToCursor(currentValue, newPos);
|
|
314
|
+
setCursorPositionInternal(newPos);
|
|
250
315
|
}
|
|
251
316
|
else {
|
|
252
317
|
const newPos = Math.min(currentValue.length, pos + 1);
|
|
253
|
-
|
|
254
|
-
scrollToCursor(currentValue, newPos);
|
|
318
|
+
setCursorPositionInternal(newPos);
|
|
255
319
|
}
|
|
256
320
|
return;
|
|
257
321
|
}
|
|
258
322
|
// Home/End or Ctrl+A/E
|
|
259
323
|
if (key.home || (key.ctrl && input === 'a')) {
|
|
260
|
-
|
|
261
|
-
scrollToCursor(currentValue, 0);
|
|
324
|
+
setCursorPositionInternal(0);
|
|
262
325
|
return;
|
|
263
326
|
}
|
|
264
327
|
if (key.end || (key.ctrl && input === 'e')) {
|
|
265
328
|
const newPos = currentValue.length;
|
|
266
|
-
|
|
267
|
-
scrollToCursor(currentValue, newPos);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
// History navigation
|
|
271
|
-
if (key.upArrow && history.length > 0) {
|
|
272
|
-
const currentIndex = historyIndex();
|
|
273
|
-
if (currentIndex === -1) {
|
|
274
|
-
setOriginalValue(currentValue);
|
|
275
|
-
}
|
|
276
|
-
const newIndex = Math.min(currentIndex + 1, history.length - 1);
|
|
277
|
-
setHistoryIndex(newIndex);
|
|
278
|
-
const historyValue = history[history.length - 1 - newIndex];
|
|
279
|
-
setValue(historyValue);
|
|
280
|
-
setCursorPosition(historyValue.length);
|
|
281
|
-
onChange?.(historyValue);
|
|
329
|
+
setCursorPositionInternal(newPos);
|
|
282
330
|
return;
|
|
283
331
|
}
|
|
284
|
-
if (key.
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
332
|
+
if (key.upArrow) {
|
|
333
|
+
const { visualLines, lineIndex, column } = getCursorLineInfo(currentValue, pos);
|
|
334
|
+
const targetColumn = preferredColumn() ?? column;
|
|
335
|
+
if (lineIndex > 0) {
|
|
336
|
+
const targetLine = visualLines[lineIndex - 1];
|
|
337
|
+
const offset = getCursorIndexFromColumn(targetLine.text, targetColumn);
|
|
338
|
+
const newPos = Math.min(currentValue.length, targetLine.start + offset);
|
|
339
|
+
if (preferredColumn() === null) {
|
|
340
|
+
setPreferredColumn(targetColumn);
|
|
341
|
+
}
|
|
342
|
+
setCursorPositionInternal(newPos, { updatePreferredColumn: false });
|
|
343
|
+
return;
|
|
292
344
|
}
|
|
293
|
-
|
|
345
|
+
if (history.length > 0) {
|
|
346
|
+
const currentIndex = historyIndex();
|
|
347
|
+
if (currentIndex === -1) {
|
|
348
|
+
setOriginalValue(currentValue);
|
|
349
|
+
}
|
|
350
|
+
const newIndex = Math.min(currentIndex + 1, history.length - 1);
|
|
351
|
+
setHistoryIndex(newIndex);
|
|
294
352
|
const historyValue = history[history.length - 1 - newIndex];
|
|
295
353
|
setValue(historyValue);
|
|
296
|
-
|
|
354
|
+
setCursorPositionInternal(historyValue.length);
|
|
297
355
|
onChange?.(historyValue);
|
|
298
356
|
}
|
|
299
357
|
return;
|
|
300
358
|
}
|
|
359
|
+
if (key.downArrow) {
|
|
360
|
+
const { visualLines, lineIndex, column } = getCursorLineInfo(currentValue, pos);
|
|
361
|
+
const targetColumn = preferredColumn() ?? column;
|
|
362
|
+
if (lineIndex < visualLines.length - 1) {
|
|
363
|
+
const targetLine = visualLines[lineIndex + 1];
|
|
364
|
+
const offset = getCursorIndexFromColumn(targetLine.text, targetColumn);
|
|
365
|
+
const newPos = Math.min(currentValue.length, targetLine.start + offset);
|
|
366
|
+
if (preferredColumn() === null) {
|
|
367
|
+
setPreferredColumn(targetColumn);
|
|
368
|
+
}
|
|
369
|
+
setCursorPositionInternal(newPos, { updatePreferredColumn: false });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (historyIndex() >= 0) {
|
|
373
|
+
const newIndex = historyIndex() - 1;
|
|
374
|
+
setHistoryIndex(newIndex);
|
|
375
|
+
if (newIndex < 0) {
|
|
376
|
+
const orig = originalValue();
|
|
377
|
+
setValue(orig);
|
|
378
|
+
setCursorPositionInternal(orig.length);
|
|
379
|
+
onChange?.(orig);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
const historyValue = history[history.length - 1 - newIndex];
|
|
383
|
+
setValue(historyValue);
|
|
384
|
+
setCursorPositionInternal(historyValue.length);
|
|
385
|
+
onChange?.(historyValue);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
// History navigation
|
|
301
391
|
// Deletion
|
|
302
392
|
if (key.backspace) {
|
|
303
393
|
if (pos > 0) {
|
|
@@ -306,14 +396,14 @@ export function createTextInput(options = {}) {
|
|
|
306
396
|
const boundary = findPrevWordBoundary(currentValue, pos);
|
|
307
397
|
const newValue = currentValue.slice(0, boundary) + currentValue.slice(pos);
|
|
308
398
|
setValue(newValue);
|
|
309
|
-
|
|
399
|
+
setCursorPositionInternal(boundary);
|
|
310
400
|
onChange?.(newValue);
|
|
311
401
|
}
|
|
312
402
|
else {
|
|
313
403
|
// Delete single char
|
|
314
404
|
const newValue = currentValue.slice(0, pos - 1) + currentValue.slice(pos);
|
|
315
405
|
setValue(newValue);
|
|
316
|
-
|
|
406
|
+
setCursorPositionInternal(pos - 1);
|
|
317
407
|
onChange?.(newValue);
|
|
318
408
|
}
|
|
319
409
|
}
|
|
@@ -352,7 +442,7 @@ export function createTextInput(options = {}) {
|
|
|
352
442
|
const startPos = lineStart >= 0 ? lineStart + 1 : 0;
|
|
353
443
|
const newValue = currentValue.slice(0, startPos) + currentValue.slice(pos);
|
|
354
444
|
setValue(newValue);
|
|
355
|
-
|
|
445
|
+
setCursorPositionInternal(startPos);
|
|
356
446
|
onChange?.(newValue);
|
|
357
447
|
return;
|
|
358
448
|
}
|
|
@@ -361,14 +451,14 @@ export function createTextInput(options = {}) {
|
|
|
361
451
|
const boundary = findPrevWordBoundary(currentValue, pos);
|
|
362
452
|
const newValue = currentValue.slice(0, boundary) + currentValue.slice(pos);
|
|
363
453
|
setValue(newValue);
|
|
364
|
-
|
|
454
|
+
setCursorPositionInternal(boundary);
|
|
365
455
|
onChange?.(newValue);
|
|
366
456
|
return;
|
|
367
457
|
}
|
|
368
458
|
// Ctrl+X - clear all (like Ctrl+C in some terminals)
|
|
369
459
|
if (key.ctrl && input === 'x') {
|
|
370
460
|
setValue('');
|
|
371
|
-
|
|
461
|
+
setCursorPositionInternal(0);
|
|
372
462
|
onChange?.('');
|
|
373
463
|
return;
|
|
374
464
|
}
|
|
@@ -383,12 +473,9 @@ export function createTextInput(options = {}) {
|
|
|
383
473
|
}
|
|
384
474
|
const newValue = currentValue.slice(0, pos) + input + currentValue.slice(pos);
|
|
385
475
|
setValue(newValue);
|
|
386
|
-
|
|
476
|
+
setCursorPositionInternal(pos + input.length);
|
|
387
477
|
setHistoryIndex(-1);
|
|
388
478
|
onChange?.(newValue);
|
|
389
|
-
// Update scroll on input
|
|
390
|
-
const newPos = pos + input.length;
|
|
391
|
-
scrollToCursor(newValue, newPos);
|
|
392
479
|
}
|
|
393
480
|
};
|
|
394
481
|
// handleInput is exposed to be registered during render phase
|
|
@@ -399,15 +486,18 @@ export function createTextInput(options = {}) {
|
|
|
399
486
|
viewportOffset,
|
|
400
487
|
isMultiline: isMultilineMode,
|
|
401
488
|
handleInput, // Expose handler to be registered during render
|
|
489
|
+
setLayout,
|
|
490
|
+
setCursorPosition: (pos, options) => {
|
|
491
|
+
setCursorPositionInternal(pos, options);
|
|
492
|
+
},
|
|
402
493
|
setValue: (v) => {
|
|
403
494
|
setValue(v);
|
|
404
|
-
|
|
405
|
-
scrollToCursor(v, v.length);
|
|
495
|
+
setCursorPositionInternal(v.length);
|
|
406
496
|
onChange?.(v);
|
|
407
497
|
},
|
|
408
498
|
clear: () => {
|
|
409
499
|
setValue('');
|
|
410
|
-
|
|
500
|
+
setCursorPositionInternal(0);
|
|
411
501
|
setHistoryIndex(-1);
|
|
412
502
|
onChange?.('');
|
|
413
503
|
},
|
|
@@ -422,6 +512,9 @@ export function createTextInput(options = {}) {
|
|
|
422
512
|
export function renderTextInput(state, options = {}) {
|
|
423
513
|
const theme = getTheme();
|
|
424
514
|
const { placeholder = 'Type here...', password = false, maskChar = '*', cursorStyle = 'block', borderStyle = 'round', focusedBorderColor = theme.accents.info, unfocusedBorderColor = theme.borders.default, prompt = getChars().arrows.right, foreground = theme.accents.info, isActive = true, fullWidth = false, } = options;
|
|
515
|
+
const autoGrow = options.autoGrow ?? false;
|
|
516
|
+
const showScrollbar = options.showScrollbar ?? true;
|
|
517
|
+
const resolvedMaxLines = autoGrow ? (options.maxLines ?? 5) : options.maxLines;
|
|
425
518
|
// Register input handler during render phase
|
|
426
519
|
useInput(state.handleInput);
|
|
427
520
|
const value = state.value();
|
|
@@ -431,7 +524,7 @@ export function renderTextInput(state, options = {}) {
|
|
|
431
524
|
// Use passed specific wrapping options or defaults
|
|
432
525
|
const width = options.width ?? 80;
|
|
433
526
|
const wordWrap = options.wordWrap ?? false;
|
|
434
|
-
const maxLines =
|
|
527
|
+
const maxLines = resolvedMaxLines;
|
|
435
528
|
const showCharCount = options.showCharCount ?? false;
|
|
436
529
|
// Build the input display with cursor
|
|
437
530
|
const beforeCursor = displayValue.slice(0, cursor);
|
|
@@ -452,6 +545,13 @@ export function renderTextInput(state, options = {}) {
|
|
|
452
545
|
boxStyle.borderColor = isActive ? focusedBorderColor : unfocusedBorderColor;
|
|
453
546
|
boxStyle.paddingX = 1;
|
|
454
547
|
}
|
|
548
|
+
const baseContentWidth = Math.max(1, (noBorder ? width - 2 : width - 6));
|
|
549
|
+
state.setLayout?.({
|
|
550
|
+
width: baseContentWidth,
|
|
551
|
+
wordWrap,
|
|
552
|
+
maxLines: options.maxLines,
|
|
553
|
+
autoGrow,
|
|
554
|
+
});
|
|
455
555
|
const isMultiline = state.isMultiline() || displayValue.includes('\n') || options.multiline;
|
|
456
556
|
// Use multiline path when any of these conditions are true
|
|
457
557
|
const shouldUseMultiline = isMultiline || wordWrap || showCharCount;
|
|
@@ -461,16 +561,41 @@ export function renderTextInput(state, options = {}) {
|
|
|
461
561
|
// Calculate actual content width for wrapping:
|
|
462
562
|
// - Border: 2 chars (left + right)
|
|
463
563
|
// - paddingX: 2 chars (left + right)
|
|
464
|
-
// - Prompt
|
|
465
|
-
const
|
|
466
|
-
|
|
564
|
+
// - Prompt plus space, or border plus space: 2 chars
|
|
565
|
+
const baseWidth = baseContentWidth;
|
|
566
|
+
let contentWidth = baseWidth;
|
|
567
|
+
let lines = getVisualLines(displayValue, contentWidth, wordWrap);
|
|
568
|
+
const hasOverflow = maxLines !== undefined && lines.length > maxLines;
|
|
569
|
+
const shouldShowScrollbar = showScrollbar && hasOverflow;
|
|
570
|
+
if (shouldShowScrollbar) {
|
|
571
|
+
contentWidth = Math.max(1, baseWidth - 1);
|
|
572
|
+
lines = getVisualLines(displayValue, contentWidth, wordWrap);
|
|
573
|
+
state.setLayout?.({
|
|
574
|
+
width: contentWidth,
|
|
575
|
+
wordWrap,
|
|
576
|
+
maxLines: options.maxLines,
|
|
577
|
+
autoGrow,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
467
580
|
// Viewport handling
|
|
581
|
+
const visibleCount = maxLines ? Math.min(maxLines, lines.length) : lines.length;
|
|
582
|
+
const maxOffset = Math.max(0, lines.length - visibleCount);
|
|
468
583
|
let offset = state.viewportOffset();
|
|
584
|
+
if (offset > maxOffset) {
|
|
585
|
+
offset = maxOffset;
|
|
586
|
+
}
|
|
587
|
+
else if (offset < 0) {
|
|
588
|
+
offset = 0;
|
|
589
|
+
}
|
|
469
590
|
let visibleLines = lines;
|
|
470
591
|
// If maxLines is set, slice the lines
|
|
471
592
|
if (maxLines) {
|
|
472
|
-
visibleLines = lines.slice(offset, offset +
|
|
593
|
+
visibleLines = lines.slice(offset, offset + visibleCount);
|
|
473
594
|
}
|
|
595
|
+
const padForScrollbar = shouldShowScrollbar;
|
|
596
|
+
const borderSize = noBorder ? 0 : 1;
|
|
597
|
+
const contentStartX = noBorder ? 0 : 2;
|
|
598
|
+
const contentStartY = borderSize;
|
|
474
599
|
// Determine visual cursor position relative to viewport
|
|
475
600
|
// We need to match the cursor index to a line and col
|
|
476
601
|
let cursorLine = -1;
|
|
@@ -488,26 +613,66 @@ export function renderTextInput(state, options = {}) {
|
|
|
488
613
|
}
|
|
489
614
|
// Adjust cursorLine to be relative to visible window
|
|
490
615
|
const relativeCursorLine = cursorLine - offset;
|
|
616
|
+
const chars = getChars();
|
|
617
|
+
const renderMode = getRenderMode();
|
|
618
|
+
const trackChar = renderMode === 'ascii' ? '|' : chars.scrollbar.track;
|
|
619
|
+
const thumbChar = renderMode === 'ascii' ? '#' : chars.scrollbar.thumb;
|
|
620
|
+
const scrollbarChars = [];
|
|
621
|
+
if (shouldShowScrollbar && visibleCount > 0) {
|
|
622
|
+
const thumbHeight = Math.max(1, Math.floor((visibleCount / lines.length) * visibleCount));
|
|
623
|
+
const thumbPosition = maxOffset > 0
|
|
624
|
+
? Math.floor((offset / maxOffset) * (visibleCount - thumbHeight))
|
|
625
|
+
: 0;
|
|
626
|
+
for (let i = 0; i < visibleCount; i++) {
|
|
627
|
+
scrollbarChars.push(i >= thumbPosition && i < thumbPosition + thumbHeight ? thumbChar : trackChar);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const renderScrollbar = (index, isThumb) => {
|
|
631
|
+
if (!shouldShowScrollbar)
|
|
632
|
+
return null;
|
|
633
|
+
const color = isThumb
|
|
634
|
+
? (isActive ? focusedBorderColor : unfocusedBorderColor)
|
|
635
|
+
: unfocusedBorderColor;
|
|
636
|
+
return Text({ color }, scrollbarChars[index] ?? ' ');
|
|
637
|
+
};
|
|
638
|
+
const handleMultilineClick = (event) => {
|
|
639
|
+
const lineIndex = event.y - contentStartY;
|
|
640
|
+
if (lineIndex < 0 || lineIndex >= visibleLines.length)
|
|
641
|
+
return;
|
|
642
|
+
const lineObj = visibleLines[lineIndex];
|
|
643
|
+
const linePrompt = lineIndex === 0 && offset === 0 ? prompt : chars.border.vertical;
|
|
644
|
+
const promptWidth = stringWidth(`${linePrompt} `);
|
|
645
|
+
const column = event.x - contentStartX - promptWidth;
|
|
646
|
+
const cursorOffset = getCursorIndexFromColumn(lineObj.text, column);
|
|
647
|
+
const nextCursor = Math.min(value.length, lineObj.start + cursorOffset);
|
|
648
|
+
state.setCursorPosition(nextCursor, { scroll: false });
|
|
649
|
+
};
|
|
491
650
|
const renderedLines = visibleLines.map((lineObj, i) => {
|
|
492
651
|
const lineIndex = offset + i; // Absolute line index
|
|
493
652
|
const isCursorLine = lineIndex === cursorLine;
|
|
494
653
|
const line = lineObj.text;
|
|
495
|
-
const linePrompt = i === 0 && offset === 0 ? prompt :
|
|
654
|
+
const linePrompt = i === 0 && offset === 0 ? prompt : chars.border.vertical;
|
|
655
|
+
const scrollbarChar = shouldShowScrollbar ? (scrollbarChars[i] ?? ' ') : '';
|
|
656
|
+
const isThumb = shouldShowScrollbar ? scrollbarChar === thumbChar : false;
|
|
496
657
|
if (isCursorLine && isActive) {
|
|
497
658
|
// Ensure cursorCol is within bounds of this line
|
|
498
659
|
const safeCol = Math.min(Math.max(0, cursorCol), line.length);
|
|
499
660
|
const before = line.slice(0, safeCol);
|
|
500
661
|
const char = line[safeCol] || ' ';
|
|
501
662
|
const after = line.slice(safeCol + 1);
|
|
502
|
-
|
|
663
|
+
const lineWidth = stringWidth(before + char + after);
|
|
664
|
+
const padCount = padForScrollbar ? Math.max(0, contentWidth - lineWidth) : 0;
|
|
665
|
+
return Box({ flexDirection: 'row' }, Text({ color: foreground }, `${linePrompt} `), Text({}, before), Text({ backgroundColor: cursorBg, color: cursorFg }, char), Text({}, after), padCount > 0 ? Text({}, ' '.repeat(padCount)) : null, renderScrollbar(i, isThumb));
|
|
503
666
|
}
|
|
504
|
-
|
|
667
|
+
const lineWidth = stringWidth(line);
|
|
668
|
+
const padCount = padForScrollbar ? Math.max(0, contentWidth - lineWidth) : 0;
|
|
669
|
+
return Box({ flexDirection: 'row' }, Text({ color: foreground }, `${linePrompt} `), Text({}, line), padCount > 0 ? Text({}, ' '.repeat(padCount)) : null, renderScrollbar(i, isThumb));
|
|
505
670
|
});
|
|
506
671
|
if (showCharCount) {
|
|
507
672
|
const countText = `${value.length}${options.maxLength ? '/' + options.maxLength : ''}`;
|
|
508
|
-
renderedLines.push(Box({ flexDirection: 'row', justifyContent: 'flex-end', paddingTop: 0 }, Text({ color: 'mutedForeground', dim: true }, countText)));
|
|
673
|
+
renderedLines.push(Box({ flexDirection: 'row', justifyContent: 'flex-end', paddingTop: 0 }, Text({ color: 'mutedForeground', dim: true }, countText), shouldShowScrollbar ? Text({}, ' ') : null));
|
|
509
674
|
}
|
|
510
|
-
return Box(boxStyle, ...renderedLines);
|
|
675
|
+
return Box({ ...boxStyle, onClick: handleMultilineClick }, ...renderedLines);
|
|
511
676
|
}
|
|
512
677
|
// Single line - simple row layout
|
|
513
678
|
const rowStyle = {
|
|
@@ -519,7 +684,20 @@ export function renderTextInput(state, options = {}) {
|
|
|
519
684
|
rowStyle.borderColor = isActive ? focusedBorderColor : unfocusedBorderColor;
|
|
520
685
|
rowStyle.paddingX = 1;
|
|
521
686
|
}
|
|
522
|
-
|
|
687
|
+
const handleSingleLineClick = (event) => {
|
|
688
|
+
const borderSize = noBorder ? 0 : 1;
|
|
689
|
+
const contentStartX = noBorder ? 0 : 2;
|
|
690
|
+
const contentStartY = borderSize;
|
|
691
|
+
const lineIndex = event.y - contentStartY;
|
|
692
|
+
if (lineIndex !== 0)
|
|
693
|
+
return;
|
|
694
|
+
const promptWidth = stringWidth(`${prompt} `);
|
|
695
|
+
const column = event.x - contentStartX - promptWidth;
|
|
696
|
+
const cursorOffset = getCursorIndexFromColumn(displayValue, column);
|
|
697
|
+
const nextCursor = Math.min(displayValue.length, cursorOffset);
|
|
698
|
+
state.setCursorPosition(nextCursor, { scroll: false });
|
|
699
|
+
};
|
|
700
|
+
return Box({ ...rowStyle, onClick: handleSingleLineClick }, Text({ color: foreground }, `${prompt} `), showPlaceholder
|
|
523
701
|
? Box({ flexDirection: 'row', flexGrow: fullWidth ? 1 : 0 }, Text({ color: 'mutedForeground', dim: true }, placeholder), isActive ? Text({ backgroundColor: cursorBg, color: cursorFg }, ' ') : Text({}, ''))
|
|
524
702
|
: Box({ flexDirection: 'row', flexGrow: fullWidth ? 1 : 0 }, Text({}, beforeCursor), isActive
|
|
525
703
|
? Text({ backgroundColor: cursorBg, color: cursorFg }, cursorChar)
|