@uniflowed/tui 0.0.0-alpha.18 → 0.15.0
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/capability.js +15 -12
- package/cells.js +1 -1
- package/clipboard.js +61 -0
- package/components.js +766 -57
- package/index.js +41 -22
- package/internal/hits.js +2 -2
- package/internal/host.js +63 -22
- package/internal/paint.js +43 -2
- package/internal/tree.js +168 -38
- package/internal/widgets.js +771 -0
- package/keys.js +214 -15
- package/layout.js +653 -132
- package/package.json +5 -4
- package/terminal.js +49 -19
- package/widths.js +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
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": "
|
|
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.
|
|
34
|
-
"react-reconciler": "^0.
|
|
34
|
+
"@uniflowed/react": "0.15.0",
|
|
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(
|
|
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
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
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
|
-
|
|
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;
|