@flowtty/core 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ import { loadYoga } from 'yoga-layout/load';
2
+
3
+ type Yoga = Awaited<ReturnType<typeof loadYoga>>;
4
+ type YogaNode = ReturnType<Yoga['Node']['create']>;
5
+ declare function getYoga(): Promise<Yoga>;
6
+
7
+ type BorderStyle = 'single' | 'double' | 'round' | 'bold' | 'classic';
8
+ /** The library-wide default for components that draw a box-border chrome
9
+ * (DialogHost wrappers, Menu panels, Table grids). Declared once so the
10
+ * default is a single edit, not one per component. */
11
+ declare const DEFAULT_BORDER_STYLE: BorderStyle;
12
+ interface GridChars {
13
+ h: string;
14
+ v: string;
15
+ tl: string;
16
+ tr: string;
17
+ bl: string;
18
+ br: string;
19
+ tDown: string;
20
+ tUp: string;
21
+ tRight: string;
22
+ tLeft: string;
23
+ cross: string;
24
+ }
25
+ declare const GRID_CHARS: Record<BorderStyle, GridChars>;
26
+ type BorderChars = Pick<GridChars, 'h' | 'v' | 'tl' | 'tr' | 'bl' | 'br'>;
27
+ declare const BORDER_CHARS: Record<BorderStyle, BorderChars>;
28
+
29
+ interface Rect {
30
+ left: number;
31
+ top: number;
32
+ width: number;
33
+ height: number;
34
+ }
35
+ declare function computeLayout(container: Container, width: number, height: number): void;
36
+ declare function layoutOf(inst: Instance, offsetX?: number, offsetY?: number): Rect;
37
+
38
+ type HostType = 'flowtty-box';
39
+ interface BoxProps {
40
+ /** Fixed size in cells. Strings like '100%' use Yoga's percentage sizing. */
41
+ width?: number | string;
42
+ height?: number | string;
43
+ flexDirection?: 'row' | 'column';
44
+ /** Default 'static' (Yoga's stack flow). 'absolute' positions via top/left/right/bottom relative to the nearest non-static ancestor. */
45
+ position?: 'static' | 'absolute';
46
+ top?: number;
47
+ left?: number;
48
+ right?: number;
49
+ bottom?: number;
50
+ /** Main-axis alignment of children (flexbox). */
51
+ justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
52
+ /** Cross-axis alignment of children. */
53
+ alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch';
54
+ wrap?: 'wrap' | 'truncate' | 'none';
55
+ color?: string;
56
+ bold?: boolean;
57
+ dim?: boolean;
58
+ underline?: boolean;
59
+ inverse?: boolean;
60
+ strikethrough?: boolean;
61
+ link?: string;
62
+ backgroundColor?: string;
63
+ border?: BorderStyle;
64
+ borderColor?: string;
65
+ borderTitle?: string;
66
+ padding?: number;
67
+ paddingX?: number;
68
+ paddingY?: number;
69
+ paddingTop?: number;
70
+ paddingRight?: number;
71
+ paddingBottom?: number;
72
+ paddingLeft?: number;
73
+ margin?: number;
74
+ marginX?: number;
75
+ marginY?: number;
76
+ marginTop?: number;
77
+ marginRight?: number;
78
+ marginBottom?: number;
79
+ marginLeft?: number;
80
+ gap?: number;
81
+ rowGap?: number;
82
+ columnGap?: number;
83
+ flexGrow?: number;
84
+ flexShrink?: number;
85
+ flexBasis?: number | 'auto' | `${number}%`;
86
+ flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
87
+ /** Cross-axis distribution of wrap lines. Only effective when flexWrap is 'wrap' or 'wrap-reverse'
88
+ * AND the parent has extra cross-axis space. Default 'flex-start'. */
89
+ alignContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch';
90
+ /** Minimum cell size — Yoga prevents the box from shrinking below this.
91
+ * Accepts a number (cells) or a percent string (e.g. '50%'). Undefined = no minimum. */
92
+ minWidth?: number | `${number}%`;
93
+ maxWidth?: number | `${number}%`;
94
+ minHeight?: number | `${number}%`;
95
+ maxHeight?: number | `${number}%`;
96
+ /** Width / height ratio. Yoga derives the missing dimension from the constrained one.
97
+ * CSS convention: `aspectRatio: 2` = twice as wide as tall; `0.5` = twice as tall as wide; `1` = square. */
98
+ aspectRatio?: number;
99
+ /** 'none' removes this box and all descendants from layout (siblings don't reserve space for it,
100
+ * and paint skips the subtree). Default 'flex'. Useful for conditional UI without unmounting. */
101
+ display?: 'flex' | 'none';
102
+ /** Fires after layout with this box's computed rect. Use to read allocated dimensions
103
+ * for responsive rendering (e.g. paginating an article reader). **Diff before setState** —
104
+ * this fires on EVERY paint, and unconditionally calling setState with a new object
105
+ * will infinite-loop. Pattern:
106
+ * onLayout={(r) => { if (!size || size.width !== r.width || size.height !== r.height) setSize(r); }} */
107
+ onLayout?: (rect: Rect) => void;
108
+ /** Stacking order within the same paint pass. Higher values paint on top.
109
+ * Default 0. Tree order is the tiebreaker. Does NOT cross pass boundaries:
110
+ * absolutes always overlay stack-flow regardless of zIndex. */
111
+ zIndex?: number;
112
+ /** Clip descendants to this box's content rect. Default 'visible' (no clipping).
113
+ * 'hidden' clips ALL descendant writes including their backgrounds and borders.
114
+ * Does NOT clip this box's own background or border (those are this box's own area). */
115
+ overflow?: 'visible' | 'hidden';
116
+ }
117
+ interface Instance {
118
+ type: 'box';
119
+ props: BoxProps;
120
+ yogaNode: YogaNode;
121
+ children: Array<Instance | TextInstance>;
122
+ }
123
+ interface TextInstance {
124
+ type: 'text';
125
+ text: string;
126
+ parent?: Instance;
127
+ }
128
+ /**
129
+ * Adapter-facing root container: a list of top-level Instances plus the loaded
130
+ * Yoga module. Each framework adapter (React reconciler, Vue, …) builds one of
131
+ * these and passes it to `computeLayout` + `paint`. Lives in host so layout /
132
+ * paint (in core) can depend on it without pulling in any adapter code.
133
+ */
134
+ interface Container {
135
+ children: Instance[];
136
+ Yoga: Yoga;
137
+ }
138
+ declare function createInstance(type: HostType, props: BoxProps, Yoga: Yoga): Instance;
139
+ declare function createTextInstance(text: string, _Yoga: Yoga): TextInstance;
140
+ declare function applyProps(inst: Instance, props: BoxProps, _Yoga: Yoga): void;
141
+ declare function measureText(text: string): {
142
+ width: number;
143
+ height: number;
144
+ };
145
+ declare function ownText(inst: Instance): string;
146
+ declare function refreshMeasure(inst: Instance, _Yoga: Yoga): void;
147
+ declare function appendChild(parent: Instance, child: Instance | TextInstance, Yoga: Yoga): void;
148
+ declare function removeChild(parent: Instance, child: Instance | TextInstance, Yoga: Yoga): void;
149
+ declare function insertBefore(parent: Instance, child: Instance | TextInstance, before: Instance | TextInstance, Yoga: Yoga): void;
150
+
151
+ export { BORDER_CHARS as B, type Container as C, DEFAULT_BORDER_STYLE as D, GRID_CHARS as G, type HostType as H, type Instance as I, type Rect as R, type TextInstance as T, type Yoga as Y, type BorderChars as a, type BorderStyle as b, type BoxProps as c, type GridChars as d, type YogaNode as e, appendChild as f, applyProps as g, computeLayout as h, createInstance as i, createTextInstance as j, getYoga as k, insertBefore as l, layoutOf as m, measureText as n, ownText as o, removeChild as p, refreshMeasure as r };
@@ -0,0 +1,143 @@
1
+ export { B as Buffer, C as Cell, S as Style } from './cells-CaXEx4lH.js';
2
+ import { K as Key } from './backend-BcB7VR87.js';
3
+ export { B as Backend } from './backend-BcB7VR87.js';
4
+ export { a as BorderChars, b as BorderStyle, c as BoxProps, D as DEFAULT_BORDER_STYLE, G as GRID_CHARS, d as GridChars } from './host-DttBG-OZ.js';
5
+ import 'yoga-layout/load';
6
+
7
+ type WrapMode = 'wrap' | 'truncate' | 'none';
8
+ /**
9
+ * Lay out `text` into display lines fitting within `width` cells.
10
+ * Assumes 1 code point = 1 cell (no CJK/emoji width awareness in M1d).
11
+ *
12
+ * - 'wrap' — word-wrap at spaces; any single word longer than width is char-wrapped.
13
+ * - 'truncate' — each source line truncated to width, with `…` in the last cell when truncated.
14
+ * - 'none' — each source line preserved unchanged (caller is responsible for overflow).
15
+ *
16
+ * Always returns at least one line (empty input → `['']`, matching measureText's height=1 default).
17
+ */
18
+ declare function wrapText(text: string, width: number, mode: WrapMode): string[];
19
+
20
+ /**
21
+ * Split a multi-line source text into visual lines for paginated rendering
22
+ * with an optional line-number gutter.
23
+ *
24
+ * - 'nowrap': one visual line per source line (caller is responsible for
25
+ * horizontal clipping — typically via wrap='truncate' on the rendering Box).
26
+ * - 'wrap': source lines longer than `width` are hard-wrapped at the cell
27
+ * boundary; continuation visual-lines carry `lineNum: null` so the gutter
28
+ * stays blank for them and only the first visual line shows the source line
29
+ * number.
30
+ *
31
+ * Width is in cells (counted via `[...line]` for grapheme-naive codepoint
32
+ * iteration — matches flowtty's paint-side width assumptions).
33
+ *
34
+ * Typical use:
35
+ * ```ts
36
+ * const lines = splitVisualLines(bodyText, wrapMode, effectiveBodyWidth);
37
+ * const page = lines.slice(pageIdx * pageH, (pageIdx + 1) * pageH);
38
+ * page.map(({ text, lineNum }) => ...);
39
+ * ```
40
+ */
41
+ interface VisualLine {
42
+ text: string;
43
+ /** Source-file line number for the first visual line of a wrapped source line; null for continuation lines. 1-based. */
44
+ lineNum: number | null;
45
+ }
46
+ declare function splitVisualLines(text: string, mode: 'wrap' | 'nowrap', width: number): VisualLine[];
47
+
48
+ /**
49
+ * Center a `visible`-sized window of items around `cursor`, clamped so the
50
+ * window never starts before 0 or extends past the end of the list. Returns
51
+ * the start index and the sliced items.
52
+ *
53
+ * Typical use — list views that scroll a cursor through long data:
54
+ * ```ts
55
+ * const { start, items } = windowAround(rows, cursor, termHeight - chrome);
56
+ * items.map((row, i) => {
57
+ * const sel = (start + i) === cursor;
58
+ * ...
59
+ * });
60
+ * ```
61
+ *
62
+ * The window prefers to center the cursor; near the edges it sticks to the
63
+ * top/bottom so the cursor remains visible without empty padding.
64
+ */
65
+ declare function windowAround<T>(items: readonly T[], cursor: number, visible: number): {
66
+ start: number;
67
+ items: T[];
68
+ };
69
+
70
+ /**
71
+ * Display width in terminal cells of a single Unicode code point: 0 for control
72
+ * / combining / zero-width, 2 for East Asian Wide & Fullwidth (and most emoji),
73
+ * 1 otherwise. Pass a code point (e.g. from `String#codePointAt`), not a string.
74
+ */
75
+ declare function charWidth(cp: number): 0 | 1 | 2;
76
+ /**
77
+ * Display width in terminal cells of a string, summing `charWidth` over its
78
+ * code points (surrogate pairs counted once). See the file header for the
79
+ * grapheme-cluster caveat. Expects plain text, not ANSI-styled.
80
+ */
81
+ declare function stringWidth(str: string): number;
82
+
83
+ interface EditorState {
84
+ value: string;
85
+ cursor: number;
86
+ }
87
+ type EditorAction = {
88
+ kind: 'edit';
89
+ state: EditorState;
90
+ } | {
91
+ kind: 'submit';
92
+ } | {
93
+ kind: 'cancel';
94
+ } | {
95
+ kind: 'noop';
96
+ };
97
+ declare function reduce$2(state: EditorState, key: Key): EditorAction;
98
+
99
+ interface SelectItem<T> {
100
+ label: string;
101
+ value: T;
102
+ }
103
+ interface SelectState {
104
+ cursor: number;
105
+ filter: string;
106
+ }
107
+ type SelectAction = {
108
+ kind: 'state';
109
+ state: SelectState;
110
+ } | {
111
+ kind: 'submit';
112
+ index: number;
113
+ } | {
114
+ kind: 'cancel';
115
+ } | {
116
+ kind: 'noop';
117
+ };
118
+ /**
119
+ * Indices into `items` of items whose label contains `filter` (case-insensitive
120
+ * substring). Empty filter → all indices.
121
+ */
122
+ declare function visibleIndices<T>(items: SelectItem<T>[], filter: string): number[];
123
+ declare function reduce$1<T>(items: SelectItem<T>[], state: SelectState, key: Key): SelectAction;
124
+
125
+ interface MultiSelectState {
126
+ cursor: number;
127
+ }
128
+ type MultiSelectAction = {
129
+ kind: 'state';
130
+ state: MultiSelectState;
131
+ } | {
132
+ kind: 'toggle';
133
+ index: number;
134
+ } | {
135
+ kind: 'submit';
136
+ } | {
137
+ kind: 'cancel';
138
+ } | {
139
+ kind: 'noop';
140
+ };
141
+ declare function reduce<T>(items: SelectItem<T>[], state: MultiSelectState, key: Key): MultiSelectAction;
142
+
143
+ export { type EditorAction, type EditorState, Key, type MultiSelectAction, type MultiSelectState, type SelectAction, type SelectItem, type SelectState, type VisualLine, type WrapMode, charWidth, reduce$2 as editorReducer, reduce as multiSelectReducer, reduce$1 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround, wrapText };
package/dist/index.js ADDED
@@ -0,0 +1,382 @@
1
+ export { Buffer, DEFAULT_BORDER_STYLE, GRID_CHARS, wrapText } from './chunk-D6HW2BNM.js';
2
+
3
+ // src/visualLines.ts
4
+ function splitVisualLines(text, mode, width) {
5
+ const sources = text.split("\n");
6
+ if (mode === "nowrap" || width <= 0) {
7
+ return sources.map((line, i) => ({ text: line, lineNum: i + 1 }));
8
+ }
9
+ const out = [];
10
+ for (let i = 0; i < sources.length; i++) {
11
+ const line = sources[i];
12
+ const chars = [...line];
13
+ if (chars.length <= width) {
14
+ out.push({ text: line, lineNum: i + 1 });
15
+ continue;
16
+ }
17
+ let first = true;
18
+ for (let j = 0; j < chars.length; j += width) {
19
+ out.push({
20
+ text: chars.slice(j, j + width).join(""),
21
+ lineNum: first ? i + 1 : null
22
+ });
23
+ first = false;
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+
29
+ // src/windowAround.ts
30
+ function windowAround(items, cursor, visible) {
31
+ const v = Math.max(1, visible);
32
+ const start = Math.max(0, Math.min(items.length - v, cursor - Math.floor(v / 2)));
33
+ return { start, items: items.slice(start, start + v) };
34
+ }
35
+
36
+ // src/displayWidth.ts
37
+ var ZERO_WIDTH = [
38
+ [768, 879],
39
+ [1155, 1158],
40
+ [1160, 1161],
41
+ [1425, 1469],
42
+ [1471, 1471],
43
+ [1473, 1474],
44
+ [1476, 1477],
45
+ [1479, 1479],
46
+ [1536, 1539],
47
+ [1552, 1557],
48
+ [1611, 1630],
49
+ [1648, 1648],
50
+ [1750, 1764],
51
+ [1767, 1768],
52
+ [1770, 1773],
53
+ [1807, 1807],
54
+ [1809, 1809],
55
+ [1840, 1866],
56
+ [1958, 1968],
57
+ [2027, 2035],
58
+ [2305, 2306],
59
+ [2364, 2364],
60
+ [2369, 2376],
61
+ [2381, 2381],
62
+ [2385, 2388],
63
+ [2402, 2403],
64
+ [2433, 2433],
65
+ [2492, 2492],
66
+ [2497, 2500],
67
+ [2509, 2509],
68
+ [2530, 2531],
69
+ [2561, 2562],
70
+ [2620, 2620],
71
+ [2625, 2626],
72
+ [2631, 2632],
73
+ [2635, 2637],
74
+ [2672, 2673],
75
+ [2689, 2690],
76
+ [2748, 2748],
77
+ [2753, 2757],
78
+ [2759, 2760],
79
+ [2765, 2765],
80
+ [2786, 2787],
81
+ [2817, 2817],
82
+ [2876, 2876],
83
+ [2879, 2879],
84
+ [2881, 2883],
85
+ [2893, 2893],
86
+ [2902, 2902],
87
+ [2946, 2946],
88
+ [3008, 3008],
89
+ [3021, 3021],
90
+ [3134, 3136],
91
+ [3142, 3144],
92
+ [3146, 3149],
93
+ [3157, 3158],
94
+ [3260, 3260],
95
+ [3263, 3263],
96
+ [3270, 3270],
97
+ [3276, 3277],
98
+ [3298, 3299],
99
+ [3393, 3395],
100
+ [3405, 3405],
101
+ [3530, 3530],
102
+ [3538, 3540],
103
+ [3542, 3542],
104
+ [3633, 3633],
105
+ [3636, 3642],
106
+ [3655, 3662],
107
+ [3761, 3761],
108
+ [3764, 3769],
109
+ [3771, 3772],
110
+ [3784, 3789],
111
+ [3864, 3865],
112
+ [3893, 3893],
113
+ [3895, 3895],
114
+ [3897, 3897],
115
+ [3953, 3966],
116
+ [3968, 3972],
117
+ [3974, 3975],
118
+ [3984, 3991],
119
+ [3993, 4028],
120
+ [4038, 4038],
121
+ [4141, 4144],
122
+ [4146, 4146],
123
+ [4150, 4151],
124
+ [4153, 4153],
125
+ [4184, 4185],
126
+ [4448, 4607],
127
+ [4959, 4959],
128
+ [5906, 5908],
129
+ [5938, 5940],
130
+ [5970, 5971],
131
+ [6002, 6003],
132
+ [6068, 6069],
133
+ [6071, 6077],
134
+ [6086, 6086],
135
+ [6089, 6099],
136
+ [6109, 6109],
137
+ [6155, 6157],
138
+ [6313, 6313],
139
+ [6432, 6434],
140
+ [6439, 6440],
141
+ [6450, 6450],
142
+ [6457, 6459],
143
+ [6679, 6680],
144
+ [6912, 6915],
145
+ [6964, 6964],
146
+ [6966, 6970],
147
+ [6972, 6972],
148
+ [6978, 6978],
149
+ [7019, 7027],
150
+ [7616, 7626],
151
+ [7678, 7679],
152
+ [8203, 8207],
153
+ [8234, 8238],
154
+ [8288, 8291],
155
+ [8298, 8303],
156
+ [8400, 8431],
157
+ [12330, 12335],
158
+ [12441, 12442],
159
+ [43014, 43014],
160
+ [43019, 43019],
161
+ [43045, 43046],
162
+ [64286, 64286],
163
+ [65024, 65039],
164
+ [65056, 65059],
165
+ [65279, 65279],
166
+ [65529, 65531],
167
+ [68097, 68099],
168
+ [68101, 68102],
169
+ [68108, 68111],
170
+ [68152, 68154],
171
+ [68159, 68159],
172
+ [119143, 119145],
173
+ [119155, 119170],
174
+ [119173, 119179],
175
+ [119210, 119213],
176
+ [119362, 119364],
177
+ [917505, 917505],
178
+ [917536, 917631],
179
+ [917760, 917999]
180
+ ];
181
+ var WIDE = [
182
+ [4352, 4447],
183
+ // Hangul Jamo (leading consonants)
184
+ [9001, 9002],
185
+ // angle brackets
186
+ [11904, 12350],
187
+ // CJK Radicals … Kangxi … CJK Symbols (stops before U+303F)
188
+ [12353, 13311],
189
+ // Hiragana … Katakana … CJK compatibility
190
+ [13312, 19903],
191
+ // CJK Unified Ideographs Extension A
192
+ [19968, 40959],
193
+ // CJK Unified Ideographs
194
+ [40960, 42191],
195
+ // Yi Syllables / Radicals
196
+ [43360, 43391],
197
+ // Hangul Jamo Extended-A
198
+ [44032, 55203],
199
+ // Hangul Syllables
200
+ [63744, 64255],
201
+ // CJK Compatibility Ideographs
202
+ [65040, 65049],
203
+ // Vertical Forms
204
+ [65072, 65135],
205
+ // CJK Compatibility Forms / Small Form Variants
206
+ [65280, 65376],
207
+ // Fullwidth Forms (stops before halfwidth katakana U+FF61)
208
+ [65504, 65510],
209
+ // Fullwidth signs
210
+ [110592, 110593],
211
+ // Kana Supplement
212
+ [127488, 127569],
213
+ // Enclosed Ideographic Supplement
214
+ [127744, 128591],
215
+ // Misc Symbols & Pictographs + Emoticons
216
+ [129280, 129535],
217
+ // Supplemental Symbols and Pictographs
218
+ [131072, 262141]
219
+ // CJK Unified Ideographs Extension B and beyond
220
+ ];
221
+ function inRanges(cp, table) {
222
+ let lo = 0;
223
+ let hi = table.length - 1;
224
+ while (lo <= hi) {
225
+ const mid = lo + hi >> 1;
226
+ const [start, end] = table[mid];
227
+ if (cp < start) hi = mid - 1;
228
+ else if (cp > end) lo = mid + 1;
229
+ else return true;
230
+ }
231
+ return false;
232
+ }
233
+ function charWidth(cp) {
234
+ if (cp < 32 || cp >= 127 && cp < 160) return 0;
235
+ if (inRanges(cp, ZERO_WIDTH)) return 0;
236
+ if (inRanges(cp, WIDE)) return 2;
237
+ return 1;
238
+ }
239
+ function stringWidth(str) {
240
+ let w = 0;
241
+ for (const ch of str) w += charWidth(ch.codePointAt(0));
242
+ return w;
243
+ }
244
+
245
+ // src/editor.ts
246
+ var OPT_MAP = {
247
+ " ": ["\xA0"],
248
+ // U+00A0 NBSP
249
+ "-": ["\u2013", "\u2014"],
250
+ // en-dash U+2013, em-dash U+2014
251
+ "[": ["\u201C", "\u201D"],
252
+ // left/right double quote
253
+ "]": ["\u2018", "\u2019"]
254
+ // left/right single quote
255
+ };
256
+ var isWord = (c) => /[\p{L}\p{N}_]/u.test(c);
257
+ function wordLeft(value, cursor) {
258
+ let c = cursor;
259
+ while (c > 0 && !isWord(value[c - 1])) c--;
260
+ while (c > 0 && isWord(value[c - 1])) c--;
261
+ return c;
262
+ }
263
+ function wordRight(value, cursor) {
264
+ let c = cursor;
265
+ while (c < value.length && !isWord(value[c])) c++;
266
+ while (c < value.length && isWord(value[c])) c++;
267
+ return c;
268
+ }
269
+ function reduce(state, key) {
270
+ const { value, cursor } = state;
271
+ const clamp = (n) => Math.max(0, Math.min(value.length, n));
272
+ if (key.name === "left" && key.meta) return { kind: "edit", state: { value, cursor: wordLeft(value, cursor) } };
273
+ if (key.name === "b" && key.meta) return { kind: "edit", state: { value, cursor: wordLeft(value, cursor) } };
274
+ if (key.name === "right" && key.meta) return { kind: "edit", state: { value, cursor: wordRight(value, cursor) } };
275
+ if (key.name === "f" && key.meta) return { kind: "edit", state: { value, cursor: wordRight(value, cursor) } };
276
+ if (key.name === "left") return { kind: "edit", state: { value, cursor: clamp(cursor - 1) } };
277
+ if (key.name === "right") return { kind: "edit", state: { value, cursor: clamp(cursor + 1) } };
278
+ if (key.name === "home") return { kind: "edit", state: { value, cursor: 0 } };
279
+ if (key.name === "end") return { kind: "edit", state: { value, cursor: value.length } };
280
+ if (key.name === "a" && key.ctrl) return { kind: "edit", state: { value, cursor: 0 } };
281
+ if (key.name === "e" && key.ctrl) return { kind: "edit", state: { value, cursor: value.length } };
282
+ if (key.name === "backspace" && key.meta) {
283
+ const start = wordLeft(value, cursor);
284
+ return { kind: "edit", state: { value: value.slice(0, start) + value.slice(cursor), cursor: start } };
285
+ }
286
+ if (key.name === "w" && key.ctrl) {
287
+ const start = wordLeft(value, cursor);
288
+ return { kind: "edit", state: { value: value.slice(0, start) + value.slice(cursor), cursor: start } };
289
+ }
290
+ if (key.name === "d" && key.meta) {
291
+ const end = wordRight(value, cursor);
292
+ return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(end), cursor } };
293
+ }
294
+ if (key.name === "delete" && key.meta) {
295
+ const end = wordRight(value, cursor);
296
+ return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(end), cursor } };
297
+ }
298
+ if (key.name === "k" && key.ctrl) {
299
+ return { kind: "edit", state: { value: value.slice(0, cursor), cursor } };
300
+ }
301
+ if (key.name === "u" && key.ctrl) {
302
+ return { kind: "edit", state: { value: value.slice(cursor), cursor: 0 } };
303
+ }
304
+ if (key.meta && OPT_MAP[key.name]) {
305
+ const entry = OPT_MAP[key.name];
306
+ const ch = key.shift && entry[1] !== void 0 ? entry[1] : entry[0];
307
+ return { kind: "edit", state: { value: value.slice(0, cursor) + ch + value.slice(cursor), cursor: cursor + 1 } };
308
+ }
309
+ if (key.name === "backspace") {
310
+ if (cursor === 0) return { kind: "edit", state };
311
+ return { kind: "edit", state: { value: value.slice(0, cursor - 1) + value.slice(cursor), cursor: cursor - 1 } };
312
+ }
313
+ if (key.name === "delete" || key.name === "d" && key.ctrl) {
314
+ if (cursor === value.length) return { kind: "edit", state };
315
+ return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(cursor + 1), cursor } };
316
+ }
317
+ if (key.name === "return") return { kind: "submit" };
318
+ if (key.name === "escape") return { kind: "cancel" };
319
+ if (!key.ctrl && !key.meta && key.name.length === 1) {
320
+ const ch = key.name;
321
+ return { kind: "edit", state: { value: value.slice(0, cursor) + ch + value.slice(cursor), cursor: cursor + 1 } };
322
+ }
323
+ return { kind: "noop" };
324
+ }
325
+
326
+ // src/selectReducer.ts
327
+ function visibleIndices(items, filter) {
328
+ if (filter === "") return items.map((_, i) => i);
329
+ const q = filter.toLowerCase();
330
+ const out = [];
331
+ for (let i = 0; i < items.length; i++) {
332
+ if (items[i].label.toLowerCase().includes(q)) out.push(i);
333
+ }
334
+ return out;
335
+ }
336
+ function reduce2(items, state, key) {
337
+ const visible = visibleIndices(items, state.filter);
338
+ const n = visible.length;
339
+ if (key.name === "escape") return { kind: "cancel" };
340
+ if (key.name === "return") {
341
+ if (n === 0) return { kind: "noop" };
342
+ const cursor = Math.min(state.cursor, n - 1);
343
+ return { kind: "submit", index: visible[cursor] };
344
+ }
345
+ if (key.name === "down" || key.name === "j" && !key.ctrl && !key.meta) {
346
+ if (n === 0) return { kind: "noop" };
347
+ return { kind: "state", state: { cursor: (state.cursor + 1) % n, filter: state.filter } };
348
+ }
349
+ if (key.name === "up" || key.name === "k" && !key.ctrl && !key.meta) {
350
+ if (n === 0) return { kind: "noop" };
351
+ return { kind: "state", state: { cursor: (state.cursor - 1 + n) % n, filter: state.filter } };
352
+ }
353
+ if (key.name === "backspace") {
354
+ if (state.filter === "") return { kind: "noop" };
355
+ return { kind: "state", state: { cursor: 0, filter: state.filter.slice(0, -1) } };
356
+ }
357
+ if (!key.ctrl && !key.meta && key.name.length === 1) {
358
+ return { kind: "state", state: { cursor: 0, filter: state.filter + key.name } };
359
+ }
360
+ return { kind: "noop" };
361
+ }
362
+
363
+ // src/multiSelectReducer.ts
364
+ function reduce3(items, state, key) {
365
+ const n = items.length;
366
+ if (key.name === "escape") return { kind: "cancel" };
367
+ if (key.name === "return") return { kind: "submit" };
368
+ if (n === 0) return { kind: "noop" };
369
+ if (key.name === "down" || key.name === "j" && !key.ctrl && !key.meta) {
370
+ return { kind: "state", state: { cursor: (state.cursor + 1) % n } };
371
+ }
372
+ if (key.name === "up" || key.name === "k" && !key.ctrl && !key.meta) {
373
+ return { kind: "state", state: { cursor: (state.cursor - 1 + n) % n } };
374
+ }
375
+ if (key.name === " ") {
376
+ const index = Math.max(0, Math.min(state.cursor, n - 1));
377
+ return { kind: "toggle", index };
378
+ }
379
+ return { kind: "noop" };
380
+ }
381
+
382
+ export { charWidth, reduce as editorReducer, reduce3 as multiSelectReducer, reduce2 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround };