@textui/core 0.1.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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +11 -0
- package/dist/app/app.d.ts +200 -0
- package/dist/app/app.d.ts.map +1 -0
- package/dist/app/app.js +980 -0
- package/dist/core/animation.d.ts +34 -0
- package/dist/core/animation.d.ts.map +1 -0
- package/dist/core/animation.js +125 -0
- package/dist/core/clipboard.d.ts +29 -0
- package/dist/core/clipboard.d.ts.map +1 -0
- package/dist/core/clipboard.js +30 -0
- package/dist/core/commands.d.ts +42 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +146 -0
- package/dist/core/components.d.ts +27 -0
- package/dist/core/components.d.ts.map +1 -0
- package/dist/core/components.js +78 -0
- package/dist/core/events.d.ts +23 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +77 -0
- package/dist/core/focus.d.ts +69 -0
- package/dist/core/focus.d.ts.map +1 -0
- package/dist/core/focus.js +336 -0
- package/dist/core/i18n.d.ts +27 -0
- package/dist/core/i18n.d.ts.map +1 -0
- package/dist/core/i18n.js +88 -0
- package/dist/core/keybindings.d.ts +53 -0
- package/dist/core/keybindings.d.ts.map +1 -0
- package/dist/core/keybindings.js +163 -0
- package/dist/core/layers.d.ts +28 -0
- package/dist/core/layers.d.ts.map +1 -0
- package/dist/core/layers.js +84 -0
- package/dist/core/manifest.d.ts +22 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +85 -0
- package/dist/core/navigation.d.ts +47 -0
- package/dist/core/navigation.d.ts.map +1 -0
- package/dist/core/navigation.js +110 -0
- package/dist/core/resources.d.ts +115 -0
- package/dist/core/resources.d.ts.map +1 -0
- package/dist/core/resources.js +321 -0
- package/dist/core/services.d.ts +21 -0
- package/dist/core/services.d.ts.map +1 -0
- package/dist/core/services.js +63 -0
- package/dist/core/store.d.ts +60 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +593 -0
- package/dist/core/surfaces.d.ts +81 -0
- package/dist/core/surfaces.d.ts.map +1 -0
- package/dist/core/surfaces.js +237 -0
- package/dist/core/syntax.d.ts +49 -0
- package/dist/core/syntax.d.ts.map +1 -0
- package/dist/core/syntax.js +172 -0
- package/dist/core/when.d.ts +15 -0
- package/dist/core/when.d.ts.map +1 -0
- package/dist/core/when.js +224 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/jsx/factory.d.ts +27 -0
- package/dist/jsx/factory.d.ts.map +1 -0
- package/dist/jsx/factory.js +102 -0
- package/dist/jsx/intrinsics.d.ts +96 -0
- package/dist/jsx/intrinsics.d.ts.map +1 -0
- package/dist/jsx/intrinsics.js +1 -0
- package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
- package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-dev-runtime.js +12 -0
- package/dist/jsx/jsx-runtime.d.ts +42 -0
- package/dist/jsx/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx/jsx-runtime.js +16 -0
- package/dist/render/buffer.d.ts +77 -0
- package/dist/render/buffer.d.ts.map +1 -0
- package/dist/render/buffer.js +275 -0
- package/dist/render/color.d.ts +35 -0
- package/dist/render/color.d.ts.map +1 -0
- package/dist/render/color.js +160 -0
- package/dist/render/diff.d.ts +37 -0
- package/dist/render/diff.d.ts.map +1 -0
- package/dist/render/diff.js +66 -0
- package/dist/render/layout.d.ts +72 -0
- package/dist/render/layout.d.ts.map +1 -0
- package/dist/render/layout.js +633 -0
- package/dist/render/static.d.ts +63 -0
- package/dist/render/static.d.ts.map +1 -0
- package/dist/render/static.js +209 -0
- package/dist/runtime/bindings.d.ts +46 -0
- package/dist/runtime/bindings.d.ts.map +1 -0
- package/dist/runtime/bindings.js +116 -0
- package/dist/runtime/hooks.d.ts +284 -0
- package/dist/runtime/hooks.d.ts.map +1 -0
- package/dist/runtime/hooks.js +846 -0
- package/dist/runtime/instance.d.ts +106 -0
- package/dist/runtime/instance.d.ts.map +1 -0
- package/dist/runtime/instance.js +181 -0
- package/dist/runtime/paint.d.ts +27 -0
- package/dist/runtime/paint.d.ts.map +1 -0
- package/dist/runtime/paint.js +567 -0
- package/dist/runtime/reconcile.d.ts +23 -0
- package/dist/runtime/reconcile.d.ts.map +1 -0
- package/dist/runtime/reconcile.js +260 -0
- package/dist/runtime/runtime.d.ts +42 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.js +1 -0
- package/dist/runtime/style.d.ts +55 -0
- package/dist/runtime/style.d.ts.map +1 -0
- package/dist/runtime/style.js +143 -0
- package/dist/themes/borders.d.ts +10 -0
- package/dist/themes/borders.d.ts.map +1 -0
- package/dist/themes/borders.js +88 -0
- package/dist/themes/builtin.d.ts +31 -0
- package/dist/themes/builtin.d.ts.map +1 -0
- package/dist/themes/builtin.js +291 -0
- package/dist/themes/glyphs.d.ts +15 -0
- package/dist/themes/glyphs.d.ts.map +1 -0
- package/dist/themes/glyphs.js +97 -0
- package/dist/themes/index.d.ts +5 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +4 -0
- package/dist/themes/registry.d.ts +20 -0
- package/dist/themes/registry.d.ts.map +1 -0
- package/dist/themes/registry.js +204 -0
- package/dist/types/adapter.d.ts +38 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +1 -0
- package/dist/types/animation.d.ts +31 -0
- package/dist/types/animation.d.ts.map +1 -0
- package/dist/types/animation.js +1 -0
- package/dist/types/app.d.ts +148 -0
- package/dist/types/app.d.ts.map +1 -0
- package/dist/types/app.js +1 -0
- package/dist/types/async.d.ts +30 -0
- package/dist/types/async.d.ts.map +1 -0
- package/dist/types/async.js +1 -0
- package/dist/types/capabilities.d.ts +44 -0
- package/dist/types/capabilities.d.ts.map +1 -0
- package/dist/types/capabilities.js +33 -0
- package/dist/types/cells.d.ts +72 -0
- package/dist/types/cells.d.ts.map +1 -0
- package/dist/types/cells.js +10 -0
- package/dist/types/command.d.ts +127 -0
- package/dist/types/command.d.ts.map +1 -0
- package/dist/types/command.js +1 -0
- package/dist/types/component-registry.d.ts +77 -0
- package/dist/types/component-registry.d.ts.map +1 -0
- package/dist/types/component-registry.js +1 -0
- package/dist/types/disposable.d.ts +8 -0
- package/dist/types/disposable.d.ts.map +1 -0
- package/dist/types/disposable.js +1 -0
- package/dist/types/focus.d.ts +72 -0
- package/dist/types/focus.d.ts.map +1 -0
- package/dist/types/focus.js +1 -0
- package/dist/types/geometry.d.ts +28 -0
- package/dist/types/geometry.d.ts.map +1 -0
- package/dist/types/geometry.js +14 -0
- package/dist/types/graph.d.ts +129 -0
- package/dist/types/graph.d.ts.map +1 -0
- package/dist/types/graph.js +31 -0
- package/dist/types/i18n.d.ts +23 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/i18n.js +1 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +31 -0
- package/dist/types/input.d.ts +65 -0
- package/dist/types/input.d.ts.map +1 -0
- package/dist/types/input.js +1 -0
- package/dist/types/keybinding.d.ts +49 -0
- package/dist/types/keybinding.d.ts.map +1 -0
- package/dist/types/keybinding.js +1 -0
- package/dist/types/layer.d.ts +54 -0
- package/dist/types/layer.d.ts.map +1 -0
- package/dist/types/layer.js +1 -0
- package/dist/types/manifest.d.ts +114 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/markdown.d.ts +58 -0
- package/dist/types/markdown.d.ts.map +1 -0
- package/dist/types/markdown.js +1 -0
- package/dist/types/navigation.d.ts +40 -0
- package/dist/types/navigation.d.ts.map +1 -0
- package/dist/types/navigation.js +1 -0
- package/dist/types/render.d.ts +72 -0
- package/dist/types/render.d.ts.map +1 -0
- package/dist/types/render.js +1 -0
- package/dist/types/resource.d.ts +170 -0
- package/dist/types/resource.d.ts.map +1 -0
- package/dist/types/resource.js +1 -0
- package/dist/types/services.d.ts +24 -0
- package/dist/types/services.d.ts.map +1 -0
- package/dist/types/services.js +3 -0
- package/dist/types/shell.d.ts +32 -0
- package/dist/types/shell.d.ts.map +1 -0
- package/dist/types/shell.js +1 -0
- package/dist/types/store.d.ts +124 -0
- package/dist/types/store.d.ts.map +1 -0
- package/dist/types/store.js +1 -0
- package/dist/types/stream.d.ts +18 -0
- package/dist/types/stream.d.ts.map +1 -0
- package/dist/types/stream.js +1 -0
- package/dist/types/style.d.ts +166 -0
- package/dist/types/style.d.ts.map +1 -0
- package/dist/types/style.js +1 -0
- package/dist/types/surface.d.ts +97 -0
- package/dist/types/surface.d.ts.map +1 -0
- package/dist/types/surface.js +1 -0
- package/dist/types/syntax.d.ts +68 -0
- package/dist/types/syntax.d.ts.map +1 -0
- package/dist/types/syntax.js +5 -0
- package/dist/types/terminal.d.ts +52 -0
- package/dist/types/terminal.d.ts.map +1 -0
- package/dist/types/terminal.js +1 -0
- package/dist/types/theme.d.ts +117 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/dist/types/theme.js +1 -0
- package/dist/types/when.d.ts +19 -0
- package/dist/types/when.d.ts.map +1 -0
- package/dist/types/when.js +1 -0
- package/dist/ui/primitives.d.ts +56 -0
- package/dist/ui/primitives.d.ts.map +1 -0
- package/dist/ui/primitives.js +108 -0
- package/dist/ui/screen.d.ts +23 -0
- package/dist/ui/screen.d.ts.map +1 -0
- package/dist/ui/screen.js +18 -0
- package/dist/util/disposable.d.ts +6 -0
- package/dist/util/disposable.d.ts.map +1 -0
- package/dist/util/disposable.js +49 -0
- package/dist/util/markdown.d.ts +24 -0
- package/dist/util/markdown.d.ts.map +1 -0
- package/dist/util/markdown.js +220 -0
- package/dist/util/paths.d.ts +47 -0
- package/dist/util/paths.d.ts.map +1 -0
- package/dist/util/paths.js +134 -0
- package/dist/util/stream.d.ts +32 -0
- package/dist/util/stream.d.ts.map +1 -0
- package/dist/util/stream.js +206 -0
- package/dist/util/text.d.ts +65 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +419 -0
- package/package.json +70 -0
- package/src/adapters/index.ts +11 -0
- package/src/app/app.ts +1096 -0
- package/src/core/animation.ts +144 -0
- package/src/core/clipboard.ts +40 -0
- package/src/core/commands.ts +169 -0
- package/src/core/components.ts +91 -0
- package/src/core/events.ts +95 -0
- package/src/core/focus.ts +358 -0
- package/src/core/i18n.ts +107 -0
- package/src/core/keybindings.ts +184 -0
- package/src/core/layers.ts +94 -0
- package/src/core/manifest.ts +84 -0
- package/src/core/navigation.ts +135 -0
- package/src/core/resources.ts +362 -0
- package/src/core/services.ts +69 -0
- package/src/core/store.ts +640 -0
- package/src/core/surfaces.ts +292 -0
- package/src/core/syntax.ts +200 -0
- package/src/core/when.ts +238 -0
- package/src/index.ts +76 -0
- package/src/jsx/factory.ts +124 -0
- package/src/jsx/intrinsics.ts +99 -0
- package/src/jsx/jsx-dev-runtime.ts +23 -0
- package/src/jsx/jsx-runtime.ts +76 -0
- package/src/render/buffer.ts +318 -0
- package/src/render/color.ts +180 -0
- package/src/render/diff.ts +99 -0
- package/src/render/layout.ts +764 -0
- package/src/render/static.ts +290 -0
- package/src/runtime/bindings.ts +150 -0
- package/src/runtime/hooks.ts +1057 -0
- package/src/runtime/instance.ts +284 -0
- package/src/runtime/paint.ts +737 -0
- package/src/runtime/reconcile.ts +335 -0
- package/src/runtime/runtime.ts +47 -0
- package/src/runtime/style.ts +205 -0
- package/src/themes/borders.ts +95 -0
- package/src/themes/builtin.ts +301 -0
- package/src/themes/glyphs.ts +101 -0
- package/src/themes/index.ts +4 -0
- package/src/themes/registry.ts +224 -0
- package/src/types/adapter.ts +41 -0
- package/src/types/animation.ts +34 -0
- package/src/types/app.ts +149 -0
- package/src/types/async.ts +35 -0
- package/src/types/capabilities.ts +79 -0
- package/src/types/cells.ts +79 -0
- package/src/types/command.ts +139 -0
- package/src/types/component-registry.ts +82 -0
- package/src/types/disposable.ts +8 -0
- package/src/types/focus.ts +77 -0
- package/src/types/geometry.ts +50 -0
- package/src/types/graph.ts +174 -0
- package/src/types/i18n.ts +25 -0
- package/src/types/index.ts +31 -0
- package/src/types/input.ts +81 -0
- package/src/types/keybinding.ts +51 -0
- package/src/types/layer.ts +46 -0
- package/src/types/manifest.ts +101 -0
- package/src/types/markdown.ts +47 -0
- package/src/types/navigation.ts +42 -0
- package/src/types/render.ts +86 -0
- package/src/types/resource.ts +185 -0
- package/src/types/services.ts +28 -0
- package/src/types/shell.ts +30 -0
- package/src/types/store.ts +148 -0
- package/src/types/stream.ts +24 -0
- package/src/types/style.ts +205 -0
- package/src/types/surface.ts +119 -0
- package/src/types/syntax.ts +93 -0
- package/src/types/terminal.ts +58 -0
- package/src/types/theme.ts +121 -0
- package/src/types/when.ts +21 -0
- package/src/ui/primitives.ts +118 -0
- package/src/ui/screen.ts +41 -0
- package/src/util/disposable.ts +49 -0
- package/src/util/markdown.ts +225 -0
- package/src/util/paths.ts +138 -0
- package/src/util/stream.ts +213 -0
- package/src/util/text.ts +428 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { Disposable } from '../types/disposable.js';
|
|
2
|
+
import type { Stream, StreamObserver, StreamSource } from '../types/stream.js';
|
|
3
|
+
import type { ReactiveStore } from '../types/store.js';
|
|
4
|
+
import type { BindingPath } from '../types/graph.js';
|
|
5
|
+
import { toDisposable } from './disposable.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The smallest useful stream, plus adapters.
|
|
9
|
+
*
|
|
10
|
+
* A LogViewer should not care whether its lines come from a child process, an
|
|
11
|
+
* async iterable or a store path - so everything is adapted to one shape here,
|
|
12
|
+
* and nowhere else has to know the difference.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export function createStream<T>(
|
|
16
|
+
start: (observer: StreamObserver<T>) => (() => void) | void,
|
|
17
|
+
): Stream<T> {
|
|
18
|
+
return {
|
|
19
|
+
subscribe(observer) {
|
|
20
|
+
let closed = false;
|
|
21
|
+
const guard: StreamObserver<T> = {
|
|
22
|
+
next: (v) => { if (!closed) observer.next(v); },
|
|
23
|
+
error: (e) => { if (!closed) observer.error?.(e); },
|
|
24
|
+
complete: () => { if (!closed) observer.complete?.(); },
|
|
25
|
+
};
|
|
26
|
+
const stop = start(guard);
|
|
27
|
+
return toDisposable(() => {
|
|
28
|
+
closed = true;
|
|
29
|
+
stop?.();
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function fromAsyncIterable<T>(iterable: AsyncIterable<T>): Stream<T> {
|
|
36
|
+
return createStream<T>((observer) => {
|
|
37
|
+
let cancelled = false;
|
|
38
|
+
void (async () => {
|
|
39
|
+
try {
|
|
40
|
+
for await (const value of iterable) {
|
|
41
|
+
if (cancelled) return;
|
|
42
|
+
observer.next(value);
|
|
43
|
+
}
|
|
44
|
+
observer.complete?.();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (!cancelled) observer.error?.(err);
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
return () => { cancelled = true; };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function fromIterable<T>(iterable: Iterable<T>): Stream<T> {
|
|
54
|
+
return createStream<T>((observer) => {
|
|
55
|
+
for (const value of iterable) observer.next(value);
|
|
56
|
+
observer.complete?.();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Emit the value at a store path each time it changes. */
|
|
61
|
+
export function fromStorePath<T>(store: ReactiveStore, path: BindingPath): Stream<T> {
|
|
62
|
+
return createStream<T>((observer) => {
|
|
63
|
+
const sub = store.subscribe(path, (value) => observer.next(value as T), { immediate: true });
|
|
64
|
+
return () => sub.dispose();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Emit each item appended to a store collection, and nothing already there. */
|
|
69
|
+
export function fromStoreCollection<T>(store: ReactiveStore, path: BindingPath): Stream<T> {
|
|
70
|
+
return createStream<T>((observer) => {
|
|
71
|
+
let seen = (store.get<T[]>(path) ?? []).length;
|
|
72
|
+
const sub = store.subscribe(path, (value) => {
|
|
73
|
+
const list = Array.isArray(value) ? (value as T[]) : [];
|
|
74
|
+
if (list.length < seen) seen = 0;
|
|
75
|
+
for (let i = seen; i < list.length; i++) observer.next(list[i] as T);
|
|
76
|
+
seen = list.length;
|
|
77
|
+
});
|
|
78
|
+
return () => sub.dispose();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A source that pushes from the outside. Adapters and tests use this. */
|
|
83
|
+
export interface Subject<T> extends Stream<T> {
|
|
84
|
+
next(value: T): void;
|
|
85
|
+
error(err: unknown): void;
|
|
86
|
+
complete(): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createSubject<T>(): Subject<T> {
|
|
90
|
+
const observers = new Set<StreamObserver<T>>();
|
|
91
|
+
let done = false;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
subscribe(observer): Disposable {
|
|
95
|
+
if (done) {
|
|
96
|
+
observer.complete?.();
|
|
97
|
+
return toDisposable(() => {});
|
|
98
|
+
}
|
|
99
|
+
observers.add(observer);
|
|
100
|
+
return toDisposable(() => observers.delete(observer));
|
|
101
|
+
},
|
|
102
|
+
next(value) {
|
|
103
|
+
if (done) return;
|
|
104
|
+
for (const o of [...observers]) o.next(value);
|
|
105
|
+
},
|
|
106
|
+
error(err) {
|
|
107
|
+
if (done) return;
|
|
108
|
+
done = true;
|
|
109
|
+
for (const o of [...observers]) o.error?.(err);
|
|
110
|
+
observers.clear();
|
|
111
|
+
},
|
|
112
|
+
complete() {
|
|
113
|
+
if (done) return;
|
|
114
|
+
done = true;
|
|
115
|
+
for (const o of [...observers]) o.complete?.();
|
|
116
|
+
observers.clear();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isStream<T>(value: unknown): value is Stream<T> {
|
|
122
|
+
return typeof value === 'object' && value !== null && 'subscribe' in value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function toStream<T>(source: StreamSource<T>): Stream<T> {
|
|
126
|
+
if (typeof source === 'function') return fromAsyncIterable(source());
|
|
127
|
+
if (isStream<T>(source)) return source;
|
|
128
|
+
if (Symbol.asyncIterator in (source as object)) {
|
|
129
|
+
return fromAsyncIterable(source as AsyncIterable<T>);
|
|
130
|
+
}
|
|
131
|
+
return fromIterable(source as Iterable<T>);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ------------------------------------------------------------- operators
|
|
135
|
+
|
|
136
|
+
export function mapStream<T, U>(source: Stream<T>, fn: (value: T) => U): Stream<U> {
|
|
137
|
+
return createStream<U>((observer) => {
|
|
138
|
+
const sub = source.subscribe({
|
|
139
|
+
next: (v) => observer.next(fn(v)),
|
|
140
|
+
error: (e) => observer.error?.(e),
|
|
141
|
+
complete: () => observer.complete?.(),
|
|
142
|
+
});
|
|
143
|
+
return () => sub.dispose();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function filterStream<T>(source: Stream<T>, pred: (value: T) => boolean): Stream<T> {
|
|
148
|
+
return createStream<T>((observer) => {
|
|
149
|
+
const sub = source.subscribe({
|
|
150
|
+
next: (v) => { if (pred(v)) observer.next(v); },
|
|
151
|
+
error: (e) => observer.error?.(e),
|
|
152
|
+
complete: () => observer.complete?.(),
|
|
153
|
+
});
|
|
154
|
+
return () => sub.dispose();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Split incoming chunks on newlines. What a log stream almost always wants. */
|
|
159
|
+
export function lines(source: Stream<string>): Stream<string> {
|
|
160
|
+
return createStream<string>((observer) => {
|
|
161
|
+
let carry = '';
|
|
162
|
+
const sub = source.subscribe({
|
|
163
|
+
next(chunk) {
|
|
164
|
+
carry += chunk;
|
|
165
|
+
const parts = carry.split('\n');
|
|
166
|
+
carry = parts.pop() ?? '';
|
|
167
|
+
for (const line of parts) observer.next(line);
|
|
168
|
+
},
|
|
169
|
+
error: (e) => observer.error?.(e),
|
|
170
|
+
complete() {
|
|
171
|
+
if (carry !== '') observer.next(carry);
|
|
172
|
+
observer.complete?.();
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return () => sub.dispose();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Emit at most one value per `ms`, keeping the latest. */
|
|
180
|
+
export function throttleStream<T>(source: Stream<T>, ms: number): Stream<T> {
|
|
181
|
+
return createStream<T>((observer) => {
|
|
182
|
+
let pending: T | undefined;
|
|
183
|
+
let has = false;
|
|
184
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
185
|
+
|
|
186
|
+
const sub = source.subscribe({
|
|
187
|
+
next(value) {
|
|
188
|
+
pending = value;
|
|
189
|
+
has = true;
|
|
190
|
+
if (timer) return;
|
|
191
|
+
timer = setInterval(() => {
|
|
192
|
+
if (has) {
|
|
193
|
+
observer.next(pending as T);
|
|
194
|
+
has = false;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (timer) clearInterval(timer);
|
|
198
|
+
timer = null;
|
|
199
|
+
}, ms);
|
|
200
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
201
|
+
observer.next(pending);
|
|
202
|
+
has = false;
|
|
203
|
+
},
|
|
204
|
+
error: (e) => observer.error?.(e),
|
|
205
|
+
complete: () => observer.complete?.(),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return () => {
|
|
209
|
+
if (timer) clearInterval(timer);
|
|
210
|
+
sub.dispose();
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
package/src/util/text.ts
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import type { TextWrap } from '../types/style.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Terminal text measurement.
|
|
5
|
+
*
|
|
6
|
+
* Everything the layout engine knows about size comes from here, so the rules
|
|
7
|
+
* are worth stating: a cell holds one grapheme cluster, a grapheme is 0, 1 or
|
|
8
|
+
* 2 cells wide, and a string's width is the sum - never its `.length`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const segmenter =
|
|
12
|
+
typeof Intl !== 'undefined' && 'Segmenter' in Intl
|
|
13
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
14
|
+
: null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* True for a string that is entirely printable ASCII.
|
|
18
|
+
*
|
|
19
|
+
* Worth asking, because the answer is yes for very nearly all of it: source
|
|
20
|
+
* code, file names, labels, key hints. Every character is then one grapheme
|
|
21
|
+
* one cell wide, and none of the Unicode machinery below has anything to do.
|
|
22
|
+
*/
|
|
23
|
+
function isAscii(text: string): boolean {
|
|
24
|
+
for (let i = 0; i < text.length; i++) {
|
|
25
|
+
const c = text.charCodeAt(i);
|
|
26
|
+
if (c < 0x20 || c > 0x7e) return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Split into grapheme clusters. Falls back to code points without Intl.
|
|
33
|
+
*
|
|
34
|
+
* The ASCII path skips `Intl.Segmenter` entirely. It is not a micro-tweak:
|
|
35
|
+
* painting walks the graphemes of every string on screen every frame, and
|
|
36
|
+
* segmentation was measuring at about a sixth of the frame in a list of plain
|
|
37
|
+
* ASCII rows - all of it spent proving that `r`, `o` and `w` are one character
|
|
38
|
+
* each.
|
|
39
|
+
*/
|
|
40
|
+
export function graphemes(text: string): string[] {
|
|
41
|
+
if (text === '') return [];
|
|
42
|
+
if (isAscii(text)) return text.split('');
|
|
43
|
+
if (segmenter) {
|
|
44
|
+
const out: string[] = [];
|
|
45
|
+
for (const { segment } of segmenter.segment(text)) out.push(segment);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
return Array.from(text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Zero-width: combining marks, variation selectors, ZWJ, control chars.
|
|
52
|
+
function isZeroWidth(cp: number): boolean {
|
|
53
|
+
return (
|
|
54
|
+
cp === 0x200b || cp === 0x200c || cp === 0x200d || cp === 0xfeff ||
|
|
55
|
+
(cp >= 0x0300 && cp <= 0x036f) ||
|
|
56
|
+
(cp >= 0x0483 && cp <= 0x0489) ||
|
|
57
|
+
(cp >= 0x0591 && cp <= 0x05bd) ||
|
|
58
|
+
(cp >= 0x0610 && cp <= 0x061a) ||
|
|
59
|
+
(cp >= 0x064b && cp <= 0x065f) ||
|
|
60
|
+
(cp >= 0x0e31 && cp <= 0x0e3a) ||
|
|
61
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
62
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) ||
|
|
63
|
+
(cp >= 0x20d0 && cp <= 0x20f0) ||
|
|
64
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) ||
|
|
65
|
+
(cp >= 0xfe20 && cp <= 0xfe2f) ||
|
|
66
|
+
(cp >= 0xe0100 && cp <= 0xe01ef)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// East Asian Wide + Fullwidth, and the emoji ranges that render double-width.
|
|
71
|
+
function isWide(cp: number): boolean {
|
|
72
|
+
return (
|
|
73
|
+
(cp >= 0x1100 && cp <= 0x115f) ||
|
|
74
|
+
(cp >= 0x2e80 && cp <= 0x303e) ||
|
|
75
|
+
(cp >= 0x3041 && cp <= 0x33ff) ||
|
|
76
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
77
|
+
(cp >= 0x4e00 && cp <= 0x9fff) ||
|
|
78
|
+
(cp >= 0xa000 && cp <= 0xa4cf) ||
|
|
79
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
80
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
81
|
+
(cp >= 0xfe10 && cp <= 0xfe19) ||
|
|
82
|
+
(cp >= 0xfe30 && cp <= 0xfe6f) ||
|
|
83
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
84
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
85
|
+
// The pictograph blocks are *mostly* wide, and the exceptions matter.
|
|
86
|
+
// `1F300..1F64F` as one range called U+1F5C0 FOLDER two cells; Unicode
|
|
87
|
+
// says East Asian Width Neutral, and a terminal following wcwidth draws
|
|
88
|
+
// it in one. Those blocks are full of such symbols - the whole
|
|
89
|
+
// `1F5A5..1F5FA` run of them - and a glyph measured as two and drawn as
|
|
90
|
+
// one leaves the rest of the row shifted by a column.
|
|
91
|
+
//
|
|
92
|
+
// Generated from EastAsianWidth (Unicode 15.1), W and F only. A symbol
|
|
93
|
+
// that takes emoji presentation is handled by the VS16 rule below, which
|
|
94
|
+
// is the thing that actually makes one of these wide when it is wide.
|
|
95
|
+
(cp >= 0x1f300 && cp <= 0x1f320) ||
|
|
96
|
+
(cp >= 0x1f32d && cp <= 0x1f335) ||
|
|
97
|
+
(cp >= 0x1f337 && cp <= 0x1f37c) ||
|
|
98
|
+
(cp >= 0x1f37e && cp <= 0x1f393) ||
|
|
99
|
+
(cp >= 0x1f3a0 && cp <= 0x1f3ca) ||
|
|
100
|
+
(cp >= 0x1f3cf && cp <= 0x1f3d3) ||
|
|
101
|
+
(cp >= 0x1f3e0 && cp <= 0x1f3f0) ||
|
|
102
|
+
cp === 0x1f3f4 ||
|
|
103
|
+
(cp >= 0x1f3f8 && cp <= 0x1f43e) ||
|
|
104
|
+
cp === 0x1f440 ||
|
|
105
|
+
(cp >= 0x1f442 && cp <= 0x1f4fc) ||
|
|
106
|
+
(cp >= 0x1f4ff && cp <= 0x1f53d) ||
|
|
107
|
+
(cp >= 0x1f54b && cp <= 0x1f54e) ||
|
|
108
|
+
(cp >= 0x1f550 && cp <= 0x1f567) ||
|
|
109
|
+
cp === 0x1f57a ||
|
|
110
|
+
(cp >= 0x1f595 && cp <= 0x1f596) ||
|
|
111
|
+
cp === 0x1f5a4 ||
|
|
112
|
+
(cp >= 0x1f5fb && cp <= 0x1f64f) ||
|
|
113
|
+
(cp >= 0x1f90c && cp <= 0x1f93a) ||
|
|
114
|
+
(cp >= 0x1f93c && cp <= 0x1f945) ||
|
|
115
|
+
(cp >= 0x1f947 && cp <= 0x1f9ff) ||
|
|
116
|
+
(cp >= 0x1fa70 && cp <= 0x1fa7c) ||
|
|
117
|
+
(cp >= 0x1fa80 && cp <= 0x1fa88) ||
|
|
118
|
+
(cp >= 0x1fa90 && cp <= 0x1fabd) ||
|
|
119
|
+
(cp >= 0x1fabf && cp <= 0x1fac5) ||
|
|
120
|
+
(cp >= 0x1face && cp <= 0x1fadb) ||
|
|
121
|
+
(cp >= 0x1fae0 && cp <= 0x1fae8) ||
|
|
122
|
+
(cp >= 0x1faf0 && cp <= 0x1faf8) ||
|
|
123
|
+
(cp >= 0x20000 && cp <= 0x3fffd)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Width of one grapheme cluster in cells. A cluster is measured by its base
|
|
129
|
+
* character; an emoji followed by VS16 counts as wide.
|
|
130
|
+
*/
|
|
131
|
+
export function graphemeWidth(cluster: string): number {
|
|
132
|
+
if (cluster === '') return 0;
|
|
133
|
+
/*
|
|
134
|
+
* One printable ASCII character is one cell, and that is the overwhelming
|
|
135
|
+
* majority of every cluster this is ever asked about.
|
|
136
|
+
*
|
|
137
|
+
* The general path below builds two arrays - `Array.from` then `.map` - for
|
|
138
|
+
* every cluster, which is two allocations per cell per frame. Answering the
|
|
139
|
+
* common case from the character code costs nothing and allocates nothing.
|
|
140
|
+
*/
|
|
141
|
+
if (cluster.length === 1) {
|
|
142
|
+
const only = cluster.charCodeAt(0);
|
|
143
|
+
if (only >= 0x20 && only <= 0x7e) return 1;
|
|
144
|
+
}
|
|
145
|
+
const cps = Array.from(cluster).map((c) => c.codePointAt(0) as number);
|
|
146
|
+
const base = cps[0] as number;
|
|
147
|
+
if (base < 0x20 || base === 0x7f) return 0;
|
|
148
|
+
if (isZeroWidth(base)) return 0;
|
|
149
|
+
// VS16 promotes a text-presentation symbol to emoji presentation.
|
|
150
|
+
if (cps.includes(0xfe0f)) return 2;
|
|
151
|
+
if (isWide(base)) return 2;
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Width of a string in terminal cells. */
|
|
156
|
+
export function stringWidth(text: string): number {
|
|
157
|
+
if (text === '') return 0;
|
|
158
|
+
// Fast path: pure ASCII printable.
|
|
159
|
+
if (isAscii(text)) return text.length;
|
|
160
|
+
|
|
161
|
+
let w = 0;
|
|
162
|
+
for (const g of graphemes(text)) w += graphemeWidth(g);
|
|
163
|
+
return w;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Take at most `width` cells from the front. Never splits a wide grapheme. */
|
|
167
|
+
export function sliceByWidth(text: string, width: number): string {
|
|
168
|
+
if (width <= 0) return '';
|
|
169
|
+
if (stringWidth(text) <= width) return text;
|
|
170
|
+
let out = '';
|
|
171
|
+
let w = 0;
|
|
172
|
+
for (const g of graphemes(text)) {
|
|
173
|
+
const gw = graphemeWidth(g);
|
|
174
|
+
if (w + gw > width) break;
|
|
175
|
+
out += g;
|
|
176
|
+
w += gw;
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The slice of `text` covering display columns `[start, start + width)`.
|
|
183
|
+
*
|
|
184
|
+
* Cutting by index is wrong the moment a line contains a wide character: the
|
|
185
|
+
* horizontal scroll would drift against the gutter, one cell per CJK glyph.
|
|
186
|
+
* A wide grapheme straddling either edge is dropped rather than split, because
|
|
187
|
+
* half of one cannot be drawn.
|
|
188
|
+
*/
|
|
189
|
+
export function sliceColumns(text: string, start: number, width: number): string {
|
|
190
|
+
if (width <= 0) return '';
|
|
191
|
+
const end = start + width;
|
|
192
|
+
let column = 0;
|
|
193
|
+
let out = '';
|
|
194
|
+
|
|
195
|
+
for (const g of graphemes(text)) {
|
|
196
|
+
const gw = graphemeWidth(g);
|
|
197
|
+
if (gw === 0) {
|
|
198
|
+
// A combining mark belongs to the grapheme before it, if that survived.
|
|
199
|
+
if (out !== '') out += g;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const next = column + gw;
|
|
204
|
+
if (next <= start) {
|
|
205
|
+
column = next;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (column >= end) break;
|
|
209
|
+
|
|
210
|
+
if (column >= start && next <= end) {
|
|
211
|
+
out += g;
|
|
212
|
+
} else {
|
|
213
|
+
// A wide grapheme half inside the window becomes blanks for the half
|
|
214
|
+
// that shows. Splitting it is impossible and dropping it silently would
|
|
215
|
+
// slide the rest of the line one cell against the gutter.
|
|
216
|
+
out += ' '.repeat(Math.min(next, end) - Math.max(column, start));
|
|
217
|
+
}
|
|
218
|
+
column = next;
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Replace tabs with spaces to the next tab stop.
|
|
225
|
+
*
|
|
226
|
+
* A terminal cell is one column, so a raw tab is one blank cell and every
|
|
227
|
+
* indented line lands in a different place than it does in an editor. Doing
|
|
228
|
+
* this before highlighting keeps token offsets and screen columns the same
|
|
229
|
+
* number.
|
|
230
|
+
*/
|
|
231
|
+
export function expandTabs(text: string, tabWidth = 4): string {
|
|
232
|
+
if (!text.includes('\t')) return text;
|
|
233
|
+
const width = Math.max(1, tabWidth);
|
|
234
|
+
|
|
235
|
+
return text
|
|
236
|
+
.split('\n')
|
|
237
|
+
.map((line) => {
|
|
238
|
+
if (!line.includes('\t')) return line;
|
|
239
|
+
let out = '';
|
|
240
|
+
let column = 0;
|
|
241
|
+
for (const g of graphemes(line)) {
|
|
242
|
+
if (g === '\t') {
|
|
243
|
+
const advance = width - (column % width);
|
|
244
|
+
out += ' '.repeat(advance);
|
|
245
|
+
column += advance;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
out += g;
|
|
249
|
+
column += graphemeWidth(g);
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
})
|
|
253
|
+
.join('\n');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export type TruncateSide = 'end' | 'start' | 'middle';
|
|
257
|
+
|
|
258
|
+
/** Fit into `width` cells, marking the cut with `ellipsis`. */
|
|
259
|
+
export function truncate(
|
|
260
|
+
text: string,
|
|
261
|
+
width: number,
|
|
262
|
+
ellipsis = '…',
|
|
263
|
+
side: TruncateSide = 'end',
|
|
264
|
+
): string {
|
|
265
|
+
const total = stringWidth(text);
|
|
266
|
+
if (total <= width) return text;
|
|
267
|
+
if (width <= 0) return '';
|
|
268
|
+
const ew = stringWidth(ellipsis);
|
|
269
|
+
if (width <= ew) return sliceByWidth(ellipsis, width);
|
|
270
|
+
|
|
271
|
+
// A space either side of the ellipsis is a typographic mistake, not a cell
|
|
272
|
+
// of content, so the cut is tidied before the mark goes on. It also means a
|
|
273
|
+
// truncated string can come back narrower than `width`, which is correct:
|
|
274
|
+
// `width` is a limit, and nothing here is padding.
|
|
275
|
+
const keep = width - ew;
|
|
276
|
+
if (side === 'end') return sliceByWidth(text, keep).trimEnd() + ellipsis;
|
|
277
|
+
|
|
278
|
+
const gs = graphemes(text);
|
|
279
|
+
if (side === 'start') {
|
|
280
|
+
let w = 0;
|
|
281
|
+
let i = gs.length;
|
|
282
|
+
while (i > 0 && w + graphemeWidth(gs[i - 1] as string) <= keep) {
|
|
283
|
+
i--;
|
|
284
|
+
w += graphemeWidth(gs[i] as string);
|
|
285
|
+
}
|
|
286
|
+
return ellipsis + gs.slice(i).join('').trimStart();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const left = Math.ceil(keep / 2);
|
|
290
|
+
const right = keep - left;
|
|
291
|
+
let w = 0;
|
|
292
|
+
let j = gs.length;
|
|
293
|
+
while (j > 0 && w + graphemeWidth(gs[j - 1] as string) <= right) {
|
|
294
|
+
j--;
|
|
295
|
+
w += graphemeWidth(gs[j] as string);
|
|
296
|
+
}
|
|
297
|
+
return sliceByWidth(text, left).trimEnd() + ellipsis + gs.slice(j).join('').trimStart();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Pad to exactly `width` cells. Over-long input is returned unchanged. */
|
|
301
|
+
export function padTo(
|
|
302
|
+
text: string,
|
|
303
|
+
width: number,
|
|
304
|
+
align: 'left' | 'center' | 'right' = 'left',
|
|
305
|
+
fill = ' ',
|
|
306
|
+
): string {
|
|
307
|
+
const w = stringWidth(text);
|
|
308
|
+
if (w >= width) return text;
|
|
309
|
+
const pad = width - w;
|
|
310
|
+
if (align === 'left') return text + fill.repeat(pad);
|
|
311
|
+
if (align === 'right') return fill.repeat(pad) + text;
|
|
312
|
+
const left = Math.floor(pad / 2);
|
|
313
|
+
return fill.repeat(left) + text + fill.repeat(pad - left);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Fit to exactly `width`: truncate when long, pad when short. */
|
|
317
|
+
export function fitTo(
|
|
318
|
+
text: string,
|
|
319
|
+
width: number,
|
|
320
|
+
align: 'left' | 'center' | 'right' = 'left',
|
|
321
|
+
ellipsis = '…',
|
|
322
|
+
): string {
|
|
323
|
+
return padTo(truncate(text, width, ellipsis), width, align);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export type WrapMode = 'none' | 'word' | 'char';
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The end a `TextWrap` cuts at, or `undefined` when it wraps instead.
|
|
330
|
+
*
|
|
331
|
+
* The wrapping modes and the truncating ones live in one union because they
|
|
332
|
+
* answer one question - what happens at the edge - and a component that had to
|
|
333
|
+
* take both a `wrap` and a `truncate` prop could be handed the two answers at
|
|
334
|
+
* once. These two helpers are how the union is taken apart again.
|
|
335
|
+
*/
|
|
336
|
+
export function truncateSideOf(wrap: TextWrap | undefined): TruncateSide | undefined {
|
|
337
|
+
switch (wrap) {
|
|
338
|
+
case 'truncate': case 'truncate-end': return 'end';
|
|
339
|
+
case 'truncate-start': return 'start';
|
|
340
|
+
case 'truncate-middle': return 'middle';
|
|
341
|
+
default: return undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The mode `wrapText` should run in. A truncating text does not wrap at all. */
|
|
346
|
+
export function wrapModeOf(wrap: TextWrap | undefined): WrapMode {
|
|
347
|
+
return wrap === 'word' || wrap === 'char' ? wrap : 'none';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Break into lines of at most `width` cells. Honours existing newlines. */
|
|
351
|
+
export function wrapText(text: string, width: number, mode: WrapMode = 'word'): string[] {
|
|
352
|
+
if (width <= 0) return [];
|
|
353
|
+
const paragraphs = text.split('\n');
|
|
354
|
+
if (mode === 'none') return paragraphs;
|
|
355
|
+
|
|
356
|
+
const out: string[] = [];
|
|
357
|
+
for (const para of paragraphs) {
|
|
358
|
+
if (para === '') { out.push(''); continue; }
|
|
359
|
+
if (stringWidth(para) <= width) { out.push(para); continue; }
|
|
360
|
+
|
|
361
|
+
if (mode === 'char') {
|
|
362
|
+
let rest = para;
|
|
363
|
+
while (stringWidth(rest) > width) {
|
|
364
|
+
const head = sliceByWidth(rest, width);
|
|
365
|
+
out.push(head);
|
|
366
|
+
rest = rest.slice(head.length);
|
|
367
|
+
}
|
|
368
|
+
if (rest !== '') out.push(rest);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// word wrap; a word longer than the line falls back to char wrap
|
|
373
|
+
let line = '';
|
|
374
|
+
let lineW = 0;
|
|
375
|
+
for (const word of para.split(/(\s+)/)) {
|
|
376
|
+
if (word === '') continue;
|
|
377
|
+
const ww = stringWidth(word);
|
|
378
|
+
const isSpace = /^\s+$/.test(word);
|
|
379
|
+
|
|
380
|
+
if (lineW + ww <= width) {
|
|
381
|
+
line += word;
|
|
382
|
+
lineW += ww;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (isSpace) {
|
|
386
|
+
out.push(line.trimEnd());
|
|
387
|
+
line = '';
|
|
388
|
+
lineW = 0;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (line !== '') { out.push(line.trimEnd()); line = ''; lineW = 0; }
|
|
392
|
+
if (ww <= width) { line = word; lineW = ww; continue; }
|
|
393
|
+
|
|
394
|
+
let rest = word;
|
|
395
|
+
while (stringWidth(rest) > width) {
|
|
396
|
+
const head = sliceByWidth(rest, width);
|
|
397
|
+
out.push(head);
|
|
398
|
+
rest = rest.slice(head.length);
|
|
399
|
+
}
|
|
400
|
+
line = rest;
|
|
401
|
+
lineW = stringWidth(rest);
|
|
402
|
+
}
|
|
403
|
+
if (line !== '') out.push(line.trimEnd());
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// CSI / OSC / SS2 / SS3 and friends. Input sanitation only, never output.
|
|
409
|
+
const ANSI_RE =
|
|
410
|
+
// eslint-disable-next-line no-control-regex
|
|
411
|
+
/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1B]*(?:\x07|\x1B\\))/g;
|
|
412
|
+
|
|
413
|
+
export function stripAnsi(text: string): string {
|
|
414
|
+
return text.replace(ANSI_RE, '');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Remove control characters that would corrupt the frame. Keeps newlines. */
|
|
418
|
+
export function sanitize(text: string): string {
|
|
419
|
+
// eslint-disable-next-line no-control-regex
|
|
420
|
+
return stripAnsi(text).replace(/[\x00-\x08\x0B-\x1F\x7F]/g, '');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Repeat a grapheme to exactly `width` cells (wide chars land short). */
|
|
424
|
+
export function repeatToWidth(char: string, width: number): string {
|
|
425
|
+
if (width <= 0) return '';
|
|
426
|
+
const w = graphemeWidth(char) || 1;
|
|
427
|
+
return char.repeat(Math.max(0, Math.floor(width / w)));
|
|
428
|
+
}
|