@greppleai/grip-tui 0.0.0-stage → 0.0.1
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/README.md +818 -2
- package/dist/autocomplete.d.ts +48 -0
- package/dist/autocomplete.d.ts.map +1 -0
- package/dist/autocomplete.js +566 -0
- package/dist/autocomplete.js.map +1 -0
- package/dist/components/alt-screen-flash.d.ts +13 -0
- package/dist/components/alt-screen-flash.d.ts.map +1 -0
- package/dist/components/alt-screen-flash.js +37 -0
- package/dist/components/alt-screen-flash.js.map +1 -0
- package/dist/components/box.d.ts +22 -0
- package/dist/components/box.d.ts.map +1 -0
- package/dist/components/box.js +104 -0
- package/dist/components/box.js.map +1 -0
- package/dist/components/cancellable-loader.d.ts +22 -0
- package/dist/components/cancellable-loader.d.ts.map +1 -0
- package/dist/components/cancellable-loader.js +35 -0
- package/dist/components/cancellable-loader.js.map +1 -0
- package/dist/components/editor.d.ts +292 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +1887 -0
- package/dist/components/editor.js.map +1 -0
- package/dist/components/h-stack.d.ts +7 -0
- package/dist/components/h-stack.d.ts.map +1 -0
- package/dist/components/h-stack.js +43 -0
- package/dist/components/h-stack.js.map +1 -0
- package/dist/components/image.d.ts +32 -0
- package/dist/components/image.d.ts.map +1 -0
- package/dist/components/image.js +76 -0
- package/dist/components/image.js.map +1 -0
- package/dist/components/input.d.ts +46 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +352 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/loader.d.ts +32 -0
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/loader.js +80 -0
- package/dist/components/loader.js.map +1 -0
- package/dist/components/markdown.d.ts +132 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +717 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/scroll-view.d.ts +53 -0
- package/dist/components/scroll-view.d.ts.map +1 -0
- package/dist/components/scroll-view.js +173 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/select-list.d.ts +50 -0
- package/dist/components/select-list.d.ts.map +1 -0
- package/dist/components/select-list.js +159 -0
- package/dist/components/select-list.js.map +1 -0
- package/dist/components/settings-list.d.ts +52 -0
- package/dist/components/settings-list.d.ts.map +1 -0
- package/dist/components/settings-list.js +187 -0
- package/dist/components/settings-list.js.map +1 -0
- package/dist/components/spacer.d.ts +12 -0
- package/dist/components/spacer.d.ts.map +1 -0
- package/dist/components/spacer.js +23 -0
- package/dist/components/spacer.js.map +1 -0
- package/dist/components/stack.d.ts +32 -0
- package/dist/components/stack.d.ts.map +1 -0
- package/dist/components/stack.js +122 -0
- package/dist/components/stack.js.map +1 -0
- package/dist/components/text.d.ts +19 -0
- package/dist/components/text.d.ts.map +1 -0
- package/dist/components/text.js +89 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/truncated-text.d.ts +13 -0
- package/dist/components/truncated-text.d.ts.map +1 -0
- package/dist/components/truncated-text.js +51 -0
- package/dist/components/truncated-text.js.map +1 -0
- package/dist/components/v-stack.d.ts +8 -0
- package/dist/components/v-stack.d.ts.map +1 -0
- package/dist/components/v-stack.js +27 -0
- package/dist/components/v-stack.js.map +1 -0
- package/dist/editor-component.d.ts +39 -0
- package/dist/editor-component.d.ts.map +1 -0
- package/dist/editor-component.js +2 -0
- package/dist/editor-component.js.map +1 -0
- package/dist/fuzzy.d.ts +16 -0
- package/dist/fuzzy.d.ts.map +1 -0
- package/dist/fuzzy.js +101 -0
- package/dist/fuzzy.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/keybindings.d.ts +243 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +209 -0
- package/dist/keybindings.js.map +1 -0
- package/dist/keys.d.ts +184 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +1053 -0
- package/dist/keys.js.map +1 -0
- package/dist/kill-ring.d.ts +30 -0
- package/dist/kill-ring.d.ts.map +1 -0
- package/dist/kill-ring.js +37 -0
- package/dist/kill-ring.js.map +1 -0
- package/dist/latex.d.ts +10 -0
- package/dist/latex.d.ts.map +1 -0
- package/dist/latex.js +1406 -0
- package/dist/latex.js.map +1 -0
- package/dist/layout-node.d.ts +43 -0
- package/dist/layout-node.d.ts.map +1 -0
- package/dist/layout-node.js +6 -0
- package/dist/layout-node.js.map +1 -0
- package/dist/layout.d.ts +40 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +386 -0
- package/dist/layout.js.map +1 -0
- package/dist/native-modifiers.d.ts +3 -0
- package/dist/native-modifiers.d.ts.map +1 -0
- package/dist/native-modifiers.js +53 -0
- package/dist/native-modifiers.js.map +1 -0
- package/dist/stdin-buffer.d.ts +58 -0
- package/dist/stdin-buffer.d.ts.map +1 -0
- package/dist/stdin-buffer.js +318 -0
- package/dist/stdin-buffer.js.map +1 -0
- package/dist/terminal-control.d.ts +5 -0
- package/dist/terminal-control.d.ts.map +1 -0
- package/dist/terminal-control.js +26 -0
- package/dist/terminal-control.js.map +1 -0
- package/dist/terminal-image.d.ts +112 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +508 -0
- package/dist/terminal-image.js.map +1 -0
- package/dist/terminal.d.ts +117 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +490 -0
- package/dist/terminal.js.map +1 -0
- package/dist/tui-alt-screen.d.ts +114 -0
- package/dist/tui-alt-screen.d.ts.map +1 -0
- package/dist/tui-alt-screen.js +950 -0
- package/dist/tui-alt-screen.js.map +1 -0
- package/dist/tui-main-screen.d.ts +52 -0
- package/dist/tui-main-screen.d.ts.map +1 -0
- package/dist/tui-main-screen.js +511 -0
- package/dist/tui-main-screen.js.map +1 -0
- package/dist/tui.d.ts +295 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +868 -0
- package/dist/tui.js.map +1 -0
- package/dist/undo-stack.d.ts +17 -0
- package/dist/undo-stack.d.ts.map +1 -0
- package/dist/undo-stack.js +25 -0
- package/dist/undo-stack.js.map +1 -0
- package/dist/utils.d.ts +96 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +1106 -0
- package/dist/utils.js.map +1 -0
- package/dist/word-navigation.d.ts +25 -0
- package/dist/word-navigation.d.ts.map +1 -0
- package/dist/word-navigation.js +94 -0
- package/dist/word-navigation.js.map +1 -0
- package/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
- package/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/package.json +47 -5
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Component, type Focusable } from "../tui.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Input component - single-line text input with horizontal scrolling
|
|
4
|
+
*/
|
|
5
|
+
export declare class Input implements Component, Focusable {
|
|
6
|
+
private value;
|
|
7
|
+
private cursor;
|
|
8
|
+
onSubmit?: (value: string) => void;
|
|
9
|
+
onEscape?: () => void;
|
|
10
|
+
/** Focusable interface - set by TUI when focus changes */
|
|
11
|
+
focused: boolean;
|
|
12
|
+
private pasteBuffer;
|
|
13
|
+
private isInPaste;
|
|
14
|
+
private killRing;
|
|
15
|
+
private lastAction;
|
|
16
|
+
private undoStack;
|
|
17
|
+
getValue(): string;
|
|
18
|
+
setValue(value: string): void;
|
|
19
|
+
private handleBracketedPasteInput;
|
|
20
|
+
private handleControlInput;
|
|
21
|
+
private handleDeletionInput;
|
|
22
|
+
private handleKillRingInput;
|
|
23
|
+
private moveCursorLeft;
|
|
24
|
+
private moveCursorRight;
|
|
25
|
+
private handleCursorMovementInput;
|
|
26
|
+
private handlePrintableInput;
|
|
27
|
+
handleInput(data: string): void;
|
|
28
|
+
private insertCharacter;
|
|
29
|
+
private handleBackspace;
|
|
30
|
+
private handleForwardDelete;
|
|
31
|
+
private deleteToLineStart;
|
|
32
|
+
private deleteToLineEnd;
|
|
33
|
+
private deleteWordBackwards;
|
|
34
|
+
private deleteWordForward;
|
|
35
|
+
private yank;
|
|
36
|
+
private yankPop;
|
|
37
|
+
private pushUndo;
|
|
38
|
+
private undo;
|
|
39
|
+
private moveWordBackwards;
|
|
40
|
+
private moveWordForwards;
|
|
41
|
+
private handlePaste;
|
|
42
|
+
invalidate(): void;
|
|
43
|
+
private getVisibleWindow;
|
|
44
|
+
render(width: number): string[];
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/components/input.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,SAAS,EAAiB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAmB1E;;GAEG;AACH,qBAAa,KAAM,YAAW,SAAS,EAAE,SAAS;IACjD,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAa;IACpB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAE7B,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAS;IAGzB,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,SAAS,CAAkB;IAGnC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,UAAU,CAA4B;IAG9C,OAAO,CAAC,SAAS,CAA+B;IAEhD,QAAQ,IAAI,MAAM,CAEjB;IAED,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAG5B;IAED,OAAO,CAAC,yBAAyB;IAoBjC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,yBAAyB;IAejC,OAAO,CAAC,oBAAoB;IAa5B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAU9B;IAED,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,mBAAmB;IAqB3B,OAAO,CAAC,iBAAiB;IAoBzB,OAAO,CAAC,IAAI;IAWZ,OAAO,CAAC,OAAO;IAkBf,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,IAAI;IAQZ,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,WAAW;IAYnB,UAAU,IAAI,IAAI,CAEjB;IAED,OAAO,CAAC,gBAAgB;IAmBxB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA+B9B;CACD","sourcesContent":["import { getKeybindings, type KeybindingsManager } from \"../keybindings.ts\";\nimport { decodeKittyPrintable } from \"../keys.ts\";\nimport { KillRing } from \"../kill-ring.ts\";\nimport { type Component, CURSOR_MARKER, type Focusable } from \"../tui.ts\";\nimport { UndoStack } from \"../undo-stack.ts\";\nimport { getGraphemeSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth } from \"../utils.ts\";\nimport { findWordBackward, findWordForward } from \"../word-navigation.ts\";\nimport type { PasteInputResult } from \"./editor.ts\";\n\nconst segmenter = getGraphemeSegmenter();\n\ninterface InputState {\n\tvalue: string;\n\tcursor: number;\n}\n\ninterface InputVisibleWindow {\n\ttext: string;\n\tcursor: number;\n}\n\ntype InputAction = \"kill\" | \"yank\" | \"type-word\";\n/**\n * Input component - single-line text input with horizontal scrolling\n */\nexport class Input implements Component, Focusable {\n\tprivate value: string = \"\";\n\tprivate cursor: number = 0; // Cursor position in the value\n\tpublic onSubmit?: (value: string) => void;\n\tpublic onEscape?: () => void;\n\n\t/** Focusable interface - set by TUI when focus changes */\n\tfocused: boolean = false;\n\n\t// Bracketed paste mode buffering\n\tprivate pasteBuffer: string = \"\";\n\tprivate isInPaste: boolean = false;\n\n\t// Kill ring for Emacs-style kill/yank operations\n\tprivate killRing = new KillRing();\n\tprivate lastAction: InputAction | null = null;\n\n\t// Undo support\n\tprivate undoStack = new UndoStack<InputState>();\n\n\tgetValue(): string {\n\t\treturn this.value;\n\t}\n\n\tsetValue(value: string): void {\n\t\tthis.value = value;\n\t\tthis.cursor = Math.min(this.cursor, value.length);\n\t}\n\n\tprivate handleBracketedPasteInput(data: string): PasteInputResult {\n\t\tlet remainingData = data;\n\t\tif (remainingData.includes(\"\\x1b[200~\")) {\n\t\t\tthis.isInPaste = true;\n\t\t\tthis.pasteBuffer = \"\";\n\t\t\tremainingData = remainingData.replace(\"\\x1b[200~\", \"\");\n\t\t}\n\t\tif (!this.isInPaste) return { data: remainingData, consumed: false };\n\t\tthis.pasteBuffer += remainingData;\n\t\tconst endIndex = this.pasteBuffer.indexOf(\"\\x1b[201~\");\n\t\tif (endIndex === -1) return { data: remainingData, consumed: true };\n\t\tconst pasteContent = this.pasteBuffer.substring(0, endIndex);\n\t\tthis.handlePaste(pasteContent);\n\t\tthis.isInPaste = false;\n\t\tconst trailingInput = this.pasteBuffer.substring(endIndex + 6);\n\t\tthis.pasteBuffer = \"\";\n\t\tif (trailingInput) this.handleInput(trailingInput);\n\t\treturn { data: remainingData, consumed: true };\n\t}\n\n\tprivate handleControlInput(data: string, keybindings: KeybindingsManager): boolean {\n\t\tif (keybindings.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.onEscape?.();\n\t\t\treturn true;\n\t\t}\n\t\tif (keybindings.matches(data, \"tui.editor.undo\")) {\n\t\t\tthis.undo();\n\t\t\treturn true;\n\t\t}\n\t\tif (!keybindings.matches(data, \"tui.input.submit\") && data !== \"\\n\") return false;\n\t\tthis.onSubmit?.(this.value);\n\t\treturn true;\n\t}\n\n\tprivate handleDeletionInput(data: string, keybindings: KeybindingsManager): boolean {\n\t\tif (keybindings.matches(data, \"tui.editor.deleteCharBackward\")) this.handleBackspace();\n\t\telse if (keybindings.matches(data, \"tui.editor.deleteCharForward\")) this.handleForwardDelete();\n\t\telse if (keybindings.matches(data, \"tui.editor.deleteWordBackward\")) this.deleteWordBackwards();\n\t\telse if (keybindings.matches(data, \"tui.editor.deleteWordForward\")) this.deleteWordForward();\n\t\telse if (keybindings.matches(data, \"tui.editor.deleteToLineStart\")) this.deleteToLineStart();\n\t\telse if (keybindings.matches(data, \"tui.editor.deleteToLineEnd\")) this.deleteToLineEnd();\n\t\telse return false;\n\t\treturn true;\n\t}\n\n\tprivate handleKillRingInput(data: string, keybindings: KeybindingsManager): boolean {\n\t\tif (keybindings.matches(data, \"tui.editor.yank\")) this.yank();\n\t\telse if (keybindings.matches(data, \"tui.editor.yankPop\")) this.yankPop();\n\t\telse return false;\n\t\treturn true;\n\t}\n\n\tprivate moveCursorLeft(): void {\n\t\tthis.lastAction = null;\n\t\tif (this.cursor === 0) return;\n\t\tconst beforeCursor = this.value.slice(0, this.cursor);\n\t\tconst graphemes = [...segmenter.segment(beforeCursor)];\n\t\tconst lastGrapheme = graphemes[graphemes.length - 1];\n\t\tthis.cursor -= lastGrapheme ? lastGrapheme.segment.length : 1;\n\t}\n\n\tprivate moveCursorRight(): void {\n\t\tthis.lastAction = null;\n\t\tif (this.cursor >= this.value.length) return;\n\t\tconst afterCursor = this.value.slice(this.cursor);\n\t\tconst graphemes = [...segmenter.segment(afterCursor)];\n\t\tconst firstGrapheme = graphemes[0];\n\t\tthis.cursor += firstGrapheme ? firstGrapheme.segment.length : 1;\n\t}\n\n\tprivate handleCursorMovementInput(data: string, keybindings: KeybindingsManager): boolean {\n\t\tif (keybindings.matches(data, \"tui.editor.cursorLeft\")) this.moveCursorLeft();\n\t\telse if (keybindings.matches(data, \"tui.editor.cursorRight\")) this.moveCursorRight();\n\t\telse if (keybindings.matches(data, \"tui.editor.cursorLineStart\")) {\n\t\t\tthis.lastAction = null;\n\t\t\tthis.cursor = 0;\n\t\t} else if (keybindings.matches(data, \"tui.editor.cursorLineEnd\")) {\n\t\t\tthis.lastAction = null;\n\t\t\tthis.cursor = this.value.length;\n\t\t} else if (keybindings.matches(data, \"tui.editor.cursorWordLeft\")) this.moveWordBackwards();\n\t\telse if (keybindings.matches(data, \"tui.editor.cursorWordRight\")) this.moveWordForwards();\n\t\telse return false;\n\t\treturn true;\n\t}\n\n\tprivate handlePrintableInput(data: string): void {\n\t\tconst kittyPrintable = decodeKittyPrintable(data);\n\t\tif (kittyPrintable !== undefined) {\n\t\t\tthis.insertCharacter(kittyPrintable);\n\t\t\treturn;\n\t\t}\n\t\tconst hasControlChars = [...data].some((character) => {\n\t\t\tconst code = character.charCodeAt(0);\n\t\t\treturn code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);\n\t\t});\n\t\tif (!hasControlChars) this.insertCharacter(data);\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst paste = this.handleBracketedPasteInput(data);\n\t\tif (paste.consumed) return;\n\t\tdata = paste.data;\n\t\tconst keybindings = getKeybindings();\n\t\tif (this.handleControlInput(data, keybindings)) return;\n\t\tif (this.handleDeletionInput(data, keybindings)) return;\n\t\tif (this.handleKillRingInput(data, keybindings)) return;\n\t\tif (this.handleCursorMovementInput(data, keybindings)) return;\n\t\tthis.handlePrintableInput(data);\n\t}\n\n\tprivate insertCharacter(char: string): void {\n\t\t// Undo coalescing: consecutive word chars coalesce into one undo unit\n\t\tif (isWhitespaceChar(char) || this.lastAction !== \"type-word\") {\n\t\t\tthis.pushUndo();\n\t\t}\n\t\tthis.lastAction = \"type-word\";\n\n\t\tthis.value = this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor);\n\t\tthis.cursor += char.length;\n\t}\n\n\tprivate handleBackspace(): void {\n\t\tthis.lastAction = null;\n\t\tif (this.cursor > 0) {\n\t\t\tthis.pushUndo();\n\t\t\tconst beforeCursor = this.value.slice(0, this.cursor);\n\t\t\tconst graphemes = [...segmenter.segment(beforeCursor)];\n\t\t\tconst lastGrapheme = graphemes[graphemes.length - 1];\n\t\t\tconst graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;\n\t\t\tthis.value = this.value.slice(0, this.cursor - graphemeLength) + this.value.slice(this.cursor);\n\t\t\tthis.cursor -= graphemeLength;\n\t\t}\n\t}\n\n\tprivate handleForwardDelete(): void {\n\t\tthis.lastAction = null;\n\t\tif (this.cursor < this.value.length) {\n\t\t\tthis.pushUndo();\n\t\t\tconst afterCursor = this.value.slice(this.cursor);\n\t\t\tconst graphemes = [...segmenter.segment(afterCursor)];\n\t\t\tconst firstGrapheme = graphemes[0];\n\t\t\tconst graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;\n\t\t\tthis.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + graphemeLength);\n\t\t}\n\t}\n\n\tprivate deleteToLineStart(): void {\n\t\tif (this.cursor === 0) return;\n\t\tthis.pushUndo();\n\t\tconst deletedText = this.value.slice(0, this.cursor);\n\t\tthis.killRing.push(deletedText, { prepend: true, accumulate: this.lastAction === \"kill\" });\n\t\tthis.lastAction = \"kill\";\n\t\tthis.value = this.value.slice(this.cursor);\n\t\tthis.cursor = 0;\n\t}\n\n\tprivate deleteToLineEnd(): void {\n\t\tif (this.cursor >= this.value.length) return;\n\t\tthis.pushUndo();\n\t\tconst deletedText = this.value.slice(this.cursor);\n\t\tthis.killRing.push(deletedText, { prepend: false, accumulate: this.lastAction === \"kill\" });\n\t\tthis.lastAction = \"kill\";\n\t\tthis.value = this.value.slice(0, this.cursor);\n\t}\n\n\tprivate deleteWordBackwards(): void {\n\t\tif (this.cursor === 0) return;\n\n\t\t// Save lastAction before cursor movement (moveWordBackwards resets it)\n\t\tconst wasKill = this.lastAction === \"kill\";\n\n\t\tthis.pushUndo();\n\n\t\tconst oldCursor = this.cursor;\n\t\tthis.moveWordBackwards();\n\t\tconst deleteFrom = this.cursor;\n\t\tthis.cursor = oldCursor;\n\n\t\tconst deletedText = this.value.slice(deleteFrom, this.cursor);\n\t\tthis.killRing.push(deletedText, { prepend: true, accumulate: wasKill });\n\t\tthis.lastAction = \"kill\";\n\n\t\tthis.value = this.value.slice(0, deleteFrom) + this.value.slice(this.cursor);\n\t\tthis.cursor = deleteFrom;\n\t}\n\n\tprivate deleteWordForward(): void {\n\t\tif (this.cursor >= this.value.length) return;\n\n\t\t// Save lastAction before cursor movement (moveWordForwards resets it)\n\t\tconst wasKill = this.lastAction === \"kill\";\n\n\t\tthis.pushUndo();\n\n\t\tconst oldCursor = this.cursor;\n\t\tthis.moveWordForwards();\n\t\tconst deleteTo = this.cursor;\n\t\tthis.cursor = oldCursor;\n\n\t\tconst deletedText = this.value.slice(this.cursor, deleteTo);\n\t\tthis.killRing.push(deletedText, { prepend: false, accumulate: wasKill });\n\t\tthis.lastAction = \"kill\";\n\n\t\tthis.value = this.value.slice(0, this.cursor) + this.value.slice(deleteTo);\n\t}\n\n\tprivate yank(): void {\n\t\tconst text = this.killRing.peek();\n\t\tif (!text) return;\n\n\t\tthis.pushUndo();\n\n\t\tthis.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);\n\t\tthis.cursor += text.length;\n\t\tthis.lastAction = \"yank\";\n\t}\n\n\tprivate yankPop(): void {\n\t\tif (this.lastAction !== \"yank\" || this.killRing.length <= 1) return;\n\n\t\tthis.pushUndo();\n\n\t\t// Delete the previously yanked text (still at end of ring before rotation)\n\t\tconst prevText = this.killRing.peek() || \"\";\n\t\tthis.value = this.value.slice(0, this.cursor - prevText.length) + this.value.slice(this.cursor);\n\t\tthis.cursor -= prevText.length;\n\n\t\t// Rotate and insert new entry\n\t\tthis.killRing.rotate();\n\t\tconst text = this.killRing.peek() || \"\";\n\t\tthis.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);\n\t\tthis.cursor += text.length;\n\t\tthis.lastAction = \"yank\";\n\t}\n\n\tprivate pushUndo(): void {\n\t\tthis.undoStack.push({ value: this.value, cursor: this.cursor });\n\t}\n\n\tprivate undo(): void {\n\t\tconst snapshot = this.undoStack.pop();\n\t\tif (!snapshot) return;\n\t\tthis.value = snapshot.value;\n\t\tthis.cursor = snapshot.cursor;\n\t\tthis.lastAction = null;\n\t}\n\n\tprivate moveWordBackwards(): void {\n\t\tif (this.cursor === 0) return;\n\t\tthis.lastAction = null;\n\t\tthis.cursor = findWordBackward(this.value, this.cursor);\n\t}\n\n\tprivate moveWordForwards(): void {\n\t\tif (this.cursor >= this.value.length) return;\n\t\tthis.lastAction = null;\n\t\tthis.cursor = findWordForward(this.value, this.cursor);\n\t}\n\n\tprivate handlePaste(pastedText: string): void {\n\t\tthis.lastAction = null;\n\t\tthis.pushUndo();\n\n\t\t// Clean the pasted text - remove newlines and carriage returns\n\t\tconst cleanText = pastedText.replace(/\\r\\n/g, \"\").replace(/\\r/g, \"\").replace(/\\n/g, \"\").replace(/\\t/g, \" \");\n\n\t\t// Insert at cursor position\n\t\tthis.value = this.value.slice(0, this.cursor) + cleanText + this.value.slice(this.cursor);\n\t\tthis.cursor += cleanText.length;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\tprivate getVisibleWindow(availableWidth: number): InputVisibleWindow {\n\t\tconst totalWidth = visibleWidth(this.value);\n\t\tif (totalWidth < availableWidth) return { text: this.value, cursor: this.cursor };\n\n\t\tconst scrollWidth = this.cursor === this.value.length ? availableWidth - 1 : availableWidth;\n\t\tif (scrollWidth <= 0) return { text: \"\", cursor: 0 };\n\n\t\tconst cursorColumn = visibleWidth(this.value.slice(0, this.cursor));\n\t\tconst halfWidth = Math.floor(scrollWidth / 2);\n\t\tlet startColumn: number;\n\t\tif (cursorColumn < halfWidth) startColumn = 0;\n\t\telse if (cursorColumn > totalWidth - halfWidth) startColumn = Math.max(0, totalWidth - scrollWidth);\n\t\telse startColumn = Math.max(0, cursorColumn - halfWidth);\n\n\t\tconst text = sliceByColumn(this.value, startColumn, scrollWidth, true);\n\t\tconst beforeCursor = sliceByColumn(this.value, startColumn, Math.max(0, cursorColumn - startColumn), true);\n\t\treturn { text, cursor: beforeCursor.length };\n\t}\n\n\trender(width: number): string[] {\n\t\tconst prompt = \"> \";\n\t\tconst availableWidth = width - prompt.length;\n\t\tif (availableWidth <= 0) return [prompt];\n\n\t\tconst visibleWindow = this.getVisibleWindow(availableWidth);\n\t\tconst visibleText = visibleWindow.text;\n\t\tconst cursorDisplay = visibleWindow.cursor;\n\n\t\t// Build line with fake cursor\n\t\t// Insert cursor character at cursor position\n\t\tconst graphemes = [...segmenter.segment(visibleText.slice(cursorDisplay))];\n\t\tconst cursorGrapheme = graphemes[0];\n\n\t\tconst beforeCursor = visibleText.slice(0, cursorDisplay);\n\t\tconst atCursor = cursorGrapheme?.segment ?? \" \"; // Character at cursor, or space if at end\n\t\tconst afterCursor = visibleText.slice(cursorDisplay + atCursor.length);\n\n\t\t// Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)\n\t\tconst marker = this.focused ? CURSOR_MARKER : \"\";\n\n\t\t// Use inverse video to show cursor\n\t\tconst cursorChar = `\\x1b[7m${atCursor}\\x1b[27m`; // ESC[7m = reverse video, ESC[27m = normal\n\t\tconst textWithCursor = beforeCursor + marker + cursorChar + afterCursor;\n\n\t\t// Calculate visual width\n\t\tconst visualLength = visibleWidth(textWithCursor);\n\t\tconst padding = \" \".repeat(Math.max(0, availableWidth - visualLength));\n\t\tconst line = prompt + textWithCursor + padding;\n\n\t\treturn [line];\n\t}\n}\n"]}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { getKeybindings } from "../keybindings.js";
|
|
2
|
+
import { decodeKittyPrintable } from "../keys.js";
|
|
3
|
+
import { KillRing } from "../kill-ring.js";
|
|
4
|
+
import { CURSOR_MARKER } from "../tui.js";
|
|
5
|
+
import { UndoStack } from "../undo-stack.js";
|
|
6
|
+
import { getGraphemeSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth } from "../utils.js";
|
|
7
|
+
import { findWordBackward, findWordForward } from "../word-navigation.js";
|
|
8
|
+
const segmenter = getGraphemeSegmenter();
|
|
9
|
+
/**
|
|
10
|
+
* Input component - single-line text input with horizontal scrolling
|
|
11
|
+
*/
|
|
12
|
+
export class Input {
|
|
13
|
+
value = "";
|
|
14
|
+
cursor = 0; // Cursor position in the value
|
|
15
|
+
onSubmit;
|
|
16
|
+
onEscape;
|
|
17
|
+
/** Focusable interface - set by TUI when focus changes */
|
|
18
|
+
focused = false;
|
|
19
|
+
// Bracketed paste mode buffering
|
|
20
|
+
pasteBuffer = "";
|
|
21
|
+
isInPaste = false;
|
|
22
|
+
// Kill ring for Emacs-style kill/yank operations
|
|
23
|
+
killRing = new KillRing();
|
|
24
|
+
lastAction = null;
|
|
25
|
+
// Undo support
|
|
26
|
+
undoStack = new UndoStack();
|
|
27
|
+
getValue() {
|
|
28
|
+
return this.value;
|
|
29
|
+
}
|
|
30
|
+
setValue(value) {
|
|
31
|
+
this.value = value;
|
|
32
|
+
this.cursor = Math.min(this.cursor, value.length);
|
|
33
|
+
}
|
|
34
|
+
handleBracketedPasteInput(data) {
|
|
35
|
+
let remainingData = data;
|
|
36
|
+
if (remainingData.includes("\x1b[200~")) {
|
|
37
|
+
this.isInPaste = true;
|
|
38
|
+
this.pasteBuffer = "";
|
|
39
|
+
remainingData = remainingData.replace("\x1b[200~", "");
|
|
40
|
+
}
|
|
41
|
+
if (!this.isInPaste)
|
|
42
|
+
return { data: remainingData, consumed: false };
|
|
43
|
+
this.pasteBuffer += remainingData;
|
|
44
|
+
const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
|
|
45
|
+
if (endIndex === -1)
|
|
46
|
+
return { data: remainingData, consumed: true };
|
|
47
|
+
const pasteContent = this.pasteBuffer.substring(0, endIndex);
|
|
48
|
+
this.handlePaste(pasteContent);
|
|
49
|
+
this.isInPaste = false;
|
|
50
|
+
const trailingInput = this.pasteBuffer.substring(endIndex + 6);
|
|
51
|
+
this.pasteBuffer = "";
|
|
52
|
+
if (trailingInput)
|
|
53
|
+
this.handleInput(trailingInput);
|
|
54
|
+
return { data: remainingData, consumed: true };
|
|
55
|
+
}
|
|
56
|
+
handleControlInput(data, keybindings) {
|
|
57
|
+
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
58
|
+
this.onEscape?.();
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (keybindings.matches(data, "tui.editor.undo")) {
|
|
62
|
+
this.undo();
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (!keybindings.matches(data, "tui.input.submit") && data !== "\n")
|
|
66
|
+
return false;
|
|
67
|
+
this.onSubmit?.(this.value);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
handleDeletionInput(data, keybindings) {
|
|
71
|
+
if (keybindings.matches(data, "tui.editor.deleteCharBackward"))
|
|
72
|
+
this.handleBackspace();
|
|
73
|
+
else if (keybindings.matches(data, "tui.editor.deleteCharForward"))
|
|
74
|
+
this.handleForwardDelete();
|
|
75
|
+
else if (keybindings.matches(data, "tui.editor.deleteWordBackward"))
|
|
76
|
+
this.deleteWordBackwards();
|
|
77
|
+
else if (keybindings.matches(data, "tui.editor.deleteWordForward"))
|
|
78
|
+
this.deleteWordForward();
|
|
79
|
+
else if (keybindings.matches(data, "tui.editor.deleteToLineStart"))
|
|
80
|
+
this.deleteToLineStart();
|
|
81
|
+
else if (keybindings.matches(data, "tui.editor.deleteToLineEnd"))
|
|
82
|
+
this.deleteToLineEnd();
|
|
83
|
+
else
|
|
84
|
+
return false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
handleKillRingInput(data, keybindings) {
|
|
88
|
+
if (keybindings.matches(data, "tui.editor.yank"))
|
|
89
|
+
this.yank();
|
|
90
|
+
else if (keybindings.matches(data, "tui.editor.yankPop"))
|
|
91
|
+
this.yankPop();
|
|
92
|
+
else
|
|
93
|
+
return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
moveCursorLeft() {
|
|
97
|
+
this.lastAction = null;
|
|
98
|
+
if (this.cursor === 0)
|
|
99
|
+
return;
|
|
100
|
+
const beforeCursor = this.value.slice(0, this.cursor);
|
|
101
|
+
const graphemes = [...segmenter.segment(beforeCursor)];
|
|
102
|
+
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
103
|
+
this.cursor -= lastGrapheme ? lastGrapheme.segment.length : 1;
|
|
104
|
+
}
|
|
105
|
+
moveCursorRight() {
|
|
106
|
+
this.lastAction = null;
|
|
107
|
+
if (this.cursor >= this.value.length)
|
|
108
|
+
return;
|
|
109
|
+
const afterCursor = this.value.slice(this.cursor);
|
|
110
|
+
const graphemes = [...segmenter.segment(afterCursor)];
|
|
111
|
+
const firstGrapheme = graphemes[0];
|
|
112
|
+
this.cursor += firstGrapheme ? firstGrapheme.segment.length : 1;
|
|
113
|
+
}
|
|
114
|
+
handleCursorMovementInput(data, keybindings) {
|
|
115
|
+
if (keybindings.matches(data, "tui.editor.cursorLeft"))
|
|
116
|
+
this.moveCursorLeft();
|
|
117
|
+
else if (keybindings.matches(data, "tui.editor.cursorRight"))
|
|
118
|
+
this.moveCursorRight();
|
|
119
|
+
else if (keybindings.matches(data, "tui.editor.cursorLineStart")) {
|
|
120
|
+
this.lastAction = null;
|
|
121
|
+
this.cursor = 0;
|
|
122
|
+
}
|
|
123
|
+
else if (keybindings.matches(data, "tui.editor.cursorLineEnd")) {
|
|
124
|
+
this.lastAction = null;
|
|
125
|
+
this.cursor = this.value.length;
|
|
126
|
+
}
|
|
127
|
+
else if (keybindings.matches(data, "tui.editor.cursorWordLeft"))
|
|
128
|
+
this.moveWordBackwards();
|
|
129
|
+
else if (keybindings.matches(data, "tui.editor.cursorWordRight"))
|
|
130
|
+
this.moveWordForwards();
|
|
131
|
+
else
|
|
132
|
+
return false;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
handlePrintableInput(data) {
|
|
136
|
+
const kittyPrintable = decodeKittyPrintable(data);
|
|
137
|
+
if (kittyPrintable !== undefined) {
|
|
138
|
+
this.insertCharacter(kittyPrintable);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const hasControlChars = [...data].some((character) => {
|
|
142
|
+
const code = character.charCodeAt(0);
|
|
143
|
+
return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
|
|
144
|
+
});
|
|
145
|
+
if (!hasControlChars)
|
|
146
|
+
this.insertCharacter(data);
|
|
147
|
+
}
|
|
148
|
+
handleInput(data) {
|
|
149
|
+
const paste = this.handleBracketedPasteInput(data);
|
|
150
|
+
if (paste.consumed)
|
|
151
|
+
return;
|
|
152
|
+
data = paste.data;
|
|
153
|
+
const keybindings = getKeybindings();
|
|
154
|
+
if (this.handleControlInput(data, keybindings))
|
|
155
|
+
return;
|
|
156
|
+
if (this.handleDeletionInput(data, keybindings))
|
|
157
|
+
return;
|
|
158
|
+
if (this.handleKillRingInput(data, keybindings))
|
|
159
|
+
return;
|
|
160
|
+
if (this.handleCursorMovementInput(data, keybindings))
|
|
161
|
+
return;
|
|
162
|
+
this.handlePrintableInput(data);
|
|
163
|
+
}
|
|
164
|
+
insertCharacter(char) {
|
|
165
|
+
// Undo coalescing: consecutive word chars coalesce into one undo unit
|
|
166
|
+
if (isWhitespaceChar(char) || this.lastAction !== "type-word") {
|
|
167
|
+
this.pushUndo();
|
|
168
|
+
}
|
|
169
|
+
this.lastAction = "type-word";
|
|
170
|
+
this.value = this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor);
|
|
171
|
+
this.cursor += char.length;
|
|
172
|
+
}
|
|
173
|
+
handleBackspace() {
|
|
174
|
+
this.lastAction = null;
|
|
175
|
+
if (this.cursor > 0) {
|
|
176
|
+
this.pushUndo();
|
|
177
|
+
const beforeCursor = this.value.slice(0, this.cursor);
|
|
178
|
+
const graphemes = [...segmenter.segment(beforeCursor)];
|
|
179
|
+
const lastGrapheme = graphemes[graphemes.length - 1];
|
|
180
|
+
const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
|
|
181
|
+
this.value = this.value.slice(0, this.cursor - graphemeLength) + this.value.slice(this.cursor);
|
|
182
|
+
this.cursor -= graphemeLength;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
handleForwardDelete() {
|
|
186
|
+
this.lastAction = null;
|
|
187
|
+
if (this.cursor < this.value.length) {
|
|
188
|
+
this.pushUndo();
|
|
189
|
+
const afterCursor = this.value.slice(this.cursor);
|
|
190
|
+
const graphemes = [...segmenter.segment(afterCursor)];
|
|
191
|
+
const firstGrapheme = graphemes[0];
|
|
192
|
+
const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
|
|
193
|
+
this.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + graphemeLength);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
deleteToLineStart() {
|
|
197
|
+
if (this.cursor === 0)
|
|
198
|
+
return;
|
|
199
|
+
this.pushUndo();
|
|
200
|
+
const deletedText = this.value.slice(0, this.cursor);
|
|
201
|
+
this.killRing.push(deletedText, { prepend: true, accumulate: this.lastAction === "kill" });
|
|
202
|
+
this.lastAction = "kill";
|
|
203
|
+
this.value = this.value.slice(this.cursor);
|
|
204
|
+
this.cursor = 0;
|
|
205
|
+
}
|
|
206
|
+
deleteToLineEnd() {
|
|
207
|
+
if (this.cursor >= this.value.length)
|
|
208
|
+
return;
|
|
209
|
+
this.pushUndo();
|
|
210
|
+
const deletedText = this.value.slice(this.cursor);
|
|
211
|
+
this.killRing.push(deletedText, { prepend: false, accumulate: this.lastAction === "kill" });
|
|
212
|
+
this.lastAction = "kill";
|
|
213
|
+
this.value = this.value.slice(0, this.cursor);
|
|
214
|
+
}
|
|
215
|
+
deleteWordBackwards() {
|
|
216
|
+
if (this.cursor === 0)
|
|
217
|
+
return;
|
|
218
|
+
// Save lastAction before cursor movement (moveWordBackwards resets it)
|
|
219
|
+
const wasKill = this.lastAction === "kill";
|
|
220
|
+
this.pushUndo();
|
|
221
|
+
const oldCursor = this.cursor;
|
|
222
|
+
this.moveWordBackwards();
|
|
223
|
+
const deleteFrom = this.cursor;
|
|
224
|
+
this.cursor = oldCursor;
|
|
225
|
+
const deletedText = this.value.slice(deleteFrom, this.cursor);
|
|
226
|
+
this.killRing.push(deletedText, { prepend: true, accumulate: wasKill });
|
|
227
|
+
this.lastAction = "kill";
|
|
228
|
+
this.value = this.value.slice(0, deleteFrom) + this.value.slice(this.cursor);
|
|
229
|
+
this.cursor = deleteFrom;
|
|
230
|
+
}
|
|
231
|
+
deleteWordForward() {
|
|
232
|
+
if (this.cursor >= this.value.length)
|
|
233
|
+
return;
|
|
234
|
+
// Save lastAction before cursor movement (moveWordForwards resets it)
|
|
235
|
+
const wasKill = this.lastAction === "kill";
|
|
236
|
+
this.pushUndo();
|
|
237
|
+
const oldCursor = this.cursor;
|
|
238
|
+
this.moveWordForwards();
|
|
239
|
+
const deleteTo = this.cursor;
|
|
240
|
+
this.cursor = oldCursor;
|
|
241
|
+
const deletedText = this.value.slice(this.cursor, deleteTo);
|
|
242
|
+
this.killRing.push(deletedText, { prepend: false, accumulate: wasKill });
|
|
243
|
+
this.lastAction = "kill";
|
|
244
|
+
this.value = this.value.slice(0, this.cursor) + this.value.slice(deleteTo);
|
|
245
|
+
}
|
|
246
|
+
yank() {
|
|
247
|
+
const text = this.killRing.peek();
|
|
248
|
+
if (!text)
|
|
249
|
+
return;
|
|
250
|
+
this.pushUndo();
|
|
251
|
+
this.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);
|
|
252
|
+
this.cursor += text.length;
|
|
253
|
+
this.lastAction = "yank";
|
|
254
|
+
}
|
|
255
|
+
yankPop() {
|
|
256
|
+
if (this.lastAction !== "yank" || this.killRing.length <= 1)
|
|
257
|
+
return;
|
|
258
|
+
this.pushUndo();
|
|
259
|
+
// Delete the previously yanked text (still at end of ring before rotation)
|
|
260
|
+
const prevText = this.killRing.peek() || "";
|
|
261
|
+
this.value = this.value.slice(0, this.cursor - prevText.length) + this.value.slice(this.cursor);
|
|
262
|
+
this.cursor -= prevText.length;
|
|
263
|
+
// Rotate and insert new entry
|
|
264
|
+
this.killRing.rotate();
|
|
265
|
+
const text = this.killRing.peek() || "";
|
|
266
|
+
this.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);
|
|
267
|
+
this.cursor += text.length;
|
|
268
|
+
this.lastAction = "yank";
|
|
269
|
+
}
|
|
270
|
+
pushUndo() {
|
|
271
|
+
this.undoStack.push({ value: this.value, cursor: this.cursor });
|
|
272
|
+
}
|
|
273
|
+
undo() {
|
|
274
|
+
const snapshot = this.undoStack.pop();
|
|
275
|
+
if (!snapshot)
|
|
276
|
+
return;
|
|
277
|
+
this.value = snapshot.value;
|
|
278
|
+
this.cursor = snapshot.cursor;
|
|
279
|
+
this.lastAction = null;
|
|
280
|
+
}
|
|
281
|
+
moveWordBackwards() {
|
|
282
|
+
if (this.cursor === 0)
|
|
283
|
+
return;
|
|
284
|
+
this.lastAction = null;
|
|
285
|
+
this.cursor = findWordBackward(this.value, this.cursor);
|
|
286
|
+
}
|
|
287
|
+
moveWordForwards() {
|
|
288
|
+
if (this.cursor >= this.value.length)
|
|
289
|
+
return;
|
|
290
|
+
this.lastAction = null;
|
|
291
|
+
this.cursor = findWordForward(this.value, this.cursor);
|
|
292
|
+
}
|
|
293
|
+
handlePaste(pastedText) {
|
|
294
|
+
this.lastAction = null;
|
|
295
|
+
this.pushUndo();
|
|
296
|
+
// Clean the pasted text - remove newlines and carriage returns
|
|
297
|
+
const cleanText = pastedText.replace(/\r\n/g, "").replace(/\r/g, "").replace(/\n/g, "").replace(/\t/g, " ");
|
|
298
|
+
// Insert at cursor position
|
|
299
|
+
this.value = this.value.slice(0, this.cursor) + cleanText + this.value.slice(this.cursor);
|
|
300
|
+
this.cursor += cleanText.length;
|
|
301
|
+
}
|
|
302
|
+
invalidate() {
|
|
303
|
+
// No cached state to invalidate currently
|
|
304
|
+
}
|
|
305
|
+
getVisibleWindow(availableWidth) {
|
|
306
|
+
const totalWidth = visibleWidth(this.value);
|
|
307
|
+
if (totalWidth < availableWidth)
|
|
308
|
+
return { text: this.value, cursor: this.cursor };
|
|
309
|
+
const scrollWidth = this.cursor === this.value.length ? availableWidth - 1 : availableWidth;
|
|
310
|
+
if (scrollWidth <= 0)
|
|
311
|
+
return { text: "", cursor: 0 };
|
|
312
|
+
const cursorColumn = visibleWidth(this.value.slice(0, this.cursor));
|
|
313
|
+
const halfWidth = Math.floor(scrollWidth / 2);
|
|
314
|
+
let startColumn;
|
|
315
|
+
if (cursorColumn < halfWidth)
|
|
316
|
+
startColumn = 0;
|
|
317
|
+
else if (cursorColumn > totalWidth - halfWidth)
|
|
318
|
+
startColumn = Math.max(0, totalWidth - scrollWidth);
|
|
319
|
+
else
|
|
320
|
+
startColumn = Math.max(0, cursorColumn - halfWidth);
|
|
321
|
+
const text = sliceByColumn(this.value, startColumn, scrollWidth, true);
|
|
322
|
+
const beforeCursor = sliceByColumn(this.value, startColumn, Math.max(0, cursorColumn - startColumn), true);
|
|
323
|
+
return { text, cursor: beforeCursor.length };
|
|
324
|
+
}
|
|
325
|
+
render(width) {
|
|
326
|
+
const prompt = "> ";
|
|
327
|
+
const availableWidth = width - prompt.length;
|
|
328
|
+
if (availableWidth <= 0)
|
|
329
|
+
return [prompt];
|
|
330
|
+
const visibleWindow = this.getVisibleWindow(availableWidth);
|
|
331
|
+
const visibleText = visibleWindow.text;
|
|
332
|
+
const cursorDisplay = visibleWindow.cursor;
|
|
333
|
+
// Build line with fake cursor
|
|
334
|
+
// Insert cursor character at cursor position
|
|
335
|
+
const graphemes = [...segmenter.segment(visibleText.slice(cursorDisplay))];
|
|
336
|
+
const cursorGrapheme = graphemes[0];
|
|
337
|
+
const beforeCursor = visibleText.slice(0, cursorDisplay);
|
|
338
|
+
const atCursor = cursorGrapheme?.segment ?? " "; // Character at cursor, or space if at end
|
|
339
|
+
const afterCursor = visibleText.slice(cursorDisplay + atCursor.length);
|
|
340
|
+
// Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)
|
|
341
|
+
const marker = this.focused ? CURSOR_MARKER : "";
|
|
342
|
+
// Use inverse video to show cursor
|
|
343
|
+
const cursorChar = `\x1b[7m${atCursor}\x1b[27m`; // ESC[7m = reverse video, ESC[27m = normal
|
|
344
|
+
const textWithCursor = beforeCursor + marker + cursorChar + afterCursor;
|
|
345
|
+
// Calculate visual width
|
|
346
|
+
const visualLength = visibleWidth(textWithCursor);
|
|
347
|
+
const padding = " ".repeat(Math.max(0, availableWidth - visualLength));
|
|
348
|
+
const line = prompt + textWithCursor + padding;
|
|
349
|
+
return [line];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.js","sourceRoot":"","sources":["../../src/components/input.ts"],"names":[],"mappings":"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","sourcesContent":["import { getKeybindings, type KeybindingsManager } from \"../keybindings.ts\";\nimport { decodeKittyPrintable } from \"../keys.ts\";\nimport { KillRing } from \"../kill-ring.ts\";\nimport { type Component, CURSOR_MARKER, type Focusable } from \"../tui.ts\";\nimport { UndoStack } from \"../undo-stack.ts\";\nimport { getGraphemeSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth } from \"../utils.ts\";\nimport { findWordBackward, findWordForward } from \"../word-navigation.ts\";\nimport type { PasteInputResult } from \"./editor.ts\";\n\nconst segmenter = getGraphemeSegmenter();\n\ninterface InputState {\n\tvalue: string;\n\tcursor: number;\n}\n\ninterface InputVisibleWindow {\n\ttext: string;\n\tcursor: number;\n}\n\ntype InputAction = \"kill\" | \"yank\" | \"type-word\";\n/**\n * Input component - single-line text input with horizontal scrolling\n */\nexport class Input implements Component, Focusable {\n\tprivate value: string = \"\";\n\tprivate cursor: number = 0; // Cursor position in the value\n\tpublic onSubmit?: (value: string) => void;\n\tpublic onEscape?: () => void;\n\n\t/** Focusable interface - set by TUI when focus changes */\n\tfocused: boolean = false;\n\n\t// Bracketed paste mode buffering\n\tprivate pasteBuffer: string = \"\";\n\tprivate isInPaste: boolean = false;\n\n\t// Kill ring for Emacs-style kill/yank operations\n\tprivate killRing = new KillRing();\n\tprivate lastAction: InputAction | null = null;\n\n\t// Undo support\n\tprivate undoStack = new UndoStack<InputState>();\n\n\tgetValue(): string {\n\t\treturn this.value;\n\t}\n\n\tsetValue(value: string): void {\n\t\tthis.value = value;\n\t\tthis.cursor = Math.min(this.cursor, value.length);\n\t}\n\n\tprivate handleBracketedPasteInput(data: string): PasteInputResult {\n\t\tlet remainingData = data;\n\t\tif (remainingData.includes(\"\\x1b[200~\")) {\n\t\t\tthis.isInPaste = true;\n\t\t\tthis.pasteBuffer = \"\";\n\t\t\tremainingData = remainingData.replace(\"\\x1b[200~\", \"\");\n\t\t}\n\t\tif (!this.isInPaste) return { data: remainingData, consumed: false };\n\t\tthis.pasteBuffer += remainingData;\n\t\tconst endIndex = this.pasteBuffer.indexOf(\"\\x1b[201~\");\n\t\tif (endIndex === -1) return { data: remainingData, consumed: true };\n\t\tconst pasteContent = this.pasteBuffer.substring(0, endIndex);\n\t\tthis.handlePaste(pasteContent);\n\t\tthis.isInPaste = false;\n\t\tconst trailingInput = this.pasteBuffer.substring(endIndex + 6);\n\t\tthis.pasteBuffer = \"\";\n\t\tif (trailingInput) this.handleInput(trailingInput);\n\t\treturn { data: remainingData, consumed: true };\n\t}\n\n\tprivate handleControlInput(data: string, keybindings: KeybindingsManager): boolean {\n\t\tif (keybindings.matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.onEscape?.();\n\t\t\treturn true;\n\t\t}\n\t\tif (keybindings.matches(data, \"tui.editor.undo\")) {\n\t\t\tthis.undo();\n\t\t\treturn true;\n\t\t}\n\t\tif (!keybindings.matches(data, \"tui.input.submit\") && data !== \"\\n\") return false;\n\t\tthis.onSubmit?.(this.value);\n\t\treturn true;\n\t}\n\n\tprivate handleDeletionInput(data: string, keybindings: KeybindingsManager): boolean {\n\t\tif (keybindings.matches(data, \"tui.editor.deleteCharBackward\")) this.handleBackspace();\n\t\telse if (keybindings.matches(data, \"tui.editor.deleteCharForward\")) this.handleForwardDelete();\n\t\telse if (keybindings.matches(data, \"tui.editor.deleteWordBackward\")) this.deleteWordBackwards();\n\t\telse if (keybindings.matches(data, \"tui.editor.deleteWordForward\")) this.deleteWordForward();\n\t\telse if (keybindings.matches(data, \"tui.editor.deleteToLineStart\")) this.deleteToLineStart();\n\t\telse if (keybindings.matches(data, \"tui.editor.deleteToLineEnd\")) this.deleteToLineEnd();\n\t\telse return false;\n\t\treturn true;\n\t}\n\n\tprivate handleKillRingInput(data: string, keybindings: KeybindingsManager): boolean {\n\t\tif (keybindings.matches(data, \"tui.editor.yank\")) this.yank();\n\t\telse if (keybindings.matches(data, \"tui.editor.yankPop\")) this.yankPop();\n\t\telse return false;\n\t\treturn true;\n\t}\n\n\tprivate moveCursorLeft(): void {\n\t\tthis.lastAction = null;\n\t\tif (this.cursor === 0) return;\n\t\tconst beforeCursor = this.value.slice(0, this.cursor);\n\t\tconst graphemes = [...segmenter.segment(beforeCursor)];\n\t\tconst lastGrapheme = graphemes[graphemes.length - 1];\n\t\tthis.cursor -= lastGrapheme ? lastGrapheme.segment.length : 1;\n\t}\n\n\tprivate moveCursorRight(): void {\n\t\tthis.lastAction = null;\n\t\tif (this.cursor >= this.value.length) return;\n\t\tconst afterCursor = this.value.slice(this.cursor);\n\t\tconst graphemes = [...segmenter.segment(afterCursor)];\n\t\tconst firstGrapheme = graphemes[0];\n\t\tthis.cursor += firstGrapheme ? firstGrapheme.segment.length : 1;\n\t}\n\n\tprivate handleCursorMovementInput(data: string, keybindings: KeybindingsManager): boolean {\n\t\tif (keybindings.matches(data, \"tui.editor.cursorLeft\")) this.moveCursorLeft();\n\t\telse if (keybindings.matches(data, \"tui.editor.cursorRight\")) this.moveCursorRight();\n\t\telse if (keybindings.matches(data, \"tui.editor.cursorLineStart\")) {\n\t\t\tthis.lastAction = null;\n\t\t\tthis.cursor = 0;\n\t\t} else if (keybindings.matches(data, \"tui.editor.cursorLineEnd\")) {\n\t\t\tthis.lastAction = null;\n\t\t\tthis.cursor = this.value.length;\n\t\t} else if (keybindings.matches(data, \"tui.editor.cursorWordLeft\")) this.moveWordBackwards();\n\t\telse if (keybindings.matches(data, \"tui.editor.cursorWordRight\")) this.moveWordForwards();\n\t\telse return false;\n\t\treturn true;\n\t}\n\n\tprivate handlePrintableInput(data: string): void {\n\t\tconst kittyPrintable = decodeKittyPrintable(data);\n\t\tif (kittyPrintable !== undefined) {\n\t\t\tthis.insertCharacter(kittyPrintable);\n\t\t\treturn;\n\t\t}\n\t\tconst hasControlChars = [...data].some((character) => {\n\t\t\tconst code = character.charCodeAt(0);\n\t\t\treturn code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);\n\t\t});\n\t\tif (!hasControlChars) this.insertCharacter(data);\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst paste = this.handleBracketedPasteInput(data);\n\t\tif (paste.consumed) return;\n\t\tdata = paste.data;\n\t\tconst keybindings = getKeybindings();\n\t\tif (this.handleControlInput(data, keybindings)) return;\n\t\tif (this.handleDeletionInput(data, keybindings)) return;\n\t\tif (this.handleKillRingInput(data, keybindings)) return;\n\t\tif (this.handleCursorMovementInput(data, keybindings)) return;\n\t\tthis.handlePrintableInput(data);\n\t}\n\n\tprivate insertCharacter(char: string): void {\n\t\t// Undo coalescing: consecutive word chars coalesce into one undo unit\n\t\tif (isWhitespaceChar(char) || this.lastAction !== \"type-word\") {\n\t\t\tthis.pushUndo();\n\t\t}\n\t\tthis.lastAction = \"type-word\";\n\n\t\tthis.value = this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor);\n\t\tthis.cursor += char.length;\n\t}\n\n\tprivate handleBackspace(): void {\n\t\tthis.lastAction = null;\n\t\tif (this.cursor > 0) {\n\t\t\tthis.pushUndo();\n\t\t\tconst beforeCursor = this.value.slice(0, this.cursor);\n\t\t\tconst graphemes = [...segmenter.segment(beforeCursor)];\n\t\t\tconst lastGrapheme = graphemes[graphemes.length - 1];\n\t\t\tconst graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;\n\t\t\tthis.value = this.value.slice(0, this.cursor - graphemeLength) + this.value.slice(this.cursor);\n\t\t\tthis.cursor -= graphemeLength;\n\t\t}\n\t}\n\n\tprivate handleForwardDelete(): void {\n\t\tthis.lastAction = null;\n\t\tif (this.cursor < this.value.length) {\n\t\t\tthis.pushUndo();\n\t\t\tconst afterCursor = this.value.slice(this.cursor);\n\t\t\tconst graphemes = [...segmenter.segment(afterCursor)];\n\t\t\tconst firstGrapheme = graphemes[0];\n\t\t\tconst graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;\n\t\t\tthis.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + graphemeLength);\n\t\t}\n\t}\n\n\tprivate deleteToLineStart(): void {\n\t\tif (this.cursor === 0) return;\n\t\tthis.pushUndo();\n\t\tconst deletedText = this.value.slice(0, this.cursor);\n\t\tthis.killRing.push(deletedText, { prepend: true, accumulate: this.lastAction === \"kill\" });\n\t\tthis.lastAction = \"kill\";\n\t\tthis.value = this.value.slice(this.cursor);\n\t\tthis.cursor = 0;\n\t}\n\n\tprivate deleteToLineEnd(): void {\n\t\tif (this.cursor >= this.value.length) return;\n\t\tthis.pushUndo();\n\t\tconst deletedText = this.value.slice(this.cursor);\n\t\tthis.killRing.push(deletedText, { prepend: false, accumulate: this.lastAction === \"kill\" });\n\t\tthis.lastAction = \"kill\";\n\t\tthis.value = this.value.slice(0, this.cursor);\n\t}\n\n\tprivate deleteWordBackwards(): void {\n\t\tif (this.cursor === 0) return;\n\n\t\t// Save lastAction before cursor movement (moveWordBackwards resets it)\n\t\tconst wasKill = this.lastAction === \"kill\";\n\n\t\tthis.pushUndo();\n\n\t\tconst oldCursor = this.cursor;\n\t\tthis.moveWordBackwards();\n\t\tconst deleteFrom = this.cursor;\n\t\tthis.cursor = oldCursor;\n\n\t\tconst deletedText = this.value.slice(deleteFrom, this.cursor);\n\t\tthis.killRing.push(deletedText, { prepend: true, accumulate: wasKill });\n\t\tthis.lastAction = \"kill\";\n\n\t\tthis.value = this.value.slice(0, deleteFrom) + this.value.slice(this.cursor);\n\t\tthis.cursor = deleteFrom;\n\t}\n\n\tprivate deleteWordForward(): void {\n\t\tif (this.cursor >= this.value.length) return;\n\n\t\t// Save lastAction before cursor movement (moveWordForwards resets it)\n\t\tconst wasKill = this.lastAction === \"kill\";\n\n\t\tthis.pushUndo();\n\n\t\tconst oldCursor = this.cursor;\n\t\tthis.moveWordForwards();\n\t\tconst deleteTo = this.cursor;\n\t\tthis.cursor = oldCursor;\n\n\t\tconst deletedText = this.value.slice(this.cursor, deleteTo);\n\t\tthis.killRing.push(deletedText, { prepend: false, accumulate: wasKill });\n\t\tthis.lastAction = \"kill\";\n\n\t\tthis.value = this.value.slice(0, this.cursor) + this.value.slice(deleteTo);\n\t}\n\n\tprivate yank(): void {\n\t\tconst text = this.killRing.peek();\n\t\tif (!text) return;\n\n\t\tthis.pushUndo();\n\n\t\tthis.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);\n\t\tthis.cursor += text.length;\n\t\tthis.lastAction = \"yank\";\n\t}\n\n\tprivate yankPop(): void {\n\t\tif (this.lastAction !== \"yank\" || this.killRing.length <= 1) return;\n\n\t\tthis.pushUndo();\n\n\t\t// Delete the previously yanked text (still at end of ring before rotation)\n\t\tconst prevText = this.killRing.peek() || \"\";\n\t\tthis.value = this.value.slice(0, this.cursor - prevText.length) + this.value.slice(this.cursor);\n\t\tthis.cursor -= prevText.length;\n\n\t\t// Rotate and insert new entry\n\t\tthis.killRing.rotate();\n\t\tconst text = this.killRing.peek() || \"\";\n\t\tthis.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);\n\t\tthis.cursor += text.length;\n\t\tthis.lastAction = \"yank\";\n\t}\n\n\tprivate pushUndo(): void {\n\t\tthis.undoStack.push({ value: this.value, cursor: this.cursor });\n\t}\n\n\tprivate undo(): void {\n\t\tconst snapshot = this.undoStack.pop();\n\t\tif (!snapshot) return;\n\t\tthis.value = snapshot.value;\n\t\tthis.cursor = snapshot.cursor;\n\t\tthis.lastAction = null;\n\t}\n\n\tprivate moveWordBackwards(): void {\n\t\tif (this.cursor === 0) return;\n\t\tthis.lastAction = null;\n\t\tthis.cursor = findWordBackward(this.value, this.cursor);\n\t}\n\n\tprivate moveWordForwards(): void {\n\t\tif (this.cursor >= this.value.length) return;\n\t\tthis.lastAction = null;\n\t\tthis.cursor = findWordForward(this.value, this.cursor);\n\t}\n\n\tprivate handlePaste(pastedText: string): void {\n\t\tthis.lastAction = null;\n\t\tthis.pushUndo();\n\n\t\t// Clean the pasted text - remove newlines and carriage returns\n\t\tconst cleanText = pastedText.replace(/\\r\\n/g, \"\").replace(/\\r/g, \"\").replace(/\\n/g, \"\").replace(/\\t/g, \" \");\n\n\t\t// Insert at cursor position\n\t\tthis.value = this.value.slice(0, this.cursor) + cleanText + this.value.slice(this.cursor);\n\t\tthis.cursor += cleanText.length;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached state to invalidate currently\n\t}\n\n\tprivate getVisibleWindow(availableWidth: number): InputVisibleWindow {\n\t\tconst totalWidth = visibleWidth(this.value);\n\t\tif (totalWidth < availableWidth) return { text: this.value, cursor: this.cursor };\n\n\t\tconst scrollWidth = this.cursor === this.value.length ? availableWidth - 1 : availableWidth;\n\t\tif (scrollWidth <= 0) return { text: \"\", cursor: 0 };\n\n\t\tconst cursorColumn = visibleWidth(this.value.slice(0, this.cursor));\n\t\tconst halfWidth = Math.floor(scrollWidth / 2);\n\t\tlet startColumn: number;\n\t\tif (cursorColumn < halfWidth) startColumn = 0;\n\t\telse if (cursorColumn > totalWidth - halfWidth) startColumn = Math.max(0, totalWidth - scrollWidth);\n\t\telse startColumn = Math.max(0, cursorColumn - halfWidth);\n\n\t\tconst text = sliceByColumn(this.value, startColumn, scrollWidth, true);\n\t\tconst beforeCursor = sliceByColumn(this.value, startColumn, Math.max(0, cursorColumn - startColumn), true);\n\t\treturn { text, cursor: beforeCursor.length };\n\t}\n\n\trender(width: number): string[] {\n\t\tconst prompt = \"> \";\n\t\tconst availableWidth = width - prompt.length;\n\t\tif (availableWidth <= 0) return [prompt];\n\n\t\tconst visibleWindow = this.getVisibleWindow(availableWidth);\n\t\tconst visibleText = visibleWindow.text;\n\t\tconst cursorDisplay = visibleWindow.cursor;\n\n\t\t// Build line with fake cursor\n\t\t// Insert cursor character at cursor position\n\t\tconst graphemes = [...segmenter.segment(visibleText.slice(cursorDisplay))];\n\t\tconst cursorGrapheme = graphemes[0];\n\n\t\tconst beforeCursor = visibleText.slice(0, cursorDisplay);\n\t\tconst atCursor = cursorGrapheme?.segment ?? \" \"; // Character at cursor, or space if at end\n\t\tconst afterCursor = visibleText.slice(cursorDisplay + atCursor.length);\n\n\t\t// Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)\n\t\tconst marker = this.focused ? CURSOR_MARKER : \"\";\n\n\t\t// Use inverse video to show cursor\n\t\tconst cursorChar = `\\x1b[7m${atCursor}\\x1b[27m`; // ESC[7m = reverse video, ESC[27m = normal\n\t\tconst textWithCursor = beforeCursor + marker + cursorChar + afterCursor;\n\n\t\t// Calculate visual width\n\t\tconst visualLength = visibleWidth(textWithCursor);\n\t\tconst padding = \" \".repeat(Math.max(0, availableWidth - visualLength));\n\t\tconst line = prompt + textWithCursor + padding;\n\n\t\treturn [line];\n\t}\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TUI } from "../tui.ts";
|
|
2
|
+
import { Text } from "./text.ts";
|
|
3
|
+
export interface LoaderIndicatorOptions {
|
|
4
|
+
/** Animation frames. Use an empty array to hide the indicator. */
|
|
5
|
+
frames?: string[];
|
|
6
|
+
/** Frame interval in milliseconds for animated indicators. */
|
|
7
|
+
intervalMs?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Loader component that updates with an optional spinning animation.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Loader extends Text {
|
|
13
|
+
private frames;
|
|
14
|
+
private intervalMs;
|
|
15
|
+
private currentFrame;
|
|
16
|
+
private intervalId;
|
|
17
|
+
private started;
|
|
18
|
+
private ui;
|
|
19
|
+
private renderIndicatorVerbatim;
|
|
20
|
+
private spinnerColorFn;
|
|
21
|
+
private messageColorFn;
|
|
22
|
+
private message;
|
|
23
|
+
constructor(ui: TUI, spinnerColorFn: (str: string) => string, messageColorFn: (str: string) => string, message?: string, indicator?: LoaderIndicatorOptions);
|
|
24
|
+
render(width: number): string[];
|
|
25
|
+
start(): void;
|
|
26
|
+
stop(): void;
|
|
27
|
+
setMessage(message: string): void;
|
|
28
|
+
setIndicator(indicator?: LoaderIndicatorOptions): void;
|
|
29
|
+
private restartAnimation;
|
|
30
|
+
private updateDisplay;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/components/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,WAAW,sBAAsB;IACtC,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD;;GAEG;AACH,qBAAa,MAAO,SAAQ,IAAI;IAC/B,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,OAAO,CAAwB;IAEvC,YACC,EAAE,EAAE,GAAG,EACP,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EACvC,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EACvC,OAAO,GAAE,MAAqB,EAC9B,SAAS,CAAC,EAAE,sBAAsB,EAQlC;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAG9B;IAED,KAAK,IAAI,IAAI,CAKZ;IAED,IAAI,IAAI,IAAI,CAMX;IAED,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhC;IAED,YAAY,CAAC,SAAS,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAOrD;IAED,OAAO,CAAC,gBAAgB;IAYxB,OAAO,CAAC,aAAa;CASrB","sourcesContent":["import type { TUI } from \"../tui.ts\";\nimport { Text } from \"./text.ts\";\n\nexport interface LoaderIndicatorOptions {\n\t/** Animation frames. Use an empty array to hide the indicator. */\n\tframes?: string[];\n\t/** Frame interval in milliseconds for animated indicators. */\n\tintervalMs?: number;\n}\n\nconst DEFAULT_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst DEFAULT_INTERVAL_MS = 80;\n\n/**\n * Loader component that updates with an optional spinning animation.\n */\nexport class Loader extends Text {\n\tprivate frames = [...DEFAULT_FRAMES];\n\tprivate intervalMs = DEFAULT_INTERVAL_MS;\n\tprivate currentFrame = 0;\n\tprivate intervalId: NodeJS.Timeout | null = null;\n\tprivate started = false;\n\tprivate ui: TUI | null = null;\n\tprivate renderIndicatorVerbatim = false;\n\tprivate spinnerColorFn: (str: string) => string;\n\tprivate messageColorFn: (str: string) => string;\n\tprivate message: string = \"Loading...\";\n\n\tconstructor(\n\t\tui: TUI,\n\t\tspinnerColorFn: (str: string) => string,\n\t\tmessageColorFn: (str: string) => string,\n\t\tmessage: string = \"Loading...\",\n\t\tindicator?: LoaderIndicatorOptions,\n\t) {\n\t\tsuper(\"\", 1, 0);\n\t\tthis.ui = ui;\n\t\tthis.spinnerColorFn = spinnerColorFn;\n\t\tthis.messageColorFn = messageColorFn;\n\t\tthis.message = message;\n\t\tthis.setIndicator(indicator);\n\t}\n\n\trender(width: number): string[] {\n\t\tif (!this.started) this.start();\n\t\treturn [\"\", ...super.render(width)];\n\t}\n\n\tstart(): void {\n\t\tif (this.started) return;\n\t\tthis.started = true;\n\t\tthis.updateDisplay();\n\t\tthis.restartAnimation();\n\t}\n\n\tstop(): void {\n\t\tthis.started = false;\n\t\tif (this.intervalId) {\n\t\t\tclearInterval(this.intervalId);\n\t\t\tthis.intervalId = null;\n\t\t}\n\t}\n\n\tsetMessage(message: string): void {\n\t\tthis.message = message;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetIndicator(indicator?: LoaderIndicatorOptions): void {\n\t\tthis.renderIndicatorVerbatim = indicator !== undefined;\n\t\tthis.frames = indicator?.frames !== undefined ? [...indicator.frames] : [...DEFAULT_FRAMES];\n\t\tthis.intervalMs = indicator?.intervalMs && indicator.intervalMs > 0 ? indicator.intervalMs : DEFAULT_INTERVAL_MS;\n\t\tthis.currentFrame = 0;\n\t\tthis.updateDisplay();\n\t\tif (this.started) this.restartAnimation();\n\t}\n\n\tprivate restartAnimation(): void {\n\t\tif (this.intervalId) clearInterval(this.intervalId);\n\t\tthis.intervalId = null;\n\t\tif (this.frames.length <= 1) {\n\t\t\treturn;\n\t\t}\n\t\tthis.intervalId = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.frames.length;\n\t\t\tthis.updateDisplay();\n\t\t}, this.intervalMs);\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tconst frame = this.frames[this.currentFrame] ?? \"\";\n\t\tconst renderedFrame = this.renderIndicatorVerbatim ? frame : this.spinnerColorFn(frame);\n\t\tconst indicator = frame.length > 0 ? `${renderedFrame} ` : \"\";\n\t\tthis.setText(`${indicator}${this.messageColorFn(this.message)}`);\n\t\tif (this.ui) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Text } from "./text.js";
|
|
2
|
+
const DEFAULT_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3
|
+
const DEFAULT_INTERVAL_MS = 80;
|
|
4
|
+
/**
|
|
5
|
+
* Loader component that updates with an optional spinning animation.
|
|
6
|
+
*/
|
|
7
|
+
export class Loader extends Text {
|
|
8
|
+
frames = [...DEFAULT_FRAMES];
|
|
9
|
+
intervalMs = DEFAULT_INTERVAL_MS;
|
|
10
|
+
currentFrame = 0;
|
|
11
|
+
intervalId = null;
|
|
12
|
+
started = false;
|
|
13
|
+
ui = null;
|
|
14
|
+
renderIndicatorVerbatim = false;
|
|
15
|
+
spinnerColorFn;
|
|
16
|
+
messageColorFn;
|
|
17
|
+
message = "Loading...";
|
|
18
|
+
constructor(ui, spinnerColorFn, messageColorFn, message = "Loading...", indicator) {
|
|
19
|
+
super("", 1, 0);
|
|
20
|
+
this.ui = ui;
|
|
21
|
+
this.spinnerColorFn = spinnerColorFn;
|
|
22
|
+
this.messageColorFn = messageColorFn;
|
|
23
|
+
this.message = message;
|
|
24
|
+
this.setIndicator(indicator);
|
|
25
|
+
}
|
|
26
|
+
render(width) {
|
|
27
|
+
if (!this.started)
|
|
28
|
+
this.start();
|
|
29
|
+
return ["", ...super.render(width)];
|
|
30
|
+
}
|
|
31
|
+
start() {
|
|
32
|
+
if (this.started)
|
|
33
|
+
return;
|
|
34
|
+
this.started = true;
|
|
35
|
+
this.updateDisplay();
|
|
36
|
+
this.restartAnimation();
|
|
37
|
+
}
|
|
38
|
+
stop() {
|
|
39
|
+
this.started = false;
|
|
40
|
+
if (this.intervalId) {
|
|
41
|
+
clearInterval(this.intervalId);
|
|
42
|
+
this.intervalId = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
setMessage(message) {
|
|
46
|
+
this.message = message;
|
|
47
|
+
this.updateDisplay();
|
|
48
|
+
}
|
|
49
|
+
setIndicator(indicator) {
|
|
50
|
+
this.renderIndicatorVerbatim = indicator !== undefined;
|
|
51
|
+
this.frames = indicator?.frames !== undefined ? [...indicator.frames] : [...DEFAULT_FRAMES];
|
|
52
|
+
this.intervalMs = indicator?.intervalMs && indicator.intervalMs > 0 ? indicator.intervalMs : DEFAULT_INTERVAL_MS;
|
|
53
|
+
this.currentFrame = 0;
|
|
54
|
+
this.updateDisplay();
|
|
55
|
+
if (this.started)
|
|
56
|
+
this.restartAnimation();
|
|
57
|
+
}
|
|
58
|
+
restartAnimation() {
|
|
59
|
+
if (this.intervalId)
|
|
60
|
+
clearInterval(this.intervalId);
|
|
61
|
+
this.intervalId = null;
|
|
62
|
+
if (this.frames.length <= 1) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.intervalId = setInterval(() => {
|
|
66
|
+
this.currentFrame = (this.currentFrame + 1) % this.frames.length;
|
|
67
|
+
this.updateDisplay();
|
|
68
|
+
}, this.intervalMs);
|
|
69
|
+
}
|
|
70
|
+
updateDisplay() {
|
|
71
|
+
const frame = this.frames[this.currentFrame] ?? "";
|
|
72
|
+
const renderedFrame = this.renderIndicatorVerbatim ? frame : this.spinnerColorFn(frame);
|
|
73
|
+
const indicator = frame.length > 0 ? `${renderedFrame} ` : "";
|
|
74
|
+
this.setText(`${indicator}${this.messageColorFn(this.message)}`);
|
|
75
|
+
if (this.ui) {
|
|
76
|
+
this.ui.requestRender();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/components/loader.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TUI } from \"../tui.ts\";\nimport { Text } from \"./text.ts\";\n\nexport interface LoaderIndicatorOptions {\n\t/** Animation frames. Use an empty array to hide the indicator. */\n\tframes?: string[];\n\t/** Frame interval in milliseconds for animated indicators. */\n\tintervalMs?: number;\n}\n\nconst DEFAULT_FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst DEFAULT_INTERVAL_MS = 80;\n\n/**\n * Loader component that updates with an optional spinning animation.\n */\nexport class Loader extends Text {\n\tprivate frames = [...DEFAULT_FRAMES];\n\tprivate intervalMs = DEFAULT_INTERVAL_MS;\n\tprivate currentFrame = 0;\n\tprivate intervalId: NodeJS.Timeout | null = null;\n\tprivate started = false;\n\tprivate ui: TUI | null = null;\n\tprivate renderIndicatorVerbatim = false;\n\tprivate spinnerColorFn: (str: string) => string;\n\tprivate messageColorFn: (str: string) => string;\n\tprivate message: string = \"Loading...\";\n\n\tconstructor(\n\t\tui: TUI,\n\t\tspinnerColorFn: (str: string) => string,\n\t\tmessageColorFn: (str: string) => string,\n\t\tmessage: string = \"Loading...\",\n\t\tindicator?: LoaderIndicatorOptions,\n\t) {\n\t\tsuper(\"\", 1, 0);\n\t\tthis.ui = ui;\n\t\tthis.spinnerColorFn = spinnerColorFn;\n\t\tthis.messageColorFn = messageColorFn;\n\t\tthis.message = message;\n\t\tthis.setIndicator(indicator);\n\t}\n\n\trender(width: number): string[] {\n\t\tif (!this.started) this.start();\n\t\treturn [\"\", ...super.render(width)];\n\t}\n\n\tstart(): void {\n\t\tif (this.started) return;\n\t\tthis.started = true;\n\t\tthis.updateDisplay();\n\t\tthis.restartAnimation();\n\t}\n\n\tstop(): void {\n\t\tthis.started = false;\n\t\tif (this.intervalId) {\n\t\t\tclearInterval(this.intervalId);\n\t\t\tthis.intervalId = null;\n\t\t}\n\t}\n\n\tsetMessage(message: string): void {\n\t\tthis.message = message;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetIndicator(indicator?: LoaderIndicatorOptions): void {\n\t\tthis.renderIndicatorVerbatim = indicator !== undefined;\n\t\tthis.frames = indicator?.frames !== undefined ? [...indicator.frames] : [...DEFAULT_FRAMES];\n\t\tthis.intervalMs = indicator?.intervalMs && indicator.intervalMs > 0 ? indicator.intervalMs : DEFAULT_INTERVAL_MS;\n\t\tthis.currentFrame = 0;\n\t\tthis.updateDisplay();\n\t\tif (this.started) this.restartAnimation();\n\t}\n\n\tprivate restartAnimation(): void {\n\t\tif (this.intervalId) clearInterval(this.intervalId);\n\t\tthis.intervalId = null;\n\t\tif (this.frames.length <= 1) {\n\t\t\treturn;\n\t\t}\n\t\tthis.intervalId = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.frames.length;\n\t\t\tthis.updateDisplay();\n\t\t}, this.intervalMs);\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tconst frame = this.frames[this.currentFrame] ?? \"\";\n\t\tconst renderedFrame = this.renderIndicatorVerbatim ? frame : this.spinnerColorFn(frame);\n\t\tconst indicator = frame.length > 0 ? `${renderedFrame} ` : \"\";\n\t\tthis.setText(`${indicator}${this.messageColorFn(this.message)}`);\n\t\tif (this.ui) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n}\n"]}
|