@flowtty/core 1.0.0-alpha.5 → 1.0.0-alpha.7

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.
@@ -1,13 +1,41 @@
1
1
  import { B as Buffer } from './cells-CaXEx4lH.js';
2
2
 
3
+ /**
4
+ * Every multi-character key name a backend produces. A printable key's name is
5
+ * the character itself (`' '`, `':'`, `'a'`) — there is no `'space'`, `'enter'`
6
+ * or `'colon'`. Sequences the key parser does not recognize surface under a
7
+ * `csi-…` name, which is deliberately not listed here.
8
+ */
9
+ declare const NAMED_KEYS: readonly ["return", "escape", "tab", "backspace", "delete", "insert", "up", "down", "left", "right", "home", "end", "pageup", "pagedown", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "paste", "wheelup", "wheeldown"];
10
+ type NamedKey = typeof NAMED_KEYS[number];
11
+ /** A key name: a named key, or the character itself for a printable key. The
12
+ * `string & {}` arm keeps any character valid while editors still complete the
13
+ * named ones — it cannot reject a typo like `'space'`; `TestBackend.press` does. */
14
+ type KeyName = NamedKey | (string & {});
3
15
  interface Key {
4
16
  /**
5
17
  * Canonical name of the key. For printable ASCII characters this is the
6
18
  * character itself ('a', '!', ' '). For named keys: 'return', 'escape',
7
19
  * 'tab', 'backspace', 'delete', 'up', 'down', 'left', 'right', 'home',
8
- * 'end', 'pageup', 'pagedown'.
20
+ * 'end', 'pageup', 'pagedown'. A bracketed paste is one key named 'paste'
21
+ * whose content is in `text`. Mouse wheel steps are 'wheelup' / 'wheeldown',
22
+ * positioned by `x` / `y`.
9
23
  */
10
- name: string;
24
+ name: KeyName;
25
+ /**
26
+ * Payload of a 'paste' key: the pasted text, line endings normalized to `\n`.
27
+ * A paste is delivered whole — its newlines and letters are text, never
28
+ * 'return' or single-letter keys — so an app inserts it instead of acting on it.
29
+ * Undefined for every other key.
30
+ */
31
+ text?: string;
32
+ /**
33
+ * Cell under the pointer for a mouse key ('wheelup' / 'wheeldown'): 0-based
34
+ * column and row, the same coordinates `onLayout` rects use. Undefined for
35
+ * every other key.
36
+ */
37
+ x?: number;
38
+ y?: number;
11
39
  /** Raw byte sequence as received from the source (empty for synthetic keys). */
12
40
  sequence: string;
13
41
  ctrl: boolean;
@@ -71,4 +99,4 @@ interface Backend {
71
99
  hyperlinks?: boolean;
72
100
  }
73
101
 
74
- export type { Backend as B, Key as K };
102
+ export { type Backend as B, type Key as K, NAMED_KEYS as N, type KeyName as a, type NamedKey as b };
@@ -0,0 +1,34 @@
1
+ // src/keys.ts
2
+ var NAMED_KEYS = [
3
+ "return",
4
+ "escape",
5
+ "tab",
6
+ "backspace",
7
+ "delete",
8
+ "insert",
9
+ "up",
10
+ "down",
11
+ "left",
12
+ "right",
13
+ "home",
14
+ "end",
15
+ "pageup",
16
+ "pagedown",
17
+ "f1",
18
+ "f2",
19
+ "f3",
20
+ "f4",
21
+ "f5",
22
+ "f6",
23
+ "f7",
24
+ "f8",
25
+ "f9",
26
+ "f10",
27
+ "f11",
28
+ "f12",
29
+ "paste",
30
+ "wheelup",
31
+ "wheeldown"
32
+ ];
33
+
34
+ export { NAMED_KEYS };
@@ -1,5 +1,5 @@
1
- import { C as Container } from '../host-zpuR6C1c.js';
2
- export { B as BORDER_CHARS, H as HostType, I as Instance, R as Rect, T as TextInstance, Y as Yoga, a as YogaNode, b as appendChild, c as applyProps, d as computeLayout, e as createInstance, f as createTextInstance, g as getYoga, i as insertBefore, l as layoutOf, m as measureText, o as ownText, r as refreshMeasure, h as removeChild } from '../host-zpuR6C1c.js';
1
+ import { C as Container } from '../host-6NVC0_E3.js';
2
+ export { B as BORDER_CHARS, H as HostType, I as Instance, R as Rect, T as TextInstance, Y as Yoga, a as YogaNode, b as appendChild, c as applyProps, d as computeLayout, e as createInstance, f as createTextInstance, g as getYoga, i as insertBefore, l as layoutOf, m as measureText, o as ownText, r as refreshMeasure, h as removeChild } from '../host-6NVC0_E3.js';
3
3
  import { B as Buffer } from '../cells-CaXEx4lH.js';
4
4
  import 'yoga-layout/load';
5
5
 
@@ -315,13 +315,22 @@ function contentRectOf(inst, box) {
315
315
  height: Math.max(0, box.height - padT - padB)
316
316
  };
317
317
  }
318
+ function paddingRectOf(inst, box) {
319
+ const n = inst.yogaNode;
320
+ const t = n.getComputedBorder(Edge.Top);
321
+ const r = n.getComputedBorder(Edge.Right);
322
+ const b = n.getComputedBorder(Edge.Bottom);
323
+ const l = n.getComputedBorder(Edge.Left);
324
+ return { left: box.left + l, top: box.top + t, width: Math.max(0, box.width - l - r), height: Math.max(0, box.height - t - b) };
325
+ }
318
326
  function paintInstance(inst, buffer, offsetX, offsetY, inheritedBg = void 0, clip = null) {
319
327
  if (inst.props.display === "none") return;
320
328
  const box = layoutOf(inst, offsetX, offsetY);
321
329
  inst.props.onLayout?.(box);
322
330
  const ownBg = inst.props.backgroundColor;
323
331
  const effectiveBg = ownBg ?? inheritedBg;
324
- if (ownBg !== void 0) {
332
+ const offscreen = clip !== null && (box.left >= clip.left + clip.width || box.left + box.width <= clip.left || box.top >= clip.top + clip.height || box.top + box.height <= clip.top);
333
+ if (!offscreen && ownBg !== void 0) {
325
334
  const fillStyle = ownBg === "default" ? {} : { bg: ownBg };
326
335
  for (let y = box.top; y < box.top + box.height; y++) {
327
336
  for (let x = box.left; x < box.left + box.width; x++) {
@@ -329,8 +338,8 @@ function paintInstance(inst, buffer, offsetX, offsetY, inheritedBg = void 0, cli
329
338
  }
330
339
  }
331
340
  }
332
- paintBorder(inst, buffer, box, clip, effectiveBg);
333
- const text = ownText(inst);
341
+ if (!offscreen) paintBorder(inst, buffer, box, clip, effectiveBg);
342
+ const text = offscreen ? "" : ownText(inst);
334
343
  if (text) {
335
344
  const content = contentRectOf(inst, box);
336
345
  const mode = inst.props.wrap ?? "none";
@@ -350,7 +359,23 @@ function paintInstance(inst, buffer, offsetX, offsetY, inheritedBg = void 0, cli
350
359
  }
351
360
  }
352
361
  }
353
- const childClip = inst.props.overflow === "hidden" ? intersectRects(clip, contentRectOf(inst, box)) : clip;
362
+ const scrolls = inst.props.scrollTop !== void 0 || inst.props.scrollBottom !== void 0;
363
+ let scrollTop = 0;
364
+ if (scrolls || inst.props.onScrollMetrics) {
365
+ const viewport = contentRectOf(inst, box);
366
+ let contentBottom = 0;
367
+ for (const child of inst.children) {
368
+ if (child.type !== "box" || child.props.position === "absolute" || child.props.display === "none") continue;
369
+ const n = child.yogaNode;
370
+ contentBottom = Math.max(contentBottom, n.getComputedTop() + n.getComputedHeight() + n.getComputedMargin(Edge.Bottom));
371
+ }
372
+ const contentHeight = Math.max(0, contentBottom - (viewport.top - box.top));
373
+ const maxScrollTop = Math.max(0, contentHeight - viewport.height);
374
+ const wanted = inst.props.scrollBottom !== void 0 ? maxScrollTop - inst.props.scrollBottom : inst.props.scrollTop ?? 0;
375
+ scrollTop = Math.max(0, Math.min(maxScrollTop, Math.round(wanted)));
376
+ inst.props.onScrollMetrics?.({ contentHeight, viewportHeight: viewport.height, scrollTop, maxScrollTop });
377
+ }
378
+ const childClip = inst.props.overflow === "hidden" || scrolls ? intersectRects(clip, contentRectOf(inst, box)) : clip;
354
379
  const stackFlow = [];
355
380
  const absolutes = [];
356
381
  for (const child of inst.children) {
@@ -360,8 +385,9 @@ function paintInstance(inst, buffer, offsetX, offsetY, inheritedBg = void 0, cli
360
385
  const byZ = (a, b) => (a.props.zIndex ?? 0) - (b.props.zIndex ?? 0);
361
386
  stackFlow.sort(byZ);
362
387
  absolutes.sort(byZ);
363
- for (const child of stackFlow) paintInstance(child, buffer, box.left, box.top, effectiveBg, childClip);
364
- for (const child of absolutes) paintInstance(child, buffer, box.left, box.top, effectiveBg, childClip);
388
+ for (const child of stackFlow) paintInstance(child, buffer, box.left, box.top - scrollTop, effectiveBg, childClip);
389
+ const overlayClip = scrolls ? intersectRects(clip, paddingRectOf(inst, box)) : childClip;
390
+ for (const child of absolutes) paintInstance(child, buffer, box.left, box.top, effectiveBg, overlayClip);
365
391
  }
366
392
 
367
393
  export { appendChild, applyProps, computeLayout, createInstance, createTextInstance, getYoga, insertBefore, layoutOf, measureText, ownText, paint, refreshMeasure, removeChild };
@@ -114,6 +114,28 @@ interface BoxProps {
114
114
  * 'hidden' clips ALL descendant writes including their backgrounds and borders.
115
115
  * Does NOT clip this box's own background or border (those are this box's own area). */
116
116
  overflow?: 'visible' | 'hidden';
117
+ /** Scroll the content up by this many rows (0 = top). Setting either scroll
118
+ * prop makes the box a scroll viewport: it clips like `overflow: 'hidden'`
119
+ * and paints its flow children shifted. The value is clamped to the scrollable
120
+ * range at paint time, against the content's CURRENT height — so it never
121
+ * needs a second frame to catch up. `position: 'absolute'` children are
122
+ * overlays: they neither scroll nor count as content (a scrollbar, a
123
+ * "new messages" badge). */
124
+ scrollTop?: number;
125
+ /** Like `scrollTop`, but counted from the END of the content: 0 pins the last
126
+ * rows into view and keeps them there as content grows. Wins over `scrollTop`
127
+ * when both are set. */
128
+ scrollBottom?: number;
129
+ /** Fires every paint (diff before setState, like onLayout) with the numbers a
130
+ * scrolling component needs: content and viewport heights, the effective
131
+ * (clamped) `scrollTop`, and its maximum. */
132
+ onScrollMetrics?: (metrics: ScrollMetrics) => void;
133
+ }
134
+ interface ScrollMetrics {
135
+ contentHeight: number;
136
+ viewportHeight: number;
137
+ scrollTop: number;
138
+ maxScrollTop: number;
117
139
  }
118
140
  interface Instance {
119
141
  type: 'box';
@@ -149,4 +171,4 @@ declare function appendChild(parent: Instance, child: Instance | TextInstance, Y
149
171
  declare function removeChild(parent: Instance, child: Instance | TextInstance, Yoga: Yoga): void;
150
172
  declare function insertBefore(parent: Instance, child: Instance | TextInstance, before: Instance | TextInstance, Yoga: Yoga): void;
151
173
 
152
- 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 YogaNode as a, appendChild as b, applyProps as c, computeLayout as d, createInstance as e, createTextInstance as f, getYoga as g, removeChild as h, insertBefore as i, type BorderChars as j, type BorderStyle as k, layoutOf as l, measureText as m, type BoxProps as n, ownText as o, type GridChars as p, refreshMeasure as r };
174
+ 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 ScrollMetrics as S, type TextInstance as T, type Yoga as Y, type YogaNode as a, appendChild as b, applyProps as c, computeLayout as d, createInstance as e, createTextInstance as f, getYoga as g, removeChild as h, insertBefore as i, type BorderChars as j, type BorderStyle as k, layoutOf as l, measureText as m, type BoxProps as n, ownText as o, type GridChars as p, refreshMeasure as r };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
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 { j as BorderChars, k as BorderStyle, n as BoxProps, D as DEFAULT_BORDER_STYLE, G as GRID_CHARS, p as GridChars } from './host-zpuR6C1c.js';
2
+ import { K as Key } from './backend-BYzQoXfC.js';
3
+ export { B as Backend, a as KeyName, N as NAMED_KEYS, b as NamedKey } from './backend-BYzQoXfC.js';
4
+ export { j as BorderChars, k as BorderStyle, n as BoxProps, D as DEFAULT_BORDER_STYLE, G as GRID_CHARS, p as GridChars, S as ScrollMetrics } from './host-6NVC0_E3.js';
5
5
  import 'yoga-layout/load';
6
6
 
7
7
  type WrapMode = 'wrap' | 'truncate' | 'none';
@@ -94,7 +94,31 @@ type EditorAction = {
94
94
  } | {
95
95
  kind: 'noop';
96
96
  };
97
- declare function reduce$2(state: EditorState, key: Key): EditorAction;
97
+ interface EditorOptions {
98
+ /** Multi-line editing: line breaks are part of the value. Shift+Enter,
99
+ * Alt+Enter and backslash-then-Enter insert one (plain Enter still submits),
100
+ * up/down move between visual rows, Home/End and the kill bindings work
101
+ * within the current line, and a paste keeps its line breaks. */
102
+ multiline?: boolean;
103
+ /** Wrap width in cells — what up/down use to walk soft-wrapped rows. Only
104
+ * read when `multiline`; without it each source line counts as one row. */
105
+ width?: number;
106
+ }
107
+ declare function reduce$2(state: EditorState, key: Key, opts?: EditorOptions): EditorAction;
108
+
109
+ interface InputRow {
110
+ /** Text of this visual row (no line break). */
111
+ text: string;
112
+ /** Index into the value where this row starts. */
113
+ start: number;
114
+ /** True for a soft-wrap continuation of the previous row's source line. */
115
+ continuation: boolean;
116
+ }
117
+ declare function inputRows(value: string, width: number, cursor?: number): InputRow[];
118
+ declare function caretPosition(value: string, cursor: number, width: number): {
119
+ row: number;
120
+ col: number;
121
+ };
98
122
 
99
123
  interface SelectItem<T> {
100
124
  label: string;
@@ -140,4 +164,4 @@ type MultiSelectAction = {
140
164
  };
141
165
  declare function reduce<T>(items: SelectItem<T>[], state: MultiSelectState, key: Key): MultiSelectAction;
142
166
 
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 };
167
+ export { type EditorAction, type EditorOptions, type EditorState, type InputRow, Key, type MultiSelectAction, type MultiSelectState, type SelectAction, type SelectItem, type SelectState, type VisualLine, type WrapMode, caretPosition, charWidth, reduce$2 as editorReducer, inputRows, reduce as multiSelectReducer, reduce$1 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround, wrapText };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Buffer, DEFAULT_BORDER_STYLE, GRID_CHARS, wrapText } from './chunk-D6HW2BNM.js';
2
+ export { NAMED_KEYS } from './chunk-AQ33T2P5.js';
2
3
 
3
4
  // src/visualLines.ts
4
5
  function splitVisualLines(text, mode, width) {
@@ -280,6 +281,36 @@ function stringWidth(str) {
280
281
  return w;
281
282
  }
282
283
 
284
+ // src/inputRows.ts
285
+ function inputRows(value, width, cursor) {
286
+ const w = Math.max(1, Math.floor(width));
287
+ const rows = [];
288
+ let lineStart = 0;
289
+ for (const line of value.split("\n")) {
290
+ if (line.length === 0) {
291
+ rows.push({ text: "", start: lineStart, continuation: false });
292
+ } else {
293
+ for (let at = 0; at < line.length; at += w) {
294
+ rows.push({ text: line.slice(at, at + w), start: lineStart + at, continuation: at > 0 });
295
+ }
296
+ if (cursor === lineStart + line.length && line.length % w === 0) {
297
+ rows.push({ text: "", start: cursor, continuation: true });
298
+ }
299
+ }
300
+ lineStart += line.length + 1;
301
+ }
302
+ return rows;
303
+ }
304
+ function caretPosition(value, cursor, width) {
305
+ const c = Math.max(0, Math.min(value.length, cursor));
306
+ const rows = inputRows(value, width, c);
307
+ for (let r = rows.length - 1; r >= 0; r--) {
308
+ const row = rows[r];
309
+ if (row.start <= c && c <= row.start + row.text.length) return { row: r, col: c - row.start };
310
+ }
311
+ return { row: 0, col: 0 };
312
+ }
313
+
283
314
  // src/editor.ts
284
315
  var OPT_MAP = {
285
316
  " ": ["\xA0"],
@@ -304,19 +335,48 @@ function wordRight(value, cursor) {
304
335
  while (c < value.length && isWord(value[c])) c++;
305
336
  return c;
306
337
  }
307
- function reduce(state, key) {
338
+ function reduce(state, key, opts = {}) {
308
339
  const { value, cursor } = state;
309
340
  const clamp = (n) => Math.max(0, Math.min(value.length, n));
341
+ const multiline = opts.multiline === true;
342
+ const lineStart = multiline ? value.lastIndexOf("\n", cursor - 1) + 1 : 0;
343
+ const nextBreak = multiline ? value.indexOf("\n", cursor) : -1;
344
+ const lineEnd = nextBreak < 0 ? value.length : nextBreak;
345
+ const insert = (text) => ({
346
+ kind: "edit",
347
+ state: { value: value.slice(0, cursor) + text + value.slice(cursor), cursor: cursor + text.length }
348
+ });
349
+ if (key.name === "paste") {
350
+ const text = multiline ? key.text ?? "" : (key.text ?? "").replace(/\n/g, " ");
351
+ if (text === "") return { kind: "noop" };
352
+ return insert(text);
353
+ }
354
+ if (multiline) {
355
+ if (key.name === "return" && (key.shift || key.meta)) return insert("\n");
356
+ if (key.name === "return" && cursor > 0 && value[cursor - 1] === "\\") {
357
+ return { kind: "edit", state: { value: value.slice(0, cursor - 1) + "\n" + value.slice(cursor), cursor } };
358
+ }
359
+ if (key.name === "up" || key.name === "down") {
360
+ const width = opts.width ?? Number.MAX_SAFE_INTEGER;
361
+ const rows = inputRows(value, width, cursor);
362
+ const at = caretPosition(value, cursor, width);
363
+ const target = at.row + (key.name === "up" ? -1 : 1);
364
+ if (target < 0) return { kind: "edit", state: { value, cursor: 0 } };
365
+ if (target >= rows.length) return { kind: "edit", state: { value, cursor: value.length } };
366
+ const row = rows[target];
367
+ return { kind: "edit", state: { value, cursor: row.start + Math.min(at.col, row.text.length) } };
368
+ }
369
+ }
310
370
  if (key.name === "left" && key.meta) return { kind: "edit", state: { value, cursor: wordLeft(value, cursor) } };
311
371
  if (key.name === "b" && key.meta) return { kind: "edit", state: { value, cursor: wordLeft(value, cursor) } };
312
372
  if (key.name === "right" && key.meta) return { kind: "edit", state: { value, cursor: wordRight(value, cursor) } };
313
373
  if (key.name === "f" && key.meta) return { kind: "edit", state: { value, cursor: wordRight(value, cursor) } };
314
374
  if (key.name === "left") return { kind: "edit", state: { value, cursor: clamp(cursor - 1) } };
315
375
  if (key.name === "right") return { kind: "edit", state: { value, cursor: clamp(cursor + 1) } };
316
- if (key.name === "home") return { kind: "edit", state: { value, cursor: 0 } };
317
- if (key.name === "end") return { kind: "edit", state: { value, cursor: value.length } };
318
- if (key.name === "a" && key.ctrl) return { kind: "edit", state: { value, cursor: 0 } };
319
- if (key.name === "e" && key.ctrl) return { kind: "edit", state: { value, cursor: value.length } };
376
+ if (key.name === "home") return { kind: "edit", state: { value, cursor: lineStart } };
377
+ if (key.name === "end") return { kind: "edit", state: { value, cursor: lineEnd } };
378
+ if (key.name === "a" && key.ctrl) return { kind: "edit", state: { value, cursor: lineStart } };
379
+ if (key.name === "e" && key.ctrl) return { kind: "edit", state: { value, cursor: lineEnd } };
320
380
  if (key.name === "backspace" && key.meta) {
321
381
  const start = wordLeft(value, cursor);
322
382
  return { kind: "edit", state: { value: value.slice(0, start) + value.slice(cursor), cursor: start } };
@@ -334,10 +394,10 @@ function reduce(state, key) {
334
394
  return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(end), cursor } };
335
395
  }
336
396
  if (key.name === "k" && key.ctrl) {
337
- return { kind: "edit", state: { value: value.slice(0, cursor), cursor } };
397
+ return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(lineEnd), cursor } };
338
398
  }
339
399
  if (key.name === "u" && key.ctrl) {
340
- return { kind: "edit", state: { value: value.slice(cursor), cursor: 0 } };
400
+ return { kind: "edit", state: { value: value.slice(0, lineStart) + value.slice(cursor), cursor: lineStart } };
341
401
  }
342
402
  if (key.meta && OPT_MAP[key.name]) {
343
403
  const entry = OPT_MAP[key.name];
@@ -417,4 +477,4 @@ function reduce3(items, state, key) {
417
477
  return { kind: "noop" };
418
478
  }
419
479
 
420
- export { charWidth, reduce as editorReducer, reduce3 as multiSelectReducer, reduce2 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround };
480
+ export { caretPosition, charWidth, reduce as editorReducer, inputRows, reduce3 as multiSelectReducer, reduce2 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround };
@@ -1,5 +1,5 @@
1
1
  import { B as Buffer } from '../cells-CaXEx4lH.js';
2
- import { B as Backend, K as Key } from '../backend-BcB7VR87.js';
2
+ import { B as Backend, K as Key } from '../backend-BYzQoXfC.js';
3
3
 
4
4
  declare class TestBackend implements Backend {
5
5
  private readonly cols;
@@ -16,12 +16,20 @@ declare class TestBackend implements Backend {
16
16
  get lastFrame(): string;
17
17
  get lastBuffer(): Buffer | null;
18
18
  onKey(handler: (key: Key) => void): () => void;
19
- /** Synchronously deliver one Key to every subscriber. */
19
+ /**
20
+ * Synchronously deliver one Key to every subscriber. Throws on a name no
21
+ * terminal can produce (`'space'`, `'enter'`): a test that presses such a key
22
+ * exercises a branch real input never reaches, and would pass anyway.
23
+ */
20
24
  press(key: Partial<Key> & {
21
25
  name: string;
22
26
  }): void;
23
27
  /** Emit one Key per character; printable chars only. */
24
28
  type(text: string): void;
29
+ /** Deliver `text` as ONE 'paste' key — what a TTY backend emits for a bracketed paste. */
30
+ paste(text: string): void;
31
+ /** Deliver one mouse-wheel step at cell (x, y) — what a TTY backend with `mouse` on emits. */
32
+ wheel(direction: 'up' | 'down', x?: number, y?: number): void;
25
33
  dispose(): void;
26
34
  }
27
35
 
@@ -1,4 +1,25 @@
1
+ import { NAMED_KEYS } from '../chunk-AQ33T2P5.js';
2
+
1
3
  // src/testing/test-backend.ts
4
+ var KEY_NAME_HINTS = {
5
+ space: " ",
6
+ enter: "return",
7
+ esc: "escape",
8
+ del: "delete",
9
+ ins: "insert",
10
+ pgup: "pageup",
11
+ pgdn: "pagedown",
12
+ pgdown: "pagedown",
13
+ bs: "backspace"
14
+ };
15
+ function assertRealKeyName(name) {
16
+ if ([...name].length === 1 || name.startsWith("csi-")) return;
17
+ if (NAMED_KEYS.includes(name)) return;
18
+ const hint = KEY_NAME_HINTS[name.toLowerCase()];
19
+ throw new Error(
20
+ `TestBackend.press: '${name}' is not a key name any terminal produces` + (hint !== void 0 ? ` \u2014 use '${hint}'.` : `. A printable key is named by its character (':' not 'colon'); named keys: ${NAMED_KEYS.join(", ")}.`)
21
+ );
22
+ }
2
23
  var TestBackend = class {
3
24
  constructor(cols = 40, rows = 10) {
4
25
  this.cols = cols;
@@ -28,9 +49,17 @@ var TestBackend = class {
28
49
  this.subscribers.delete(handler);
29
50
  };
30
51
  }
31
- /** Synchronously deliver one Key to every subscriber. */
52
+ /**
53
+ * Synchronously deliver one Key to every subscriber. Throws on a name no
54
+ * terminal can produce (`'space'`, `'enter'`): a test that presses such a key
55
+ * exercises a branch real input never reaches, and would pass anyway.
56
+ */
32
57
  press(key) {
58
+ assertRealKeyName(key.name);
33
59
  const k = {
60
+ ...key.text !== void 0 ? { text: key.text } : {},
61
+ ...key.x !== void 0 ? { x: key.x } : {},
62
+ ...key.y !== void 0 ? { y: key.y } : {},
34
63
  sequence: key.sequence ?? "",
35
64
  ctrl: key.ctrl ?? false,
36
65
  meta: key.meta ?? false,
@@ -43,6 +72,14 @@ var TestBackend = class {
43
72
  type(text) {
44
73
  for (const ch of text) this.press({ name: ch, sequence: ch });
45
74
  }
75
+ /** Deliver `text` as ONE 'paste' key — what a TTY backend emits for a bracketed paste. */
76
+ paste(text) {
77
+ this.press({ name: "paste", text: text.replace(/\r\n?/g, "\n") });
78
+ }
79
+ /** Deliver one mouse-wheel step at cell (x, y) — what a TTY backend with `mouse` on emits. */
80
+ wheel(direction, x = 0, y = 0) {
81
+ this.press({ name: direction === "up" ? "wheelup" : "wheeldown", x, y });
82
+ }
46
83
  // eslint-disable-next-line @typescript-eslint/no-empty-function
47
84
  dispose() {
48
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowtty/core",
3
- "version": "1.0.0-alpha.5",
3
+ "version": "1.0.0-alpha.7",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {