@code-yeongyu/senpi-tui 2026.9.12 → 2026.9.13

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.
Files changed (94) hide show
  1. package/README.md +53 -4
  2. package/dist/alt-screen-search.d.ts +21 -1
  3. package/dist/alt-screen-search.d.ts.map +1 -1
  4. package/dist/alt-screen-search.js +199 -42
  5. package/dist/alt-screen-search.js.map +1 -1
  6. package/dist/components/box.d.ts +3 -1
  7. package/dist/components/box.d.ts.map +1 -1
  8. package/dist/components/box.js +28 -0
  9. package/dist/components/box.js.map +1 -1
  10. package/dist/components/editor.d.ts +6 -1
  11. package/dist/components/editor.d.ts.map +1 -1
  12. package/dist/components/editor.js +98 -18
  13. package/dist/components/editor.js.map +1 -1
  14. package/dist/components/input.d.ts +12 -1
  15. package/dist/components/input.d.ts.map +1 -1
  16. package/dist/components/input.js +40 -6
  17. package/dist/components/input.js.map +1 -1
  18. package/dist/components/latex.d.ts.map +1 -1
  19. package/dist/components/latex.js +7 -0
  20. package/dist/components/latex.js.map +1 -1
  21. package/dist/components/loader.d.ts +3 -0
  22. package/dist/components/loader.d.ts.map +1 -1
  23. package/dist/components/loader.js +20 -8
  24. package/dist/components/loader.js.map +1 -1
  25. package/dist/components/mouse-region.d.ts +12 -0
  26. package/dist/components/mouse-region.d.ts.map +1 -0
  27. package/dist/components/mouse-region.js +19 -0
  28. package/dist/components/mouse-region.js.map +1 -0
  29. package/dist/components/scroll-view.d.ts +6 -3
  30. package/dist/components/scroll-view.d.ts.map +1 -1
  31. package/dist/components/scroll-view.js +6 -1
  32. package/dist/components/scroll-view.js.map +1 -1
  33. package/dist/components/select-list.d.ts +4 -1
  34. package/dist/components/select-list.d.ts.map +1 -1
  35. package/dist/components/select-list.js +48 -2
  36. package/dist/components/select-list.js.map +1 -1
  37. package/dist/components/settings-list.d.ts +5 -1
  38. package/dist/components/settings-list.d.ts.map +1 -1
  39. package/dist/components/settings-list.js +54 -5
  40. package/dist/components/settings-list.js.map +1 -1
  41. package/dist/index.d.ts +3 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/layout.d.ts +3 -1
  46. package/dist/layout.d.ts.map +1 -1
  47. package/dist/layout.js +32 -9
  48. package/dist/layout.js.map +1 -1
  49. package/dist/native-modifiers.d.ts +2 -1
  50. package/dist/native-modifiers.d.ts.map +1 -1
  51. package/dist/native-modifiers.js +3 -44
  52. package/dist/native-modifiers.js.map +1 -1
  53. package/dist/native-platform.d.ts +18 -0
  54. package/dist/native-platform.d.ts.map +1 -0
  55. package/dist/native-platform.js +42 -0
  56. package/dist/native-platform.js.map +1 -0
  57. package/dist/terminal.d.ts.map +1 -1
  58. package/dist/terminal.js +2 -20
  59. package/dist/terminal.js.map +1 -1
  60. package/dist/tui-alt-screen.d.ts +31 -1
  61. package/dist/tui-alt-screen.d.ts.map +1 -1
  62. package/dist/tui-alt-screen.js +335 -41
  63. package/dist/tui-alt-screen.js.map +1 -1
  64. package/dist/tui.d.ts +83 -5
  65. package/dist/tui.d.ts.map +1 -1
  66. package/dist/tui.js +118 -4
  67. package/dist/tui.js.map +1 -1
  68. package/dist/utils.d.ts +2 -0
  69. package/dist/utils.d.ts.map +1 -1
  70. package/dist/utils.js +9 -0
  71. package/dist/utils.js.map +1 -1
  72. package/native/clipboard.h +170 -0
  73. package/native/darwin/README.md +8 -6
  74. package/native/darwin/build.sh +12 -5
  75. package/native/darwin/prebuilds/darwin-arm64/{darwin-modifiers.node → darwin-platform.node} +0 -0
  76. package/native/darwin/prebuilds/darwin-x64/{darwin-modifiers.node → darwin-platform.node} +0 -0
  77. package/native/darwin/src/darwin-platform.m +93 -0
  78. package/native/linux/README.md +23 -0
  79. package/native/linux/build.sh +65 -0
  80. package/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  81. package/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  82. package/native/linux/src/clipboard-worker.h +70 -0
  83. package/native/linux/src/linux-platform-x11.c +274 -0
  84. package/native/napi.h +89 -0
  85. package/native/win32/README.md +18 -7
  86. package/native/win32/build.mjs +7 -4
  87. package/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  88. package/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  89. package/native/win32/src/win32-platform.c +241 -0
  90. package/package.json +10 -3
  91. package/native/darwin/src/darwin-modifiers.c +0 -70
  92. package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  93. package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  94. package/native/win32/src/win32-console-mode.c +0 -119
package/README.md CHANGED
@@ -11,7 +11,7 @@ Minimal terminal UI framework with differential rendering and synchronized outpu
11
11
  - **Bracketed Paste Mode**: Handles large pastes correctly with markers for >10 line pastes
12
12
  - **Component-based**: Simple Component interface with render() method
13
13
  - **Theme Support**: Components accept theme interfaces for customizable styling
14
- - **Built-in Components**: Text, TruncatedText, Input, Editor, Markdown, Loader, SelectList, SettingsList, Spacer, Image, Box, Container, VStack, HStack, ScrollView
14
+ - **Built-in Components**: Text, TruncatedText, Input, Editor, Markdown, Loader, SelectList, SettingsList, MouseRegion, Spacer, Image, Box, Container, VStack, HStack, ScrollView
15
15
  - **Inline Images**: Renders images in terminals that support Kitty or iTerm2 graphics protocols
16
16
  - **Autocomplete Support**: File paths and slash commands
17
17
 
@@ -121,7 +121,7 @@ if (isViewportTUI(tui)) {
121
121
  }
122
122
  ```
123
123
 
124
- Stack entries support `basis`, `grow`, `shrink`, `minSize`, `maxSize`, and responsive `visible` callbacks. Mouse-wheel input targets the scroll view under the pointer and unused delta chains to outer scroll views by default. The primary scroll view receives the alternate-screen keyboard navigation actions and wheel input over non-scrollable regions. It can also jump between OSC 133 semantic prompt markers, matching common terminal prompt-navigation shortcuts. Press `Ctrl+Shift+F` to search its rendered content, `Enter`/`Ctrl+G` and `Shift+Enter`/`Ctrl+Shift+G` to move between matches, and `Escape` to close search. `TuiAltScreenOptions.searchMatchStyle` and `searchCurrentMatchStyle` customize match highlighting.
124
+ Stack entries support `basis`, `grow`, `shrink`, `minSize`, `maxSize`, and responsive `visible` callbacks. Mouse-wheel input targets the scroll view under the pointer and unused delta chains to outer scroll views by default. The primary scroll view receives the alternate-screen keyboard navigation actions and wheel input over non-scrollable regions. It can also jump between OSC 133 semantic prompt markers, matching common terminal prompt-navigation shortcuts. Press `Ctrl+Shift+F` to open or close its bordered search panel. The panel shows the configured previous/next shortcuts and provides clickable arrow controls; by default, `Enter`/`Ctrl+G` and `Shift+Enter`/`Ctrl+Shift+G` move between matches, and `Escape` also closes search. `TuiAltScreenOptions.searchMatchStyle` and `searchCurrentMatchStyle` customize match highlighting, while `searchNavigationButtonStyle` styles each arrow button and receives its hover state. `TuiAltScreenOptions.scrollToEndIndicator` renders a clickable label centered on the last row of a `follow: "end"` primary scroll view while it is scrolled away from the end; clicking it resumes end-following.
125
125
 
126
126
  Layout geometry is rebuilt for each requested frame. Stateful components are retained, and their existing rendered-line caches remain effective. Calling `render(width)` directly on these layout components produces an unbounded document, which is also used when alt mode restores the main screen.
127
127
 
@@ -177,6 +177,7 @@ handle.unfocus(); // Release focus to normal fallback
177
177
  handle.unfocus({ target: baseComponent }); // Release this overlay to a specific component
178
178
  handle.unfocus({ target: null }); // Release this overlay and leave focus empty
179
179
  handle.isFocused(); // Check if overlay has focus
180
+ handle.getBounds(); // Get last rendered terminal-relative bounds
180
181
 
181
182
  handle.unfocus();
182
183
  // Overlay loses focus; TUI falls back to another visible capturing overlay or the previous focus target.
@@ -211,6 +212,7 @@ All components implement:
211
212
  interface Component {
212
213
  render(width: number): string[];
213
214
  handleInput?(data: string): void;
215
+ handleMouse?(event: TuiMouseEvent): TuiMouseEventResult | undefined;
214
216
  invalidate?(): void;
215
217
  }
216
218
  ```
@@ -219,10 +221,50 @@ interface Component {
219
221
  |--------|-------------|
220
222
  | `render(width)` | Returns an array of strings, one per line. Each line **must not exceed `width`** or the TUI will error. Use `truncateToWidth()` or manual wrapping to ensure this. |
221
223
  | `handleInput?(data)` | Called when the component has focus and receives keyboard input. The `data` string contains raw terminal input (may include ANSI escape sequences). |
224
+ | `handleMouse?(event)` | Called by `TuiAltScreen` for normalized pointer input targeted at the component. |
222
225
  | `invalidate?()` | Called to clear any cached render state. Components should re-render from scratch on the next `render()` call. |
223
226
 
224
227
  The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
225
228
 
229
+ ### Mouse Input
230
+
231
+ `TuiAltScreen` normalizes SGR mouse input and hit-tests components and overlays. Events contain component-local `x`/`y`, absolute `screenX`/`screenY`, bounds, button, modifiers, click count, and wheel delta. `TuiMainScreen` does not capture mouse input because the terminal owns its scrollback.
232
+
233
+ ```typescript
234
+ import type { TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
235
+
236
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
237
+ if (event.type === "click" && event.button === "left") {
238
+ this.expanded = !this.expanded;
239
+ return { handled: true };
240
+ }
241
+ if (event.type === "press" && event.button === "left") {
242
+ return { handled: true, capture: true, focus: true };
243
+ }
244
+ if (event.type === "drag") {
245
+ this.updateFromPointer(event.x, event.y);
246
+ return { handled: true, render: true };
247
+ }
248
+ return undefined;
249
+ }
250
+ ```
251
+
252
+ Returning `handled` suppresses renderer-level fallback behavior. `capture` keeps subsequent drag and release events routed to the same component. `focus` requests keyboard focus. The optional `render` flag controls repainting: press, click, drag, and wheel default to rendering; move and release do not. Set `render: true` for a hover state that visibly changed, or `render: false` for a handled no-op. Render requests are coalesced and terminal output remains differential.
253
+
254
+ Unhandled gestures retain alternate-screen defaults: wheel input scrolls the nearest `ScrollView` and chains unused delta, primary-button drags select text, OSC 8 links open before parent click handlers, and unhandled right-click preserves configured paste behavior. A click is emitted only when press/release completes without a drag.
255
+
256
+ Use `MouseRegion` to add mouse behavior without changing a component's rendering:
257
+
258
+ ```typescript
259
+ const collapsible = new MouseRegion(content, (event) => {
260
+ if (event.type !== "click" || event.button !== "left") return undefined;
261
+ expanded = !expanded;
262
+ return { handled: true };
263
+ });
264
+ ```
265
+
266
+ `Container` and `Box` route events to nested children using geometry recorded by the last rendered frame, so pointer motion does not rerender children merely to hit-test them. Explicit `VStack`, `HStack`, and `ScrollView` layouts use the alternate-screen layout frame directly.
267
+
226
268
  ### Focusable Interface (IME Support)
227
269
 
228
270
  Components that display a text cursor and need IME (Input Method Editor) support should implement the `Focusable` interface:
@@ -247,7 +289,7 @@ When a `Focusable` component has focus, TUI:
247
289
  3. Positions the hardware terminal cursor at that location
248
290
  4. Shows the hardware cursor only when `showHardwareCursor` is enabled
249
291
 
250
- The cursor remains hidden by default. This keeps the fake cursor rendering, while still positioning the hardware cursor for terminals that track IME candidate windows with hidden cursors. Some terminals require a visible hardware cursor for IME positioning; enable it with the renderer constructor's `showHardwareCursor` argument, `setShowHardwareCursor(true)`, or `PI_HARDWARE_CURSOR=1`. The `Editor` and `Input` built-in components already implement this interface.
292
+ The cursor remains hidden by default. This keeps the fake cursor rendering, while still positioning the hardware cursor for terminals that track IME candidate windows with hidden cursors. Some terminals require a visible hardware cursor for IME positioning; enable it with the renderer constructor's `showHardwareCursor` argument or `setShowHardwareCursor(true)`. The `Editor` and `Input` built-in components already implement this interface.
251
293
 
252
294
  **Container components with embedded inputs:** When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child:
253
295
 
@@ -339,6 +381,8 @@ input.setValue("initial");
339
381
  input.getValue();
340
382
  ```
341
383
 
384
+ Clicking positions the cursor and gives the input keyboard focus in alternate-screen mode.
385
+
342
386
  **Key Bindings:**
343
387
  - `Enter` - Submit
344
388
  - `Ctrl+A` / `Ctrl+E` - Line start/end
@@ -374,6 +418,7 @@ editor.getPaddingX(); // Get current padding
374
418
  ```
375
419
 
376
420
  **Features:**
421
+ - Click-to-position cursor and clickable autocomplete rows in alternate-screen mode
377
422
  - Multi-line editing with word wrap
378
423
  - Slash command autocomplete (type `/`)
379
424
  - File path autocomplete (press `Tab`)
@@ -514,6 +559,8 @@ list.setFilter("opt"); // Filter items
514
559
  ```
515
560
 
516
561
  **Controls:**
562
+ - Mouse move/wheel: Highlight rows in alternate-screen mode
563
+ - Click: Select a row
517
564
  - Arrow keys: Navigate
518
565
  - Enter: Select
519
566
  - Escape: Cancel
@@ -554,6 +601,8 @@ settings.updateValue("theme", "light");
554
601
  ```
555
602
 
556
603
  **Controls:**
604
+ - Mouse move/wheel: Highlight rows in alternate-screen mode
605
+ - Click: Activate a row
557
606
  - Arrow keys: Navigate
558
607
  - Enter/Space: Activate (cycle value or open submenu)
559
608
  - Escape: Cancel
@@ -657,7 +706,7 @@ if (matchesKey(data, Key.enter)) {
657
706
  2. **Width Changed or Change Above Viewport**: Clear screen and fully re-render
658
707
  3. **Normal Update**: Move the cursor to the first changed line, clear to the end, and render changed lines
659
708
 
660
- `TuiAltScreen` owns a terminal-height viewport. Without an explicit layout root it preserves the legacy single-document scrolling behavior. With `setLayoutRoot()`, `VStack`, `HStack`, and nested `ScrollView` components can reserve fixed regions and independently scroll constrained regions. It updates changed viewport rows in place, follows streaming output while at the bottom, and preserves a manually selected scroll position while content grows. Mouse-wheel and configurable keyboard navigation scroll without modifying terminal scrollback, including jumps between OSC 133 semantic prompt markers. Clicking an OSC 8 hyperlink opens it with the configured URL handler. Dragging with the primary mouse button selects text and, unless `TuiAltScreenOptions.copyOnSelect` is `false`, copies it to the clipboard with OSC 52; holding the drag at a scroll view's top or bottom edge auto-scrolls and extends the selection into off-screen content. Kitty images support vertical viewport cropping; iTerm2 inline images fall back to text because the iTerm2 protocol cannot delete or crop placements during viewport repainting.
709
+ `TuiAltScreen` owns a terminal-height viewport. Without an explicit layout root it preserves the legacy single-document scrolling behavior. With `setLayoutRoot()`, `VStack`, `HStack`, and nested `ScrollView` components can reserve fixed regions and independently scroll constrained regions. It updates changed viewport rows in place, follows streaming output while at the bottom, and preserves a manually selected scroll position while content grows. Mouse-wheel and configurable keyboard navigation scroll without modifying terminal scrollback, including jumps between OSC 133 semantic prompt markers. Scrollbars support hover expansion, thumb dragging, and track-click jumping. Clicking an OSC 8 hyperlink opens it with the configured URL handler. Dragging with the primary mouse button selects text and, unless `TuiAltScreenOptions.copyOnSelect` is `false`, copies it to the clipboard with OSC 52; holding the drag at a scroll view's top or bottom edge auto-scrolls and extends the selection into off-screen content. Kitty images support vertical viewport cropping; iTerm2 inline images fall back to text because the iTerm2 protocol cannot delete or crop placements during viewport repainting.
661
710
 
662
711
  Both renderers wrap updates in **synchronized output** (`\x1b[?2026h` ... `\x1b[?2026l`) for atomic, flicker-free rendering.
663
712
 
@@ -7,18 +7,38 @@ export interface AltScreenSearchSegment {
7
7
  export interface AltScreenSearchMatch {
8
8
  segments: AltScreenSearchSegment[];
9
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
+ }
10
22
  export declare function findAltScreenSearchMatches(lines: readonly string[], query: string): AltScreenSearchMatch[];
11
23
  export declare function getAltScreenSearchMatchKey(match: AltScreenSearchMatch): string;
12
24
  export declare class AltScreenSearchComponent implements Component, Focusable {
13
25
  private readonly input;
14
26
  private readonly onQueryChange;
27
+ private readonly navigationButtonStyle;
15
28
  private resultCount;
16
29
  private resultIndex;
30
+ private previousButtonStart;
31
+ private previousButtonEnd;
32
+ private nextButtonStart;
33
+ private nextButtonEnd;
34
+ private hoveredNavigationDirection;
17
35
  private _focused;
18
- constructor(onQueryChange: (query: string) => void);
36
+ constructor(onQueryChange: (query: string) => void, navigationButtonStyle?: (text: string, hovered: boolean) => string);
19
37
  get focused(): boolean;
20
38
  set focused(value: boolean);
21
39
  setResult(index: number, count: number): void;
40
+ getNavigationDirectionAt(row: number, column: number): -1 | 1 | undefined;
41
+ setHoveredNavigationDirection(direction: -1 | 1 | undefined): boolean;
22
42
  handleInput(data: string): void;
23
43
  invalidate(): void;
24
44
  render(width: number): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"alt-screen-search.d.ts","sourceRoot":"","sources":["../src/alt-screen-search.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAWrD,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;AAkDD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,oBAAoB,EAAE,CA0B1G;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAI9E;AAED,qBAAa,wBAAyB,YAAW,SAAS,EAAE,SAAS;IACpE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA0B;IACxD,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EAEjD;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,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAK9B;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAe9B;CACD"}
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"}
@@ -1,37 +1,82 @@
1
1
  import { Input } from "./components/input.js";
2
+ import { getKeybindings } from "./keybindings.js";
2
3
  import { getGraphemeSegmenter, stripTerminalSequences, truncateToWidth, visibleWidth } from "./utils.js";
3
4
  const segmenter = getGraphemeSegmenter();
4
- function appendMappedText(text, span, corpus) {
5
- corpus.text += text;
6
- for (let index = 0; index < text.length; index++)
7
- corpus.source.push(span);
8
- }
5
+ const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
9
6
  function buildSearchCorpus(lines) {
10
- const corpus = { text: "", source: [] };
7
+ const chunks = [];
8
+ const spans = [];
9
+ let textLength = 0;
11
10
  let pendingSeparator = false;
11
+ const appendSeparator = () => {
12
+ if (!pendingSeparator)
13
+ return;
14
+ chunks.push(" ");
15
+ textLength += 1;
16
+ pendingSeparator = false;
17
+ };
12
18
  for (let row = 0; row < lines.length; row++) {
13
19
  const line = stripTerminalSequences(lines[row] ?? "");
14
20
  let column = 0;
15
- for (const grapheme of segmenter.segment(line)) {
16
- const text = grapheme.segment;
17
- const width = visibleWidth(text);
18
- if (/^\s+$/u.test(text)) {
19
- if (corpus.text.length > 0)
20
- pendingSeparator = true;
21
- column += width;
22
- continue;
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;
23
50
  }
24
- if (pendingSeparator) {
25
- appendMappedText(" ", undefined, corpus);
26
- pendingSeparator = false;
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;
27
74
  }
28
- appendMappedText(text, { row, startCol: column, endCol: column + width }, corpus);
29
- column += width;
30
75
  }
31
- if (corpus.text.length > 0)
76
+ if (textLength > 0)
32
77
  pendingSeparator = true;
33
78
  }
34
- return corpus;
79
+ return { text: chunks.join(""), spans };
35
80
  }
36
81
  function normalizeQuery(query) {
37
82
  return query.replace(/\s+/gu, " ").trim();
@@ -39,46 +84,96 @@ function normalizeQuery(query) {
39
84
  function escapeRegExp(text) {
40
85
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
41
86
  }
42
- export function findAltScreenSearchMatches(lines, query) {
43
- const normalizedQuery = normalizeQuery(query);
87
+ function findSearchCorpusMatches(corpus, normalizedQuery) {
44
88
  if (!normalizedQuery)
45
89
  return [];
46
- const corpus = buildSearchCorpus(lines);
47
90
  const expression = new RegExp(escapeRegExp(normalizedQuery), "giu");
48
91
  const matches = [];
92
+ let spanIndex = 0;
49
93
  for (const match of corpus.text.matchAll(expression)) {
50
94
  const start = match.index;
51
95
  const end = start + match[0].length;
96
+ while (spanIndex < corpus.spans.length && corpus.spans[spanIndex].textEnd <= start)
97
+ spanIndex += 1;
52
98
  const segments = [];
53
- for (let index = start; index < end; index++) {
54
- const span = corpus.source[index];
55
- if (!span)
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)
56
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;
57
109
  const previous = segments[segments.length - 1];
58
- if (previous && previous.row === span.row && span.startCol <= previous.endCol) {
59
- previous.endCol = Math.max(previous.endCol, span.endCol);
110
+ if (previous && previous.row === span.row && startCol <= previous.endCol) {
111
+ previous.endCol = Math.max(previous.endCol, endCol);
60
112
  }
61
113
  else {
62
- segments.push({ ...span });
114
+ segments.push({ row: span.row, startCol, endCol });
63
115
  }
64
116
  }
117
+ while (spanIndex < corpus.spans.length && corpus.spans[spanIndex].textEnd <= end)
118
+ spanIndex += 1;
65
119
  if (segments.length > 0)
66
120
  matches.push({ segments });
67
121
  }
68
122
  return matches;
69
123
  }
124
+ /** Cache the searchable corpus and matches while rendered transcript lines remain unchanged. */
125
+ export class AltScreenSearchIndex {
126
+ constructor() {
127
+ this.matches = [];
128
+ }
129
+ search(lines, query) {
130
+ let sourceChanged = this.sourceLines?.length !== lines.length;
131
+ if (!sourceChanged && this.sourceLines) {
132
+ for (let index = 0; index < lines.length; index++) {
133
+ if (this.sourceLines[index] === lines[index])
134
+ continue;
135
+ sourceChanged = true;
136
+ break;
137
+ }
138
+ }
139
+ if (sourceChanged || !this.corpus) {
140
+ this.sourceLines = Array.from(lines);
141
+ this.corpus = buildSearchCorpus(lines);
142
+ }
143
+ const normalizedQuery = normalizeQuery(query);
144
+ const changed = sourceChanged || normalizedQuery !== this.normalizedQuery;
145
+ if (changed) {
146
+ this.normalizedQuery = normalizedQuery;
147
+ this.matches = findSearchCorpusMatches(this.corpus, normalizedQuery);
148
+ }
149
+ return { matches: this.matches, changed };
150
+ }
151
+ }
152
+ export function findAltScreenSearchMatches(lines, query) {
153
+ const normalizedQuery = normalizeQuery(query);
154
+ return normalizedQuery ? findSearchCorpusMatches(buildSearchCorpus(lines), normalizedQuery) : [];
155
+ }
70
156
  export function getAltScreenSearchMatchKey(match) {
71
157
  const first = match.segments[0];
72
158
  const last = match.segments[match.segments.length - 1];
73
159
  return first && last ? `${first.row}:${first.startCol}:${last.row}:${last.endCol}` : "";
74
160
  }
75
161
  export class AltScreenSearchComponent {
76
- constructor(onQueryChange) {
77
- this.input = new Input();
162
+ constructor(onQueryChange, navigationButtonStyle = (text) => text) {
163
+ this.input = new Input({
164
+ prompt: " ",
165
+ placeholder: "Find in transcript",
166
+ placeholderStyle: (text) => `\x1b[2m${text}\x1b[22m`,
167
+ });
78
168
  this.resultCount = 0;
79
169
  this.resultIndex = -1;
170
+ this.previousButtonStart = -1;
171
+ this.previousButtonEnd = -1;
172
+ this.nextButtonStart = -1;
173
+ this.nextButtonEnd = -1;
80
174
  this._focused = false;
81
175
  this.onQueryChange = onQueryChange;
176
+ this.navigationButtonStyle = navigationButtonStyle;
82
177
  }
83
178
  get focused() {
84
179
  return this._focused;
@@ -91,6 +186,21 @@ export class AltScreenSearchComponent {
91
186
  this.resultIndex = index;
92
187
  this.resultCount = count;
93
188
  }
189
+ getNavigationDirectionAt(row, column) {
190
+ if (row !== 2)
191
+ return undefined;
192
+ if (column >= this.previousButtonStart && column < this.previousButtonEnd)
193
+ return -1;
194
+ if (column >= this.nextButtonStart && column < this.nextButtonEnd)
195
+ return 1;
196
+ return undefined;
197
+ }
198
+ setHoveredNavigationDirection(direction) {
199
+ if (direction === this.hoveredNavigationDirection)
200
+ return false;
201
+ this.hoveredNavigationDirection = direction;
202
+ return true;
203
+ }
94
204
  handleInput(data) {
95
205
  const previous = this.input.getValue();
96
206
  this.input.handleInput(data);
@@ -103,19 +213,66 @@ export class AltScreenSearchComponent {
103
213
  }
104
214
  render(width) {
105
215
  const safeWidth = Math.max(1, width);
106
- const label = " Find transcript";
216
+ const innerWidth = Math.max(0, safeWidth - 2);
217
+ const formatKey = (key) => key
218
+ ? key
219
+ .split("+")
220
+ .map((part) => {
221
+ if (process.platform === "darwin" && part.toLowerCase() === "alt")
222
+ return "Option";
223
+ return part.charAt(0).toUpperCase() + part.slice(1);
224
+ })
225
+ .join("+")
226
+ : "Unbound";
227
+ const keybindings = getKeybindings();
228
+ const previousKey = formatKey(keybindings.getKeys("tui.altScreen.searchPrevious")[0]);
229
+ const nextKey = formatKey(keybindings.getKeys("tui.altScreen.searchNext")[0]);
107
230
  const query = this.input.getValue();
108
- const status = !query
231
+ const result = !query
109
232
  ? ""
110
233
  : this.resultCount === 0
111
- ? "No matches "
112
- : `${this.resultIndex + 1}/${this.resultCount} `;
113
- const labelWidth = visibleWidth(label);
114
- const statusWidth = visibleWidth(status);
115
- const gap = " ".repeat(Math.max(1, safeWidth - labelWidth - statusWidth));
116
- const title = truncateToWidth(`${label}${gap}${status}`, safeWidth, "");
117
- const padding = " ".repeat(Math.max(0, safeWidth - visibleWidth(title)));
118
- return [`\x1b[7m${title}${padding}\x1b[27m`, ...this.input.render(safeWidth)];
234
+ ? "No matches"
235
+ : `${this.resultIndex + 1}/${this.resultCount}`;
236
+ const resultSpace = Math.max(0, innerWidth - 3);
237
+ const visibleResult = truncateToWidth(result, resultSpace, "");
238
+ const resultText = visibleResult ? `\x1b[2m ${visibleResult} \x1b[22m` : "";
239
+ const inputWidth = Math.max(0, innerWidth - visibleWidth(resultText));
240
+ const inputLine = truncateToWidth(this.input.render(Math.max(1, inputWidth))[0] ?? "", inputWidth, "");
241
+ const inputPadding = " ".repeat(Math.max(0, inputWidth - visibleWidth(inputLine)));
242
+ const content = `${inputLine}${inputPadding}${resultText}`;
243
+ let previousButton = `↑ ${previousKey}`;
244
+ let nextButton = `↓ ${nextKey}`;
245
+ let separator = " · ";
246
+ const outerGapWidth = 1;
247
+ const availableControlsWidth = Math.max(0, innerWidth - outerGapWidth * 2 - 1);
248
+ let controlsWidth = visibleWidth(previousButton) + visibleWidth(separator) + visibleWidth(nextButton);
249
+ if (controlsWidth > availableControlsWidth) {
250
+ previousButton = "↑";
251
+ nextButton = "↓";
252
+ separator = " ";
253
+ controlsWidth = visibleWidth(previousButton) + visibleWidth(separator) + visibleWidth(nextButton);
254
+ }
255
+ const showButtons = controlsWidth <= availableControlsWidth;
256
+ const renderedButtons = showButtons
257
+ ? this.navigationButtonStyle(previousButton, this.hoveredNavigationDirection === -1) +
258
+ separator +
259
+ this.navigationButtonStyle(nextButton, this.hoveredNavigationDirection === 1)
260
+ : "";
261
+ const outerGapsWidth = showButtons ? outerGapWidth * 2 : 0;
262
+ const rightRuleWidth = renderedButtons && innerWidth > controlsWidth + outerGapsWidth ? 1 : 0;
263
+ const leftRuleWidth = Math.max(0, innerWidth - (showButtons ? controlsWidth : 0) - outerGapsWidth - rightRuleWidth);
264
+ const previousStart = 1 + leftRuleWidth + outerGapWidth;
265
+ this.previousButtonStart = showButtons ? previousStart : -1;
266
+ this.previousButtonEnd = showButtons ? previousStart + visibleWidth(previousButton) : -1;
267
+ this.nextButtonStart = showButtons ? this.previousButtonEnd + visibleWidth(separator) : -1;
268
+ this.nextButtonEnd = showButtons ? this.nextButtonStart + visibleWidth(nextButton) : -1;
269
+ if (safeWidth === 1)
270
+ return ["┌", "│", "└"];
271
+ return [
272
+ `┌${"─".repeat(innerWidth)}┐`,
273
+ `│${content}│`,
274
+ `└${"─".repeat(leftRuleWidth)}${renderedButtons ? " " : ""}${renderedButtons}${renderedButtons ? " " : ""}${"─".repeat(rightRuleWidth)}┘`,
275
+ ];
119
276
  }
120
277
  }
121
278
  //# sourceMappingURL=alt-screen-search.js.map
@@ -1 +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;AAE9C,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEzG,MAAM,SAAS,GAAG,oBAAoB,EAAE,CAAC;AAkBzC,SAAS,gBAAgB,CACxB,IAAY,EACZ,IAAkC,EAClC,MAAqE;IAErE,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC;IACpB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE;QAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAwB;IAIlD,MAAM,MAAM,GAAkE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACvG,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAE7B,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;QACf,KAAK,MAAM,QAAQ,IAAI,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC;YAC9B,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACzB,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;oBAAE,gBAAgB,GAAG,IAAI,CAAC;gBACpD,MAAM,IAAI,KAAK,CAAC;gBAChB,SAAS;YACV,CAAC;YACD,IAAI,gBAAgB,EAAE,CAAC;gBACtB,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;gBACzC,gBAAgB,GAAG,KAAK,CAAC;YAC1B,CAAC;YACD,gBAAgB,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,EAAE,MAAM,CAAC,CAAC;YAClF,MAAM,IAAI,KAAK,CAAC;QACjB,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,gBAAgB,GAAG,IAAI,CAAC;IACrD,CAAC;IAED,OAAO,MAAM,CAAC;AACf,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACpC,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAC3C,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IACjC,OAAO,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,KAAwB,EAAE,KAAa;IACjF,MAAM,eAAe,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,CAAC,eAAe;QAAE,OAAO,EAAE,CAAC;IAEhC,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC,CAAC;IACpE,MAAM,OAAO,GAA2B,EAAE,CAAC;IAE3C,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,MAAM,QAAQ,GAA6B,EAAE,CAAC;QAC9C,KAAK,IAAI,KAAK,GAAG,KAAK,EAAE,KAAK,GAAG,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC;YAC9C,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAClC,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,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,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;gBAC/E,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC1D,CAAC;iBAAM,CAAC;gBACP,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;YAC5B,CAAC;QACF,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,KAA2B;IACrE,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;AACzF,CAAC;AAED,MAAM,OAAO,wBAAwB;IAOpC,YAAY,aAAsC;QANjC,UAAK,GAAG,IAAI,KAAK,EAAE,CAAC;QAE7B,gBAAW,GAAG,CAAC,CAAC;QAChB,gBAAW,GAAG,CAAC,CAAC,CAAC;QACjB,aAAQ,GAAG,KAAK,CAAC;QAGxB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACpC,CAAC;IAED,IAAI,OAAO;QACV,OAAO,IAAI,CAAC,QAAQ,CAAC;IACtB,CAAC;IAED,IAAI,OAAO,CAAC,KAAc;QACzB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC;IAC5B,CAAC;IAED,SAAS,CAAC,KAAa,EAAE,KAAa;QACrC,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;IAC1B,CAAC;IAED,WAAW,CAAC,IAAY;QACvB,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;IACnD,CAAC;IAED,UAAU;QACT,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;IACzB,CAAC;IAED,MAAM,CAAC,KAAa;QACnB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,kBAAkB,CAAC;QACjC,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,aAAa;gBACf,CAAC,CAAC,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,IAAI,IAAI,CAAC,WAAW,GAAG,CAAC;QACnD,MAAM,UAAU,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QACvC,MAAM,WAAW,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC;QAC1E,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,KAAK,GAAG,GAAG,GAAG,MAAM,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC;QACxE,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACzE,OAAO,CAAC,UAAU,KAAK,GAAG,OAAO,UAAU,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/E,CAAC;CACD","sourcesContent":["import { Input } from \"./components/input.ts\";\nimport type { Component, Focusable } from \"./tui.ts\";\nimport { getGraphemeSegmenter, stripTerminalSequences, truncateToWidth, visibleWidth } from \"./utils.ts\";\n\nconst segmenter = getGraphemeSegmenter();\n\ninterface SearchSourceSpan {\n\trow: number;\n\tstartCol: number;\n\tendCol: number;\n}\n\nexport interface AltScreenSearchSegment {\n\trow: number;\n\tstartCol: number;\n\tendCol: number;\n}\n\nexport interface AltScreenSearchMatch {\n\tsegments: AltScreenSearchSegment[];\n}\n\nfunction appendMappedText(\n\ttext: string,\n\tspan: SearchSourceSpan | undefined,\n\tcorpus: { text: string; source: Array<SearchSourceSpan | undefined> },\n): void {\n\tcorpus.text += text;\n\tfor (let index = 0; index < text.length; index++) corpus.source.push(span);\n}\n\nfunction buildSearchCorpus(lines: readonly string[]): {\n\ttext: string;\n\tsource: Array<SearchSourceSpan | undefined>;\n} {\n\tconst corpus: { text: string; source: Array<SearchSourceSpan | undefined> } = { text: \"\", source: [] };\n\tlet pendingSeparator = false;\n\n\tfor (let row = 0; row < lines.length; row++) {\n\t\tconst line = stripTerminalSequences(lines[row] ?? \"\");\n\t\tlet column = 0;\n\t\tfor (const grapheme of segmenter.segment(line)) {\n\t\t\tconst text = grapheme.segment;\n\t\t\tconst width = visibleWidth(text);\n\t\t\tif (/^\\s+$/u.test(text)) {\n\t\t\t\tif (corpus.text.length > 0) pendingSeparator = true;\n\t\t\t\tcolumn += width;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (pendingSeparator) {\n\t\t\t\tappendMappedText(\" \", undefined, corpus);\n\t\t\t\tpendingSeparator = false;\n\t\t\t}\n\t\t\tappendMappedText(text, { row, startCol: column, endCol: column + width }, corpus);\n\t\t\tcolumn += width;\n\t\t}\n\t\tif (corpus.text.length > 0) pendingSeparator = true;\n\t}\n\n\treturn corpus;\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\nexport function findAltScreenSearchMatches(lines: readonly string[], query: string): AltScreenSearchMatch[] {\n\tconst normalizedQuery = normalizeQuery(query);\n\tif (!normalizedQuery) return [];\n\n\tconst corpus = buildSearchCorpus(lines);\n\tconst expression = new RegExp(escapeRegExp(normalizedQuery), \"giu\");\n\tconst matches: AltScreenSearchMatch[] = [];\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\tconst segments: AltScreenSearchSegment[] = [];\n\t\tfor (let index = start; index < end; index++) {\n\t\t\tconst span = corpus.source[index];\n\t\t\tif (!span) continue;\n\t\t\tconst previous = segments[segments.length - 1];\n\t\t\tif (previous && previous.row === span.row && span.startCol <= previous.endCol) {\n\t\t\t\tprevious.endCol = Math.max(previous.endCol, span.endCol);\n\t\t\t} else {\n\t\t\t\tsegments.push({ ...span });\n\t\t\t}\n\t\t}\n\t\tif (segments.length > 0) matches.push({ segments });\n\t}\n\n\treturn matches;\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\tprivate readonly onQueryChange: (query: string) => void;\n\tprivate resultCount = 0;\n\tprivate resultIndex = -1;\n\tprivate _focused = false;\n\n\tconstructor(onQueryChange: (query: string) => void) {\n\t\tthis.onQueryChange = onQueryChange;\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\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 label = \" Find transcript\";\n\t\tconst query = this.input.getValue();\n\t\tconst status = !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 labelWidth = visibleWidth(label);\n\t\tconst statusWidth = visibleWidth(status);\n\t\tconst gap = \" \".repeat(Math.max(1, safeWidth - labelWidth - statusWidth));\n\t\tconst title = truncateToWidth(`${label}${gap}${status}`, safeWidth, \"\");\n\t\tconst padding = \" \".repeat(Math.max(0, safeWidth - visibleWidth(title)));\n\t\treturn [`\\x1b[7m${title}${padding}\\x1b[27m`, ...this.input.render(safeWidth)];\n\t}\n}\n"]}
1
+ {"version":3,"file":"alt-screen-search.js","sourceRoot":"","sources":["../src/alt-screen-search.ts"],"names":[],"mappings":"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","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"]}
@@ -1,4 +1,4 @@
1
- import type { Component } from "../tui.ts";
1
+ import { type Component, type TuiMouseDispatchResult, type TuiMouseEvent } from "../tui.ts";
2
2
  /**
3
3
  * Box component - a container that applies padding and background to all children
4
4
  */
@@ -9,6 +9,7 @@ export declare class Box implements Component {
9
9
  private bgFn?;
10
10
  private disposed;
11
11
  private cache?;
12
+ private mouseLayout?;
12
13
  constructor(paddingX?: number, paddingY?: number, bgFn?: (text: string) => string);
13
14
  addChild(component: Component): void;
14
15
  removeChild(component: Component): void;
@@ -19,6 +20,7 @@ export declare class Box implements Component {
19
20
  private invalidateCache;
20
21
  private matchCache;
21
22
  invalidate(): void;
23
+ handleMouse(event: TuiMouseEvent): TuiMouseDispatchResult | undefined;
22
24
  render(width: number): string[];
23
25
  private applyBg;
24
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAU3C;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IACxC,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,KAAK,CAAC,CAAc;IAE5B,YAAY,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAItE;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAGnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAOtC;IAED,KAAK,IAAI,IAAI,CAMZ;IAED,SAAS,IAAI,IAAI,CAGhB;IAED,OAAO,IAAI,IAAI,CAOd;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAG7C;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAWlB,UAAU,IAAI,IAAI,CAKjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAmD9B;IAED,OAAO,CAAC,OAAO;CAUf"}
1
+ {"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../../src/components/box.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAsB,KAAK,sBAAsB,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAUhH;;GAEG;AACH,qBAAa,GAAI,YAAW,SAAS;IACpC,QAAQ,EAAE,SAAS,EAAE,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAC,CAA2B;IACxC,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,WAAW,CAAC,CAA+E;IAEnG,YAAY,QAAQ,SAAI,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAItE;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAGnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAOtC;IAED,KAAK,IAAI,IAAI,CAMZ;IAED,SAAS,IAAI,IAAI,CAGhB;IAED,OAAO,IAAI,IAAI,CAOd;IAED,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAG7C;IAED,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAWlB,UAAU,IAAI,IAAI,CAKjB;IAED,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,sBAAsB,GAAG,SAAS,CAwBpE;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAsD9B;IAED,OAAO,CAAC,OAAO;CAUf"}