@textui/core 0.2.0 → 0.4.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/dist/app/app.d.ts +28 -0
- package/dist/app/app.d.ts.map +1 -1
- package/dist/app/app.js +93 -1
- package/dist/render/buffer.d.ts +16 -1
- package/dist/render/buffer.d.ts.map +1 -1
- package/dist/render/buffer.js +27 -2
- package/dist/render/layout.d.ts +18 -0
- package/dist/render/layout.d.ts.map +1 -1
- package/dist/render/layout.js +16 -6
- package/dist/runtime/bindings.d.ts.map +1 -1
- package/dist/runtime/bindings.js +31 -4
- package/dist/runtime/hooks.d.ts +14 -2
- package/dist/runtime/hooks.d.ts.map +1 -1
- package/dist/runtime/hooks.js +15 -4
- package/dist/runtime/paint.d.ts.map +1 -1
- package/dist/runtime/paint.js +55 -16
- package/dist/types/app.d.ts +15 -0
- package/dist/types/app.d.ts.map +1 -1
- package/dist/util/text.d.ts +18 -1
- package/dist/util/text.d.ts.map +1 -1
- package/dist/util/text.js +55 -4
- package/package.json +1 -1
- package/src/app/app.ts +96 -1
- package/src/render/buffer.ts +28 -2
- package/src/render/layout.ts +27 -6
- package/src/runtime/bindings.ts +29 -3
- package/src/runtime/hooks.ts +15 -4
- package/src/runtime/paint.ts +58 -17
- package/src/types/app.ts +13 -0
- package/src/util/text.ts +55 -4
package/dist/types/app.d.ts
CHANGED
|
@@ -64,6 +64,21 @@ export interface TextUIApp extends Disposable {
|
|
|
64
64
|
stop(): Promise<void>;
|
|
65
65
|
/** Force a frame now, outside the scheduler. Tests and screenshots use it. */
|
|
66
66
|
flush(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Render until nothing is left to render, and answer whether it went quiet.
|
|
69
|
+
*
|
|
70
|
+
* The question `flush` cannot answer. A frame settles in more than one pass -
|
|
71
|
+
* an effect marks something dirty, a measurement runs the layout again - so a
|
|
72
|
+
* program that wants one true frame has to wait rather than force one.
|
|
73
|
+
*
|
|
74
|
+
* It returns as soon as a pass finds nothing pending, so an animating
|
|
75
|
+
* application settles between its frames. `false` means the passes kept
|
|
76
|
+
* producing work until the limit ran out - a render loop that does not
|
|
77
|
+
* converge, rather than one that is merely busy.
|
|
78
|
+
*/
|
|
79
|
+
settled(options?: {
|
|
80
|
+
limit?: number;
|
|
81
|
+
}): Promise<boolean>;
|
|
67
82
|
/** The last painted frame. */
|
|
68
83
|
buffer(): CellBuffer;
|
|
69
84
|
/**
|
package/dist/types/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/types/app.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/types/app.ts"],"names":[],"mappings":"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"}
|
package/dist/util/text.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { TextWrap } from '../types/style.js';
|
|
2
|
+
/**
|
|
3
|
+
* True for a string that is entirely printable ASCII.
|
|
4
|
+
*
|
|
5
|
+
* Worth asking, because the answer is yes for very nearly all of it: source
|
|
6
|
+
* code, file names, labels, key hints. Every character is then one grapheme
|
|
7
|
+
* one cell wide, and none of the Unicode machinery below has anything to do.
|
|
8
|
+
*/
|
|
9
|
+
export declare function isAscii(text: string): boolean;
|
|
2
10
|
/**
|
|
3
11
|
* Split into grapheme clusters. Falls back to code points without Intl.
|
|
4
12
|
*
|
|
@@ -58,7 +66,16 @@ export declare function wrapModeOf(wrap: TextWrap | undefined): WrapMode;
|
|
|
58
66
|
/** Break into lines of at most `width` cells. Honours existing newlines. */
|
|
59
67
|
export declare function wrapText(text: string, width: number, mode?: WrapMode): string[];
|
|
60
68
|
export declare function stripAnsi(text: string): string;
|
|
61
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Remove control characters that would corrupt the frame. Keeps newlines.
|
|
71
|
+
*
|
|
72
|
+
* Two regular expressions over every string on screen, every frame, and for
|
|
73
|
+
* almost all of them the answer is the string it was given. `test` before
|
|
74
|
+
* `replace` is one scan that stops at the first match instead of two that
|
|
75
|
+
* always run to the end and allocate a copy each - and it returns the original
|
|
76
|
+
* string, so the callers that follow it get a cache hit on the width they
|
|
77
|
+
* already worked out rather than an equal-but-different string.
|
|
78
|
+
*/
|
|
62
79
|
export declare function sanitize(text: string): string;
|
|
63
80
|
/** Repeat a grapheme to exactly `width` cells (wide chars land short). */
|
|
64
81
|
export declare function repeatToWidth(char: string, width: number): string;
|
package/dist/util/text.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/util/text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/util/text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAelD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAM7C;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAShD;AA8ED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAsBrD;AAsBD,2CAA2C;AAC3C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBhD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYhE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAgC/E;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAuB7D;AAED,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEtD,+DAA+D;AAC/D,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,SAAM,EACd,IAAI,GAAE,YAAoB,GACzB,MAAM,CAkCR;AAED,2EAA2E;AAC3E,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,OAAgB,EAC3C,IAAI,SAAM,GACT,MAAM,CAQR;AAED,kEAAkE;AAClE,wBAAgB,KAAK,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,OAAgB,EAC3C,QAAQ,SAAM,GACb,MAAM,CAER;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAOnF;AAED,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAE/D;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,QAAiB,GAAG,MAAM,EAAE,CAuDvF;AAOD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7C;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjE"}
|
package/dist/util/text.js
CHANGED
|
@@ -15,7 +15,7 @@ const segmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl
|
|
|
15
15
|
* code, file names, labels, key hints. Every character is then one grapheme
|
|
16
16
|
* one cell wide, and none of the Unicode machinery below has anything to do.
|
|
17
17
|
*/
|
|
18
|
-
function isAscii(text) {
|
|
18
|
+
export function isAscii(text) {
|
|
19
19
|
for (let i = 0; i < text.length; i++) {
|
|
20
20
|
const c = text.charCodeAt(i);
|
|
21
21
|
if (c < 0x20 || c > 0x7e)
|
|
@@ -148,6 +148,25 @@ export function graphemeWidth(cluster) {
|
|
|
148
148
|
return 2;
|
|
149
149
|
return 1;
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Widths already worked out, for strings that are not plain ASCII.
|
|
153
|
+
*
|
|
154
|
+
* The same strings are measured over and over. A frame measures the tree, lays
|
|
155
|
+
* it out and paints it - which is three walks over the same text - and then
|
|
156
|
+
* the next frame does all three again for content that has not changed. Nearly
|
|
157
|
+
* every row in a terminal application carries one glyph among its ASCII, a box
|
|
158
|
+
* rule or a bullet or an icon, and a single non-ASCII character sends the whole
|
|
159
|
+
* string down the slow path: an array of one-character strings, and a width
|
|
160
|
+
* classification for each of them. That was a quarter of the frame in a long
|
|
161
|
+
* transcript, spent proving the same sentences were the same width they were a
|
|
162
|
+
* thirtieth of a second ago.
|
|
163
|
+
*
|
|
164
|
+
* Only the slow path is remembered. ASCII is answered by `.length` before the
|
|
165
|
+
* map is even consulted, which is cheaper than the lookup would be, so the
|
|
166
|
+
* cache holds only what it saves anything on.
|
|
167
|
+
*/
|
|
168
|
+
const widths = new Map();
|
|
169
|
+
const WIDTH_CACHE = 8192;
|
|
151
170
|
/** Width of a string in terminal cells. */
|
|
152
171
|
export function stringWidth(text) {
|
|
153
172
|
if (text === '')
|
|
@@ -155,9 +174,18 @@ export function stringWidth(text) {
|
|
|
155
174
|
// Fast path: pure ASCII printable.
|
|
156
175
|
if (isAscii(text))
|
|
157
176
|
return text.length;
|
|
177
|
+
const seen = widths.get(text);
|
|
178
|
+
if (seen !== undefined)
|
|
179
|
+
return seen;
|
|
158
180
|
let w = 0;
|
|
159
181
|
for (const g of graphemes(text))
|
|
160
182
|
w += graphemeWidth(g);
|
|
183
|
+
// Emptied rather than evicted one at a time: a terminal's vocabulary of
|
|
184
|
+
// strings is small and slow-moving, so the cap is only ever reached by
|
|
185
|
+
// something streaming new text, and for that the whole map is stale.
|
|
186
|
+
if (widths.size >= WIDTH_CACHE)
|
|
187
|
+
widths.clear();
|
|
188
|
+
widths.set(text, w);
|
|
161
189
|
return w;
|
|
162
190
|
}
|
|
163
191
|
/** Take at most `width` cells from the front. Never splits a wide grapheme. */
|
|
@@ -405,10 +433,33 @@ const ANSI_RE =
|
|
|
405
433
|
export function stripAnsi(text) {
|
|
406
434
|
return text.replace(ANSI_RE, '');
|
|
407
435
|
}
|
|
408
|
-
|
|
436
|
+
/*
|
|
437
|
+
* Test copies, without the `g` flag.
|
|
438
|
+
*
|
|
439
|
+
* `test` on a global regular expression advances its `lastIndex` and starts
|
|
440
|
+
* the next call from there, so the same string tested twice answers yes then
|
|
441
|
+
* no. Asking is a different question from replacing and gets its own pattern.
|
|
442
|
+
*/
|
|
443
|
+
// eslint-disable-next-line no-control-regex
|
|
444
|
+
const ANSI_TEST = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1B]*(?:\x07|\x1B\\))/;
|
|
445
|
+
// eslint-disable-next-line no-control-regex
|
|
446
|
+
const CONTROL_TEST = /[\x00-\x08\x0B-\x1F\x7F]/;
|
|
447
|
+
// eslint-disable-next-line no-control-regex
|
|
448
|
+
const CONTROL_RE = /[\x00-\x08\x0B-\x1F\x7F]/g;
|
|
449
|
+
/**
|
|
450
|
+
* Remove control characters that would corrupt the frame. Keeps newlines.
|
|
451
|
+
*
|
|
452
|
+
* Two regular expressions over every string on screen, every frame, and for
|
|
453
|
+
* almost all of them the answer is the string it was given. `test` before
|
|
454
|
+
* `replace` is one scan that stops at the first match instead of two that
|
|
455
|
+
* always run to the end and allocate a copy each - and it returns the original
|
|
456
|
+
* string, so the callers that follow it get a cache hit on the width they
|
|
457
|
+
* already worked out rather than an equal-but-different string.
|
|
458
|
+
*/
|
|
409
459
|
export function sanitize(text) {
|
|
410
|
-
|
|
411
|
-
|
|
460
|
+
if (ANSI_TEST.test(text))
|
|
461
|
+
return stripAnsi(text).replace(CONTROL_RE, '');
|
|
462
|
+
return CONTROL_TEST.test(text) ? text.replace(CONTROL_RE, '') : text;
|
|
412
463
|
}
|
|
413
464
|
/** Repeat a grapheme to exactly `width` cells (wide chars land short). */
|
|
414
465
|
export function repeatToWidth(char, width) {
|
package/package.json
CHANGED
package/src/app/app.ts
CHANGED
|
@@ -304,6 +304,22 @@ export class App implements TextUIApp {
|
|
|
304
304
|
this.store.set('$/layout/shell', id);
|
|
305
305
|
const shell = this.shells.get(id);
|
|
306
306
|
if (shell?.theme && this.themes.get(shell.theme)) this.setTheme(shell.theme);
|
|
307
|
+
|
|
308
|
+
// `root` reaches the screen two ways, and this is the moment it changes
|
|
309
|
+
// which. With no shell registered at boot it was never opened into `main` -
|
|
310
|
+
// `rootNode` wraps it directly, which is the path an application built out
|
|
311
|
+
// of primitives takes. A shell arriving afterwards makes `rootNode` return
|
|
312
|
+
// the shell instead, and the application's entire content was simply gone:
|
|
313
|
+
// a framed, themed, empty screen.
|
|
314
|
+
//
|
|
315
|
+
// `setRoot` has always handled both paths, and says why - "setting one and
|
|
316
|
+
// not the other works in exactly half of the programs that can exist". This
|
|
317
|
+
// is the other half of the same sentence. Opening the same key twice
|
|
318
|
+
// replaces the mount rather than stacking one, so no guard is needed.
|
|
319
|
+
if (this.options.root) {
|
|
320
|
+
this.surfaces.open({ surface: 'main', key: ROOT_KEY, target: this.options.root });
|
|
321
|
+
}
|
|
322
|
+
|
|
307
323
|
this.buffer_.invalidate();
|
|
308
324
|
this.requestRender(true);
|
|
309
325
|
}
|
|
@@ -466,6 +482,44 @@ export class App implements TextUIApp {
|
|
|
466
482
|
this.renderFrame();
|
|
467
483
|
}
|
|
468
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Render until there is nothing left to render.
|
|
487
|
+
*
|
|
488
|
+
* `flush` forces one frame; this is the other question - *has it finished?* -
|
|
489
|
+
* and there was no way to ask it. What a program wanting one true frame did
|
|
490
|
+
* instead was guess: every example that writes a still ended with a sleep
|
|
491
|
+
* loop of four milliseconds times a number somebody tried until the picture
|
|
492
|
+
* looked right. Eight, mostly. Four in one, twelve in another. A number too
|
|
493
|
+
* small does not fail; it writes a half-drawn frame.
|
|
494
|
+
*
|
|
495
|
+
* A frame settles in more than one pass by design - an effect may mark
|
|
496
|
+
* something dirty, and a measurement changing runs the layout again - so the
|
|
497
|
+
* answer is a loop rather than a flag. Each turn yields to the task queue
|
|
498
|
+
* first, because what has not run yet cannot have marked anything.
|
|
499
|
+
*
|
|
500
|
+
* It returns as soon as a pass finds nothing pending, so an application that
|
|
501
|
+
* animates settles *between* its frames - which is what makes a still of one
|
|
502
|
+
* possible at all, and why the answer is not "has it stopped moving".
|
|
503
|
+
*
|
|
504
|
+
* `false` is the other thing: passes that kept producing work until the
|
|
505
|
+
* limit ran out, which is a render loop that does not converge - an effect
|
|
506
|
+
* with no dependency list setting the state it reads. Worth reporting rather
|
|
507
|
+
* than hanging on, and worth a number rather than a promise a caller can
|
|
508
|
+
* wait on for ever.
|
|
509
|
+
*/
|
|
510
|
+
async settled(options: { limit?: number } = {}): Promise<boolean> {
|
|
511
|
+
const limit = options.limit ?? 100;
|
|
512
|
+
for (let i = 0; i < limit; i++) {
|
|
513
|
+
// Deliberately not `unref`'d: this timer is what keeps the process alive
|
|
514
|
+
// while a still is being rendered, and one that lets it exit would leave
|
|
515
|
+
// the await unresolved and the frame unwritten.
|
|
516
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
517
|
+
if (!this.frameScheduled && !this.isDirty()) return true;
|
|
518
|
+
this.flush();
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
|
|
469
523
|
/**
|
|
470
524
|
* The tree the frame renders: the shell, always, when one is registered.
|
|
471
525
|
*
|
|
@@ -762,7 +816,21 @@ export class App implements TextUIApp {
|
|
|
762
816
|
walkInstances(this.root, (instance) => {
|
|
763
817
|
if (position) return;
|
|
764
818
|
const cursor = instance.props.cursor;
|
|
765
|
-
|
|
819
|
+
/*
|
|
820
|
+
* Absent, not falsy.
|
|
821
|
+
*
|
|
822
|
+
* `cursor` is a column, and column 0 is a real one - it is where the
|
|
823
|
+
* caret sits in an empty field, and where it sits at the start of any
|
|
824
|
+
* field. `!cursor` treated that as "no caret here", so a text input
|
|
825
|
+
* published no cursor position at all until its first character was
|
|
826
|
+
* typed: the terminal cursor stayed hidden, and the only thing marking
|
|
827
|
+
* the focused control was its border colour.
|
|
828
|
+
*
|
|
829
|
+
* `true` still means "here, at offset 0", which is what the `typeof`
|
|
830
|
+
* below is for - so this rejects the three ways of saying nothing and
|
|
831
|
+
* nothing else.
|
|
832
|
+
*/
|
|
833
|
+
if (cursor === undefined || cursor === null || cursor === false || !instance.box) return;
|
|
766
834
|
const id = typeof instance.props.id === 'string' ? instance.props.id : instance.id;
|
|
767
835
|
if (id !== focused && `${instance.id}:focus` !== focused) return;
|
|
768
836
|
|
|
@@ -887,6 +955,33 @@ export class App implements TextUIApp {
|
|
|
887
955
|
} catch (err) {
|
|
888
956
|
this.handleError(err, `input:${event.type}`);
|
|
889
957
|
}
|
|
958
|
+
|
|
959
|
+
/*
|
|
960
|
+
* Pointer motion and the wheel are streams, and only where they got to
|
|
961
|
+
* means anything.
|
|
962
|
+
*
|
|
963
|
+
* A terminal reports every cell the pointer crosses and every notch of the
|
|
964
|
+
* wheel, and one read can carry twenty of them - so rendering per event
|
|
965
|
+
* lays out and paints the whole tree twenty times to draw one gesture. The
|
|
966
|
+
* cost is the size of the tree, which is why it is invisible on a short
|
|
967
|
+
* screen and, on a long transcript, is the thing being dragged trailing
|
|
968
|
+
* seconds behind the pointer and a wheel that goes on scrolling after the
|
|
969
|
+
* hand has stopped. That second one is the tell: it is not momentum, it is
|
|
970
|
+
* a backlog of events still being drawn one frame each.
|
|
971
|
+
*
|
|
972
|
+
* Only the drawing is coalesced. Every event is still dispatched, so the
|
|
973
|
+
* notches still add up and the pointer still ends where it ended; what
|
|
974
|
+
* they share is the one frame `handleMouse` asked for, at the animation
|
|
975
|
+
* ceiling rather than at whatever rate the terminal felt like reporting.
|
|
976
|
+
*
|
|
977
|
+
* Keys are the opposite and stay synchronous: every one of them is a
|
|
978
|
+
* separate thing a person did, and a handler closes over the props from
|
|
979
|
+
* its last render, so typing "ab" without a frame in between makes the
|
|
980
|
+
* handler for "b" see the state from before "a".
|
|
981
|
+
*/
|
|
982
|
+
if (event.type === 'mouse'
|
|
983
|
+
&& (event.action === 'move' || event.action === 'drag' || event.action === 'wheel')) return;
|
|
984
|
+
|
|
890
985
|
if (this.running_ && this.isDirty()) this.renderFrame();
|
|
891
986
|
}
|
|
892
987
|
|
package/src/render/buffer.ts
CHANGED
|
@@ -2,6 +2,16 @@ import type { Rect } from '../types/geometry.js';
|
|
|
2
2
|
import type { Cell, CellBuffer } from '../types/cells.js';
|
|
3
3
|
import { COLOR_DEFAULT, packColor, type PackedColor } from './color.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* A character no cell can hold, for the previous frame when it must not match.
|
|
7
|
+
*
|
|
8
|
+
* `put` refuses the empty string and every real glyph is one the terminal can
|
|
9
|
+
* print, so NUL is a value that never arrives from a render and never leaves
|
|
10
|
+
* one: `diffFrame` reads `chars`, never `prevChars`, so this is only ever a
|
|
11
|
+
* comparison and is never written out.
|
|
12
|
+
*/
|
|
13
|
+
const NEVER = '\u0000';
|
|
14
|
+
|
|
5
15
|
/** One cell as it is stored: colours packed, nothing allocated to read it. */
|
|
6
16
|
export interface PackedCell {
|
|
7
17
|
char: string;
|
|
@@ -238,7 +248,7 @@ export class Buffer implements CellBuffer {
|
|
|
238
248
|
|
|
239
249
|
// A resize invalidates the previous frame entirely.
|
|
240
250
|
const n = width * height;
|
|
241
|
-
this.prevChars = new Array<string>(n).fill(
|
|
251
|
+
this.prevChars = new Array<string>(n).fill(NEVER);
|
|
242
252
|
this.prevFg = new Int32Array(n).fill(COLOR_DEFAULT);
|
|
243
253
|
this.prevBg = new Int32Array(n).fill(COLOR_DEFAULT);
|
|
244
254
|
this.prevAttrs = new Uint16Array(n);
|
|
@@ -282,9 +292,25 @@ export class Buffer implements CellBuffer {
|
|
|
282
292
|
this.committed = true;
|
|
283
293
|
}
|
|
284
294
|
|
|
285
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* Force the next diff to repaint everything. After a resize or a redraw.
|
|
297
|
+
*
|
|
298
|
+
* Both halves, and the second one is the one that was missing. Clearing
|
|
299
|
+
* `committed` makes `dirtyRows` return every row, but `diffFrame` still
|
|
300
|
+
* skips each cell that matches the previous frame - and a blank cell in the
|
|
301
|
+
* new frame matched the reset previous frame exactly, so it was never
|
|
302
|
+
* written. Under a theme with an opaque canvas nothing showed, because every
|
|
303
|
+
* cell carries a background colour and therefore differs anyway. Under a
|
|
304
|
+
* theme with a transparent one the blanks are default-on-default, and the
|
|
305
|
+
* terminal kept whatever the old layout had left in them: a resize scattered
|
|
306
|
+
* the previous frame across the new one and it never cleared.
|
|
307
|
+
*
|
|
308
|
+
* So the previous frame is filled with a character no cell can hold, which
|
|
309
|
+
* is a comparison that cannot come out equal.
|
|
310
|
+
*/
|
|
286
311
|
invalidate(): void {
|
|
287
312
|
this.committed = false;
|
|
313
|
+
this.prevChars.fill(NEVER);
|
|
288
314
|
}
|
|
289
315
|
|
|
290
316
|
toText(rect?: Rect): string {
|
package/src/render/layout.ts
CHANGED
|
@@ -40,6 +40,19 @@ export interface LayoutBox {
|
|
|
40
40
|
/** Content larger than `content`, when overflow is 'scroll'. */
|
|
41
41
|
scrollSize?: Size;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Frame and gaps, worked out once for this box.
|
|
45
|
+
*
|
|
46
|
+
* Neither depends on the space being offered - they are the box's own
|
|
47
|
+
* margin, border, padding and gap - and the box is measured about four
|
|
48
|
+
* times a frame at different bounds, each of which was rebuilding both and
|
|
49
|
+
* allocating three objects to do it. Same lifetime as `measured` below, and
|
|
50
|
+
* for the same reason: a layout box is rebuilt from the instance tree every
|
|
51
|
+
* frame, so nothing here can go stale.
|
|
52
|
+
*/
|
|
53
|
+
frame?: { margin: Edges; inset: Edges };
|
|
54
|
+
gaps?: { main: number; cross: number };
|
|
55
|
+
|
|
43
56
|
/**
|
|
44
57
|
* The last answer `measureBox` gave for this box, and what it was asked.
|
|
45
58
|
*
|
|
@@ -145,11 +158,15 @@ function mainOverflow(box: LayoutBox): Overflow {
|
|
|
145
158
|
* that sets both reads the same either way round.
|
|
146
159
|
*/
|
|
147
160
|
function gapsOf(box: LayoutBox): { main: number; cross: number } {
|
|
161
|
+
const seen = box.gaps;
|
|
162
|
+
if (seen !== undefined) return seen;
|
|
148
163
|
const vertical = box.style.rowGap ?? box.style.gap ?? 0;
|
|
149
164
|
const horizontal = box.style.columnGap ?? box.style.gap ?? 0;
|
|
150
|
-
|
|
165
|
+
const gaps = isColumn(box)
|
|
151
166
|
? { main: vertical, cross: horizontal }
|
|
152
167
|
: { main: horizontal, cross: vertical };
|
|
168
|
+
box.gaps = gaps;
|
|
169
|
+
return gaps;
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
/**
|
|
@@ -181,10 +198,12 @@ function splitLines(mains: number[], gap: number, limit: number): [number, numbe
|
|
|
181
198
|
|
|
182
199
|
/** Space this box consumes outside its content: margin, border, padding. */
|
|
183
200
|
function frameOf(box: LayoutBox): { margin: Edges; inset: Edges } {
|
|
201
|
+
const seen = box.frame;
|
|
202
|
+
if (seen !== undefined) return seen;
|
|
184
203
|
const margin = resolveEdges(box.style.margin);
|
|
185
204
|
const padding = resolveEdges(box.style.padding);
|
|
186
205
|
const b = box.borderEdges;
|
|
187
|
-
|
|
206
|
+
const frame = {
|
|
188
207
|
margin,
|
|
189
208
|
inset: {
|
|
190
209
|
top: b.top + padding.top,
|
|
@@ -193,6 +212,8 @@ function frameOf(box: LayoutBox): { margin: Edges; inset: Edges } {
|
|
|
193
212
|
left: b.left + padding.left,
|
|
194
213
|
},
|
|
195
214
|
};
|
|
215
|
+
box.frame = frame;
|
|
216
|
+
return frame;
|
|
196
217
|
}
|
|
197
218
|
|
|
198
219
|
/**
|
|
@@ -247,7 +268,7 @@ export function measureBox(box: LayoutBox, availW: number, availH: number): Size
|
|
|
247
268
|
if (isAbsolute(child) || isHidden(child)) continue;
|
|
248
269
|
count++;
|
|
249
270
|
const m = measureBox(child, innerAvailW, innerAvailH);
|
|
250
|
-
const cm =
|
|
271
|
+
const cm = frameOf(child).margin;
|
|
251
272
|
mains.push(column ? m.height + edgeV(cm) : m.width + edgeH(cm));
|
|
252
273
|
crosses.push(column ? m.width + edgeH(cm) : m.height + edgeV(cm));
|
|
253
274
|
}
|
|
@@ -289,7 +310,7 @@ export function measureBox(box: LayoutBox, availW: number, availH: number): Size
|
|
|
289
310
|
if (isAbsolute(child) || isHidden(child)) continue;
|
|
290
311
|
const flex = Math.max(0, child.style.flex ?? 0);
|
|
291
312
|
if (flex > 0) {
|
|
292
|
-
const cm =
|
|
313
|
+
const cm = frameOf(child).margin;
|
|
293
314
|
const share = Math.max(0, Math.floor((room * flex) / totalFlex) - edgeH(cm));
|
|
294
315
|
const m = measureBox(child, share, innerAvailH);
|
|
295
316
|
crosses[index] = m.height + edgeV(cm);
|
|
@@ -519,7 +540,7 @@ function layoutWrapped(box: LayoutBox, flow: LayoutBox[]): void {
|
|
|
519
540
|
const mains: number[] = [];
|
|
520
541
|
const crosses: number[] = [];
|
|
521
542
|
for (const child of flow) {
|
|
522
|
-
const margin =
|
|
543
|
+
const margin = frameOf(child).margin;
|
|
523
544
|
const basis = child.style.basis ?? (column ? child.style.height : child.style.width);
|
|
524
545
|
const fixed = resolveDimension(basis, mainAvail);
|
|
525
546
|
const m = measureBox(
|
|
@@ -580,7 +601,7 @@ function layoutLine(
|
|
|
580
601
|
const gapTotal = flow.length > 1 ? gap * (flow.length - 1) : 0;
|
|
581
602
|
|
|
582
603
|
// 1. base sizes
|
|
583
|
-
const margins = flow.map((c) =>
|
|
604
|
+
const margins = flow.map((c) => frameOf(c).margin);
|
|
584
605
|
const bases: number[] = [];
|
|
585
606
|
for (let i = 0; i < flow.length; i++) {
|
|
586
607
|
const child = flow[i] as LayoutBox;
|
package/src/runtime/bindings.ts
CHANGED
|
@@ -94,13 +94,39 @@ export function resolveValue(ctx: ResolveContext, value: unknown): unknown {
|
|
|
94
94
|
if (isComponentNode(value)) return value;
|
|
95
95
|
if (isAction(value)) return value;
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
/*
|
|
98
|
+
* The same array back when there was nothing in it to resolve.
|
|
99
|
+
*
|
|
100
|
+
* Identity is what the reconciler compares - a component whose props are
|
|
101
|
+
* all unchanged is not re-run, and neither is anything under it - so
|
|
102
|
+
* copying unconditionally made that test impossible to pass for exactly
|
|
103
|
+
* the props worth passing it for. A list of four hundred rows arrived as a
|
|
104
|
+
* new array on every pass, its holder re-rendered every frame whatever it
|
|
105
|
+
* had been told, and the memoisation callers wrote to prevent that could
|
|
106
|
+
* not reach this far.
|
|
107
|
+
*
|
|
108
|
+
* Copying at all is for the bindings: a `{ path }` inside an array has to
|
|
109
|
+
* become the value it names, and that is a different array. So the copy is
|
|
110
|
+
* kept and returned only when something in it actually changed.
|
|
111
|
+
*/
|
|
112
|
+
if (Array.isArray(value)) {
|
|
113
|
+
let changed = false;
|
|
114
|
+
const items = value.map((v) => {
|
|
115
|
+
const resolved = resolveValue(ctx, v);
|
|
116
|
+
if (resolved !== v) changed = true;
|
|
117
|
+
return resolved;
|
|
118
|
+
});
|
|
119
|
+
return changed ? items : value;
|
|
120
|
+
}
|
|
98
121
|
|
|
122
|
+
let changed = false;
|
|
99
123
|
const out: Record<string, unknown> = {};
|
|
100
124
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
101
|
-
|
|
125
|
+
const resolved = resolveValue(ctx, v);
|
|
126
|
+
if (resolved !== v) changed = true;
|
|
127
|
+
out[k] = resolved;
|
|
102
128
|
}
|
|
103
|
-
return out;
|
|
129
|
+
return changed ? out : value;
|
|
104
130
|
}
|
|
105
131
|
|
|
106
132
|
/**
|
package/src/runtime/hooks.ts
CHANGED
|
@@ -1001,14 +1001,25 @@ export function useTicker(
|
|
|
1001
1001
|
}, [enabled, options.fps]);
|
|
1002
1002
|
}
|
|
1003
1003
|
|
|
1004
|
-
/**
|
|
1005
|
-
|
|
1004
|
+
/**
|
|
1005
|
+
* A frame counter, for spinners and marquees. Frozen when animation is off.
|
|
1006
|
+
*
|
|
1007
|
+
* `enabled` is not a convenience. A ticker is a standing invalidation - it
|
|
1008
|
+
* marks its component dirty `fps` times a second for as long as it is
|
|
1009
|
+
* mounted - so a component that animates only sometimes and calls this
|
|
1010
|
+
* unconditionally keeps the whole application rendering while it sits still.
|
|
1011
|
+
* That is invisible in a small tree and is the entire frame budget in a large
|
|
1012
|
+
* one, which is the case nobody tests. Frozen at 0 while off, so a caret or a
|
|
1013
|
+
* spinner drawn from it is steady rather than absent.
|
|
1014
|
+
*/
|
|
1015
|
+
export function useFrame(fps = 10, options: { enabled?: boolean } = {}): number {
|
|
1006
1016
|
const instance = currentInstance();
|
|
1007
1017
|
const [frame, setFrame] = useState(0);
|
|
1008
1018
|
const disabled = instance.runtime.animation.disabled;
|
|
1019
|
+
const running = (options.enabled ?? true) && !disabled;
|
|
1009
1020
|
|
|
1010
|
-
useTicker(() => setFrame((f) => f + 1), { fps, enabled:
|
|
1011
|
-
return
|
|
1021
|
+
useTicker(() => setFrame((f) => f + 1), { fps, enabled: running });
|
|
1022
|
+
return running ? frame : 0;
|
|
1012
1023
|
}
|
|
1013
1024
|
|
|
1014
1025
|
/** A value that eases towards its target. Snaps when animation is off. */
|
package/src/runtime/paint.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Rect } from '../types/geometry.js';
|
|
2
|
-
import type { Style } from '../types/style.js';
|
|
2
|
+
import type { Style, TextWrap } from '../types/style.js';
|
|
3
3
|
import type { ResolvedTheme } from '../types/theme.js';
|
|
4
4
|
import type { TerminalCapabilities } from '../types/capabilities.js';
|
|
5
5
|
import type { Cell, Color } from '../types/cells.js';
|
|
@@ -12,7 +12,7 @@ import type { Buffer } from '../render/buffer.js';
|
|
|
12
12
|
import { COLOR_DEFAULT, mix, packColor, type PackedColor } from '../render/color.js';
|
|
13
13
|
import { rectIntersect } from '../types/geometry.js';
|
|
14
14
|
import {
|
|
15
|
-
graphemes, graphemeWidth, sanitize, stringWidth, truncate,
|
|
15
|
+
graphemes, graphemeWidth, isAscii, sanitize, stringWidth, truncate,
|
|
16
16
|
truncateSideOf, wrapModeOf, wrapText,
|
|
17
17
|
} from '../util/text.js';
|
|
18
18
|
import {
|
|
@@ -87,8 +87,23 @@ class Surface implements PaintSurface {
|
|
|
87
87
|
const link = style?.link;
|
|
88
88
|
const oy = this.rect.y + y;
|
|
89
89
|
|
|
90
|
+
const clean = sanitize(text);
|
|
91
|
+
|
|
92
|
+
// Plain ASCII is one cell per character and needs none of the clustering
|
|
93
|
+
// machinery - and it is nearly every string a terminal ever draws. The
|
|
94
|
+
// general path allocates an array of one-character strings for the whole
|
|
95
|
+
// run before writing any of it.
|
|
90
96
|
let cx = x;
|
|
91
|
-
|
|
97
|
+
if (isAscii(clean)) {
|
|
98
|
+
for (let i = 0; i < clean.length; i++) {
|
|
99
|
+
const ax = this.rect.x + cx;
|
|
100
|
+
if (this.visible(ax, oy)) this.buffer.put(ax, oy, clean[i] as string, fg, bg, attrs, link);
|
|
101
|
+
cx += 1;
|
|
102
|
+
}
|
|
103
|
+
return cx - x;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const g of graphemes(clean)) {
|
|
92
107
|
const w = graphemeWidth(g);
|
|
93
108
|
if (w === 0) continue;
|
|
94
109
|
const ax = this.rect.x + cx;
|
|
@@ -337,6 +352,17 @@ function oneLine(text: string): string {
|
|
|
337
352
|
return text.includes('\n') ? text.split('\n').join(' ') : text;
|
|
338
353
|
}
|
|
339
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Text measurements already taken.
|
|
357
|
+
*
|
|
358
|
+
* A measurement is a pure function of the string, how it wraps and the width
|
|
359
|
+
* it is offered - and a layout asks for the same three several times in a
|
|
360
|
+
* frame, then the next frame asks for all of them again. The text in a
|
|
361
|
+
* transcript does not change; only the one line somebody is typing does.
|
|
362
|
+
*/
|
|
363
|
+
const measurements = new Map<string, { width: number; height: number }>();
|
|
364
|
+
const MEASURE_CACHE = 4096;
|
|
365
|
+
|
|
340
366
|
function textMeasure(instance: Instance, style: Style): (w: number, h: number) => { width: number; height: number } {
|
|
341
367
|
return (maxWidth: number) => {
|
|
342
368
|
const text = sanitize(textContent(instance));
|
|
@@ -344,26 +370,41 @@ function textMeasure(instance: Instance, style: Style): (w: number, h: number) =
|
|
|
344
370
|
|
|
345
371
|
const wrap = style.wrap ?? 'none';
|
|
346
372
|
|
|
373
|
+
const key = `${wrap}\u0000${String(maxWidth)}\u0000${text}`;
|
|
374
|
+
const seen = measurements.get(key);
|
|
375
|
+
if (seen !== undefined) return seen;
|
|
376
|
+
|
|
347
377
|
// A truncating text is one row, always. It still asks for the width it
|
|
348
378
|
// would like - a box sizing itself around it gets the whole string when
|
|
349
379
|
// there is room, and only cuts when there is not.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
380
|
+
const size = measure(text, wrap, maxWidth);
|
|
381
|
+
if (measurements.size >= MEASURE_CACHE) measurements.clear();
|
|
382
|
+
measurements.set(key, size);
|
|
383
|
+
return size;
|
|
384
|
+
};
|
|
385
|
+
}
|
|
353
386
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
387
|
+
/** The measurement itself, with nothing remembered. */
|
|
388
|
+
function measure(text: string, wrap: TextWrap, maxWidth: number): { width: number; height: number } {
|
|
389
|
+
// A truncating text is one row, always. It still asks for the width it
|
|
390
|
+
// would like - a box sizing itself around it gets the whole string when
|
|
391
|
+
// there is room, and only cuts when there is not.
|
|
392
|
+
if (truncateSideOf(wrap) !== undefined) {
|
|
393
|
+
return { width: stringWidth(oneLine(text)), height: 1 };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (wrap === 'none') {
|
|
397
|
+
const lines = text.split('\n');
|
|
363
398
|
return {
|
|
364
|
-
width: Math.
|
|
365
|
-
height:
|
|
399
|
+
width: Math.max(...lines.map(stringWidth)),
|
|
400
|
+
height: lines.length,
|
|
366
401
|
};
|
|
402
|
+
}
|
|
403
|
+
const limit = Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : stringWidth(text);
|
|
404
|
+
const lines = wrapText(text, limit, wrapModeOf(wrap));
|
|
405
|
+
return {
|
|
406
|
+
width: Math.min(limit, Math.max(0, ...lines.map(stringWidth))),
|
|
407
|
+
height: Math.max(1, lines.length),
|
|
367
408
|
};
|
|
368
409
|
}
|
|
369
410
|
|