@ai-matrx/terminal 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1
4
+
5
+ From the first independent phone walk.
6
+
7
+ - Accessory keys are 44px tall (a full touch target); the bar is 52px (`ACCESSORY_BAR_HEIGHT`).
8
+ - Text zoom: pass `onFontSizeChange` (and feed the size back as `fontSize`) and the person can
9
+ pinch on touch or press ⌘+ / ⌘− / ⌘0; the host decides where the size is remembered. A pinch zooms
10
+ the text, never the page. Helpers `stepFontSize`, `pinchFontSize`, `FONT_SIZE_MIN/MAX`.
11
+
12
+ Consumer action: none required. Hosts that reserve the bar's height read `ACCESSORY_BAR_HEIGHT`.
13
+
14
+ ## 0.1.0
15
+
16
+ First release — Matrx 2 lane E (the phone console), extracted from AI Matrx web.
17
+
18
+ - `<Terminal>`: xterm 6 (DOM renderer); fit to its box or to the visual viewport above the
19
+ on-screen keyboard; theme that follows the host's `.dark` class; a transport-free
20
+ `TerminalHandle` (`write` resolves after parse — pace credit-based streams on it; `reset`,
21
+ `clear`, `getText`); hardware Ctrl-C hook (`onInterrupt`).
22
+ - Phone layer: tap focuses, drag scrolls with momentum (arrows in the alternate screen),
23
+ press-and-hold selects a word and drags to extend, Copy pill; hidden input at 16px with
24
+ autocorrect and autocapitalize off.
25
+ - `<AccessoryBar>`: Esc Tab Ctrl Alt | ← ↓ ↑ → | `| / ~ -` | Home End PgUp PgDn | Paste, hide
26
+ keyboard; Ctrl/Alt one-shot or locked; keys keep the keyboard up.
27
+ - `createLineEditor`: a shell's line discipline for backends that run one command per call —
28
+ prompt, cursor editing, ↑/↓ history, Ctrl-A/E/K/U/L/C, multi-line paste run line by line,
29
+ typeahead kept while a command runs, `setLine` for text staged from outside.
30
+ - Stylesheets ship in the package and `<Terminal>` imports them; `tokens.css` holds defaults.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI Matrix Engine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,53 @@
1
- # Temporary Holding Version
1
+ # `@ai-matrx/terminal`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The AI Matrx terminal: xterm 6 with everything a host would otherwise rebuild, phone first.
4
+
5
+ ```bash
6
+ pnpm add @ai-matrx/terminal
7
+ ```
8
+
9
+ | Entry | What it is |
10
+ |---|---|
11
+ | `@ai-matrx/terminal` | Framework-free core: accessory keys and their bytes, the Ctrl/Alt latch, the touch gesture recognizer, selection and visual-viewport math, the theme, and `createLineEditor`. |
12
+ | `@ai-matrx/terminal/react` | `<Terminal>` and `<AccessoryBar>`. |
13
+ | `@ai-matrx/terminal/styles.css` | Structural CSS (imported by `<Terminal>` itself). |
14
+ | `@ai-matrx/terminal/tokens.css` | Default `--mxt-*` colours, for a host without its own theme. |
15
+
16
+ ## A PTY
17
+
18
+ ```tsx
19
+ import { Terminal } from "@ai-matrx/terminal/react";
20
+
21
+ <Terminal
22
+ fit="viewport" // phones: grow to the bottom of the visual viewport, above the keyboard
23
+ onReady={(term) => pty.onOutput((bytes) => term.write(bytes))} // write() resolves after parse
24
+ onData={(keys) => pty.write(keys)} // already modified by the bar's Ctrl / Alt
25
+ onResize={({ cols, rows }) => pty.resize(cols, rows)}
26
+ />;
27
+ ```
28
+
29
+ ## One command per call (no PTY)
30
+
31
+ ```ts
32
+ import { createLineEditor } from "@ai-matrx/terminal";
33
+
34
+ const editor = createLineEditor({
35
+ write: (d) => term.xterm.write(d),
36
+ prompt: () => "$ ",
37
+ history: () => commands,
38
+ onSubmit: async (line) => {
39
+ term.write(await run(line));
40
+ editor.done(); // prompt again, then any typeahead or pasted lines
41
+ },
42
+ });
43
+ // <Terminal onData={(d) => editor.input(d)} … />
44
+ ```
45
+
46
+ On a coarse pointer the terminal turns on its phone layer: tap to type, drag to scroll with
47
+ momentum, press and hold to select a word (drag to extend, Copy), and a 44px keyboard bar
48
+ (Esc Tab Ctrl Alt, arrows, `| / ~ -`, Home End PgUp PgDn, Paste, hide keyboard). Ctrl: tap = the
49
+ next key, double-tap = locked.
50
+
51
+ Colours: map the `--mxt-*` tokens to your theme, or import `tokens.css`.
52
+
53
+ MIT © AI Matrix Engine
package/dist/index.cjs ADDED
@@ -0,0 +1,567 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ ACCESSORY_BAR_HEIGHT: () => ACCESSORY_BAR_HEIGHT,
24
+ ACCESSORY_KEYS: () => ACCESSORY_KEYS,
25
+ DOUBLE_TAP_MS: () => DOUBLE_TAP_MS,
26
+ FLING_FRICTION: () => FLING_FRICTION,
27
+ FLING_STOP: () => FLING_STOP,
28
+ FONT_SIZE_MAX: () => FONT_SIZE_MAX,
29
+ FONT_SIZE_MIN: () => FONT_SIZE_MIN,
30
+ KEYBOARD_MIN_PX: () => KEYBOARD_MIN_PX,
31
+ LONG_PRESS_MS: () => LONG_PRESS_MS,
32
+ MODIFIER_OFF: () => MODIFIER_OFF,
33
+ TERMINAL_DEFAULTS: () => TERMINAL_DEFAULTS,
34
+ TERMINAL_FONT_FAMILY: () => TERMINAL_FONT_FAMILY,
35
+ afterKey: () => afterKey,
36
+ applyModifiers: () => applyModifiers,
37
+ cellAt: () => cellAt,
38
+ controlCode: () => controlCode,
39
+ createGestureRecognizer: () => createGestureRecognizer,
40
+ createLineEditor: () => createLineEditor,
41
+ flingStep: () => flingStep,
42
+ isActive: () => isActive,
43
+ keySequence: () => keySequence,
44
+ keyboardInset: () => keyboardInset,
45
+ pinchFontSize: () => pinchFontSize,
46
+ spanBetween: () => spanBetween,
47
+ stepFontSize: () => stepFontSize,
48
+ tapModifier: () => tapModifier,
49
+ terminalTheme: () => terminalTheme,
50
+ visibleHeightBelow: () => visibleHeightBelow,
51
+ wordBounds: () => wordBounds
52
+ });
53
+ module.exports = __toCommonJS(src_exports);
54
+
55
+ // src/core/keys.ts
56
+ var ACCESSORY_KEYS = [
57
+ { id: "esc", label: "Esc", aria: "Escape", kind: "send", group: 0 },
58
+ { id: "tab", label: "Tab", aria: "Tab", kind: "send", group: 0 },
59
+ { id: "ctrl", label: "Ctrl", aria: "Control", kind: "modifier", group: 0 },
60
+ { id: "alt", label: "Alt", aria: "Alt", kind: "modifier", group: 0 },
61
+ { id: "left", label: "\u2190", aria: "Left arrow", kind: "send", group: 1 },
62
+ { id: "down", label: "\u2193", aria: "Down arrow", kind: "send", group: 1 },
63
+ { id: "up", label: "\u2191", aria: "Up arrow", kind: "send", group: 1 },
64
+ { id: "right", label: "\u2192", aria: "Right arrow", kind: "send", group: 1 },
65
+ { id: "pipe", label: "|", aria: "Pipe", kind: "send", group: 2 },
66
+ { id: "slash", label: "/", aria: "Slash", kind: "send", group: 2 },
67
+ { id: "tilde", label: "~", aria: "Tilde", kind: "send", group: 2 },
68
+ { id: "dash", label: "-", aria: "Dash", kind: "send", group: 2 },
69
+ { id: "home", label: "Home", aria: "Home", kind: "send", group: 3 },
70
+ { id: "end", label: "End", aria: "End", kind: "send", group: 3 },
71
+ { id: "pgup", label: "PgUp", aria: "Page up", kind: "send", group: 3 },
72
+ { id: "pgdn", label: "PgDn", aria: "Page down", kind: "send", group: 3 },
73
+ { id: "paste", label: "Paste", aria: "Paste", kind: "action", group: 4 },
74
+ { id: "hide", label: "Hide", aria: "Hide keyboard", kind: "action", group: 4 }
75
+ ];
76
+ var ESC = "\x1B";
77
+ var CURSOR = { up: "A", down: "B", right: "C", left: "D", home: "H", end: "F" };
78
+ var TILDE = { pgup: "5", pgdn: "6" };
79
+ var LITERAL = { pipe: "|", slash: "/", tilde: "~", dash: "-", tab: " ", esc: ESC };
80
+ function modParam(mods) {
81
+ return 1 + (mods.alt ? 2 : 0) + (mods.ctrl ? 4 : 0);
82
+ }
83
+ function keySequence(id, mods, modes) {
84
+ const cursor = CURSOR[id];
85
+ if (cursor !== void 0) {
86
+ const m = modParam(mods);
87
+ if (m > 1) return `${ESC}[1;${m}${cursor}`;
88
+ return modes.applicationCursor ? `${ESC}O${cursor}` : `${ESC}[${cursor}`;
89
+ }
90
+ const tilde = TILDE[id];
91
+ if (tilde !== void 0) {
92
+ const m = modParam(mods);
93
+ return m > 1 ? `${ESC}[${tilde};${m}~` : `${ESC}[${tilde}~`;
94
+ }
95
+ const literal = LITERAL[id];
96
+ if (literal !== void 0) {
97
+ if (id === "tab" && mods.ctrl === false && mods.alt === false) return literal;
98
+ if (id === "esc") return mods.alt ? ESC + ESC : ESC;
99
+ return applyModifiers(literal, mods).data;
100
+ }
101
+ return null;
102
+ }
103
+ function controlCode(char) {
104
+ if (char.length !== 1) return null;
105
+ const code = char.charCodeAt(0);
106
+ if (code >= 97 && code <= 122) return String.fromCharCode(code - 96);
107
+ if (code >= 64 && code <= 95) return String.fromCharCode(code - 64);
108
+ switch (char) {
109
+ case " ":
110
+ case "2":
111
+ return "\0";
112
+ case "3":
113
+ return "\x1B";
114
+ case "4":
115
+ return "";
116
+ case "5":
117
+ return "";
118
+ case "6":
119
+ return "";
120
+ case "7":
121
+ case "/":
122
+ case "-":
123
+ return "";
124
+ case "8":
125
+ case "?":
126
+ return "\x7F";
127
+ default:
128
+ return null;
129
+ }
130
+ }
131
+ function applyModifiers(data, mods) {
132
+ if (!mods.ctrl && !mods.alt) return { data, consumed: false };
133
+ const chars = Array.from(data);
134
+ if (chars.length !== 1) {
135
+ const m = /^\x1b(?:\[|O)([ABCDHF])$/.exec(data);
136
+ if (m) return { data: `${ESC}[1;${modParam(mods)}${m[1]}`, consumed: true };
137
+ return { data, consumed: false };
138
+ }
139
+ let out = data;
140
+ if (mods.ctrl) {
141
+ const ctl = controlCode(data);
142
+ if (ctl !== null) out = ctl;
143
+ }
144
+ if (mods.alt) out = ESC + out;
145
+ return { data: out, consumed: true };
146
+ }
147
+
148
+ // src/core/modifiers.ts
149
+ var DOUBLE_TAP_MS = 350;
150
+ var MODIFIER_OFF = { state: "off", tappedAt: null };
151
+ function tapModifier(latch, now) {
152
+ switch (latch.state) {
153
+ case "off":
154
+ return { state: "once", tappedAt: now };
155
+ case "once":
156
+ return latch.tappedAt !== null && now - latch.tappedAt <= DOUBLE_TAP_MS ? { state: "locked", tappedAt: now } : { state: "off", tappedAt: now };
157
+ case "locked":
158
+ return { state: "off", tappedAt: now };
159
+ }
160
+ }
161
+ function afterKey(latch) {
162
+ return latch.state === "once" ? { state: "off", tappedAt: latch.tappedAt } : latch;
163
+ }
164
+ function isActive(latch) {
165
+ return latch.state !== "off";
166
+ }
167
+
168
+ // src/core/gesture.ts
169
+ var LONG_PRESS_MS = 450;
170
+ var FLING_FRICTION = 0.95;
171
+ var FLING_STOP = 2e-3;
172
+ var FLING_MIN = 0.01;
173
+ function createGestureRecognizer(options) {
174
+ const slop = options.slopPx ?? 8;
175
+ const lineHeight = Math.max(1, options.lineHeight);
176
+ let phase = "idle";
177
+ let start = { x: 0, y: 0, t: 0 };
178
+ let last = start;
179
+ let carryPx = 0;
180
+ let trail = [];
181
+ function scrollBy(fromY, toY) {
182
+ carryPx += fromY - toY;
183
+ const lines = carryPx > 0 ? Math.floor(carryPx / lineHeight) : Math.ceil(carryPx / lineHeight);
184
+ if (lines === 0) return [];
185
+ carryPx -= lines * lineHeight;
186
+ return [{ kind: "scroll", lines }];
187
+ }
188
+ return {
189
+ get phase() {
190
+ return phase;
191
+ },
192
+ down(s) {
193
+ phase = "pressed";
194
+ start = s;
195
+ last = s;
196
+ carryPx = 0;
197
+ trail = [s];
198
+ return [];
199
+ },
200
+ move(s) {
201
+ if (phase === "idle") return [];
202
+ const out = [];
203
+ if (phase === "pressed") {
204
+ if (Math.hypot(s.x - start.x, s.y - start.y) <= slop) return [];
205
+ phase = "scrolling";
206
+ }
207
+ if (phase === "scrolling") {
208
+ out.push(...scrollBy(last.y, s.y));
209
+ trail.push(s);
210
+ while (trail.length > 2 && s.t - trail[0].t > 100) trail.shift();
211
+ } else if (phase === "selecting") {
212
+ out.push({ kind: "select-extend", x: s.x, y: s.y });
213
+ }
214
+ last = s;
215
+ return out;
216
+ },
217
+ up(s) {
218
+ const was = phase;
219
+ phase = "idle";
220
+ if (was === "pressed") return [{ kind: "tap", x: start.x, y: start.y }];
221
+ if (was === "selecting") return [{ kind: "select-end" }];
222
+ if (was === "scrolling") {
223
+ const out = scrollBy(last.y, s.y);
224
+ trail.push(s);
225
+ const first = trail[0];
226
+ const dt = s.t - first.t;
227
+ const velocity = dt > 0 ? (first.y - s.y) / dt / lineHeight : 0;
228
+ if (Math.abs(velocity) >= FLING_MIN) out.push({ kind: "fling", velocity });
229
+ return out;
230
+ }
231
+ return [];
232
+ },
233
+ cancel() {
234
+ const was = phase;
235
+ phase = "idle";
236
+ return was === "selecting" ? [{ kind: "select-end" }] : [];
237
+ },
238
+ longPress() {
239
+ if (phase !== "pressed") return [];
240
+ phase = "selecting";
241
+ return [{ kind: "select-start", x: start.x, y: start.y }];
242
+ }
243
+ };
244
+ }
245
+ function flingStep(velocity, dtMs) {
246
+ const lines = velocity * dtMs;
247
+ const next = velocity * Math.pow(FLING_FRICTION, dtMs / 16);
248
+ return { lines, velocity: Math.abs(next) < FLING_STOP ? 0 : next };
249
+ }
250
+
251
+ // src/core/selection.ts
252
+ var WORD = /[A-Za-z0-9_\-./~:@%+=,?&#]/;
253
+ function wordBounds(line, col) {
254
+ const chars = Array.from(line);
255
+ if (col < 0 || col >= chars.length || !WORD.test(chars[col] ?? " ")) return { start: col, end: col + 1 };
256
+ let start = col;
257
+ let end = col + 1;
258
+ while (start > 0 && WORD.test(chars[start - 1])) start--;
259
+ while (end < chars.length && WORD.test(chars[end])) end++;
260
+ return { start, end };
261
+ }
262
+ function spanBetween(anchor, focus, cols) {
263
+ const a = anchor.row * cols + anchor.col;
264
+ const b = focus.row * cols + focus.col;
265
+ const from = Math.min(a, b);
266
+ const to = Math.max(a, b);
267
+ return { col: from % cols, row: Math.floor(from / cols), length: to - from + 1 };
268
+ }
269
+ function cellAt(point, box, grid) {
270
+ const cw = box.width / Math.max(1, grid.cols);
271
+ const ch = box.height / Math.max(1, grid.rows);
272
+ const col = Math.min(grid.cols - 1, Math.max(0, Math.floor((point.x - box.left) / cw)));
273
+ const screenRow = Math.min(grid.rows - 1, Math.max(0, Math.floor((point.y - box.top) / ch)));
274
+ return { col, row: grid.viewportY + screenRow };
275
+ }
276
+
277
+ // src/core/viewport.ts
278
+ var KEYBOARD_MIN_PX = 120;
279
+ function keyboardInset(layoutHeight, vv) {
280
+ if (!vv) return 0;
281
+ const inset = Math.round(layoutHeight - vv.height - vv.offsetTop);
282
+ return inset >= KEYBOARD_MIN_PX ? inset : 0;
283
+ }
284
+ function visibleHeightBelow(top, vv, layoutHeight) {
285
+ const bottom = vv ? vv.offsetTop + vv.height : layoutHeight;
286
+ return Math.max(0, Math.floor(bottom - top));
287
+ }
288
+
289
+ // src/core/theme.ts
290
+ var TERMINAL_FONT_FAMILY = 'ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace';
291
+ var TERMINAL_DEFAULTS = {
292
+ fontSize: 13,
293
+ lineHeight: 1.2,
294
+ scrollback: 5e3,
295
+ /** Side padding inside the terminal box, px. */
296
+ padding: 8
297
+ };
298
+ var ACCESSORY_BAR_HEIGHT = 52;
299
+ var FONT_SIZE_MIN = 9;
300
+ var FONT_SIZE_MAX = 24;
301
+ function stepFontSize(current, delta) {
302
+ return Math.min(FONT_SIZE_MAX, Math.max(FONT_SIZE_MIN, Math.round(current) + delta));
303
+ }
304
+ function pinchFontSize(startSize, startDistance, distance) {
305
+ if (startDistance <= 0) return startSize;
306
+ return Math.min(FONT_SIZE_MAX, Math.max(FONT_SIZE_MIN, Math.round(startSize * distance / startDistance)));
307
+ }
308
+ function terminalTheme(dark) {
309
+ return dark ? {
310
+ background: "#1e1e1e",
311
+ foreground: "#e4e4e7",
312
+ cursor: "#e4e4e7",
313
+ cursorAccent: "#1e1e1e",
314
+ black: "#1e1e1e",
315
+ red: "#f87171",
316
+ green: "#4ade80",
317
+ yellow: "#facc15",
318
+ blue: "#60a5fa",
319
+ magenta: "#c084fc",
320
+ cyan: "#22d3ee",
321
+ white: "#e5e7eb",
322
+ brightBlack: "#71717a",
323
+ brightRed: "#fca5a5",
324
+ brightGreen: "#86efac",
325
+ brightYellow: "#fde047",
326
+ brightBlue: "#93c5fd",
327
+ brightMagenta: "#d8b4fe",
328
+ brightCyan: "#67e8f9",
329
+ brightWhite: "#f4f4f5",
330
+ selectionBackground: "#3b82f680"
331
+ } : {
332
+ background: "#ffffff",
333
+ foreground: "#18181b",
334
+ cursor: "#18181b",
335
+ cursorAccent: "#ffffff",
336
+ black: "#18181b",
337
+ red: "#b91c1c",
338
+ green: "#15803d",
339
+ yellow: "#b45309",
340
+ blue: "#1d4ed8",
341
+ magenta: "#7c3aed",
342
+ cyan: "#0e7490",
343
+ white: "#d4d4d8",
344
+ brightBlack: "#71717a",
345
+ brightRed: "#dc2626",
346
+ brightGreen: "#16a34a",
347
+ brightYellow: "#d97706",
348
+ brightBlue: "#2563eb",
349
+ brightMagenta: "#9333ea",
350
+ brightCyan: "#0891b2",
351
+ brightWhite: "#a1a1aa",
352
+ selectionBackground: "#93c5fd99"
353
+ };
354
+ }
355
+
356
+ // src/core/line-editor.ts
357
+ var CSI = "\x1B[";
358
+ function createLineEditor(host) {
359
+ let line = "";
360
+ let cursor = 0;
361
+ let recall = null;
362
+ let busy = false;
363
+ const queue = [];
364
+ let typeahead = "";
365
+ const changed = () => host.onChange?.(line);
366
+ function redraw() {
367
+ host.write(`\r${CSI}K${host.prompt()}${line}`);
368
+ const back = line.length - cursor;
369
+ if (back > 0) host.write(`${CSI}${back}D`);
370
+ }
371
+ function promptRow() {
372
+ host.write(`${host.prompt()}${line}`);
373
+ const back = line.length - cursor;
374
+ if (back > 0) host.write(`${CSI}${back}D`);
375
+ }
376
+ function submit() {
377
+ const text = line;
378
+ line = "";
379
+ cursor = 0;
380
+ recall = null;
381
+ host.write("\r\n");
382
+ if (!text.trim()) {
383
+ promptRow();
384
+ next();
385
+ return;
386
+ }
387
+ busy = true;
388
+ host.onSubmit(text);
389
+ }
390
+ function next() {
391
+ const queued = queue.shift();
392
+ if (queued === void 0) return;
393
+ line = queued;
394
+ cursor = line.length;
395
+ host.write(line);
396
+ submit();
397
+ }
398
+ function insert(text) {
399
+ line = line.slice(0, cursor) + text + line.slice(cursor);
400
+ cursor += text.length;
401
+ if (cursor === line.length) host.write(text);
402
+ else redraw();
403
+ }
404
+ function setFromHistory(index) {
405
+ const history = host.history();
406
+ recall = index;
407
+ line = index === null ? "" : history[index] ?? "";
408
+ cursor = line.length;
409
+ redraw();
410
+ changed();
411
+ }
412
+ function paste(data) {
413
+ const parts = data.replace(/\r\n?/g, "\n").split("\n");
414
+ const first = parts.shift() ?? "";
415
+ if (first) insert(first);
416
+ if (parts.length === 0) {
417
+ changed();
418
+ return;
419
+ }
420
+ queue.push(...parts.filter((p, i) => p !== "" || i < parts.length - 1));
421
+ changed();
422
+ submit();
423
+ }
424
+ function input(data) {
425
+ if (busy) {
426
+ if (data === "") {
427
+ typeahead = "";
428
+ host.onInterrupt?.();
429
+ } else {
430
+ typeahead += data.replace(/\x1b(?:\[[0-9;]*[A-Za-z~]|O[A-Za-z])/g, "").replace(/[^\r\n\t\x20-\x7e\u00a0-\uffff]/g, "");
431
+ }
432
+ return;
433
+ }
434
+ const clean = data.replace(/\x1b\[20[01]~/g, "");
435
+ if (clean.length > 1 && !clean.startsWith("\x1B")) {
436
+ paste(clean);
437
+ return;
438
+ }
439
+ switch (clean) {
440
+ case "\r":
441
+ case "\n":
442
+ submit();
443
+ return;
444
+ case "\x7F":
445
+ case "\b":
446
+ if (cursor === 0) return;
447
+ line = line.slice(0, cursor - 1) + line.slice(cursor);
448
+ cursor -= 1;
449
+ redraw();
450
+ changed();
451
+ return;
452
+ case `${CSI}3~`:
453
+ if (cursor >= line.length) return;
454
+ line = line.slice(0, cursor) + line.slice(cursor + 1);
455
+ redraw();
456
+ changed();
457
+ return;
458
+ case `${CSI}A`:
459
+ case "\x1BOA": {
460
+ const history = host.history();
461
+ if (history.length === 0) return;
462
+ setFromHistory(recall === null ? history.length - 1 : Math.max(0, recall - 1));
463
+ return;
464
+ }
465
+ case `${CSI}B`:
466
+ case "\x1BOB": {
467
+ if (recall === null) return;
468
+ const nextIndex = recall + 1;
469
+ setFromHistory(nextIndex >= host.history().length ? null : nextIndex);
470
+ return;
471
+ }
472
+ case `${CSI}C`:
473
+ case "\x1BOC":
474
+ if (cursor < line.length) {
475
+ cursor += 1;
476
+ host.write(`${CSI}C`);
477
+ }
478
+ return;
479
+ case `${CSI}D`:
480
+ case "\x1BOD":
481
+ if (cursor > 0) {
482
+ cursor -= 1;
483
+ host.write(`${CSI}D`);
484
+ }
485
+ return;
486
+ case "":
487
+ case `${CSI}H`:
488
+ case "\x1BOH":
489
+ if (cursor > 0) {
490
+ host.write(`${CSI}${cursor}D`);
491
+ cursor = 0;
492
+ }
493
+ return;
494
+ case "":
495
+ case `${CSI}F`:
496
+ case "\x1BOF": {
497
+ const distance = line.length - cursor;
498
+ if (distance > 0) {
499
+ host.write(`${CSI}${distance}C`);
500
+ cursor = line.length;
501
+ }
502
+ return;
503
+ }
504
+ case "\v":
505
+ line = line.slice(0, cursor);
506
+ redraw();
507
+ changed();
508
+ return;
509
+ case "":
510
+ line = line.slice(cursor);
511
+ cursor = 0;
512
+ redraw();
513
+ changed();
514
+ return;
515
+ case "\f":
516
+ host.onClearScreen?.();
517
+ redraw();
518
+ return;
519
+ case "":
520
+ host.write("^C\r\n");
521
+ line = "";
522
+ cursor = 0;
523
+ recall = null;
524
+ queue.length = 0;
525
+ promptRow();
526
+ changed();
527
+ return;
528
+ default:
529
+ if (clean.length === 1 && clean >= " ") {
530
+ insert(clean);
531
+ changed();
532
+ }
533
+ }
534
+ }
535
+ return {
536
+ input,
537
+ prompt: promptRow,
538
+ refresh: redraw,
539
+ setLine(text) {
540
+ line = text;
541
+ cursor = text.length;
542
+ recall = null;
543
+ if (!busy) redraw();
544
+ },
545
+ done() {
546
+ busy = false;
547
+ promptRow();
548
+ if (queue.length > 0) {
549
+ next();
550
+ return;
551
+ }
552
+ if (typeahead) {
553
+ const pending = typeahead;
554
+ typeahead = "";
555
+ if (pending.length === 1) input(pending);
556
+ else paste(pending);
557
+ }
558
+ },
559
+ get line() {
560
+ return line;
561
+ },
562
+ get busy() {
563
+ return busy;
564
+ }
565
+ };
566
+ }
567
+ //# sourceMappingURL=index.cjs.map