@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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-tui",
3
- "version": "2026.10.31-1",
3
+ "version": "2026.10.31-2",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",