@uniflowed/tui 0.0.0-alpha.18 → 0.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/tui",
3
- "version": "0.0.0-alpha.18",
3
+ "version": "0.14.1",
4
4
  "description": "A React renderer whose host is a terminal, following OpenTUI, for the Unified Toolchain for Flow.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -8,12 +8,13 @@
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/ubugeeei-prod/uf.git",
11
- "directory": "packages/tui"
11
+ "directory": "npm/tui"
12
12
  },
13
13
  "exports": {
14
14
  ".": "./index.js",
15
15
  "./capability": "./capability.js",
16
16
  "./cells": "./cells.js",
17
+ "./clipboard": "./clipboard.js",
17
18
  "./components": "./components.js",
18
19
  "./diff": "./diff.js",
19
20
  "./keys": "./keys.js",
@@ -30,7 +31,7 @@
30
31
  "!*.test.js"
31
32
  ],
32
33
  "dependencies": {
33
- "@uniflowed/react": "0.0.0-alpha.18",
34
- "react-reconciler": "^0.33.0"
34
+ "@uniflowed/react": "0.14.1",
35
+ "react-reconciler": "^0.34.0"
35
36
  }
36
37
  }
package/terminal.js CHANGED
@@ -37,6 +37,7 @@ import type { Capabilities, ColorChoice, TerminalEnv } from "./capability.js";
37
37
  import { FALLBACK_COLUMNS, FALLBACK_ROWS, detectCapabilities, detectSize } from "./capability.js";
38
38
  import type { Frame } from "./cells.js";
39
39
  import { frameText } from "./cells.js";
40
+ import { osc52Clipboard } from "./clipboard.js";
40
41
  import type { Update } from "./diff.js";
41
42
  import type { Renderer } from "./internal/host.js";
42
43
  import {
@@ -74,6 +75,17 @@ const CLEAR = "\u001b[2J\u001b[H";
74
75
  */
75
76
  const ENABLE_PASTE = "\u001b[?2004h";
76
77
  const DISABLE_PASTE = "\u001b[?2004l";
78
+ /**
79
+ * Ask terminals that understand Kitty's keyboard protocol to report all keys
80
+ * as `CSI u`, including repeat/release event types and associated text.
81
+ *
82
+ * The flags are pushed onto the terminal's keyboard-mode stack and popped on
83
+ * exit, so a shell or parent program gets back whatever mode it had before.
84
+ * Terminals that do not implement the protocol ignore these bytes and keep
85
+ * sending the legacy sequences `keys.js` already decodes.
86
+ */
87
+ const ENABLE_KEYBOARD_PROTOCOL = "\u001b[>27u";
88
+ const DISABLE_KEYBOARD_PROTOCOL = "\u001b[<u";
77
89
  /**
78
90
  * Ask the terminal to report the mouse, in the four modes that answer.
79
91
  *
@@ -136,20 +148,20 @@ export type OutputStream = {
136
148
  readonly rows?: number,
137
149
  readonly isTTY?: boolean,
138
150
  /** A real `process.stdout` emits `"resize"`; a string collector does not. */
139
- on?: (event: string, listener: () => mixed) => mixed,
140
- off?: (event: string, listener: () => mixed) => mixed,
151
+ readonly on?: (event: string, listener: () => mixed) => mixed,
152
+ readonly off?: (event: string, listener: () => mixed) => mixed,
141
153
  ...
142
154
  };
143
155
 
144
156
  /** Anything keys arrive from; `process.stdin`. */
145
157
  export type InputStream = {
146
158
  readonly isTTY?: boolean,
147
- setRawMode?: (raw: boolean) => mixed,
148
- resume?: () => mixed,
149
- pause?: () => mixed,
150
- setEncoding?: (encoding: string) => mixed,
151
- on?: (event: string, listener: (chunk: string) => mixed) => mixed,
152
- off?: (event: string, listener: (chunk: string) => mixed) => mixed,
159
+ readonly setRawMode?: (raw: boolean) => mixed,
160
+ readonly resume?: () => mixed,
161
+ readonly pause?: () => mixed,
162
+ readonly setEncoding?: (encoding: string) => mixed,
163
+ readonly on?: (event: string, listener: (chunk: string) => mixed) => mixed,
164
+ readonly off?: (event: string, listener: (chunk: string) => mixed) => mixed,
153
165
  ...
154
166
  };
155
167
 
@@ -188,6 +200,14 @@ export type RenderOptions = {
188
200
  * told that this window is the one where dragging does nothing.
189
201
  */
190
202
  readonly mouse?: boolean,
203
+ /**
204
+ * Whether `useClipboard()` may ask the terminal to copy text with OSC 52.
205
+ *
206
+ * On for interactive terminals, off for redirected output, and explicitly
207
+ * disableable for applications that prefer to expose "copy failed" over
208
+ * sending a sequence a terminal policy may reject without acknowledgement.
209
+ */
210
+ readonly clipboard?: boolean,
191
211
  };
192
212
 
193
213
  /**
@@ -208,7 +228,7 @@ function deliver(renderer: Renderer, event: InputEvent): void {
208
228
  /** Mount a tree into a renderer and return the pieces both drivers need. */
209
229
  function mount(element: React.Node, renderer: Renderer) {
210
230
  const root = createRoot(renderer);
211
- root.render(React.createElement(RendererContext.Provider, { value: renderer }, element));
231
+ root.render(<RendererContext.Provider value={renderer}>{element}</RendererContext.Provider>);
212
232
  return root;
213
233
  }
214
234
 
@@ -297,15 +317,18 @@ export function testRender(
297
317
  * that has to call `stop()`.
298
318
  */
299
319
  export function render(element: React.Node, options: RenderOptions = {}): Handle {
300
- // Three casts, and the same reason for all of them: Flow's library
301
- // definition for `process` describes Node's classes, and these types
302
- // describe the three things this renderer actually needs — so that a test
303
- // can pass a string collector, and so that a runtime whose streams are not
304
- // Node's is not excluded by a type. The narrowing is checked at run time by
305
- // the `!= null` guards below rather than trusted.
306
- const stdout: OutputStream = options.stdout ?? (process.stdout: $FlowFixMe);
307
- const stdin: InputStream = options.stdin ?? (process.stdin: $FlowFixMe);
308
- const env: TerminalEnv = options.env ?? (process.env: $FlowFixMe);
320
+ // Two casts, and the same reason for both: Flow's library definition for
321
+ // `process` describes Node's classes, and these types describe the things
322
+ // this renderer actually needs — so that a test can pass a string collector,
323
+ // and so that a runtime whose streams are not Node's is not excluded by a
324
+ // type. A class instance is not a subtype of an object type, and an
325
+ // interface cannot say that `on` is an optional *method*, which is what a
326
+ // collector without listeners needs. The narrowing is checked at run time by
327
+ // the `!= null` guards below rather than trusted. `process.env` needs no
328
+ // cast: `TerminalEnv` is the inexact subset of it rendering reads.
329
+ const stdout: OutputStream = options.stdout ?? (process.stdout as $FlowFixMe);
330
+ const stdin: InputStream = options.stdin ?? (process.stdin as $FlowFixMe);
331
+ const env: TerminalEnv = options.env ?? process.env;
309
332
  const capabilities = detectCapabilities(
310
333
  options.color ?? "auto",
311
334
  stdout.isTTY === true ? "interactive" : "piped",
@@ -322,6 +345,9 @@ export function render(element: React.Node, options: RenderOptions = {}): Handle
322
345
  const size = detectSize(env, stdout);
323
346
  const mouse = (options.mouse ?? false) && interactive;
324
347
  const renderer = createRenderer(size.columns, size.rows, capabilities, mouse);
348
+ if ((options.clipboard ?? true) && interactive) {
349
+ renderer.clipboard = osc52Clipboard((chunk) => stdout.write(chunk));
350
+ }
325
351
 
326
352
  let stopped = false;
327
353
  let scheduled = false;
@@ -356,6 +382,7 @@ export function render(element: React.Node, options: RenderOptions = {}): Handle
356
382
  (alternateScreen ? ENTER_ALTERNATE : "") +
357
383
  HIDE_CURSOR +
358
384
  ENABLE_PASTE +
385
+ ENABLE_KEYBOARD_PROTOCOL +
359
386
  (mouse ? ENABLE_MOUSE : "") +
360
387
  CLEAR,
361
388
  );
@@ -392,7 +419,9 @@ export function render(element: React.Node, options: RenderOptions = {}): Handle
392
419
  if (stdin.resume != null) {
393
420
  stdin.resume();
394
421
  }
395
- stdin.on("data", onData);
422
+ // Optional call: the calls above may have changed `stdin`, as far as Flow
423
+ // knows, so the `!= null` test at the top of this block no longer holds.
424
+ stdin.on?.("data", onData);
396
425
  }
397
426
  if (interactive && stdout.on != null) {
398
427
  stdout.on("resize", onResize);
@@ -431,6 +460,7 @@ export function render(element: React.Node, options: RenderOptions = {}): Handle
431
460
  }
432
461
  stdout.write(
433
462
  (mouse ? DISABLE_MOUSE : "") +
463
+ DISABLE_KEYBOARD_PROTOCOL +
434
464
  DISABLE_PASTE +
435
465
  SHOW_CURSOR +
436
466
  (alternateScreen ? LEAVE_ALTERNATE : "\n"),
package/widths.js CHANGED
@@ -141,7 +141,10 @@ export function scalarWidth(code: number): number {
141
141
  export function graphemeWidth(cluster: string): number {
142
142
  let width = 0;
143
143
  let previousNarrow = false;
144
- for (const character of cluster) {
144
+ // `new String(…)` because Flow does not let a primitive string stand for an
145
+ // `Iterable` (facebook/flow's tests/interface/string.js). The wrapper
146
+ // iterates the same code points.
147
+ for (const character of new String(cluster)) {
145
148
  const code = character.codePointAt(0) ?? 0;
146
149
  if (code === ZWJ) {
147
150
  previousNarrow = false;