@code-yeongyu/senpi-tui 2026.10.31-1 → 2026.10.31-2
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.
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type Component, type Focusable, type TuiMouseEvent, type TuiMouseEventResult } from "../tui.ts";
|
|
2
2
|
export interface InputOptions {
|
|
3
|
+
/** Render only masks; editing state belongs to this instance and must be disposed at settlement. */
|
|
4
|
+
secret?: boolean;
|
|
5
|
+
onInvalidInput?: () => void;
|
|
3
6
|
prompt?: string;
|
|
4
7
|
placeholder?: string;
|
|
5
8
|
placeholderStyle?: (text: string) => string;
|
|
@@ -11,6 +14,8 @@ export declare class Input implements Component, Focusable {
|
|
|
11
14
|
private value;
|
|
12
15
|
private cursor;
|
|
13
16
|
private readonly prompt;
|
|
17
|
+
private readonly secret;
|
|
18
|
+
private readonly onInvalidInput;
|
|
14
19
|
private readonly placeholder;
|
|
15
20
|
private readonly placeholderStyle;
|
|
16
21
|
private renderedStartColumn;
|
|
@@ -43,6 +48,8 @@ export declare class Input implements Component, Focusable {
|
|
|
43
48
|
private moveWordForwards;
|
|
44
49
|
private handlePaste;
|
|
45
50
|
invalidate(): void;
|
|
51
|
+
/** Drop every private editing reference; this cannot promise erasure of immutable JS strings. */
|
|
52
|
+
dispose(): void;
|
|
46
53
|
render(width: number): string[];
|
|
47
54
|
}
|
|
48
55
|
//# sourceMappingURL=input.d.ts.map
|
package/dist/components/input.js
CHANGED
|
@@ -24,6 +24,8 @@ export class Input {
|
|
|
24
24
|
this.lastAction = null;
|
|
25
25
|
// Undo support
|
|
26
26
|
this.undoStack = new UndoStack();
|
|
27
|
+
this.secret = options.secret === true;
|
|
28
|
+
this.onInvalidInput = options.onInvalidInput;
|
|
27
29
|
this.prompt = options.prompt ?? "> ";
|
|
28
30
|
this.placeholder = options.placeholder ?? "";
|
|
29
31
|
this.placeholderStyle = options.placeholderStyle ?? ((text) => text);
|
|
@@ -32,6 +34,10 @@ export class Input {
|
|
|
32
34
|
return this.value;
|
|
33
35
|
}
|
|
34
36
|
setValue(value) {
|
|
37
|
+
if (this.secret && /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(value)) {
|
|
38
|
+
this.onInvalidInput?.();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
35
41
|
this.value = value;
|
|
36
42
|
this.cursor = Math.min(this.cursor, value.length);
|
|
37
43
|
}
|
|
@@ -175,6 +181,9 @@ export class Input {
|
|
|
175
181
|
if (!hasControlChars) {
|
|
176
182
|
this.insertCharacter(data);
|
|
177
183
|
}
|
|
184
|
+
else if (this.secret && !data.startsWith("\x1b")) {
|
|
185
|
+
this.onInvalidInput?.();
|
|
186
|
+
}
|
|
178
187
|
}
|
|
179
188
|
handleMouse(event) {
|
|
180
189
|
if (event.type !== "press" || event.button !== "left" || event.y !== 0)
|
|
@@ -184,7 +193,7 @@ export class Input {
|
|
|
184
193
|
let currentColumn = 0;
|
|
185
194
|
this.cursor = this.value.length;
|
|
186
195
|
for (const grapheme of segmenter.segment(this.value)) {
|
|
187
|
-
const nextColumn = currentColumn + visibleWidth(grapheme.segment);
|
|
196
|
+
const nextColumn = currentColumn + (this.secret ? 1 : visibleWidth(grapheme.segment));
|
|
188
197
|
if (targetColumn < nextColumn) {
|
|
189
198
|
this.cursor = grapheme.index;
|
|
190
199
|
break;
|
|
@@ -195,6 +204,10 @@ export class Input {
|
|
|
195
204
|
return { handled: true, focus: true };
|
|
196
205
|
}
|
|
197
206
|
insertCharacter(char) {
|
|
207
|
+
if (this.secret && /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(char)) {
|
|
208
|
+
this.onInvalidInput?.();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
198
211
|
// Undo coalescing: consecutive word chars coalesce into one undo unit
|
|
199
212
|
if (isWhitespaceChar(char) || this.lastAction !== "type-word") {
|
|
200
213
|
this.pushUndo();
|
|
@@ -324,6 +337,10 @@ export class Input {
|
|
|
324
337
|
this.cursor = findWordForward(this.value, this.cursor);
|
|
325
338
|
}
|
|
326
339
|
handlePaste(pastedText) {
|
|
340
|
+
if (this.secret) {
|
|
341
|
+
this.insertCharacter(pastedText);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
327
344
|
this.lastAction = null;
|
|
328
345
|
this.pushUndo();
|
|
329
346
|
// Clean the pasted text - remove newlines and carriage returns
|
|
@@ -335,12 +352,37 @@ export class Input {
|
|
|
335
352
|
invalidate() {
|
|
336
353
|
// No cached state to invalidate currently
|
|
337
354
|
}
|
|
355
|
+
/** Drop every private editing reference; this cannot promise erasure of immutable JS strings. */
|
|
356
|
+
dispose() {
|
|
357
|
+
if (!this.secret)
|
|
358
|
+
return;
|
|
359
|
+
this.value = "";
|
|
360
|
+
this.cursor = 0;
|
|
361
|
+
this.pasteBuffer = "";
|
|
362
|
+
this.isInPaste = false;
|
|
363
|
+
this.killRing = new KillRing();
|
|
364
|
+
this.undoStack.clear();
|
|
365
|
+
this.lastAction = null;
|
|
366
|
+
this.onSubmit = undefined;
|
|
367
|
+
this.onEscape = undefined;
|
|
368
|
+
}
|
|
338
369
|
render(width) {
|
|
339
370
|
// Calculate visible window
|
|
340
371
|
const availableWidth = width - visibleWidth(this.prompt);
|
|
341
372
|
if (availableWidth <= 0) {
|
|
342
373
|
return [truncateToWidth(this.prompt, width, "")];
|
|
343
374
|
}
|
|
375
|
+
if (this.secret) {
|
|
376
|
+
const length = [...segmenter.segment(this.value)].length;
|
|
377
|
+
const cursor = [...segmenter.segment(this.value.slice(0, this.cursor))].length;
|
|
378
|
+
const start = Math.max(0, cursor - availableWidth + 1);
|
|
379
|
+
this.renderedStartColumn = start;
|
|
380
|
+
const shown = "*".repeat(Math.min(length - start, availableWidth));
|
|
381
|
+
const offset = cursor - start;
|
|
382
|
+
const marker = this.focused ? CURSOR_MARKER : "";
|
|
383
|
+
const rendered = shown.slice(0, offset) + marker + "\x1b[7m" + (shown[offset] ?? " ") + "\x1b[27m" + shown.slice(offset + 1);
|
|
384
|
+
return [this.prompt + rendered + " ".repeat(Math.max(0, availableWidth - visibleWidth(rendered)))];
|
|
385
|
+
}
|
|
344
386
|
if (this.value.length === 0 && this.placeholder) {
|
|
345
387
|
const placeholder = truncateToWidth(this.placeholder, availableWidth, "");
|
|
346
388
|
const graphemes = [...segmenter.segment(placeholder)];
|
package/package.json
CHANGED