@openadapter/koda-tui 1.0.0-beta.3

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 (60) hide show
  1. package/README.md +791 -0
  2. package/dist/autocomplete.d.ts +53 -0
  3. package/dist/autocomplete.js +2 -0
  4. package/dist/components/box.d.ts +21 -0
  5. package/dist/components/box.js +1 -0
  6. package/dist/components/cancellable-loader.d.ts +21 -0
  7. package/dist/components/cancellable-loader.js +1 -0
  8. package/dist/components/editor.d.ts +249 -0
  9. package/dist/components/editor.js +17 -0
  10. package/dist/components/image.d.ts +27 -0
  11. package/dist/components/image.js +1 -0
  12. package/dist/components/input.d.ts +36 -0
  13. package/dist/components/input.js +2 -0
  14. package/dist/components/loader.d.ts +30 -0
  15. package/dist/components/loader.js +1 -0
  16. package/dist/components/markdown.d.ts +95 -0
  17. package/dist/components/markdown.js +5 -0
  18. package/dist/components/select-list.d.ts +49 -0
  19. package/dist/components/select-list.js +1 -0
  20. package/dist/components/settings-list.d.ts +49 -0
  21. package/dist/components/settings-list.js +1 -0
  22. package/dist/components/spacer.d.ts +11 -0
  23. package/dist/components/spacer.js +1 -0
  24. package/dist/components/text.d.ts +18 -0
  25. package/dist/components/text.js +1 -0
  26. package/dist/components/truncated-text.d.ts +12 -0
  27. package/dist/components/truncated-text.js +2 -0
  28. package/dist/editor-component.d.ts +38 -0
  29. package/dist/editor-component.js +0 -0
  30. package/dist/fuzzy.d.ts +15 -0
  31. package/dist/fuzzy.js +1 -0
  32. package/dist/index.d.ts +22 -0
  33. package/dist/index.js +1 -0
  34. package/dist/keybindings.d.ts +192 -0
  35. package/dist/keybindings.js +1 -0
  36. package/dist/keys.d.ts +183 -0
  37. package/dist/keys.js +5 -0
  38. package/dist/kill-ring.d.ts +27 -0
  39. package/dist/kill-ring.js +1 -0
  40. package/dist/native-modifiers.d.ts +2 -0
  41. package/dist/native-modifiers.js +1 -0
  42. package/dist/stdin-buffer.d.ts +49 -0
  43. package/dist/stdin-buffer.js +1 -0
  44. package/dist/terminal-image.d.ts +89 -0
  45. package/dist/terminal-image.js +1 -0
  46. package/dist/terminal.d.ts +112 -0
  47. package/dist/terminal.js +1 -0
  48. package/dist/tui.d.ts +241 -0
  49. package/dist/tui.js +11 -0
  50. package/dist/undo-stack.d.ts +16 -0
  51. package/dist/undo-stack.js +1 -0
  52. package/dist/utils.d.ts +83 -0
  53. package/dist/utils.js +2 -0
  54. package/dist/word-navigation.d.ts +24 -0
  55. package/dist/word-navigation.js +1 -0
  56. package/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  57. package/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  58. package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  59. package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  60. package/package.json +55 -0
package/dist/tui.d.ts ADDED
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Minimal TUI implementation with differential rendering
3
+ */
4
+ import type { Terminal } from "./terminal.ts";
5
+ import { visibleWidth } from "./utils.ts";
6
+ /**
7
+ * Component interface - all components must implement this
8
+ */
9
+ export interface Component {
10
+ /**
11
+ * Render the component to lines for the given viewport width
12
+ * @param width - Current viewport width
13
+ * @returns Array of strings, each representing a line
14
+ */
15
+ render(width: number): string[];
16
+ /**
17
+ * Optional handler for keyboard input when component has focus
18
+ */
19
+ handleInput?(data: string): void;
20
+ /**
21
+ * If true, component receives key release events (Kitty protocol).
22
+ * Default is false - release events are filtered out.
23
+ */
24
+ wantsKeyRelease?: boolean;
25
+ /**
26
+ * Invalidate any cached rendering state.
27
+ * Called when theme changes or when component needs to re-render from scratch.
28
+ */
29
+ invalidate(): void;
30
+ }
31
+ type InputListenerResult = {
32
+ consume?: boolean;
33
+ data?: string;
34
+ } | undefined;
35
+ type InputListener = (data: string) => InputListenerResult;
36
+ /**
37
+ * Interface for components that can receive focus and display a hardware cursor.
38
+ * When focused, the component should emit CURSOR_MARKER at the cursor position
39
+ * in its render output. TUI will find this marker and position the hardware
40
+ * cursor there for proper IME candidate window positioning.
41
+ */
42
+ export interface Focusable {
43
+ /** Set by TUI when focus changes. Component should emit CURSOR_MARKER when true. */
44
+ focused: boolean;
45
+ }
46
+ /** Type guard to check if a component implements Focusable */
47
+ export declare function isFocusable(component: Component | null): component is Component & Focusable;
48
+ /**
49
+ * Cursor position marker - APC (Application Program Command) sequence.
50
+ * This is a zero-width escape sequence that terminals ignore.
51
+ * Components emit this at the cursor position when focused.
52
+ * TUI finds and strips this marker, then positions the hardware cursor there.
53
+ */
54
+ export declare const CURSOR_MARKER = "\u001B_pi:c\u0007";
55
+ export { visibleWidth };
56
+ /**
57
+ * Anchor position for overlays
58
+ */
59
+ export type OverlayAnchor = "center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top-center" | "bottom-center" | "left-center" | "right-center";
60
+ /**
61
+ * Margin configuration for overlays
62
+ */
63
+ export interface OverlayMargin {
64
+ top?: number;
65
+ right?: number;
66
+ bottom?: number;
67
+ left?: number;
68
+ }
69
+ /** Value that can be absolute (number) or percentage (string like "50%") */
70
+ export type SizeValue = number | `${number}%`;
71
+ /**
72
+ * Options for overlay positioning and sizing.
73
+ * Values can be absolute numbers or percentage strings (e.g., "50%").
74
+ */
75
+ export interface OverlayOptions {
76
+ /** Width in columns, or percentage of terminal width (e.g., "50%") */
77
+ width?: SizeValue;
78
+ /** Minimum width in columns */
79
+ minWidth?: number;
80
+ /** Maximum height in rows, or percentage of terminal height (e.g., "50%") */
81
+ maxHeight?: SizeValue;
82
+ /** Anchor point for positioning (default: 'center') */
83
+ anchor?: OverlayAnchor;
84
+ /** Horizontal offset from anchor position (positive = right) */
85
+ offsetX?: number;
86
+ /** Vertical offset from anchor position (positive = down) */
87
+ offsetY?: number;
88
+ /** Row position: absolute number, or percentage (e.g., "25%" = 25% from top) */
89
+ row?: SizeValue;
90
+ /** Column position: absolute number, or percentage (e.g., "50%" = centered horizontally) */
91
+ col?: SizeValue;
92
+ /** Margin from terminal edges. Number applies to all sides. */
93
+ margin?: OverlayMargin | number;
94
+ /**
95
+ * Control overlay visibility based on terminal dimensions.
96
+ * If provided, overlay is only rendered when this returns true.
97
+ * Called each render cycle with current terminal dimensions.
98
+ */
99
+ visible?: (termWidth: number, termHeight: number) => boolean;
100
+ /** If true, don't capture keyboard focus when shown */
101
+ nonCapturing?: boolean;
102
+ }
103
+ /** Options for {@link OverlayHandle.unfocus}. */
104
+ export interface OverlayUnfocusOptions {
105
+ /** Explicit target to focus after releasing this overlay. */
106
+ target: Component | null;
107
+ }
108
+ /**
109
+ * Handle returned by showOverlay for controlling the overlay
110
+ */
111
+ export interface OverlayHandle {
112
+ /** Permanently remove the overlay (cannot be shown again) */
113
+ hide(): void;
114
+ /** Temporarily hide or show the overlay */
115
+ setHidden(hidden: boolean): void;
116
+ /** Check if overlay is temporarily hidden */
117
+ isHidden(): boolean;
118
+ /** Focus this overlay and bring it to the visual front */
119
+ focus(): void;
120
+ /** Release focus to the next visible capturing overlay or previous target, or to an explicit target when provided */
121
+ unfocus(options?: OverlayUnfocusOptions): void;
122
+ /** Check if this overlay currently has focus */
123
+ isFocused(): boolean;
124
+ }
125
+ /**
126
+ * Container - a component that contains other components
127
+ */
128
+ export declare class Container implements Component {
129
+ children: Component[];
130
+ addChild(component: Component): void;
131
+ removeChild(component: Component): void;
132
+ clear(): void;
133
+ invalidate(): void;
134
+ render(width: number): string[];
135
+ }
136
+ /**
137
+ * TUI - Main class for managing terminal UI with differential rendering
138
+ */
139
+ export declare class TUI extends Container {
140
+ terminal: Terminal;
141
+ private previousLines;
142
+ private previousKittyImageIds;
143
+ private previousWidth;
144
+ private previousHeight;
145
+ private focusedComponent;
146
+ private inputListeners;
147
+ /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
148
+ onDebug?: () => void;
149
+ private renderRequested;
150
+ private renderTimer;
151
+ private lastRenderAt;
152
+ private static readonly MIN_RENDER_INTERVAL_MS;
153
+ private cursorRow;
154
+ private hardwareCursorRow;
155
+ private showHardwareCursor;
156
+ private clearOnShrink;
157
+ private maxLinesRendered;
158
+ private previousViewportTop;
159
+ private fullRedrawCount;
160
+ private stopped;
161
+ private focusOrderCounter;
162
+ private overlayStack;
163
+ private overlayFocusRestore;
164
+ constructor(terminal: Terminal, showHardwareCursor?: boolean);
165
+ get fullRedraws(): number;
166
+ getShowHardwareCursor(): boolean;
167
+ setShowHardwareCursor(enabled: boolean): void;
168
+ getClearOnShrink(): boolean;
169
+ /**
170
+ * Set whether to trigger full re-render when content shrinks.
171
+ * When true (default), empty rows are cleared when content shrinks.
172
+ * When false, empty rows remain (reduces redraws on slower terminals).
173
+ */
174
+ setClearOnShrink(enabled: boolean): void;
175
+ setFocus(component: Component | null): void;
176
+ private setFocusInternal;
177
+ private clearOverlayFocusRestore;
178
+ private clearOverlayFocusRestoreFor;
179
+ private resolveBlockedOverlayFocusResume;
180
+ private getVisibleOverlayFocusRestore;
181
+ private isOverlayFocusAncestor;
182
+ private retargetOverlayPreFocus;
183
+ private isComponentMounted;
184
+ private containsComponent;
185
+ /**
186
+ * Show an overlay component with configurable positioning and sizing.
187
+ * Returns a handle to control the overlay's visibility.
188
+ */
189
+ showOverlay(component: Component, options?: OverlayOptions): OverlayHandle;
190
+ /** Hide the topmost overlay and restore previous focus. */
191
+ hideOverlay(): void;
192
+ /** Check if there are any visible overlays */
193
+ hasOverlay(): boolean;
194
+ /** Check if an overlay entry is currently visible */
195
+ private isOverlayVisible;
196
+ /** Find the visual-frontmost visible capturing overlay, if any */
197
+ private getTopmostVisibleOverlay;
198
+ invalidate(): void;
199
+ start(): void;
200
+ addInputListener(listener: InputListener): () => void;
201
+ removeInputListener(listener: InputListener): void;
202
+ private queryCellSize;
203
+ stop(): void;
204
+ requestRender(force?: boolean): void;
205
+ private scheduleRender;
206
+ private handleInput;
207
+ private consumeCellSizeResponse;
208
+ /**
209
+ * Resolve overlay layout from options.
210
+ * Returns { width, row, col, maxHeight } for rendering.
211
+ */
212
+ private resolveOverlayLayout;
213
+ private resolveAnchorRow;
214
+ private resolveAnchorCol;
215
+ /** Composite all overlays into content lines (sorted by focusOrder, higher = on top). */
216
+ private compositeOverlays;
217
+ private static readonly SEGMENT_RESET;
218
+ private applyLineResets;
219
+ private collectKittyImageIds;
220
+ private deleteKittyImages;
221
+ private expandLastChangedForKittyImages;
222
+ private deleteChangedKittyImages;
223
+ /** Splice overlay content into a base line at a specific column. Single-pass optimized. */
224
+ private compositeLineAt;
225
+ /**
226
+ * Find and extract cursor position from rendered lines.
227
+ * Searches for CURSOR_MARKER, calculates its position, and strips it from the output.
228
+ * Only scans the bottom terminal height lines (visible viewport).
229
+ * @param lines - Rendered lines to search
230
+ * @param height - Terminal height (visible viewport size)
231
+ * @returns Cursor position { row, col } or null if no marker found
232
+ */
233
+ private extractCursorPosition;
234
+ private doRender;
235
+ /**
236
+ * Position the hardware cursor for IME candidate window.
237
+ * @param cursorPos The cursor position extracted from rendered output, or null
238
+ * @param totalLines Total number of rendered lines
239
+ */
240
+ private positionHardwareCursor;
241
+ }
package/dist/tui.js ADDED
@@ -0,0 +1,11 @@
1
+ var J=Object.defineProperty;var x=(b,e)=>J(b,"name",{value:e,configurable:!0});import*as I from"node:fs";import*as q from"node:os";import*as V from"node:path";import{performance as A}from"node:perf_hooks";import{isKeyRelease as X,matchesKey as Y}from"./keys.js";import{deleteKittyImage as Q,getCapabilities as Z,isImageLine as K,setCellDimensions as ee}from"./terminal-image.js";import{extractSegments as te,normalizeTerminalOutput as se,sliceByColumn as _,sliceWithWidth as re,visibleWidth as S}from"./utils.js";const N="\x1B_G";function B(b){const e=b.indexOf(N);if(e===-1)return[];const s=e+N.length,r=b.indexOf(";",s);if(r===-1)return[];const t=b.slice(s,r);for(const i of t.split(",")){const[n,l]=i.split("=",2);if(n!=="i"||l===void 0)continue;const u=Number(l);if(Number.isInteger(u)&&u>0&&u<=4294967295)return[u]}return[]}x(B,"extractKittyImageIds");function P(b){return b!==null&&"focused"in b}x(P,"isFocusable");const W="\x1B_pi:c\x07";function z(b,e){if(b===void 0)return;if(typeof b=="number")return b;const s=b.match(/^(\d+(?:\.\d+)?)%$/);if(s)return Math.floor(e*parseFloat(s[1])/100)}x(z,"parseSizeValue");function ie(){return!!process.env.TERMUX_VERSION}x(ie,"isTermuxSession");class G{static{x(this,"Container")}children=[];addChild(e){this.children.push(e)}removeChild(e){const s=this.children.indexOf(e);s!==-1&&this.children.splice(s,1)}clear(){this.children=[]}invalidate(){for(const e of this.children)e.invalidate?.()}render(e){const s=[];for(const r of this.children){const t=r.render(e);for(const i of t)s.push(i)}return s}}class E extends G{static{x(this,"TUI")}terminal;previousLines=[];previousKittyImageIds=new Set;previousWidth=0;previousHeight=0;focusedComponent=null;inputListeners=new Set;onDebug;renderRequested=!1;renderTimer;lastRenderAt=0;static MIN_RENDER_INTERVAL_MS=16;cursorRow=0;hardwareCursorRow=0;showHardwareCursor=process.env.KODA_HARDWARE_CURSOR==="1";clearOnShrink=process.env.KODA_CLEAR_ON_SHRINK==="1";maxLinesRendered=0;previousViewportTop=0;fullRedrawCount=0;stopped=!1;focusOrderCounter=0;overlayStack=[];overlayFocusRestore={status:"inactive"};constructor(e,s){super(),this.terminal=e,s!==void 0&&(this.showHardwareCursor=s)}get fullRedraws(){return this.fullRedrawCount}getShowHardwareCursor(){return this.showHardwareCursor}setShowHardwareCursor(e){this.showHardwareCursor!==e&&(this.showHardwareCursor=e,e||this.terminal.hideCursor(),this.requestRender())}getClearOnShrink(){return this.clearOnShrink}setClearOnShrink(e){this.clearOnShrink=e}setFocus(e){this.setFocusInternal({component:e,overlayFocusRestore:"clear"})}setFocusInternal({component:e,overlayFocusRestore:s}){const r=this.focusedComponent;let t=e;const i=r?this.overlayStack.find(m=>m.component===r&&this.isOverlayVisible(m)):void 0,n=t?this.overlayStack.some(m=>m.component===t):!1,l=this.getVisibleOverlayFocusRestore();t&&!n?l.status==="blocked"&&l.blockedBy===r?l.resume.status==="focus-target"||!this.isComponentMounted(l.blockedBy)?t=this.resolveBlockedOverlayFocusResume(l):this.overlayFocusRestore={status:"blocked",overlay:l.overlay,blockedBy:t,resume:l.resume}:i&&l.status!=="inactive"&&l.overlay===i&&!this.isOverlayFocusAncestor(i,t)&&(this.overlayFocusRestore={status:"blocked",overlay:i,blockedBy:t,resume:{status:"restore-overlay"}}):t===null&&(l.status==="blocked"&&l.blockedBy===r?t=this.resolveBlockedOverlayFocusResume(l):s==="clear"&&this.clearOverlayFocusRestore()),P(this.focusedComponent)&&(this.focusedComponent.focused=!1),this.focusedComponent=t,P(t)&&(t.focused=!0);const u=t?this.overlayStack.find(m=>m.component===t&&this.isOverlayVisible(m)):void 0;u&&(this.overlayFocusRestore={status:"eligible",overlay:u})}clearOverlayFocusRestore(){this.overlayFocusRestore={status:"inactive"}}clearOverlayFocusRestoreFor(e){this.overlayFocusRestore.status!=="inactive"&&this.overlayFocusRestore.overlay===e&&this.clearOverlayFocusRestore()}resolveBlockedOverlayFocusResume(e){return e.resume.status==="restore-overlay"?e.overlay.component:(this.clearOverlayFocusRestore(),e.resume.target)}getVisibleOverlayFocusRestore(){const e=this.overlayFocusRestore;return e.status==="inactive"?e:!this.overlayStack.includes(e.overlay)||!this.isOverlayVisible(e.overlay)?{status:"inactive"}:e}isOverlayFocusAncestor(e,s){const r=new Set;let t=e.preFocus;for(;t&&!r.has(t);){if(r.add(t),t===s)return!0;t=this.overlayStack.find(i=>i.component===t)?.preFocus??null}return!1}retargetOverlayPreFocus(e){for(const s of this.overlayStack)s!==e&&s.preFocus===e.component&&(s.preFocus=e.preFocus)}isComponentMounted(e){return this.children.some(s=>this.containsComponent(s,e))}containsComponent(e,s){return e===s?!0:e instanceof G?e.children.some(r=>this.containsComponent(r,s)):!1}showOverlay(e,s){const r={component:e,...s===void 0?{}:{options:s},preFocus:this.focusedComponent,hidden:!1,focusOrder:++this.focusOrderCounter};return this.overlayStack.push(r),!s?.nonCapturing&&this.isOverlayVisible(r)&&this.setFocus(e),this.terminal.hideCursor(),this.requestRender(),{hide:x(()=>{const t=this.overlayStack.indexOf(r);if(t!==-1){if(this.clearOverlayFocusRestoreFor(r),this.retargetOverlayPreFocus(r),this.overlayStack.splice(t,1),this.focusedComponent===e){const i=this.getTopmostVisibleOverlay();this.setFocus(i?.component??r.preFocus)}this.overlayStack.length===0&&this.terminal.hideCursor(),this.requestRender()}},"hide"),setHidden:x(t=>{if(r.hidden!==t){if(r.hidden=t,t){if(this.clearOverlayFocusRestoreFor(r),this.focusedComponent===e){const i=this.getTopmostVisibleOverlay();this.setFocus(i?.component??r.preFocus)}}else!s?.nonCapturing&&this.isOverlayVisible(r)&&(r.focusOrder=++this.focusOrderCounter,this.setFocus(e));this.requestRender()}},"setHidden"),isHidden:x(()=>r.hidden,"isHidden"),focus:x(()=>{!this.overlayStack.includes(r)||!this.isOverlayVisible(r)||(r.focusOrder=++this.focusOrderCounter,this.setFocus(e),this.requestRender())},"focus"),unfocus:x(t=>{const i=this.focusedComponent===e,n=this.overlayFocusRestore,l=n.status!=="inactive"&&n.overlay===r;if(!(!i&&!l)){if(n.status==="blocked"&&n.overlay===r&&this.focusedComponent===n.blockedBy){t?this.overlayFocusRestore={status:"blocked",overlay:r,blockedBy:n.blockedBy,resume:{status:"focus-target",target:t.target}}:this.clearOverlayFocusRestore(),this.requestRender();return}if(this.clearOverlayFocusRestoreFor(r),i||t){const u=this.getTopmostVisibleOverlay(),m=u&&u!==r?u.component:r.preFocus;this.setFocus(t?t.target:m)}this.requestRender()}},"unfocus"),isFocused:x(()=>this.focusedComponent===e,"isFocused")}}hideOverlay(){const e=this.overlayStack[this.overlayStack.length-1];if(e){if(this.clearOverlayFocusRestoreFor(e),this.retargetOverlayPreFocus(e),this.overlayStack.pop(),this.focusedComponent===e.component){const s=this.getTopmostVisibleOverlay();this.setFocus(s?.component??e.preFocus)}this.overlayStack.length===0&&this.terminal.hideCursor(),this.requestRender()}}hasOverlay(){return this.overlayStack.some(e=>this.isOverlayVisible(e))}isOverlayVisible(e){return e.hidden?!1:e.options?.visible?e.options.visible(this.terminal.columns,this.terminal.rows):!0}getTopmostVisibleOverlay(){let e;for(const s of this.overlayStack)s.options?.nonCapturing||!this.isOverlayVisible(s)||(!e||s.focusOrder>e.focusOrder)&&(e=s);return e}invalidate(){super.invalidate();for(const e of this.overlayStack)e.component.invalidate?.()}start(){this.stopped=!1,this.terminal.start(e=>this.handleInput(e),()=>this.requestRender()),this.terminal.hideCursor(),this.queryCellSize(),this.requestRender()}addInputListener(e){return this.inputListeners.add(e),()=>{this.inputListeners.delete(e)}}removeInputListener(e){this.inputListeners.delete(e)}queryCellSize(){Z().images&&this.terminal.write("\x1B[16t")}stop(){if(this.stopped=!0,this.renderTimer&&(clearTimeout(this.renderTimer),this.renderTimer=void 0),this.previousLines.length>0){const s=this.previousLines.length-this.hardwareCursorRow;s>0?this.terminal.write(`\x1B[${s}B`):s<0&&this.terminal.write(`\x1B[${-s}A`),this.terminal.write(`\r
2
+ `)}this.terminal.showCursor(),this.terminal.stop()}requestRender(e=!1){if(e){this.previousLines=[],this.previousWidth=-1,this.previousHeight=-1,this.cursorRow=0,this.hardwareCursorRow=0,this.maxLinesRendered=0,this.previousViewportTop=0,this.renderTimer&&(clearTimeout(this.renderTimer),this.renderTimer=void 0),this.renderRequested=!0,process.nextTick(()=>{this.stopped||!this.renderRequested||(this.renderRequested=!1,this.lastRenderAt=A.now(),this.doRender())});return}this.renderRequested||(this.renderRequested=!0,process.nextTick(()=>this.scheduleRender()))}scheduleRender(){if(this.stopped||this.renderTimer||!this.renderRequested)return;const e=A.now()-this.lastRenderAt,s=Math.max(0,E.MIN_RENDER_INTERVAL_MS-e);this.renderTimer=setTimeout(()=>{this.renderTimer=void 0,!(this.stopped||!this.renderRequested)&&(this.renderRequested=!1,this.lastRenderAt=A.now(),this.doRender(),this.renderRequested&&this.scheduleRender())},s)}handleInput(e){if(this.inputListeners.size>0){let t=e;for(const i of this.inputListeners){const n=i(t);if(n?.consume)return;n?.data!==void 0&&(t=n.data)}if(t.length===0)return;e=t}if(this.consumeCellSizeResponse(e))return;if(Y(e,"shift+ctrl+d")&&this.onDebug){this.onDebug();return}const s=this.overlayStack.find(t=>t.component===this.focusedComponent);if(s&&!this.isOverlayVisible(s)){const t=this.getTopmostVisibleOverlay();t?this.setFocus(t.component):this.setFocusInternal({component:s.preFocus,overlayFocusRestore:"preserve"})}if(!this.overlayStack.some(t=>t.component===this.focusedComponent)){const t=this.getVisibleOverlayFocusRestore();t.status==="eligible"?this.setFocus(t.overlay.component):t.status==="blocked"&&t.blockedBy!==this.focusedComponent&&(t.resume.status==="restore-overlay"?this.setFocus(t.overlay.component):(this.clearOverlayFocusRestore(),this.setFocus(t.resume.target)))}if(this.focusedComponent?.handleInput){if(X(e)&&!this.focusedComponent.wantsKeyRelease)return;this.focusedComponent.handleInput(e),this.requestRender()}}consumeCellSizeResponse(e){const s=e.match(/^\x1b\[6;(\d+);(\d+)t$/);if(!s)return!1;const r=parseInt(s[1],10),t=parseInt(s[2],10);return r<=0||t<=0||(ee({widthPx:t,heightPx:r}),this.invalidate(),this.requestRender()),!0}resolveOverlayLayout(e,s,r,t){const i=e??{},n=typeof i.margin=="number"?{top:i.margin,right:i.margin,bottom:i.margin,left:i.margin}:i.margin??{},l=Math.max(0,n.top??0),u=Math.max(0,n.right??0),m=Math.max(0,n.bottom??0),o=Math.max(0,n.left??0),v=Math.max(1,r-o-u),g=Math.max(1,t-l-m);let p=z(i.width,r)??Math.min(80,v);i.minWidth!==void 0&&(p=Math.max(p,i.minWidth)),p=Math.max(1,Math.min(p,v));let d=z(i.maxHeight,t);d!==void 0&&(d=Math.max(1,Math.min(d,g)));const h=d!==void 0?Math.min(s,d):s;let f,C;if(i.row!==void 0)if(typeof i.row=="string"){const O=i.row.match(/^(\d+(?:\.\d+)?)%$/);if(O){const F=Math.max(0,g-h),y=parseFloat(O[1])/100;f=l+Math.floor(F*y)}else f=this.resolveAnchorRow("center",h,g,l)}else f=i.row;else{const O=i.anchor??"center";f=this.resolveAnchorRow(O,h,g,l)}if(i.col!==void 0)if(typeof i.col=="string"){const O=i.col.match(/^(\d+(?:\.\d+)?)%$/);if(O){const F=Math.max(0,v-p),y=parseFloat(O[1])/100;C=o+Math.floor(F*y)}else C=this.resolveAnchorCol("center",p,v,o)}else C=i.col;else{const O=i.anchor??"center";C=this.resolveAnchorCol(O,p,v,o)}return i.offsetY!==void 0&&(f+=i.offsetY),i.offsetX!==void 0&&(C+=i.offsetX),f=Math.max(l,Math.min(f,t-m-h)),C=Math.max(o,Math.min(C,r-u-p)),{width:p,row:f,col:C,maxHeight:d}}resolveAnchorRow(e,s,r,t){switch(e){case"top-left":case"top-center":case"top-right":return t;case"bottom-left":case"bottom-center":case"bottom-right":return t+r-s;case"left-center":case"center":case"right-center":return t+Math.floor((r-s)/2)}}resolveAnchorCol(e,s,r,t){switch(e){case"top-left":case"left-center":case"bottom-left":return t;case"top-right":case"right-center":case"bottom-right":return t+r-s;case"top-center":case"center":case"bottom-center":return t+Math.floor((r-s)/2)}}compositeOverlays(e,s,r){if(this.overlayStack.length===0)return e;const t=[...e],i=[];let n=t.length;const l=this.overlayStack.filter(o=>this.isOverlayVisible(o));l.sort((o,v)=>o.focusOrder-v.focusOrder);for(const o of l){const{component:v,options:g}=o,{width:p,maxHeight:d}=this.resolveOverlayLayout(g,0,s,r);let h=v.render(p);d!==void 0&&h.length>d&&(h=h.slice(0,d));const{row:f,col:C}=this.resolveOverlayLayout(g,h.length,s,r);i.push({overlayLines:h,row:f,col:C,w:p}),n=Math.max(n,f+h.length)}const u=Math.max(t.length,r,n);for(;t.length<u;)t.push("");const m=Math.max(0,u-r);for(const{overlayLines:o,row:v,col:g,w:p}of i)for(let d=0;d<o.length;d++){const h=m+v+d;if(h>=0&&h<t.length){const f=S(o[d])>p?_(o[d],0,p,!0):o[d];t[h]=this.compositeLineAt(t[h],f,g,p,s)}}return t}static SEGMENT_RESET="\x1B[0m\x1B]8;;\x07";applyLineResets(e){const s=E.SEGMENT_RESET;for(let r=0;r<e.length;r++){const t=e[r];K(t)||(e[r]=se(t)+s)}return e}collectKittyImageIds(e){const s=new Set;for(const r of e)for(const t of B(r))s.add(t);return s}deleteKittyImages(e){let s="";for(const r of e)s+=Q(r);return s}expandLastChangedForKittyImages(e,s){let r=s;for(let t=e;t<this.previousLines.length;t++)B(this.previousLines[t]).length>0&&(r=Math.max(r,t));return r}deleteChangedKittyImages(e,s){if(e<0||s<e)return"";const r=new Set,t=Math.min(s,this.previousLines.length-1);for(let i=e;i<=t;i++)for(const n of B(this.previousLines[i]??""))r.add(n);return this.deleteKittyImages(r)}compositeLineAt(e,s,r,t,i){if(K(e))return e;const n=r+t,l=te(e,r,n,i-n,!0),u=re(s,0,t,!0),m=Math.max(0,r-l.beforeWidth),o=Math.max(0,t-u.width),v=Math.max(r,l.beforeWidth),g=Math.max(t,u.width),p=Math.max(0,i-v-g),d=Math.max(0,p-l.afterWidth),h=E.SEGMENT_RESET,f=l.before+" ".repeat(m)+h+u.text+" ".repeat(o)+h+l.after+" ".repeat(d);return S(f)<=i?f:_(f,0,i,!0)}extractCursorPosition(e,s){const r=Math.max(0,e.length-s);for(let t=e.length-1;t>=r;t--){const i=e[t],n=i.indexOf(W);if(n!==-1){const l=i.slice(0,n),u=S(l);return e[t]=i.slice(0,n)+i.slice(n+W.length),{row:t,col:u}}}return null}doRender(){if(this.stopped)return;const e=this.terminal.columns,s=this.terminal.rows,r=this.previousWidth!==0&&this.previousWidth!==e,t=this.previousHeight!==0&&this.previousHeight!==s,i=this.previousHeight>0?this.previousViewportTop+this.previousHeight:s;let n=t?Math.max(0,i-s):this.previousViewportTop,l=n,u=this.hardwareCursorRow;const m=x(a=>{const c=u-n;return a-l-c},"computeLineDiff");let o=this.render(e);this.overlayStack.length>0&&(o=this.compositeOverlays(o,e,s));const v=this.extractCursorPosition(o,s);o=this.applyLineResets(o);const g=x(a=>{this.fullRedrawCount+=1;let c="\x1B[?2026h";a&&(c+=this.deleteKittyImages(this.previousKittyImageIds),c+="\x1B[2J\x1B[H\x1B[3J");for(let R=0;R<o.length;R++)R>0&&(c+=`\r
3
+ `),c+=o[R];c+="\x1B[?2026l",this.terminal.write(c),this.cursorRow=Math.max(0,o.length-1),this.hardwareCursorRow=this.cursorRow,a?this.maxLinesRendered=o.length:this.maxLinesRendered=Math.max(this.maxLinesRendered,o.length);const w=Math.max(s,o.length);this.previousViewportTop=Math.max(0,w-s),this.positionHardwareCursor(v,o.length),this.previousLines=o,this.previousKittyImageIds=this.collectKittyImageIds(o),this.previousWidth=e,this.previousHeight=s},"fullRender"),p=process.env.KODA_DEBUG_REDRAW==="1",d=x(a=>{if(!p)return;const c=V.join(q.homedir(),".pi","agent","pi-debug.log"),w=`[${new Date().toISOString()}] fullRender: ${a} (prev=${this.previousLines.length}, new=${o.length}, height=${s})
4
+ `;I.appendFileSync(c,w)},"logRedraw");if(this.previousLines.length===0&&!r&&!t){d("first render"),g(!1);return}if(r){d(`terminal width changed (${this.previousWidth} -> ${e})`),g(!0);return}if(t&&!ie()){d(`terminal height changed (${this.previousHeight} -> ${s})`),g(!0);return}if(this.clearOnShrink&&o.length<this.maxLinesRendered&&this.overlayStack.length===0){d(`clearOnShrink (maxLinesRendered=${this.maxLinesRendered})`),g(!0);return}let h=-1,f=-1;const C=Math.max(o.length,this.previousLines.length);for(let a=0;a<C;a++){const c=a<this.previousLines.length?this.previousLines[a]:"",w=a<o.length?o[a]:"";c!==w&&(h===-1&&(h=a),f=a)}const O=o.length>this.previousLines.length;O&&(h===-1&&(h=this.previousLines.length),f=o.length-1),h!==-1&&(f=this.expandLastChangedForKittyImages(h,f));const F=O&&h===this.previousLines.length&&h>0;if(h===-1){this.positionHardwareCursor(v,o.length),this.previousViewportTop=n,this.previousHeight=s;return}if(h>=o.length){if(this.previousLines.length>o.length){let a="\x1B[?2026h";a+=this.deleteChangedKittyImages(h,f);const c=Math.max(0,o.length-1);if(c<n){d(`deleted lines moved viewport up (${c} < ${n})`),g(!0);return}const w=m(c);w>0?a+=`\x1B[${w}B`:w<0&&(a+=`\x1B[${-w}A`),a+="\r";const R=this.previousLines.length-o.length;if(R>s){d(`extraLines > height (${R} > ${s})`),g(!0);return}R>0&&(a+="\x1B[1B");for(let k=0;k<R;k++)a+="\r\x1B[2K",k<R-1&&(a+="\x1B[1B");R>0&&(a+=`\x1B[${R}A`),a+="\x1B[?2026l",this.terminal.write(a),this.cursorRow=c,this.hardwareCursorRow=c}this.positionHardwareCursor(v,o.length),this.previousLines=o,this.previousKittyImageIds=this.collectKittyImageIds(o),this.previousWidth=e,this.previousHeight=s,this.previousViewportTop=n;return}if(h<n){d(`firstChanged < viewportTop (${h} < ${n})`),g(!0);return}let y="\x1B[?2026h";y+=this.deleteChangedKittyImages(h,f);const D=n+s-1,$=F?h-1:h;if($>D){const a=Math.max(0,Math.min(s-1,u-n)),c=s-1-a;c>0&&(y+=`\x1B[${c}B`);const w=$-D;y+=`\r
5
+ `.repeat(w),n+=w,l+=w,u=$}const L=m($);L>0?y+=`\x1B[${L}B`:L<0&&(y+=`\x1B[${-L}A`),y+=F?`\r
6
+ `:"\r";const M=Math.min(f,o.length-1);for(let a=h;a<=M;a++){a>h&&(y+=`\r
7
+ `),y+="\x1B[2K";const c=o[a];if(!K(c)&&S(c)>e){const R=V.join(q.homedir(),".pi","agent","pi-crash.log"),k=[`Crash at ${new Date().toISOString()}`,`Terminal width: ${e}`,`Line ${a} visible width: ${S(c)}`,"","=== All rendered lines ===",...o.map((H,j)=>`[${j}] (w=${S(H)}) ${H}`),""].join(`
8
+ `);I.mkdirSync(V.dirname(R),{recursive:!0}),I.writeFileSync(R,k),this.stop();const U=[`Rendered line ${a} exceeds terminal width (${S(c)} > ${e}).`,"","This is likely caused by a custom TUI component not truncating its output.","Use visibleWidth() to measure and truncateToWidth() to truncate lines.","",`Debug log written to: ${R}`].join(`
9
+ `);throw new Error(U)}y+=c}let T=M;if(this.previousLines.length>o.length){if(M<o.length-1){const c=o.length-1-M;y+=`\x1B[${c}B`,T=o.length-1}const a=this.previousLines.length-o.length;for(let c=o.length;c<this.previousLines.length;c++)y+=`\r
10
+ \x1B[2K`;y+=`\x1B[${a}A`}if(y+="\x1B[?2026l",process.env.KODA_TUI_DEBUG==="1"){const a="/tmp/tui";I.mkdirSync(a,{recursive:!0});const c=V.join(a,`render-${Date.now()}-${Math.random().toString(36).slice(2)}.log`),w=[`firstChanged: ${h}`,`viewportTop: ${l}`,`cursorRow: ${this.cursorRow}`,`height: ${s}`,`lineDiff: ${L}`,`hardwareCursorRow: ${u}`,`renderEnd: ${M}`,`finalCursorRow: ${T}`,`cursorPos: ${JSON.stringify(v)}`,`newLines.length: ${o.length}`,`previousLines.length: ${this.previousLines.length}`,"","=== newLines ===",JSON.stringify(o,null,2),"","=== previousLines ===",JSON.stringify(this.previousLines,null,2),"","=== buffer ===",JSON.stringify(y)].join(`
11
+ `);I.writeFileSync(c,w)}this.terminal.write(y),this.cursorRow=Math.max(0,o.length-1),this.hardwareCursorRow=T,this.maxLinesRendered=Math.max(this.maxLinesRendered,o.length),this.previousViewportTop=Math.max(n,T-s+1),this.positionHardwareCursor(v,o.length),this.previousLines=o,this.previousKittyImageIds=this.collectKittyImageIds(o),this.previousWidth=e,this.previousHeight=s}positionHardwareCursor(e,s){if(!e||s<=0){this.terminal.hideCursor();return}const r=Math.max(0,Math.min(e.row,s-1)),t=Math.max(0,e.col),i=r-this.hardwareCursorRow;let n="";i>0?n+=`\x1B[${i}B`:i<0&&(n+=`\x1B[${-i}A`),n+=`\x1B[${t+1}G`,n&&this.terminal.write(n),this.hardwareCursorRow=r,this.showHardwareCursor?this.terminal.showCursor():this.terminal.hideCursor()}}export{W as CURSOR_MARKER,G as Container,E as TUI,P as isFocusable,S as visibleWidth};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Generic undo stack with clone-on-push semantics.
3
+ *
4
+ * Stores deep clones of state snapshots. Popped snapshots are returned
5
+ * directly (no re-cloning) since they are already detached.
6
+ */
7
+ export declare class UndoStack<S> {
8
+ private stack;
9
+ /** Push a deep clone of the given state onto the stack. */
10
+ push(state: S): void;
11
+ /** Pop and return the most recent snapshot, or undefined if empty. */
12
+ pop(): S | undefined;
13
+ /** Remove all snapshots. */
14
+ clear(): void;
15
+ get length(): number;
16
+ }
@@ -0,0 +1 @@
1
+ var c=Object.defineProperty;var e=(s,t)=>c(s,"name",{value:t,configurable:!0});class a{static{e(this,"UndoStack")}stack=[];push(t){this.stack.push(structuredClone(t))}pop(){return this.stack.pop()}clear(){this.stack.length=0}get length(){return this.stack.length}}export{a as UndoStack};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Get the shared grapheme segmenter instance.
3
+ */
4
+ export declare function getGraphemeSegmenter(): Intl.Segmenter;
5
+ /**
6
+ * Get the shared word segmenter instance.
7
+ */
8
+ export declare function getWordSegmenter(): Intl.Segmenter;
9
+ /**
10
+ * Calculate the visible width of a string in terminal columns.
11
+ */
12
+ export declare function visibleWidth(str: string): number;
13
+ export declare function normalizeTerminalOutput(str: string): string;
14
+ /**
15
+ * Extract ANSI escape sequences from a string at the given position.
16
+ */
17
+ export declare function extractAnsiCode(str: string, pos: number): {
18
+ code: string;
19
+ length: number;
20
+ } | null;
21
+ /**
22
+ * Wrap text with ANSI codes preserved.
23
+ *
24
+ * ONLY does word wrapping - NO padding, NO background colors.
25
+ * Returns lines where each line is <= width visible chars.
26
+ * Active ANSI codes are preserved across line breaks.
27
+ *
28
+ * @param text - Text to wrap (may contain ANSI codes and newlines)
29
+ * @param width - Maximum visible width per line
30
+ * @returns Array of wrapped lines (NOT padded to width)
31
+ */
32
+ export declare function wrapTextWithAnsi(text: string, width: number): string[];
33
+ export declare const PUNCTUATION_REGEX: RegExp;
34
+ /**
35
+ * Check if a character is whitespace.
36
+ */
37
+ export declare function isWhitespaceChar(char: string): boolean;
38
+ /**
39
+ * Check if a character is punctuation.
40
+ */
41
+ export declare function isPunctuationChar(char: string): boolean;
42
+ /**
43
+ * Apply background color to a line, padding to full width.
44
+ *
45
+ * @param line - Line of text (may contain ANSI codes)
46
+ * @param width - Total width to pad to
47
+ * @param bgFn - Background color function
48
+ * @returns Line with background applied and padded to width
49
+ */
50
+ export declare function applyBackgroundToLine(line: string, width: number, bgFn: (text: string) => string): string;
51
+ /**
52
+ * Truncate text to fit within a maximum visible width, adding ellipsis if needed.
53
+ * Optionally pad with spaces to reach exactly maxWidth.
54
+ * Properly handles ANSI escape codes (they don't count toward width).
55
+ *
56
+ * @param text - Text to truncate (may contain ANSI codes)
57
+ * @param maxWidth - Maximum visible width
58
+ * @param ellipsis - Ellipsis string to append when truncating (default: "...")
59
+ * @param pad - If true, pad result with spaces to exactly maxWidth (default: false)
60
+ * @returns Truncated text, optionally padded to exactly maxWidth
61
+ */
62
+ export declare function truncateToWidth(text: string, maxWidth: number, ellipsis?: string, pad?: boolean): string;
63
+ /**
64
+ * Extract a range of visible columns from a line. Handles ANSI codes and wide chars.
65
+ * @param strict - If true, exclude wide chars at boundary that would extend past the range
66
+ */
67
+ export declare function sliceByColumn(line: string, startCol: number, length: number, strict?: boolean): string;
68
+ /** Like sliceByColumn but also returns the actual visible width of the result. */
69
+ export declare function sliceWithWidth(line: string, startCol: number, length: number, strict?: boolean): {
70
+ text: string;
71
+ width: number;
72
+ };
73
+ /**
74
+ * Extract "before" and "after" segments from a line in a single pass.
75
+ * Used for overlay compositing where we need content before and after the overlay region.
76
+ * Preserves styling from before the overlay that should affect content after it.
77
+ */
78
+ export declare function extractSegments(line: string, beforeEnd: number, afterStart: number, afterLen: number, strictAfter?: boolean): {
79
+ before: string;
80
+ beforeWidth: number;
81
+ after: string;
82
+ afterWidth: number;
83
+ };
package/dist/utils.js ADDED
@@ -0,0 +1,2 @@
1
+ var I=Object.defineProperty;var a=(e,t)=>I(e,"name",{value:t,configurable:!0});import{eastAsianWidth as _}from"get-east-asian-width";const A=new Intl.Segmenter(void 0,{granularity:"grapheme"}),S=new Intl.Segmenter(void 0,{granularity:"word"});function Q(){return A}a(Q,"getGraphemeSegmenter");function Y(){return S}a(Y,"getWordSegmenter");function P(e){const t=e.codePointAt(0);return t>=126976&&t<=130047||t>=8960&&t<=9215||t>=9728&&t<=10175||t>=11088&&t<=11093||e.includes("\uFE0F")||e.length>2}a(P,"couldBeEmoji");const W=/^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Mark}|\p{Surrogate})+$/v,O=/^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+/v,M=/^\p{RGI_Emoji}$/v,j=512,x=new Map;function y(e){for(let t=0;t<e.length;t++){const s=e.charCodeAt(t);if(s<32||s>126)return!1}return!0}a(y,"isPrintableAscii");function G(e,t){if(t<=0||e.length===0)return{text:"",width:0};if(y(e)){const c=e.slice(0,t);return{text:c,width:c.length}}const s=e.includes("\x1B"),n=e.includes(" ");if(!s&&!n){let c="",f=0;for(const{segment:u}of A.segment(e)){const h=C(u);if(f+h>t)break;c+=u,f+=h}return{text:c,width:f}}let o="",r=0,i=0,l="";for(;i<e.length;){const c=k(e,i);if(c){l+=c.code,i+=c.length;continue}if(e[i]===" "){if(r+3>t)break;l&&(o+=l,l=""),o+=" ",r+=3,i++;continue}let f=i;for(;f<e.length&&e[f]!==" "&&!k(e,f);)f++;for(const{segment:u}of A.segment(e.slice(i,f))){const h=C(u);if(r+h>t)return{text:o,width:r};l&&(o+=l,l=""),o+=u,r+=h}i=f}return{text:o,width:r}}a(G,"truncateFragmentToWidth");function L(e,t,s,n,o,r){const i="\x1B[0m",l=t+n;let c;return s.length>0?c=`${e}${i}${s}${i}`:c=`${e}${i}`,r?c+" ".repeat(Math.max(0,o-l)):c}a(L,"finalizeTruncatedResult");function C(e){if(e===" ")return 3;if(W.test(e))return 0;if(P(e)&&M.test(e))return 2;const s=e.replace(O,"").codePointAt(0);if(s===void 0)return 0;if(s>=127462&&s<=127487)return 2;let n=_(s);if(e.length>1)for(const o of e.slice(1)){const r=o.codePointAt(0);r>=65280&&r<=65519?n+=_(r):(r===3635||r===3763)&&(n+=1)}return n}a(C,"graphemeWidth");function v(e){if(e.length===0)return 0;if(y(e))return e.length;const t=x.get(e);if(t!==void 0)return t;let s=e;if(e.includes(" ")&&(s=s.replace(/\t/g," ")),s.includes("\x1B")){let o="",r=0;for(;r<s.length;){const i=k(s,r);if(i){r+=i.length;continue}o+=s[r],r++}s=o}let n=0;for(const{segment:o}of A.segment(s))n+=C(o);if(x.size>=j){const o=x.keys().next().value;o!==void 0&&x.delete(o)}return x.set(e,n),n}a(v,"visibleWidth");const N=/[\u0e33\u0eb3]/,F=/[\u0e33\u0eb3]/g;function ee(e){return N.test(e)?e.replace(F,t=>t==="\u0E33"?"\u0E4D\u0E32":"\u0ECD\u0EB2"):e}a(ee,"normalizeTerminalOutput");function k(e,t){if(t>=e.length||e[t]!=="\x1B")return null;const s=e[t+1];if(s==="["){let n=t+2;for(;n<e.length&&!/[mGKHJ]/.test(e[n]);)n++;return n<e.length?{code:e.substring(t,n+1),length:n+1-t}:null}if(s==="]"){let n=t+2;for(;n<e.length;){if(e[n]==="\x07")return{code:e.substring(t,n+1),length:n+1-t};if(e[n]==="\x1B"&&e[n+1]==="\\")return{code:e.substring(t,n+2),length:n+2-t};n++}return null}if(s==="_"){let n=t+2;for(;n<e.length;){if(e[n]==="\x07")return{code:e.substring(t,n+1),length:n+1-t};if(e[n]==="\x1B"&&e[n+1]==="\\")return{code:e.substring(t,n+2),length:n+2-t};n++}return null}return null}a(k,"extractAnsiCode");function B(e){if(!e.startsWith("\x1B]8;"))return;const t=e.endsWith("\x07")?"\x07":"\x1B\\",s=e.slice(4,t==="\x07"?-1:-2),n=s.indexOf(";");if(n===-1)return;const o=s.slice(0,n),r=s.slice(n+1);return r?{params:o,url:r,terminator:t}:null}a(B,"parseOsc8Hyperlink");function z(e){return`\x1B]8;${e.params};${e.url}${e.terminator}`}a(z,"formatOsc8Hyperlink");function D(e){return`\x1B]8;;${e}`}a(D,"formatOsc8Close");class R{static{a(this,"AnsiCodeTracker")}bold=!1;dim=!1;italic=!1;underline=!1;blink=!1;inverse=!1;hidden=!1;strikethrough=!1;fgColor=null;bgColor=null;activeHyperlink=null;process(t){const s=B(t);if(s!==void 0){this.activeHyperlink=s;return}if(!t.endsWith("m"))return;const n=t.match(/\x1b\[([\d;]*)m/);if(!n)return;const o=n[1];if(o===""||o==="0"){this.reset();return}const r=o.split(";");let i=0;for(;i<r.length;){const l=Number.parseInt(r[i],10);if(l===38||l===48){if(r[i+1]==="5"&&r[i+2]!==void 0){const c=`${r[i]};${r[i+1]};${r[i+2]}`;l===38?this.fgColor=c:this.bgColor=c,i+=3;continue}else if(r[i+1]==="2"&&r[i+4]!==void 0){const c=`${r[i]};${r[i+1]};${r[i+2]};${r[i+3]};${r[i+4]}`;l===38?this.fgColor=c:this.bgColor=c,i+=5;continue}}switch(l){case 0:this.reset();break;case 1:this.bold=!0;break;case 2:this.dim=!0;break;case 3:this.italic=!0;break;case 4:this.underline=!0;break;case 5:this.blink=!0;break;case 7:this.inverse=!0;break;case 8:this.hidden=!0;break;case 9:this.strikethrough=!0;break;case 21:this.bold=!1;break;case 22:this.bold=!1,this.dim=!1;break;case 23:this.italic=!1;break;case 24:this.underline=!1;break;case 25:this.blink=!1;break;case 27:this.inverse=!1;break;case 28:this.hidden=!1;break;case 29:this.strikethrough=!1;break;case 39:this.fgColor=null;break;case 49:this.bgColor=null;break;default:l>=30&&l<=37||l>=90&&l<=97?this.fgColor=String(l):(l>=40&&l<=47||l>=100&&l<=107)&&(this.bgColor=String(l));break}i++}}reset(){this.bold=!1,this.dim=!1,this.italic=!1,this.underline=!1,this.blink=!1,this.inverse=!1,this.hidden=!1,this.strikethrough=!1,this.fgColor=null,this.bgColor=null}clear(){this.reset(),this.activeHyperlink=null}getActiveCodes(){const t=[];this.bold&&t.push("1"),this.dim&&t.push("2"),this.italic&&t.push("3"),this.underline&&t.push("4"),this.blink&&t.push("5"),this.inverse&&t.push("7"),this.hidden&&t.push("8"),this.strikethrough&&t.push("9"),this.fgColor&&t.push(this.fgColor),this.bgColor&&t.push(this.bgColor);let s=t.length>0?`\x1B[${t.join(";")}m`:"";return this.activeHyperlink&&(s+=z(this.activeHyperlink)),s}hasActiveCodes(){return this.bold||this.dim||this.italic||this.underline||this.blink||this.inverse||this.hidden||this.strikethrough||this.fgColor!==null||this.bgColor!==null||this.activeHyperlink!==null}getLineEndReset(){let t="";return this.underline&&(t+="\x1B[24m"),this.activeHyperlink&&(t+=D(this.activeHyperlink.terminator)),t}}function H(e,t){let s=0;for(;s<e.length;){const n=k(e,s);n?(t.process(n.code),s+=n.length):s++}}a(H,"updateTrackerFromText");function X(e){const t=[];let s="",n="",o=!1,r=0;for(;r<e.length;){const i=k(e,r);if(i){n+=i.code,r+=i.length;continue}const l=e[r],c=l===" ";c!==o&&s&&(t.push(s),s=""),n&&(s+=n,n=""),o=c,s+=l,r++}return n&&(s+=n),s&&t.push(s),t}a(X,"splitIntoTokensWithAnsi");function te(e,t){if(!e)return[""];const s=e.split(`
2
+ `),n=[],o=new R;for(const r of s){const i=n.length>0?o.getActiveCodes():"",l=K(i+r,t);for(const c of l)n.push(c);H(r,o)}return n.length>0?n:[""]}a(te,"wrapTextWithAnsi");function K(e,t){if(!e)return[""];if(v(e)<=t)return[e];const n=[],o=new R,r=X(e);let i="",l=0;for(const c of r){const f=v(c),u=c.trim()==="";if(f>t&&!u){if(i){const d=o.getLineEndReset();d&&(i+=d),n.push(i),i="",l=0}const p=V(c,t,o);for(let d=0;d<p.length-1;d++)n.push(p[d]);i=p[p.length-1],l=v(i);continue}if(l+f>t&&l>0){let p=i.trimEnd();const d=o.getLineEndReset();d&&(p+=d),n.push(p),u?(i=o.getActiveCodes(),l=0):(i=o.getActiveCodes()+c,l=f)}else i+=c,l+=f;H(c,o)}return i&&n.push(i),n.length>0?n.map(c=>c.trimEnd()):[""]}a(K,"wrapSingleLine");const U=/[(){}[\]<>.,;:'"!?+\-=*/\\|&%^$#@~`]/;function ne(e){return/\s/.test(e)}a(ne,"isWhitespaceChar");function ie(e){return U.test(e)}a(ie,"isPunctuationChar");function V(e,t,s){const n=[];let o=s.getActiveCodes(),r=0,i=0;const l=[];for(;i<e.length;){const c=k(e,i);if(c)l.push({type:"ansi",value:c.code}),i+=c.length;else{let f=i;for(;f<e.length&&!k(e,f);)f++;const u=e.slice(i,f);for(const h of A.segment(u))l.push({type:"grapheme",value:h.segment});i=f}}for(const c of l){if(c.type==="ansi"){o+=c.value,s.process(c.value);continue}const f=c.value;if(!f)continue;const u=v(f);if(r+u>t){const h=s.getLineEndReset();h&&(o+=h),n.push(o),o=s.getActiveCodes(),r=0}o+=f,r+=u}return o&&n.push(o),n.length>0?n:[""]}a(V,"breakLongWord");function se(e,t,s){const n=v(e),o=Math.max(0,t-n),r=" ".repeat(o),i=e+r;return s(i)}a(se,"applyBackgroundToLine");function re(e,t,s="...",n=!1){if(t<=0)return"";if(e.length===0)return n?" ".repeat(t):"";const o=v(s);if(o>=t){const g=v(e);if(g<=t)return n?e+" ".repeat(t-g):e;const b=G(s,t);return b.width===0?n?" ".repeat(t):"":L("",0,b.text,b.width,t,n)}if(y(e)){if(e.length<=t)return n?e+" ".repeat(t-e.length):e;const g=t-o;return L(e.slice(0,g),g,s,o,t,n)}const r=t-o;let i="",l="",c=0,f=0,u=!0,h=!1,p=!1;const d=e.includes("\x1B"),w=e.includes(" ");if(!d&&!w){for(const{segment:g}of A.segment(e)){const b=C(g);if(u&&f+b<=r?(i+=g,f+=b):u=!1,c+=b,c>t){h=!0;break}}p=!h}else{let g=0;for(;g<e.length;){const b=k(e,g);if(b){l+=b.code,g+=b.length;continue}if(e[g]===" "){if(u&&f+3<=r?(l&&(i+=l,l=""),i+=" ",f+=3):(u=!1,l=""),c+=3,c>t){h=!0;break}g++;continue}let m=g;for(;m<e.length&&e[m]!==" "&&!k(e,m);)m++;for(const{segment:E}of A.segment(e.slice(g,m))){const $=C(E);if(u&&f+$<=r?(l&&(i+=l,l=""),i+=E,f+=$):(u=!1,l=""),c+=$,c>t){h=!0;break}}if(h)break;g=m}p=g>=e.length}return!h&&p?n?e+" ".repeat(Math.max(0,t-c)):e:L(i,f,s,o,t,n)}a(re,"truncateToWidth");function le(e,t,s,n=!1){return J(e,t,s,n).text}a(le,"sliceByColumn");function J(e,t,s,n=!1){if(s<=0)return{text:"",width:0};const o=t+s;let r="",i=0,l=0,c=0,f="";for(;c<e.length;){const u=k(e,c);if(u){l>=t&&l<o?r+=u.code:l<t&&(f+=u.code),c+=u.length;continue}let h=c;for(;h<e.length&&!k(e,h);)h++;for(const{segment:p}of A.segment(e.slice(c,h))){const d=C(p),w=l>=t&&l<o,g=!n||l+d<=o;if(w&&g&&(f&&(r+=f,f=""),r+=p,i+=d),l+=d,l>=o)break}if(c=h,l>=o)break}return{text:r,width:i}}a(J,"sliceWithWidth");const T=new R;function oe(e,t,s,n,o=!1){let r="",i=0,l="",c=0,f=0,u=0,h="",p=!1;const d=s+n;for(T.clear();u<e.length;){const w=k(e,u);if(w){T.process(w.code),f<t?h+=w.code:f>=s&&f<d&&p&&(l+=w.code),u+=w.length;continue}let g=u;for(;g<e.length&&!k(e,g);)g++;for(const{segment:b}of A.segment(e.slice(u,g))){const m=C(b);if(f<t?(h&&(r+=h,h=""),r+=b,i+=m):f>=s&&f<d&&(!o||f+m<=d)&&(p||(l+=T.getActiveCodes(),p=!0),l+=b,c+=m),f+=m,n<=0?f>=t:f>=d)break}if(u=g,n<=0?f>=t:f>=d)break}return{before:r,beforeWidth:i,after:l,afterWidth:c}}a(oe,"extractSegments");export{U as PUNCTUATION_REGEX,se as applyBackgroundToLine,k as extractAnsiCode,oe as extractSegments,Q as getGraphemeSegmenter,Y as getWordSegmenter,ie as isPunctuationChar,ne as isWhitespaceChar,ee as normalizeTerminalOutput,le as sliceByColumn,J as sliceWithWidth,re as truncateToWidth,v as visibleWidth,te as wrapTextWithAnsi};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Options for word navigation functions.
3
+ * When omitted, uses the default Intl.Segmenter word segmentation.
4
+ */
5
+ export interface WordNavigationOptions {
6
+ /** Custom segmenter returning word segments for the given text. */
7
+ segment?: (text: string) => Iterable<Intl.SegmentData>;
8
+ /** Predicate identifying atomic segments that should be treated as single units (e.g. paste markers). */
9
+ isAtomicSegment?: (segment: string) => boolean;
10
+ }
11
+ /**
12
+ * Find the cursor position after moving one word backward from `cursor` in `text`.
13
+ * Skips trailing whitespace, then stops at the next word/punctuation boundary.
14
+ *
15
+ * Pure function - does not mutate any state.
16
+ */
17
+ export declare function findWordBackward(text: string, cursor: number, options?: WordNavigationOptions): number;
18
+ /**
19
+ * Find the cursor position after moving one word forward from `cursor` in `text`.
20
+ * Skips leading whitespace, then stops at the next word/punctuation boundary.
21
+ *
22
+ * Pure function - does not mutate any state.
23
+ */
24
+ export declare function findWordForward(text: string, cursor: number, options?: WordNavigationOptions): number;
@@ -0,0 +1 @@
1
+ var v=Object.defineProperty;var c=(l,g)=>v(l,"name",{value:g,configurable:!0});import{getWordSegmenter as w,isWhitespaceChar as h,PUNCTUATION_REGEX as d}from"./utils.js";const f=w();function p(l,g,i){if(g<=0)return 0;const o=l.slice(0,g),r=i?.segment,m=i?.isAtomicSegment,t=r?[...r(o)]:[...f.segment(o)];let n=g;for(;t.length>0&&!m?.(t[t.length-1]?.segment||"")&&h(t[t.length-1]?.segment||"");)n-=t.pop()?.segment.length||0;if(t.length===0)return n;const e=t[t.length-1];if(m?.(e.segment))n-=e.segment.length;else if(e.isWordLike){const s=e.segment,a=[...s.matchAll(new RegExp(d,"g"))];if(a.length<=0)n-=s.length;else{const u=a[a.length-1];n-=s.length-(u.index+u[0].length)}}else for(;t.length>0&&!m?.(t[t.length-1]?.segment||"")&&!t[t.length-1]?.isWordLike&&!h(t[t.length-1]?.segment||"");)n-=t.pop()?.segment.length||0;return n}c(p,"findWordBackward");function A(l,g,i){if(g>=l.length)return l.length;const o=l.slice(g),r=i?.segment,m=i?.isAtomicSegment,n=(r?r(o):f.segment(o))[Symbol.iterator]();let e=n.next(),s=g;for(;!e.done&&!m?.(e.value.segment)&&h(e.value.segment);)s+=e.value.segment.length,e=n.next();if(e.done)return s;if(m?.(e.value.segment))s+=e.value.segment.length;else if(e.value.isWordLike)s+=d.exec(e.value.segment)?.index??e.value.segment.length;else for(;!e.done&&!m?.(e.value.segment)&&!e.value.isWordLike&&!h(e.value.segment);)s+=e.value.segment.length,e=n.next();return s}c(A,"findWordForward");export{p as findWordBackward,A as findWordForward};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@openadapter/koda-tui",
3
+ "version": "1.0.0-beta.3",
4
+ "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "clean": "shx rm -rf dist",
9
+ "build": "tsgo -p tsconfig.build.json",
10
+ "test": "node --test test/*.test.ts",
11
+ "prepublishOnly": "npm run clean && npm run build",
12
+ "postbuild": "node ../../scripts/minify-pkg-dist.mjs"
13
+ },
14
+ "files": [
15
+ "dist/**/*",
16
+ "native/win32/prebuilds/**/*.node",
17
+ "native/darwin/prebuilds/**/*.node",
18
+ "README.md"
19
+ ],
20
+ "keywords": [
21
+ "tui",
22
+ "terminal",
23
+ "ui",
24
+ "text-editor",
25
+ "differential-rendering",
26
+ "typescript",
27
+ "cli"
28
+ ],
29
+ "author": "OpenAdapter",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/claraverse-space/koda.git",
34
+ "directory": "packages/tui"
35
+ },
36
+ "engines": {
37
+ "node": ">=22.19.0"
38
+ },
39
+ "types": "./dist/index.d.ts",
40
+ "dependencies": {
41
+ "get-east-asian-width": "1.6.0",
42
+ "marked": "15.0.12"
43
+ },
44
+ "devDependencies": {
45
+ "@xterm/headless": "5.5.0",
46
+ "chalk": "5.6.2"
47
+ },
48
+ "homepage": "https://openadapter.in",
49
+ "bugs": {
50
+ "url": "https://github.com/claraverse-space/koda/issues"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }