@m4ike1/ion-tui 0.1.0
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 +114 -0
- package/dist/alt-screen-search.d.ts +46 -0
- package/dist/alt-screen-search.d.ts.map +1 -0
- package/dist/alt-screen-search.js +282 -0
- package/dist/alt-screen-search.js.map +1 -0
- package/dist/autocomplete.d.ts +75 -0
- package/dist/autocomplete.d.ts.map +1 -0
- package/dist/autocomplete.js +749 -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 +24 -0
- package/dist/components/box.d.ts.map +1 -0
- package/dist/components/box.js +133 -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 +266 -0
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/editor.js +2087 -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 +28 -0
- package/dist/components/image.d.ts.map +1 -0
- package/dist/components/image.js +90 -0
- package/dist/components/image.js.map +1 -0
- package/dist/components/input.d.ts +48 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +417 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/loader.d.ts +33 -0
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/loader.js +76 -0
- package/dist/components/loader.js.map +1 -0
- package/dist/components/markdown.d.ts +103 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +858 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/mouse-region.d.ts +12 -0
- package/dist/components/mouse-region.d.ts.map +1 -0
- package/dist/components/mouse-region.js +21 -0
- package/dist/components/mouse-region.js.map +1 -0
- package/dist/components/scroll-view.d.ts +68 -0
- package/dist/components/scroll-view.d.ts.map +1 -0
- package/dist/components/scroll-view.js +199 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/select-list.d.ts +53 -0
- package/dist/components/select-list.d.ts.map +1 -0
- package/dist/components/select-list.js +206 -0
- package/dist/components/select-list.js.map +1 -0
- package/dist/components/settings-list.d.ts +60 -0
- package/dist/components/settings-list.d.ts.map +1 -0
- package/dist/components/settings-list.js +252 -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 +111 -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 +90 -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 +26 -0
- package/dist/components/v-stack.js.map +1 -0
- package/dist/components/virtual-list.d.ts +51 -0
- package/dist/components/virtual-list.d.ts.map +1 -0
- package/dist/components/virtual-list.js +170 -0
- package/dist/components/virtual-list.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 +110 -0
- package/dist/fuzzy.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/keybindings.d.ts +366 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +331 -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 +1174 -0
- package/dist/keys.js.map +1 -0
- package/dist/kill-ring.d.ts +28 -0
- package/dist/kill-ring.d.ts.map +1 -0
- package/dist/kill-ring.js +44 -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 +1285 -0
- package/dist/latex.js.map +1 -0
- package/dist/layout-node.d.ts +59 -0
- package/dist/layout-node.d.ts.map +1 -0
- package/dist/layout-node.js +12 -0
- package/dist/layout-node.js.map +1 -0
- package/dist/layout.d.ts +42 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +404 -0
- package/dist/layout.js.map +1 -0
- package/dist/native-modifiers.d.ts +4 -0
- package/dist/native-modifiers.d.ts.map +1 -0
- package/dist/native-modifiers.js +13 -0
- package/dist/native-modifiers.js.map +1 -0
- package/dist/native-module-path.d.ts +7 -0
- package/dist/native-module-path.d.ts.map +1 -0
- package/dist/native-module-path.js +19 -0
- package/dist/native-module-path.js.map +1 -0
- package/dist/native-platform.d.ts +18 -0
- package/dist/native-platform.d.ts.map +1 -0
- package/dist/native-platform.js +42 -0
- package/dist/native-platform.js.map +1 -0
- package/dist/pty-host.d.ts +224 -0
- package/dist/pty-host.d.ts.map +1 -0
- package/dist/pty-host.js +347 -0
- package/dist/pty-host.js.map +1 -0
- package/dist/stdin-buffer.d.ts +56 -0
- package/dist/stdin-buffer.d.ts.map +1 -0
- package/dist/stdin-buffer.js +366 -0
- package/dist/stdin-buffer.js.map +1 -0
- package/dist/terminal-colors.d.ts +10 -0
- package/dist/terminal-colors.d.ts.map +1 -0
- package/dist/terminal-colors.js +59 -0
- package/dist/terminal-colors.js.map +1 -0
- package/dist/terminal-image.d.ts +118 -0
- package/dist/terminal-image.d.ts.map +1 -0
- package/dist/terminal-image.js +550 -0
- package/dist/terminal-image.js.map +1 -0
- package/dist/terminal.d.ts +123 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +442 -0
- package/dist/terminal.js.map +1 -0
- package/dist/tui-alt-screen.d.ts +165 -0
- package/dist/tui-alt-screen.d.ts.map +1 -0
- package/dist/tui-alt-screen.js +1515 -0
- package/dist/tui-alt-screen.js.map +1 -0
- package/dist/tui-main-screen.d.ts +39 -0
- package/dist/tui-main-screen.d.ts.map +1 -0
- package/dist/tui-main-screen.js +594 -0
- package/dist/tui-main-screen.js.map +1 -0
- package/dist/tui.d.ts +431 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +1142 -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 +98 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +1200 -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 +96 -0
- package/dist/word-navigation.js.map +1 -0
- package/native/clipboard.h +170 -0
- package/native/darwin/README.md +22 -0
- package/native/darwin/build.sh +69 -0
- package/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/native/darwin/src/darwin-platform.m +93 -0
- package/native/linux/README.md +23 -0
- package/native/linux/build.sh +65 -0
- package/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/native/linux/src/clipboard-worker.h +70 -0
- package/native/linux/src/linux-platform-x11.c +274 -0
- package/native/napi.h +89 -0
- package/native/win32/README.md +31 -0
- package/native/win32/build.mjs +232 -0
- package/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/native/win32/src/win32-platform.c +241 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @m4ike1/ion-tui
|
|
2
|
+
|
|
3
|
+
Terminal UI library with differential rendering for interactive CLI applications. Renders a tree of components to the terminal, updating only changed lines for flicker-free output.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Part of the ion monorepo. From the repo root:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Import from the package entrypoint:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { TuiMainScreen, Text, type TUI } from "@m4ike1/ion-tui";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Node >= 22.19.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { type TUI, Text, Editor, ProcessTerminal, TuiMainScreen, matchesKey } from "@m4ike1/ion-tui";
|
|
25
|
+
|
|
26
|
+
const terminal = new ProcessTerminal();
|
|
27
|
+
const tui: TUI = new TuiMainScreen(terminal);
|
|
28
|
+
|
|
29
|
+
tui.addChild(new Text("Welcome to my app!"));
|
|
30
|
+
|
|
31
|
+
const editor = new Editor(tui, { borderColor: (s) => s, selectList: { /* ... */ } });
|
|
32
|
+
editor.onSubmit = (text) => {
|
|
33
|
+
tui.addChild(new Text(`You said: ${text}`));
|
|
34
|
+
};
|
|
35
|
+
tui.addChild(editor);
|
|
36
|
+
tui.setFocus(editor);
|
|
37
|
+
|
|
38
|
+
// In raw mode Ctrl+C does not send SIGINT; handle exit explicitly.
|
|
39
|
+
tui.addInputListener((data) => {
|
|
40
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
41
|
+
tui.stop();
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
tui.start();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See `test/chat-simple.ts` for a complete example (run with `npx tsx test/chat-simple.ts`).
|
|
51
|
+
|
|
52
|
+
## Choosing a renderer
|
|
53
|
+
|
|
54
|
+
`TUI` is the shared interface for component management, focus, overlays, input, lifecycle, and rendering. Pick a concrete renderer at construction:
|
|
55
|
+
|
|
56
|
+
- `TuiMainScreen(terminal, showHardwareCursor?, logDirectory?)` renders into the main buffer and preserves terminal scrollback.
|
|
57
|
+
- `TuiAltScreen(terminal, showHardwareCursor?, logDirectory?, options?: TuiAltScreenOptions)` renders a fixed-height viewport in the alternate buffer with application-owned scrolling. On `stop()` it restores the main buffer and prints the final document.
|
|
58
|
+
|
|
59
|
+
Use `isViewportTUI(tui)` to narrow to `ViewportTUI` before calling `setLayoutRoot()`. `VStack`, `HStack`, and `ScrollView` layout semantics apply to the alternate-screen viewport; on `TuiMainScreen` the terminal owns scrollback. See `docs/explanation.md`.
|
|
60
|
+
|
|
61
|
+
## Components
|
|
62
|
+
|
|
63
|
+
| Component | Purpose |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `Container` | Groups children (`addChild`, `removeChild`, `clear`) |
|
|
66
|
+
| `Box` | Container with padding and background function |
|
|
67
|
+
| `Text` / `TruncatedText` | Wrapped multi-line text / single truncated line |
|
|
68
|
+
| `Input` | Single-line input with history, undo, kill ring |
|
|
69
|
+
| `Editor` | Multi-line editor with autocomplete and paste handling |
|
|
70
|
+
| `Markdown` | Markdown rendering with theme and LaTeX option |
|
|
71
|
+
| `Loader` / `CancellableLoader` | Animated spinner / abortable spinner (`signal`, `onAbort`) |
|
|
72
|
+
| `SelectList` | Keyboard-navigable option list with filtering |
|
|
73
|
+
| `SettingsList` | Settings rows with value cycling and submenus |
|
|
74
|
+
| `ScrollView` | Scrollable region for alt-screen layouts |
|
|
75
|
+
| `VStack` / `HStack` | Constrained flex layout for alt-screen layouts |
|
|
76
|
+
| `VirtualList` | Windowed per-message cache for large transcripts |
|
|
77
|
+
| `Image` | Inline image (Kitty / iTerm2 protocols, text fallback) |
|
|
78
|
+
| `MouseRegion` | Adds mouse handling without changing rendering |
|
|
79
|
+
| `Spacer` | Vertical spacing |
|
|
80
|
+
|
|
81
|
+
Full constructor and option details: `docs/reference.md`.
|
|
82
|
+
|
|
83
|
+
## Input
|
|
84
|
+
|
|
85
|
+
Detect keys with `matchesKey()` and the `Key` helper (Kitty keyboard protocol aware):
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { matchesKey, Key } from "@m4ike1/ion-tui";
|
|
89
|
+
|
|
90
|
+
if (matchesKey(data, Key.ctrl("c"))) process.exit(0);
|
|
91
|
+
else if (matchesKey(data, Key.enter)) submit();
|
|
92
|
+
else if (matchesKey(data, Key.escape)) cancel();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
String identifiers (`"enter"`, `"ctrl+c"`, `"shift+tab"`) also work. Default bindings for editor and alt-screen actions live in `TUI_KEYBINDINGS` and are customizable via `KeybindingsManager` and `setKeybindings()`. `TuiAltScreen` normalizes SGR mouse input into `TuiMouseEvent`; `TuiMainScreen` does not capture mouse input.
|
|
96
|
+
|
|
97
|
+
## Custom components
|
|
98
|
+
|
|
99
|
+
Every line returned by `render(width)` must fit within `width`; the TUI errors otherwise. Use `truncateToWidth()`, `wrapTextWithAnsi()`, and `visibleWidth()` from the package. Cache rendered output and clear it in `invalidate()`. Components showing a text cursor implement `Focusable` and emit `CURSOR_MARKER` at the cursor position; containers with an embedded `Input`/`Editor` must propagate `focused` to the child for correct IME candidate-window placement. Full contract: `docs/reference.md`; recipes: `docs/how-to.md`.
|
|
100
|
+
|
|
101
|
+
## Testing
|
|
102
|
+
|
|
103
|
+
There is no `VirtualTerminal` export in the public API. Tests in this package use the in-repo helper `test/virtual-terminal.ts` (backed by `@xterm/headless`) with `TuiMainScreen` / `TuiAltScreen`. Package tests run with `node --test` (see `package.json`); run the suite via `./test.sh` from the repo root.
|
|
104
|
+
|
|
105
|
+
## Debug
|
|
106
|
+
|
|
107
|
+
- `ION_TUI_WRITE_LOG=/tmp/tui-ansi.log` captures the raw ANSI stream written to stdout.
|
|
108
|
+
- `ION_TUI_ESC_TIMEOUT` overrides the lone-Escape reassembly timeout in milliseconds (higher on SSH transports by default).
|
|
109
|
+
|
|
110
|
+
## Further docs
|
|
111
|
+
|
|
112
|
+
- `docs/reference.md` — complete public API reference.
|
|
113
|
+
- `docs/how-to.md` — task recipes (layouts, overlays, custom components, keybindings, testing).
|
|
114
|
+
- `docs/explanation.md` — rendering model and architecture concepts.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Component, Focusable } from "./tui.ts";
|
|
2
|
+
export interface AltScreenSearchSegment {
|
|
3
|
+
row: number;
|
|
4
|
+
startCol: number;
|
|
5
|
+
endCol: number;
|
|
6
|
+
}
|
|
7
|
+
export interface AltScreenSearchMatch {
|
|
8
|
+
segments: AltScreenSearchSegment[];
|
|
9
|
+
}
|
|
10
|
+
export interface AltScreenSearchResult {
|
|
11
|
+
matches: AltScreenSearchMatch[];
|
|
12
|
+
changed: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Cache the searchable corpus and matches while rendered transcript lines remain unchanged. */
|
|
15
|
+
export declare class AltScreenSearchIndex {
|
|
16
|
+
private sourceLines;
|
|
17
|
+
private corpus;
|
|
18
|
+
private normalizedQuery;
|
|
19
|
+
private matches;
|
|
20
|
+
search(lines: readonly string[], query: string): AltScreenSearchResult;
|
|
21
|
+
}
|
|
22
|
+
export declare function findAltScreenSearchMatches(lines: readonly string[], query: string): AltScreenSearchMatch[];
|
|
23
|
+
export declare function getAltScreenSearchMatchKey(match: AltScreenSearchMatch): string;
|
|
24
|
+
export declare class AltScreenSearchComponent implements Component, Focusable {
|
|
25
|
+
private readonly input;
|
|
26
|
+
private readonly onQueryChange;
|
|
27
|
+
private readonly navigationButtonStyle;
|
|
28
|
+
private resultCount;
|
|
29
|
+
private resultIndex;
|
|
30
|
+
private previousButtonStart;
|
|
31
|
+
private previousButtonEnd;
|
|
32
|
+
private nextButtonStart;
|
|
33
|
+
private nextButtonEnd;
|
|
34
|
+
private hoveredNavigationDirection;
|
|
35
|
+
private _focused;
|
|
36
|
+
constructor(onQueryChange: (query: string) => void, navigationButtonStyle?: (text: string, hovered: boolean) => string);
|
|
37
|
+
get focused(): boolean;
|
|
38
|
+
set focused(value: boolean);
|
|
39
|
+
setResult(index: number, count: number): void;
|
|
40
|
+
getNavigationDirectionAt(row: number, column: number): -1 | 1 | undefined;
|
|
41
|
+
setHoveredNavigationDirection(direction: -1 | 1 | undefined): boolean;
|
|
42
|
+
handleInput(data: string): void;
|
|
43
|
+
invalidate(): void;
|
|
44
|
+
render(width: number): string[];
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=alt-screen-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alt-screen-search.d.ts","sourceRoot":"","sources":["../src/alt-screen-search.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAmBrD,MAAM,WAAW,sBAAsB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,EAAE,sBAAsB,EAAE,CAAC;CACnC;AAwHD,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,gGAAgG;AAChG,qBAAa,oBAAoB;IAChC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,OAAO,CAA8B;IAE7C,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,qBAAqB,CAqBrE;CACD;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAG1G;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAI9E;AAED,qBAAa,wBAAyB,YAAW,SAAS,EAAE,SAAS;IACpE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAInB;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA0B;IACxD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA6C;IACnF,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,mBAAmB,CAAM;IACjC,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,eAAe,CAAM;IAC7B,OAAO,CAAC,aAAa,CAAM;IAC3B,OAAO,CAAC,0BAA0B,CAAqB;IACvD,OAAO,CAAC,QAAQ,CAAS;IAEzB,YACC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACtC,qBAAqB,GAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,MAAuB,EAIlF;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IAED,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAG5C;IAED,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAKxE;IAED,6BAA6B,CAAC,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,GAAG,OAAO,CAIpE;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAK9B;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAkE9B;CACD","sourcesContent":["import { Input } from \"./components/input.ts\";\nimport { getKeybindings } from \"./keybindings.ts\";\nimport type { Component, Focusable } from \"./tui.ts\";\nimport { getGraphemeSegmenter, stripTerminalSequences, truncateToWidth, visibleWidth } from \"./utils.ts\";\n\nconst segmenter = getGraphemeSegmenter();\n\ninterface SearchSourceSpan {\n\ttextStart: number;\n\ttextEnd: number;\n\trow: number;\n\tstartCol: number;\n\tendCol: number;\n\tlinearColumns: boolean;\n}\n\ninterface SearchCorpus {\n\ttext: string;\n\tspans: SearchSourceSpan[];\n}\n\nexport interface AltScreenSearchSegment {\n\trow: number;\n\tstartCol: number;\n\tendCol: number;\n}\n\nexport interface AltScreenSearchMatch {\n\tsegments: AltScreenSearchSegment[];\n}\n\nconst PRINTABLE_ASCII = /^[\\x20-\\x7e]*$/;\n\nfunction buildSearchCorpus(lines: readonly string[]): SearchCorpus {\n\tconst chunks: string[] = [];\n\tconst spans: SearchSourceSpan[] = [];\n\tlet textLength = 0;\n\tlet pendingSeparator = false;\n\n\tconst appendSeparator = (): void => {\n\t\tif (!pendingSeparator) return;\n\t\tchunks.push(\" \");\n\t\ttextLength += 1;\n\t\tpendingSeparator = false;\n\t};\n\n\tfor (let row = 0; row < lines.length; row++) {\n\t\tconst line = stripTerminalSequences(lines[row] ?? \"\");\n\t\tlet column = 0;\n\n\t\t// Rendered transcripts are overwhelmingly ASCII. Index complete non-space\n\t\t// runs at once instead of segmenting and allocating one mapping per cell.\n\t\tif (PRINTABLE_ASCII.test(line)) {\n\t\t\tlet index = 0;\n\t\t\twhile (index < line.length) {\n\t\t\t\tif (line.charCodeAt(index) === 0x20) {\n\t\t\t\t\tif (textLength > 0) pendingSeparator = true;\n\t\t\t\t\tcolumn += 1;\n\t\t\t\t\tindex += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tlet end = index + 1;\n\t\t\t\twhile (end < line.length && line.charCodeAt(end) !== 0x20) end += 1;\n\t\t\t\tappendSeparator();\n\t\t\t\tconst text = line.slice(index, end);\n\t\t\t\tchunks.push(text);\n\t\t\t\tspans.push({\n\t\t\t\t\ttextStart: textLength,\n\t\t\t\t\ttextEnd: textLength + text.length,\n\t\t\t\t\trow,\n\t\t\t\t\tstartCol: column,\n\t\t\t\t\tendCol: column + text.length,\n\t\t\t\t\tlinearColumns: true,\n\t\t\t\t});\n\t\t\t\ttextLength += text.length;\n\t\t\t\tcolumn += text.length;\n\t\t\t\tindex = end;\n\t\t\t}\n\t\t} else {\n\t\t\tfor (const grapheme of segmenter.segment(line)) {\n\t\t\t\tconst text = grapheme.segment;\n\t\t\t\tconst width = visibleWidth(text);\n\t\t\t\tif (/^\\s+$/u.test(text)) {\n\t\t\t\t\tif (textLength > 0) pendingSeparator = true;\n\t\t\t\t\tcolumn += width;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tappendSeparator();\n\t\t\t\tchunks.push(text);\n\t\t\t\tspans.push({\n\t\t\t\t\ttextStart: textLength,\n\t\t\t\t\ttextEnd: textLength + text.length,\n\t\t\t\t\trow,\n\t\t\t\t\tstartCol: column,\n\t\t\t\t\tendCol: column + width,\n\t\t\t\t\tlinearColumns: false,\n\t\t\t\t});\n\t\t\t\ttextLength += text.length;\n\t\t\t\tcolumn += width;\n\t\t\t}\n\t\t}\n\t\tif (textLength > 0) pendingSeparator = true;\n\t}\n\n\treturn { text: chunks.join(\"\"), spans };\n}\n\nfunction normalizeQuery(query: string): string {\n\treturn query.replace(/\\s+/gu, \" \").trim();\n}\n\nfunction escapeRegExp(text: string): string {\n\treturn text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction findSearchCorpusMatches(corpus: SearchCorpus, normalizedQuery: string): AltScreenSearchMatch[] {\n\tif (!normalizedQuery) return [];\n\tconst expression = new RegExp(escapeRegExp(normalizedQuery), \"giu\");\n\tconst matches: AltScreenSearchMatch[] = [];\n\tlet spanIndex = 0;\n\n\tfor (const match of corpus.text.matchAll(expression)) {\n\t\tconst start = match.index;\n\t\tconst end = start + match[0].length;\n\t\twhile (spanIndex < corpus.spans.length && corpus.spans[spanIndex]!.textEnd <= start) spanIndex += 1;\n\n\t\tconst segments: AltScreenSearchSegment[] = [];\n\t\tfor (let index = spanIndex; index < corpus.spans.length; index++) {\n\t\t\tconst span = corpus.spans[index]!;\n\t\t\tif (span.textStart >= end) break;\n\t\t\tif (span.textEnd <= start) continue;\n\t\t\tconst startCol = span.linearColumns\n\t\t\t\t? span.startCol + Math.max(start, span.textStart) - span.textStart\n\t\t\t\t: span.startCol;\n\t\t\tconst endCol = span.linearColumns ? span.startCol + Math.min(end, span.textEnd) - span.textStart : span.endCol;\n\t\t\tconst previous = segments[segments.length - 1];\n\t\t\tif (previous && previous.row === span.row && startCol <= previous.endCol) {\n\t\t\t\tprevious.endCol = Math.max(previous.endCol, endCol);\n\t\t\t} else {\n\t\t\t\tsegments.push({ row: span.row, startCol, endCol });\n\t\t\t}\n\t\t}\n\t\twhile (spanIndex < corpus.spans.length && corpus.spans[spanIndex]!.textEnd <= end) spanIndex += 1;\n\t\tif (segments.length > 0) matches.push({ segments });\n\t}\n\n\treturn matches;\n}\n\nexport interface AltScreenSearchResult {\n\tmatches: AltScreenSearchMatch[];\n\tchanged: boolean;\n}\n\n/** Cache the searchable corpus and matches while rendered transcript lines remain unchanged. */\nexport class AltScreenSearchIndex {\n\tprivate sourceLines: string[] | undefined;\n\tprivate corpus: SearchCorpus | undefined;\n\tprivate normalizedQuery: string | undefined;\n\tprivate matches: AltScreenSearchMatch[] = [];\n\n\tsearch(lines: readonly string[], query: string): AltScreenSearchResult {\n\t\tlet sourceChanged = this.sourceLines?.length !== lines.length;\n\t\tif (!sourceChanged && this.sourceLines) {\n\t\t\tfor (let index = 0; index < lines.length; index++) {\n\t\t\t\tif (this.sourceLines[index] === lines[index]) continue;\n\t\t\t\tsourceChanged = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (sourceChanged || !this.corpus) {\n\t\t\tthis.sourceLines = Array.from(lines);\n\t\t\tthis.corpus = buildSearchCorpus(lines);\n\t\t}\n\n\t\tconst normalizedQuery = normalizeQuery(query);\n\t\tconst changed = sourceChanged || normalizedQuery !== this.normalizedQuery;\n\t\tif (changed) {\n\t\t\tthis.normalizedQuery = normalizedQuery;\n\t\t\tthis.matches = findSearchCorpusMatches(this.corpus, normalizedQuery);\n\t\t}\n\t\treturn { matches: this.matches, changed };\n\t}\n}\n\nexport function findAltScreenSearchMatches(lines: readonly string[], query: string): AltScreenSearchMatch[] {\n\tconst normalizedQuery = normalizeQuery(query);\n\treturn normalizedQuery ? findSearchCorpusMatches(buildSearchCorpus(lines), normalizedQuery) : [];\n}\n\nexport function getAltScreenSearchMatchKey(match: AltScreenSearchMatch): string {\n\tconst first = match.segments[0];\n\tconst last = match.segments[match.segments.length - 1];\n\treturn first && last ? `${first.row}:${first.startCol}:${last.row}:${last.endCol}` : \"\";\n}\n\nexport class AltScreenSearchComponent implements Component, Focusable {\n\tprivate readonly input = new Input({\n\t\tprompt: \" \",\n\t\tplaceholder: \"Find in transcript\",\n\t\tplaceholderStyle: (text) => `\\x1b[2m${text}\\x1b[22m`,\n\t});\n\tprivate readonly onQueryChange: (query: string) => void;\n\tprivate readonly navigationButtonStyle: (text: string, hovered: boolean) => string;\n\tprivate resultCount = 0;\n\tprivate resultIndex = -1;\n\tprivate previousButtonStart = -1;\n\tprivate previousButtonEnd = -1;\n\tprivate nextButtonStart = -1;\n\tprivate nextButtonEnd = -1;\n\tprivate hoveredNavigationDirection: -1 | 1 | undefined;\n\tprivate _focused = false;\n\n\tconstructor(\n\t\tonQueryChange: (query: string) => void,\n\t\tnavigationButtonStyle: (text: string, hovered: boolean) => string = (text) => text,\n\t) {\n\t\tthis.onQueryChange = onQueryChange;\n\t\tthis.navigationButtonStyle = navigationButtonStyle;\n\t}\n\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.input.focused = value;\n\t}\n\n\tsetResult(index: number, count: number): void {\n\t\tthis.resultIndex = index;\n\t\tthis.resultCount = count;\n\t}\n\n\tgetNavigationDirectionAt(row: number, column: number): -1 | 1 | undefined {\n\t\tif (row !== 2) return undefined;\n\t\tif (column >= this.previousButtonStart && column < this.previousButtonEnd) return -1;\n\t\tif (column >= this.nextButtonStart && column < this.nextButtonEnd) return 1;\n\t\treturn undefined;\n\t}\n\n\tsetHoveredNavigationDirection(direction: -1 | 1 | undefined): boolean {\n\t\tif (direction === this.hoveredNavigationDirection) return false;\n\t\tthis.hoveredNavigationDirection = direction;\n\t\treturn true;\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst previous = this.input.getValue();\n\t\tthis.input.handleInput(data);\n\t\tconst query = this.input.getValue();\n\t\tif (query !== previous) this.onQueryChange(query);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.input.invalidate();\n\t}\n\n\trender(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\tconst innerWidth = Math.max(0, safeWidth - 2);\n\t\tconst formatKey = (key: string | undefined): string =>\n\t\t\tkey\n\t\t\t\t? key\n\t\t\t\t\t\t.split(\"+\")\n\t\t\t\t\t\t.map((part) => {\n\t\t\t\t\t\t\tif (process.platform === \"darwin\" && part.toLowerCase() === \"alt\") return \"Option\";\n\t\t\t\t\t\t\treturn part.charAt(0).toUpperCase() + part.slice(1);\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.join(\"+\")\n\t\t\t\t: \"Unbound\";\n\t\tconst keybindings = getKeybindings();\n\t\tconst previousKey = formatKey(keybindings.getKeys(\"tui.altScreen.searchPrevious\")[0]);\n\t\tconst nextKey = formatKey(keybindings.getKeys(\"tui.altScreen.searchNext\")[0]);\n\t\tconst query = this.input.getValue();\n\t\tconst result = !query\n\t\t\t? \"\"\n\t\t\t: this.resultCount === 0\n\t\t\t\t? \"No matches\"\n\t\t\t\t: `${this.resultIndex + 1}/${this.resultCount}`;\n\t\tconst resultSpace = Math.max(0, innerWidth - 3);\n\t\tconst visibleResult = truncateToWidth(result, resultSpace, \"\");\n\t\tconst resultText = visibleResult ? `\\x1b[2m ${visibleResult} \\x1b[22m` : \"\";\n\t\tconst inputWidth = Math.max(0, innerWidth - visibleWidth(resultText));\n\t\tconst inputLine = truncateToWidth(this.input.render(Math.max(1, inputWidth))[0] ?? \"\", inputWidth, \"\");\n\t\tconst inputPadding = \" \".repeat(Math.max(0, inputWidth - visibleWidth(inputLine)));\n\t\tconst content = `${inputLine}${inputPadding}${resultText}`;\n\n\t\tlet previousButton = `↑ ${previousKey}`;\n\t\tlet nextButton = `↓ ${nextKey}`;\n\t\tlet separator = \" · \";\n\t\tconst outerGapWidth = 1;\n\t\tconst availableControlsWidth = Math.max(0, innerWidth - outerGapWidth * 2 - 1);\n\t\tlet controlsWidth = visibleWidth(previousButton) + visibleWidth(separator) + visibleWidth(nextButton);\n\t\tif (controlsWidth > availableControlsWidth) {\n\t\t\tpreviousButton = \"↑\";\n\t\t\tnextButton = \"↓\";\n\t\t\tseparator = \" \";\n\t\t\tcontrolsWidth = visibleWidth(previousButton) + visibleWidth(separator) + visibleWidth(nextButton);\n\t\t}\n\t\tconst showButtons = controlsWidth <= availableControlsWidth;\n\t\tconst renderedButtons = showButtons\n\t\t\t? this.navigationButtonStyle(previousButton, this.hoveredNavigationDirection === -1) +\n\t\t\t\tseparator +\n\t\t\t\tthis.navigationButtonStyle(nextButton, this.hoveredNavigationDirection === 1)\n\t\t\t: \"\";\n\t\tconst outerGapsWidth = showButtons ? outerGapWidth * 2 : 0;\n\t\tconst rightRuleWidth = renderedButtons && innerWidth > controlsWidth + outerGapsWidth ? 1 : 0;\n\t\tconst leftRuleWidth = Math.max(\n\t\t\t0,\n\t\t\tinnerWidth - (showButtons ? controlsWidth : 0) - outerGapsWidth - rightRuleWidth,\n\t\t);\n\t\tconst previousStart = 1 + leftRuleWidth + outerGapWidth;\n\t\tthis.previousButtonStart = showButtons ? previousStart : -1;\n\t\tthis.previousButtonEnd = showButtons ? previousStart + visibleWidth(previousButton) : -1;\n\t\tthis.nextButtonStart = showButtons ? this.previousButtonEnd + visibleWidth(separator) : -1;\n\t\tthis.nextButtonEnd = showButtons ? this.nextButtonStart + visibleWidth(nextButton) : -1;\n\n\t\tif (safeWidth === 1) return [\"┌\", \"│\", \"└\"];\n\t\treturn [\n\t\t\t`┌${\"─\".repeat(innerWidth)}┐`,\n\t\t\t`│${content}│`,\n\t\t\t`└${\"─\".repeat(leftRuleWidth)}${renderedButtons ? \" \" : \"\"}${renderedButtons}${renderedButtons ? \" \" : \"\"}${\"─\".repeat(rightRuleWidth)}┘`,\n\t\t];\n\t}\n}\n"]}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { Input } from "./components/input.js";
|
|
2
|
+
import { getKeybindings } from "./keybindings.js";
|
|
3
|
+
import { getGraphemeSegmenter, stripTerminalSequences, truncateToWidth, visibleWidth } from "./utils.js";
|
|
4
|
+
const segmenter = getGraphemeSegmenter();
|
|
5
|
+
const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
|
|
6
|
+
function buildSearchCorpus(lines) {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
const spans = [];
|
|
9
|
+
let textLength = 0;
|
|
10
|
+
let pendingSeparator = false;
|
|
11
|
+
const appendSeparator = () => {
|
|
12
|
+
if (!pendingSeparator)
|
|
13
|
+
return;
|
|
14
|
+
chunks.push(" ");
|
|
15
|
+
textLength += 1;
|
|
16
|
+
pendingSeparator = false;
|
|
17
|
+
};
|
|
18
|
+
for (let row = 0; row < lines.length; row++) {
|
|
19
|
+
const line = stripTerminalSequences(lines[row] ?? "");
|
|
20
|
+
let column = 0;
|
|
21
|
+
// Rendered transcripts are overwhelmingly ASCII. Index complete non-space
|
|
22
|
+
// runs at once instead of segmenting and allocating one mapping per cell.
|
|
23
|
+
if (PRINTABLE_ASCII.test(line)) {
|
|
24
|
+
let index = 0;
|
|
25
|
+
while (index < line.length) {
|
|
26
|
+
if (line.charCodeAt(index) === 0x20) {
|
|
27
|
+
if (textLength > 0)
|
|
28
|
+
pendingSeparator = true;
|
|
29
|
+
column += 1;
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
let end = index + 1;
|
|
34
|
+
while (end < line.length && line.charCodeAt(end) !== 0x20)
|
|
35
|
+
end += 1;
|
|
36
|
+
appendSeparator();
|
|
37
|
+
const text = line.slice(index, end);
|
|
38
|
+
chunks.push(text);
|
|
39
|
+
spans.push({
|
|
40
|
+
textStart: textLength,
|
|
41
|
+
textEnd: textLength + text.length,
|
|
42
|
+
row,
|
|
43
|
+
startCol: column,
|
|
44
|
+
endCol: column + text.length,
|
|
45
|
+
linearColumns: true,
|
|
46
|
+
});
|
|
47
|
+
textLength += text.length;
|
|
48
|
+
column += text.length;
|
|
49
|
+
index = end;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
for (const grapheme of segmenter.segment(line)) {
|
|
54
|
+
const text = grapheme.segment;
|
|
55
|
+
const width = visibleWidth(text);
|
|
56
|
+
if (/^\s+$/u.test(text)) {
|
|
57
|
+
if (textLength > 0)
|
|
58
|
+
pendingSeparator = true;
|
|
59
|
+
column += width;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
appendSeparator();
|
|
63
|
+
chunks.push(text);
|
|
64
|
+
spans.push({
|
|
65
|
+
textStart: textLength,
|
|
66
|
+
textEnd: textLength + text.length,
|
|
67
|
+
row,
|
|
68
|
+
startCol: column,
|
|
69
|
+
endCol: column + width,
|
|
70
|
+
linearColumns: false,
|
|
71
|
+
});
|
|
72
|
+
textLength += text.length;
|
|
73
|
+
column += width;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (textLength > 0)
|
|
77
|
+
pendingSeparator = true;
|
|
78
|
+
}
|
|
79
|
+
return { text: chunks.join(""), spans };
|
|
80
|
+
}
|
|
81
|
+
function normalizeQuery(query) {
|
|
82
|
+
return query.replace(/\s+/gu, " ").trim();
|
|
83
|
+
}
|
|
84
|
+
function escapeRegExp(text) {
|
|
85
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
86
|
+
}
|
|
87
|
+
function findSearchCorpusMatches(corpus, normalizedQuery) {
|
|
88
|
+
if (!normalizedQuery)
|
|
89
|
+
return [];
|
|
90
|
+
const expression = new RegExp(escapeRegExp(normalizedQuery), "giu");
|
|
91
|
+
const matches = [];
|
|
92
|
+
let spanIndex = 0;
|
|
93
|
+
for (const match of corpus.text.matchAll(expression)) {
|
|
94
|
+
const start = match.index;
|
|
95
|
+
const end = start + match[0].length;
|
|
96
|
+
while (spanIndex < corpus.spans.length && corpus.spans[spanIndex].textEnd <= start)
|
|
97
|
+
spanIndex += 1;
|
|
98
|
+
const segments = [];
|
|
99
|
+
for (let index = spanIndex; index < corpus.spans.length; index++) {
|
|
100
|
+
const span = corpus.spans[index];
|
|
101
|
+
if (span.textStart >= end)
|
|
102
|
+
break;
|
|
103
|
+
if (span.textEnd <= start)
|
|
104
|
+
continue;
|
|
105
|
+
const startCol = span.linearColumns
|
|
106
|
+
? span.startCol + Math.max(start, span.textStart) - span.textStart
|
|
107
|
+
: span.startCol;
|
|
108
|
+
const endCol = span.linearColumns ? span.startCol + Math.min(end, span.textEnd) - span.textStart : span.endCol;
|
|
109
|
+
const previous = segments[segments.length - 1];
|
|
110
|
+
if (previous && previous.row === span.row && startCol <= previous.endCol) {
|
|
111
|
+
previous.endCol = Math.max(previous.endCol, endCol);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
segments.push({ row: span.row, startCol, endCol });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
while (spanIndex < corpus.spans.length && corpus.spans[spanIndex].textEnd <= end)
|
|
118
|
+
spanIndex += 1;
|
|
119
|
+
if (segments.length > 0)
|
|
120
|
+
matches.push({ segments });
|
|
121
|
+
}
|
|
122
|
+
return matches;
|
|
123
|
+
}
|
|
124
|
+
/** Cache the searchable corpus and matches while rendered transcript lines remain unchanged. */
|
|
125
|
+
export class AltScreenSearchIndex {
|
|
126
|
+
sourceLines;
|
|
127
|
+
corpus;
|
|
128
|
+
normalizedQuery;
|
|
129
|
+
matches = [];
|
|
130
|
+
search(lines, query) {
|
|
131
|
+
let sourceChanged = this.sourceLines?.length !== lines.length;
|
|
132
|
+
if (!sourceChanged && this.sourceLines) {
|
|
133
|
+
for (let index = 0; index < lines.length; index++) {
|
|
134
|
+
if (this.sourceLines[index] === lines[index])
|
|
135
|
+
continue;
|
|
136
|
+
sourceChanged = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (sourceChanged || !this.corpus) {
|
|
141
|
+
this.sourceLines = Array.from(lines);
|
|
142
|
+
this.corpus = buildSearchCorpus(lines);
|
|
143
|
+
}
|
|
144
|
+
const normalizedQuery = normalizeQuery(query);
|
|
145
|
+
const changed = sourceChanged || normalizedQuery !== this.normalizedQuery;
|
|
146
|
+
if (changed) {
|
|
147
|
+
this.normalizedQuery = normalizedQuery;
|
|
148
|
+
this.matches = findSearchCorpusMatches(this.corpus, normalizedQuery);
|
|
149
|
+
}
|
|
150
|
+
return { matches: this.matches, changed };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function findAltScreenSearchMatches(lines, query) {
|
|
154
|
+
const normalizedQuery = normalizeQuery(query);
|
|
155
|
+
return normalizedQuery ? findSearchCorpusMatches(buildSearchCorpus(lines), normalizedQuery) : [];
|
|
156
|
+
}
|
|
157
|
+
export function getAltScreenSearchMatchKey(match) {
|
|
158
|
+
const first = match.segments[0];
|
|
159
|
+
const last = match.segments[match.segments.length - 1];
|
|
160
|
+
return first && last ? `${first.row}:${first.startCol}:${last.row}:${last.endCol}` : "";
|
|
161
|
+
}
|
|
162
|
+
export class AltScreenSearchComponent {
|
|
163
|
+
input = new Input({
|
|
164
|
+
prompt: " ",
|
|
165
|
+
placeholder: "Find in transcript",
|
|
166
|
+
placeholderStyle: (text) => `\x1b[2m${text}\x1b[22m`,
|
|
167
|
+
});
|
|
168
|
+
onQueryChange;
|
|
169
|
+
navigationButtonStyle;
|
|
170
|
+
resultCount = 0;
|
|
171
|
+
resultIndex = -1;
|
|
172
|
+
previousButtonStart = -1;
|
|
173
|
+
previousButtonEnd = -1;
|
|
174
|
+
nextButtonStart = -1;
|
|
175
|
+
nextButtonEnd = -1;
|
|
176
|
+
hoveredNavigationDirection;
|
|
177
|
+
_focused = false;
|
|
178
|
+
constructor(onQueryChange, navigationButtonStyle = (text) => text) {
|
|
179
|
+
this.onQueryChange = onQueryChange;
|
|
180
|
+
this.navigationButtonStyle = navigationButtonStyle;
|
|
181
|
+
}
|
|
182
|
+
get focused() {
|
|
183
|
+
return this._focused;
|
|
184
|
+
}
|
|
185
|
+
set focused(value) {
|
|
186
|
+
this._focused = value;
|
|
187
|
+
this.input.focused = value;
|
|
188
|
+
}
|
|
189
|
+
setResult(index, count) {
|
|
190
|
+
this.resultIndex = index;
|
|
191
|
+
this.resultCount = count;
|
|
192
|
+
}
|
|
193
|
+
getNavigationDirectionAt(row, column) {
|
|
194
|
+
if (row !== 2)
|
|
195
|
+
return undefined;
|
|
196
|
+
if (column >= this.previousButtonStart && column < this.previousButtonEnd)
|
|
197
|
+
return -1;
|
|
198
|
+
if (column >= this.nextButtonStart && column < this.nextButtonEnd)
|
|
199
|
+
return 1;
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
setHoveredNavigationDirection(direction) {
|
|
203
|
+
if (direction === this.hoveredNavigationDirection)
|
|
204
|
+
return false;
|
|
205
|
+
this.hoveredNavigationDirection = direction;
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
handleInput(data) {
|
|
209
|
+
const previous = this.input.getValue();
|
|
210
|
+
this.input.handleInput(data);
|
|
211
|
+
const query = this.input.getValue();
|
|
212
|
+
if (query !== previous)
|
|
213
|
+
this.onQueryChange(query);
|
|
214
|
+
}
|
|
215
|
+
invalidate() {
|
|
216
|
+
this.input.invalidate();
|
|
217
|
+
}
|
|
218
|
+
render(width) {
|
|
219
|
+
const safeWidth = Math.max(1, width);
|
|
220
|
+
const innerWidth = Math.max(0, safeWidth - 2);
|
|
221
|
+
const formatKey = (key) => key
|
|
222
|
+
? key
|
|
223
|
+
.split("+")
|
|
224
|
+
.map((part) => {
|
|
225
|
+
if (process.platform === "darwin" && part.toLowerCase() === "alt")
|
|
226
|
+
return "Option";
|
|
227
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
228
|
+
})
|
|
229
|
+
.join("+")
|
|
230
|
+
: "Unbound";
|
|
231
|
+
const keybindings = getKeybindings();
|
|
232
|
+
const previousKey = formatKey(keybindings.getKeys("tui.altScreen.searchPrevious")[0]);
|
|
233
|
+
const nextKey = formatKey(keybindings.getKeys("tui.altScreen.searchNext")[0]);
|
|
234
|
+
const query = this.input.getValue();
|
|
235
|
+
const result = !query
|
|
236
|
+
? ""
|
|
237
|
+
: this.resultCount === 0
|
|
238
|
+
? "No matches"
|
|
239
|
+
: `${this.resultIndex + 1}/${this.resultCount}`;
|
|
240
|
+
const resultSpace = Math.max(0, innerWidth - 3);
|
|
241
|
+
const visibleResult = truncateToWidth(result, resultSpace, "");
|
|
242
|
+
const resultText = visibleResult ? `\x1b[2m ${visibleResult} \x1b[22m` : "";
|
|
243
|
+
const inputWidth = Math.max(0, innerWidth - visibleWidth(resultText));
|
|
244
|
+
const inputLine = truncateToWidth(this.input.render(Math.max(1, inputWidth))[0] ?? "", inputWidth, "");
|
|
245
|
+
const inputPadding = " ".repeat(Math.max(0, inputWidth - visibleWidth(inputLine)));
|
|
246
|
+
const content = `${inputLine}${inputPadding}${resultText}`;
|
|
247
|
+
let previousButton = `↑ ${previousKey}`;
|
|
248
|
+
let nextButton = `↓ ${nextKey}`;
|
|
249
|
+
let separator = " · ";
|
|
250
|
+
const outerGapWidth = 1;
|
|
251
|
+
const availableControlsWidth = Math.max(0, innerWidth - outerGapWidth * 2 - 1);
|
|
252
|
+
let controlsWidth = visibleWidth(previousButton) + visibleWidth(separator) + visibleWidth(nextButton);
|
|
253
|
+
if (controlsWidth > availableControlsWidth) {
|
|
254
|
+
previousButton = "↑";
|
|
255
|
+
nextButton = "↓";
|
|
256
|
+
separator = " ";
|
|
257
|
+
controlsWidth = visibleWidth(previousButton) + visibleWidth(separator) + visibleWidth(nextButton);
|
|
258
|
+
}
|
|
259
|
+
const showButtons = controlsWidth <= availableControlsWidth;
|
|
260
|
+
const renderedButtons = showButtons
|
|
261
|
+
? this.navigationButtonStyle(previousButton, this.hoveredNavigationDirection === -1) +
|
|
262
|
+
separator +
|
|
263
|
+
this.navigationButtonStyle(nextButton, this.hoveredNavigationDirection === 1)
|
|
264
|
+
: "";
|
|
265
|
+
const outerGapsWidth = showButtons ? outerGapWidth * 2 : 0;
|
|
266
|
+
const rightRuleWidth = renderedButtons && innerWidth > controlsWidth + outerGapsWidth ? 1 : 0;
|
|
267
|
+
const leftRuleWidth = Math.max(0, innerWidth - (showButtons ? controlsWidth : 0) - outerGapsWidth - rightRuleWidth);
|
|
268
|
+
const previousStart = 1 + leftRuleWidth + outerGapWidth;
|
|
269
|
+
this.previousButtonStart = showButtons ? previousStart : -1;
|
|
270
|
+
this.previousButtonEnd = showButtons ? previousStart + visibleWidth(previousButton) : -1;
|
|
271
|
+
this.nextButtonStart = showButtons ? this.previousButtonEnd + visibleWidth(separator) : -1;
|
|
272
|
+
this.nextButtonEnd = showButtons ? this.nextButtonStart + visibleWidth(nextButton) : -1;
|
|
273
|
+
if (safeWidth === 1)
|
|
274
|
+
return ["┌", "│", "└"];
|
|
275
|
+
return [
|
|
276
|
+
`┌${"─".repeat(innerWidth)}┐`,
|
|
277
|
+
`│${content}│`,
|
|
278
|
+
`└${"─".repeat(leftRuleWidth)}${renderedButtons ? " " : ""}${renderedButtons}${renderedButtons ? " " : ""}${"─".repeat(rightRuleWidth)}┘`,
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=alt-screen-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alt-screen-search.js","sourceRoot":"","sources":["../src/alt-screen-search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEzG,MAAM,SAAS,GAAG,oBAAoB,EAAE,CAAC;AA0BzC,MAAM,eAAe,GAAG,gBAAgB,CAAC;AAEzC,SAAS,iBAAiB,CAAC,KAAwB,EAAgB;IAClE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAuB,EAAE,CAAC;IACrC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAE7B,MAAM,eAAe,GAAG,GAAS,EAAE,CAAC;QACnC,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAC9B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjB,UAAU,IAAI,CAAC,CAAC;QAChB,gBAAgB,GAAG,KAAK,CAAC;IAAA,CACzB,CAAC;IAEF,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,sBAAsB,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACtD,IAAI,MAAM,GAAG,CAAC,CAAC;QAEf,0EAA0E;QAC1E,0EAA0E;QAC1E,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,IAAI,KAAK,GAAG,CAAC,CAAC;YACd,OAAO,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC5B,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;oBACrC,IAAI,UAAU,GAAG,CAAC;wBAAE,gBAAgB,GAAG,IAAI,CAAC;oBAC5C,MAAM,IAAI,CAAC,CAAC;oBACZ,KAAK,IAAI,CAAC,CAAC;oBACX,SAAS;gBACV,CAAC;gBACD,IAAI,GAAG,GAAG,KAAK,GAAG,CAAC,CAAC;gBACpB,OAAO,GAAG,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,IAAI;oBAAE,GAAG,IAAI,CAAC,CAAC;gBACpE,eAAe,EAAE,CAAC;gBAClB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;gBACpC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAClB,KAAK,CAAC,IAAI,CAAC;oBACV,SAAS,EAAE,UAAU;oBACrB,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC,MAAM;oBACjC,GAAG;oBACH,QAAQ,EAAE,MAAM;oBAChB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM;oBAC5B,aAAa,EAAE,IAAI;iBACnB,CAAC,CAAC;gBACH,UAAU,IAAI,IAAI,CAAC,MAAM,CAAC;gBAC1B,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;gBACtB,KAAK,GAAG,GAAG,CAAC;YACb,CAAC;QACF,CAAC;aAAM,CAAC;YACP,KAAK,MAAM,QAAQ,IAAI,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC;gBAC9B,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;gBACjC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBACzB,IAAI,UAAU,GAAG,CAAC;wBAAE,gBAAgB,GAAG,IAAI,CAAC;oBAC5C,MAAM,IAAI,KAAK,CAAC;oBAChB,SAAS;gBACV,CAAC;gBACD,eAAe,EAAE,CAAC;gBAClB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAClB,KAAK,CAAC,IAAI,CAAC;oBACV,SAAS,EAAE,UAAU;oBACrB,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC,MAAM;oBACjC,GAAG;oBACH,QAAQ,EAAE,MAAM;oBAChB,MAAM,EAAE,MAAM,GAAG,KAAK;oBACtB,aAAa,EAAE,KAAK;iBACpB,CAAC,CAAC;gBACH,UAAU,IAAI,IAAI,CAAC,MAAM,CAAC;gBAC1B,MAAM,IAAI,KAAK,CAAC;YACjB,CAAC;QACF,CAAC;QACD,IAAI,UAAU,GAAG,CAAC;YAAE,gBAAgB,GAAG,IAAI,CAAC;IAC7C,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC;AAAA,CACxC;AAED,SAAS,cAAc,CAAC,KAAa,EAAU;IAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAAA,CAC1C;AAED,SAAS,YAAY,CAAC,IAAY,EAAU;IAC3C,OAAO,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAAA,CACnD;AAED,SAAS,uBAAuB,CAAC,MAAoB,EAAE,eAAuB,EAA0B;IACvG,IAAI,CAAC,eAAe;QAAE,OAAO,EAAE,CAAC;IAChC,MAAM,UAAU,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC,CAAC;IACpE,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QAC1B,MAAM,GAAG,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QACpC,OAAO,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,CAAC,SAAS,CAAE,CAAC,OAAO,IAAI,KAAK;YAAE,SAAS,IAAI,CAAC,CAAC;QAEpG,MAAM,QAAQ,GAA6B,EAAE,CAAC;QAC9C,KAAK,IAAI,KAAK,GAAG,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;YAClE,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAE,CAAC;YAClC,IAAI,IAAI,CAAC,SAAS,IAAI,GAAG;gBAAE,MAAM;YACjC,IAAI,IAAI,CAAC,OAAO,IAAI,KAAK;gBAAE,SAAS;YACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa;gBAClC,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,SAAS;gBAClE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC;YACjB,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;YAC/G,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC/C,IAAI,QAAQ,IAAI,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;gBAC1E,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACrD,CAAC;iBAAM,CAAC;gBACP,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YACpD,CAAC;QACF,CAAC;QACD,OAAO,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,CAAC,SAAS,CAAE,CAAC,OAAO,IAAI,GAAG;YAAE,SAAS,IAAI,CAAC,CAAC;QAClG,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,OAAO,CAAC;AAAA,CACf;AAOD,gGAAgG;AAChG,MAAM,OAAO,oBAAoB;IACxB,WAAW,CAAuB;IAClC,MAAM,CAA2B;IACjC,eAAe,CAAqB;IACpC,OAAO,GAA2B,EAAE,CAAC;IAE7C,MAAM,CAAC,KAAwB,EAAE,KAAa,EAAyB;QACtE,IAAI,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC;QAC9D,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACxC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;gBACnD,IAAI,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC;oBAAE,SAAS;gBACvD,aAAa,GAAG,IAAI,CAAC;gBACrB,MAAM;YACP,CAAC;QACF,CAAC;QACD,IAAI,aAAa,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACnC,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrC,IAAI,CAAC,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;QACxC,CAAC;QAED,MAAM,eAAe,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QAC9C,MAAM,OAAO,GAAG,aAAa,IAAI,eAAe,KAAK,IAAI,CAAC,eAAe,CAAC;QAC1E,IAAI,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;YACvC,IAAI,CAAC,OAAO,GAAG,uBAAuB,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;QACtE,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;IAAA,CAC1C;CACD;AAED,MAAM,UAAU,0BAA0B,CAAC,KAAwB,EAAE,KAAa,EAA0B;IAC3G,MAAM,eAAe,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO,eAAe,CAAC,CAAC,CAAC,uBAAuB,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CACjG;AAED,MAAM,UAAU,0BAA0B,CAAC,KAA2B,EAAU;IAC/E,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACvD,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAAA,CACxF;AAED,MAAM,OAAO,wBAAwB;IACnB,KAAK,GAAG,IAAI,KAAK,CAAC;QAClC,MAAM,EAAE,GAAG;QACX,WAAW,EAAE,oBAAoB;QACjC,gBAAgB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,IAAI,UAAU;KACpD,CAAC,CAAC;IACc,aAAa,CAA0B;IACvC,qBAAqB,CAA6C;IAC3E,WAAW,GAAG,CAAC,CAAC;IAChB,WAAW,GAAG,CAAC,CAAC,CAAC;IACjB,mBAAmB,GAAG,CAAC,CAAC,CAAC;IACzB,iBAAiB,GAAG,CAAC,CAAC,CAAC;IACvB,eAAe,GAAG,CAAC,CAAC,CAAC;IACrB,aAAa,GAAG,CAAC,CAAC,CAAC;IACnB,0BAA0B,CAAqB;IAC/C,QAAQ,GAAG,KAAK,CAAC;IAEzB,YACC,aAAsC,EACtC,qBAAqB,GAA+C,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EACjF;QACD,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,qBAAqB,GAAG,qBAAqB,CAAC;IAAA,CACnD;IAED,IAAI,OAAO,GAAY;QACtB,OAAO,IAAI,CAAC,QAAQ,CAAC;IAAA,CACrB;IAED,IAAI,OAAO,CAAC,KAAc,EAAE;QAC3B,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;IAAA,CAC3B;IAED,SAAS,CAAC,KAAa,EAAE,KAAa,EAAQ;QAC7C,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;IAAA,CACzB;IAED,wBAAwB,CAAC,GAAW,EAAE,MAAc,EAAsB;QACzE,IAAI,GAAG,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAChC,IAAI,MAAM,IAAI,IAAI,CAAC,mBAAmB,IAAI,MAAM,GAAG,IAAI,CAAC,iBAAiB;YAAE,OAAO,CAAC,CAAC,CAAC;QACrF,IAAI,MAAM,IAAI,IAAI,CAAC,eAAe,IAAI,MAAM,GAAG,IAAI,CAAC,aAAa;YAAE,OAAO,CAAC,CAAC;QAC5E,OAAO,SAAS,CAAC;IAAA,CACjB;IAED,6BAA6B,CAAC,SAA6B,EAAW;QACrE,IAAI,SAAS,KAAK,IAAI,CAAC,0BAA0B;YAAE,OAAO,KAAK,CAAC;QAChE,IAAI,CAAC,0BAA0B,GAAG,SAAS,CAAC;QAC5C,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,WAAW,CAAC,IAAY,EAAQ;QAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACvC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACpC,IAAI,KAAK,KAAK,QAAQ;YAAE,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAAA,CAClD;IAED,UAAU,GAAS;QAClB,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;IAAA,CACxB;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,CAAC,GAAuB,EAAU,EAAE,CACrD,GAAG;YACF,CAAC,CAAC,GAAG;iBACF,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;gBACd,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,KAAK;oBAAE,OAAO,QAAQ,CAAC;gBACnF,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAA,CACpD,CAAC;iBACD,IAAI,CAAC,GAAG,CAAC;YACZ,CAAC,CAAC,SAAS,CAAC;QACd,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;QACrC,MAAM,WAAW,GAAG,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACtF,MAAM,OAAO,GAAG,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACpC,MAAM,MAAM,GAAG,CAAC,KAAK;YACpB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,IAAI,CAAC,WAAW,KAAK,CAAC;gBACvB,CAAC,CAAC,YAAY;gBACd,CAAC,CAAC,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QAClD,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC;QAChD,MAAM,aAAa,GAAG,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;QAC/D,MAAM,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,WAAW,aAAa,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC;QACtE,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;QACvG,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QACnF,MAAM,OAAO,GAAG,GAAG,SAAS,GAAG,YAAY,GAAG,UAAU,EAAE,CAAC;QAE3D,IAAI,cAAc,GAAG,OAAK,WAAW,EAAE,CAAC;QACxC,IAAI,UAAU,GAAG,OAAK,OAAO,EAAE,CAAC;QAChC,IAAI,SAAS,GAAG,MAAK,CAAC;QACtB,MAAM,aAAa,GAAG,CAAC,CAAC;QACxB,MAAM,sBAAsB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,aAAa,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/E,IAAI,aAAa,GAAG,YAAY,CAAC,cAAc,CAAC,GAAG,YAAY,CAAC,SAAS,CAAC,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;QACtG,IAAI,aAAa,GAAG,sBAAsB,EAAE,CAAC;YAC5C,cAAc,GAAG,KAAG,CAAC;YACrB,UAAU,GAAG,KAAG,CAAC;YACjB,SAAS,GAAG,GAAG,CAAC;YAChB,aAAa,GAAG,YAAY,CAAC,cAAc,CAAC,GAAG,YAAY,CAAC,SAAS,CAAC,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;QACnG,CAAC;QACD,MAAM,WAAW,GAAG,aAAa,IAAI,sBAAsB,CAAC;QAC5D,MAAM,eAAe,GAAG,WAAW;YAClC,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,cAAc,EAAE,IAAI,CAAC,0BAA0B,KAAK,CAAC,CAAC,CAAC;gBACnF,SAAS;gBACT,IAAI,CAAC,qBAAqB,CAAC,UAAU,EAAE,IAAI,CAAC,0BAA0B,KAAK,CAAC,CAAC;YAC9E,CAAC,CAAC,EAAE,CAAC;QACN,MAAM,cAAc,GAAG,WAAW,CAAC,CAAC,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,cAAc,GAAG,eAAe,IAAI,UAAU,GAAG,aAAa,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9F,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAC7B,CAAC,EACD,UAAU,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,cAAc,GAAG,cAAc,CAChF,CAAC;QACF,MAAM,aAAa,GAAG,CAAC,GAAG,aAAa,GAAG,aAAa,CAAC;QACxD,IAAI,CAAC,mBAAmB,GAAG,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5D,IAAI,CAAC,iBAAiB,GAAG,WAAW,CAAC,CAAC,CAAC,aAAa,GAAG,YAAY,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzF,IAAI,CAAC,eAAe,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,iBAAiB,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3F,IAAI,CAAC,aAAa,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAExF,IAAI,SAAS,KAAK,CAAC;YAAE,OAAO,CAAC,KAAG,EAAE,KAAG,EAAE,KAAG,CAAC,CAAC;QAC5C,OAAO;YACN,MAAI,KAAG,CAAC,MAAM,CAAC,UAAU,CAAC,KAAG;YAC7B,MAAI,OAAO,KAAG;YACd,MAAI,KAAG,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,eAAe,GAAG,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,KAAG,CAAC,MAAM,CAAC,cAAc,CAAC,KAAG;SACzI,CAAC;IAAA,CACF;CACD","sourcesContent":["import { Input } from \"./components/input.ts\";\nimport { getKeybindings } from \"./keybindings.ts\";\nimport type { Component, Focusable } from \"./tui.ts\";\nimport { getGraphemeSegmenter, stripTerminalSequences, truncateToWidth, visibleWidth } from \"./utils.ts\";\n\nconst segmenter = getGraphemeSegmenter();\n\ninterface SearchSourceSpan {\n\ttextStart: number;\n\ttextEnd: number;\n\trow: number;\n\tstartCol: number;\n\tendCol: number;\n\tlinearColumns: boolean;\n}\n\ninterface SearchCorpus {\n\ttext: string;\n\tspans: SearchSourceSpan[];\n}\n\nexport interface AltScreenSearchSegment {\n\trow: number;\n\tstartCol: number;\n\tendCol: number;\n}\n\nexport interface AltScreenSearchMatch {\n\tsegments: AltScreenSearchSegment[];\n}\n\nconst PRINTABLE_ASCII = /^[\\x20-\\x7e]*$/;\n\nfunction buildSearchCorpus(lines: readonly string[]): SearchCorpus {\n\tconst chunks: string[] = [];\n\tconst spans: SearchSourceSpan[] = [];\n\tlet textLength = 0;\n\tlet pendingSeparator = false;\n\n\tconst appendSeparator = (): void => {\n\t\tif (!pendingSeparator) return;\n\t\tchunks.push(\" \");\n\t\ttextLength += 1;\n\t\tpendingSeparator = false;\n\t};\n\n\tfor (let row = 0; row < lines.length; row++) {\n\t\tconst line = stripTerminalSequences(lines[row] ?? \"\");\n\t\tlet column = 0;\n\n\t\t// Rendered transcripts are overwhelmingly ASCII. Index complete non-space\n\t\t// runs at once instead of segmenting and allocating one mapping per cell.\n\t\tif (PRINTABLE_ASCII.test(line)) {\n\t\t\tlet index = 0;\n\t\t\twhile (index < line.length) {\n\t\t\t\tif (line.charCodeAt(index) === 0x20) {\n\t\t\t\t\tif (textLength > 0) pendingSeparator = true;\n\t\t\t\t\tcolumn += 1;\n\t\t\t\t\tindex += 1;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tlet end = index + 1;\n\t\t\t\twhile (end < line.length && line.charCodeAt(end) !== 0x20) end += 1;\n\t\t\t\tappendSeparator();\n\t\t\t\tconst text = line.slice(index, end);\n\t\t\t\tchunks.push(text);\n\t\t\t\tspans.push({\n\t\t\t\t\ttextStart: textLength,\n\t\t\t\t\ttextEnd: textLength + text.length,\n\t\t\t\t\trow,\n\t\t\t\t\tstartCol: column,\n\t\t\t\t\tendCol: column + text.length,\n\t\t\t\t\tlinearColumns: true,\n\t\t\t\t});\n\t\t\t\ttextLength += text.length;\n\t\t\t\tcolumn += text.length;\n\t\t\t\tindex = end;\n\t\t\t}\n\t\t} else {\n\t\t\tfor (const grapheme of segmenter.segment(line)) {\n\t\t\t\tconst text = grapheme.segment;\n\t\t\t\tconst width = visibleWidth(text);\n\t\t\t\tif (/^\\s+$/u.test(text)) {\n\t\t\t\t\tif (textLength > 0) pendingSeparator = true;\n\t\t\t\t\tcolumn += width;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tappendSeparator();\n\t\t\t\tchunks.push(text);\n\t\t\t\tspans.push({\n\t\t\t\t\ttextStart: textLength,\n\t\t\t\t\ttextEnd: textLength + text.length,\n\t\t\t\t\trow,\n\t\t\t\t\tstartCol: column,\n\t\t\t\t\tendCol: column + width,\n\t\t\t\t\tlinearColumns: false,\n\t\t\t\t});\n\t\t\t\ttextLength += text.length;\n\t\t\t\tcolumn += width;\n\t\t\t}\n\t\t}\n\t\tif (textLength > 0) pendingSeparator = true;\n\t}\n\n\treturn { text: chunks.join(\"\"), spans };\n}\n\nfunction normalizeQuery(query: string): string {\n\treturn query.replace(/\\s+/gu, \" \").trim();\n}\n\nfunction escapeRegExp(text: string): string {\n\treturn text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction findSearchCorpusMatches(corpus: SearchCorpus, normalizedQuery: string): AltScreenSearchMatch[] {\n\tif (!normalizedQuery) return [];\n\tconst expression = new RegExp(escapeRegExp(normalizedQuery), \"giu\");\n\tconst matches: AltScreenSearchMatch[] = [];\n\tlet spanIndex = 0;\n\n\tfor (const match of corpus.text.matchAll(expression)) {\n\t\tconst start = match.index;\n\t\tconst end = start + match[0].length;\n\t\twhile (spanIndex < corpus.spans.length && corpus.spans[spanIndex]!.textEnd <= start) spanIndex += 1;\n\n\t\tconst segments: AltScreenSearchSegment[] = [];\n\t\tfor (let index = spanIndex; index < corpus.spans.length; index++) {\n\t\t\tconst span = corpus.spans[index]!;\n\t\t\tif (span.textStart >= end) break;\n\t\t\tif (span.textEnd <= start) continue;\n\t\t\tconst startCol = span.linearColumns\n\t\t\t\t? span.startCol + Math.max(start, span.textStart) - span.textStart\n\t\t\t\t: span.startCol;\n\t\t\tconst endCol = span.linearColumns ? span.startCol + Math.min(end, span.textEnd) - span.textStart : span.endCol;\n\t\t\tconst previous = segments[segments.length - 1];\n\t\t\tif (previous && previous.row === span.row && startCol <= previous.endCol) {\n\t\t\t\tprevious.endCol = Math.max(previous.endCol, endCol);\n\t\t\t} else {\n\t\t\t\tsegments.push({ row: span.row, startCol, endCol });\n\t\t\t}\n\t\t}\n\t\twhile (spanIndex < corpus.spans.length && corpus.spans[spanIndex]!.textEnd <= end) spanIndex += 1;\n\t\tif (segments.length > 0) matches.push({ segments });\n\t}\n\n\treturn matches;\n}\n\nexport interface AltScreenSearchResult {\n\tmatches: AltScreenSearchMatch[];\n\tchanged: boolean;\n}\n\n/** Cache the searchable corpus and matches while rendered transcript lines remain unchanged. */\nexport class AltScreenSearchIndex {\n\tprivate sourceLines: string[] | undefined;\n\tprivate corpus: SearchCorpus | undefined;\n\tprivate normalizedQuery: string | undefined;\n\tprivate matches: AltScreenSearchMatch[] = [];\n\n\tsearch(lines: readonly string[], query: string): AltScreenSearchResult {\n\t\tlet sourceChanged = this.sourceLines?.length !== lines.length;\n\t\tif (!sourceChanged && this.sourceLines) {\n\t\t\tfor (let index = 0; index < lines.length; index++) {\n\t\t\t\tif (this.sourceLines[index] === lines[index]) continue;\n\t\t\t\tsourceChanged = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (sourceChanged || !this.corpus) {\n\t\t\tthis.sourceLines = Array.from(lines);\n\t\t\tthis.corpus = buildSearchCorpus(lines);\n\t\t}\n\n\t\tconst normalizedQuery = normalizeQuery(query);\n\t\tconst changed = sourceChanged || normalizedQuery !== this.normalizedQuery;\n\t\tif (changed) {\n\t\t\tthis.normalizedQuery = normalizedQuery;\n\t\t\tthis.matches = findSearchCorpusMatches(this.corpus, normalizedQuery);\n\t\t}\n\t\treturn { matches: this.matches, changed };\n\t}\n}\n\nexport function findAltScreenSearchMatches(lines: readonly string[], query: string): AltScreenSearchMatch[] {\n\tconst normalizedQuery = normalizeQuery(query);\n\treturn normalizedQuery ? findSearchCorpusMatches(buildSearchCorpus(lines), normalizedQuery) : [];\n}\n\nexport function getAltScreenSearchMatchKey(match: AltScreenSearchMatch): string {\n\tconst first = match.segments[0];\n\tconst last = match.segments[match.segments.length - 1];\n\treturn first && last ? `${first.row}:${first.startCol}:${last.row}:${last.endCol}` : \"\";\n}\n\nexport class AltScreenSearchComponent implements Component, Focusable {\n\tprivate readonly input = new Input({\n\t\tprompt: \" \",\n\t\tplaceholder: \"Find in transcript\",\n\t\tplaceholderStyle: (text) => `\\x1b[2m${text}\\x1b[22m`,\n\t});\n\tprivate readonly onQueryChange: (query: string) => void;\n\tprivate readonly navigationButtonStyle: (text: string, hovered: boolean) => string;\n\tprivate resultCount = 0;\n\tprivate resultIndex = -1;\n\tprivate previousButtonStart = -1;\n\tprivate previousButtonEnd = -1;\n\tprivate nextButtonStart = -1;\n\tprivate nextButtonEnd = -1;\n\tprivate hoveredNavigationDirection: -1 | 1 | undefined;\n\tprivate _focused = false;\n\n\tconstructor(\n\t\tonQueryChange: (query: string) => void,\n\t\tnavigationButtonStyle: (text: string, hovered: boolean) => string = (text) => text,\n\t) {\n\t\tthis.onQueryChange = onQueryChange;\n\t\tthis.navigationButtonStyle = navigationButtonStyle;\n\t}\n\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.input.focused = value;\n\t}\n\n\tsetResult(index: number, count: number): void {\n\t\tthis.resultIndex = index;\n\t\tthis.resultCount = count;\n\t}\n\n\tgetNavigationDirectionAt(row: number, column: number): -1 | 1 | undefined {\n\t\tif (row !== 2) return undefined;\n\t\tif (column >= this.previousButtonStart && column < this.previousButtonEnd) return -1;\n\t\tif (column >= this.nextButtonStart && column < this.nextButtonEnd) return 1;\n\t\treturn undefined;\n\t}\n\n\tsetHoveredNavigationDirection(direction: -1 | 1 | undefined): boolean {\n\t\tif (direction === this.hoveredNavigationDirection) return false;\n\t\tthis.hoveredNavigationDirection = direction;\n\t\treturn true;\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst previous = this.input.getValue();\n\t\tthis.input.handleInput(data);\n\t\tconst query = this.input.getValue();\n\t\tif (query !== previous) this.onQueryChange(query);\n\t}\n\n\tinvalidate(): void {\n\t\tthis.input.invalidate();\n\t}\n\n\trender(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\tconst innerWidth = Math.max(0, safeWidth - 2);\n\t\tconst formatKey = (key: string | undefined): string =>\n\t\t\tkey\n\t\t\t\t? key\n\t\t\t\t\t\t.split(\"+\")\n\t\t\t\t\t\t.map((part) => {\n\t\t\t\t\t\t\tif (process.platform === \"darwin\" && part.toLowerCase() === \"alt\") return \"Option\";\n\t\t\t\t\t\t\treturn part.charAt(0).toUpperCase() + part.slice(1);\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.join(\"+\")\n\t\t\t\t: \"Unbound\";\n\t\tconst keybindings = getKeybindings();\n\t\tconst previousKey = formatKey(keybindings.getKeys(\"tui.altScreen.searchPrevious\")[0]);\n\t\tconst nextKey = formatKey(keybindings.getKeys(\"tui.altScreen.searchNext\")[0]);\n\t\tconst query = this.input.getValue();\n\t\tconst result = !query\n\t\t\t? \"\"\n\t\t\t: this.resultCount === 0\n\t\t\t\t? \"No matches\"\n\t\t\t\t: `${this.resultIndex + 1}/${this.resultCount}`;\n\t\tconst resultSpace = Math.max(0, innerWidth - 3);\n\t\tconst visibleResult = truncateToWidth(result, resultSpace, \"\");\n\t\tconst resultText = visibleResult ? `\\x1b[2m ${visibleResult} \\x1b[22m` : \"\";\n\t\tconst inputWidth = Math.max(0, innerWidth - visibleWidth(resultText));\n\t\tconst inputLine = truncateToWidth(this.input.render(Math.max(1, inputWidth))[0] ?? \"\", inputWidth, \"\");\n\t\tconst inputPadding = \" \".repeat(Math.max(0, inputWidth - visibleWidth(inputLine)));\n\t\tconst content = `${inputLine}${inputPadding}${resultText}`;\n\n\t\tlet previousButton = `↑ ${previousKey}`;\n\t\tlet nextButton = `↓ ${nextKey}`;\n\t\tlet separator = \" · \";\n\t\tconst outerGapWidth = 1;\n\t\tconst availableControlsWidth = Math.max(0, innerWidth - outerGapWidth * 2 - 1);\n\t\tlet controlsWidth = visibleWidth(previousButton) + visibleWidth(separator) + visibleWidth(nextButton);\n\t\tif (controlsWidth > availableControlsWidth) {\n\t\t\tpreviousButton = \"↑\";\n\t\t\tnextButton = \"↓\";\n\t\t\tseparator = \" \";\n\t\t\tcontrolsWidth = visibleWidth(previousButton) + visibleWidth(separator) + visibleWidth(nextButton);\n\t\t}\n\t\tconst showButtons = controlsWidth <= availableControlsWidth;\n\t\tconst renderedButtons = showButtons\n\t\t\t? this.navigationButtonStyle(previousButton, this.hoveredNavigationDirection === -1) +\n\t\t\t\tseparator +\n\t\t\t\tthis.navigationButtonStyle(nextButton, this.hoveredNavigationDirection === 1)\n\t\t\t: \"\";\n\t\tconst outerGapsWidth = showButtons ? outerGapWidth * 2 : 0;\n\t\tconst rightRuleWidth = renderedButtons && innerWidth > controlsWidth + outerGapsWidth ? 1 : 0;\n\t\tconst leftRuleWidth = Math.max(\n\t\t\t0,\n\t\t\tinnerWidth - (showButtons ? controlsWidth : 0) - outerGapsWidth - rightRuleWidth,\n\t\t);\n\t\tconst previousStart = 1 + leftRuleWidth + outerGapWidth;\n\t\tthis.previousButtonStart = showButtons ? previousStart : -1;\n\t\tthis.previousButtonEnd = showButtons ? previousStart + visibleWidth(previousButton) : -1;\n\t\tthis.nextButtonStart = showButtons ? this.previousButtonEnd + visibleWidth(separator) : -1;\n\t\tthis.nextButtonEnd = showButtons ? this.nextButtonStart + visibleWidth(nextButton) : -1;\n\n\t\tif (safeWidth === 1) return [\"┌\", \"│\", \"└\"];\n\t\treturn [\n\t\t\t`┌${\"─\".repeat(innerWidth)}┐`,\n\t\t\t`│${content}│`,\n\t\t\t`└${\"─\".repeat(leftRuleWidth)}${renderedButtons ? \" \" : \"\"}${renderedButtons}${renderedButtons ? \" \" : \"\"}${\"─\".repeat(rightRuleWidth)}┘`,\n\t\t];\n\t}\n}\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface AutocompleteItem {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
group?: string;
|
|
6
|
+
}
|
|
7
|
+
/** A loaded skill offered as a `$name` reference or `/skill:name` alias completion */
|
|
8
|
+
export interface SkillCompletion {
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
type Awaitable<T> = T | Promise<T>;
|
|
13
|
+
export interface SlashCommand {
|
|
14
|
+
name: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
argumentHint?: string;
|
|
17
|
+
group?: string;
|
|
18
|
+
getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
|
|
19
|
+
}
|
|
20
|
+
export interface AutocompleteSuggestions {
|
|
21
|
+
items: AutocompleteItem[];
|
|
22
|
+
prefix: string;
|
|
23
|
+
}
|
|
24
|
+
export interface AutocompleteProvider {
|
|
25
|
+
/** Characters that should naturally trigger this provider at token boundaries. */
|
|
26
|
+
triggerCharacters?: string[];
|
|
27
|
+
getSuggestions(lines: string[], cursorLine: number, cursorCol: number, options: {
|
|
28
|
+
signal: AbortSignal;
|
|
29
|
+
force?: boolean;
|
|
30
|
+
}): Promise<AutocompleteSuggestions | null>;
|
|
31
|
+
applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string): {
|
|
32
|
+
lines: string[];
|
|
33
|
+
cursorLine: number;
|
|
34
|
+
cursorCol: number;
|
|
35
|
+
};
|
|
36
|
+
shouldTriggerFileCompletion?(lines: string[], cursorLine: number, cursorCol: number): boolean;
|
|
37
|
+
shouldTriggerMidWord?(textBeforeCursor: string, char: string): boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
40
|
+
private commands;
|
|
41
|
+
private basePath;
|
|
42
|
+
private fdPath;
|
|
43
|
+
private skillCompletions?;
|
|
44
|
+
triggerCharacters?: string[];
|
|
45
|
+
constructor(commands: (AutocompleteItem | SlashCommand)[] | undefined, basePath: string, fdPath?: string | null, skillCompletions?: (partial: string) => SkillCompletion[]);
|
|
46
|
+
/**
|
|
47
|
+
* Build suggestion items from skill candidates. Returns null when nothing should
|
|
48
|
+
* be suggested: no candidates, or the typed partial already exactly names the only
|
|
49
|
+
* candidate (lets Enter submit right after completing a full `$name` token).
|
|
50
|
+
* Labels show the bare skill name; the trigger symbol is already in the prompt.
|
|
51
|
+
*/
|
|
52
|
+
private buildSkillItems;
|
|
53
|
+
getSuggestions(lines: string[], cursorLine: number, cursorCol: number, options: {
|
|
54
|
+
signal: AbortSignal;
|
|
55
|
+
force?: boolean;
|
|
56
|
+
}): Promise<AutocompleteSuggestions | null>;
|
|
57
|
+
applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string): {
|
|
58
|
+
lines: string[];
|
|
59
|
+
cursorLine: number;
|
|
60
|
+
cursorCol: number;
|
|
61
|
+
};
|
|
62
|
+
private extractAtPrefix;
|
|
63
|
+
private extractPathPrefix;
|
|
64
|
+
private expandHomePath;
|
|
65
|
+
private resolveScopedFuzzyQuery;
|
|
66
|
+
private scopedPathForDisplay;
|
|
67
|
+
private getFileSuggestions;
|
|
68
|
+
private scoreEntry;
|
|
69
|
+
private getBaseDirSuggestions;
|
|
70
|
+
private getFuzzyFileSuggestions;
|
|
71
|
+
shouldTriggerMidWord(textBeforeCursor: string, char: string): boolean;
|
|
72
|
+
shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean;
|
|
73
|
+
}
|
|
74
|
+
export {};
|
|
75
|
+
//# sourceMappingURL=autocomplete.d.ts.map
|