@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,144 @@
|
|
|
1
|
+
import type { AnimationDriver, Easing, TickerOptions, TweenOptions } from '../types/animation.js';
|
|
2
|
+
import type { Disposable } from '../types/disposable.js';
|
|
3
|
+
import { toDisposable } from '../util/disposable.js';
|
|
4
|
+
|
|
5
|
+
const EASINGS: Record<string, (t: number) => number> = {
|
|
6
|
+
linear: (t) => t,
|
|
7
|
+
easeIn: (t) => t * t,
|
|
8
|
+
easeOut: (t) => t * (2 - t),
|
|
9
|
+
easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function easingFn(easing: Easing | undefined): (t: number) => number {
|
|
13
|
+
if (typeof easing === 'function') return easing;
|
|
14
|
+
return EASINGS[easing ?? 'easeOut'] as (t: number) => number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Ticker {
|
|
18
|
+
fps: number;
|
|
19
|
+
onTick(frame: number, elapsedMs: number): void;
|
|
20
|
+
frame: number;
|
|
21
|
+
accumulated: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One driver for everything that moves.
|
|
26
|
+
*
|
|
27
|
+
* Spinners, progress, value transitions and appearing elements all run off the
|
|
28
|
+
* same clock, so `enabled = false` is a single flag that stops all of it, and
|
|
29
|
+
* a slow link can drop the frame rate for all of it at once. The clock can
|
|
30
|
+
* also be driven by hand, which is how the test harness advances time.
|
|
31
|
+
*/
|
|
32
|
+
export class Animation implements AnimationDriver {
|
|
33
|
+
enabled = true;
|
|
34
|
+
maxFps = 30;
|
|
35
|
+
|
|
36
|
+
private tickers = new Set<Ticker>();
|
|
37
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
38
|
+
private lastTime = 0;
|
|
39
|
+
private manual: boolean;
|
|
40
|
+
|
|
41
|
+
constructor(options: { enabled?: boolean; maxFps?: number; manual?: boolean } = {}) {
|
|
42
|
+
this.enabled = options.enabled ?? true;
|
|
43
|
+
this.maxFps = options.maxFps ?? 30;
|
|
44
|
+
this.manual = options.manual ?? false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get disabled(): boolean {
|
|
48
|
+
return !this.enabled;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ticker(options: TickerOptions): Disposable {
|
|
52
|
+
const entry: Ticker = {
|
|
53
|
+
fps: Math.min(options.fps ?? this.maxFps, this.maxFps),
|
|
54
|
+
onTick: options.onTick,
|
|
55
|
+
frame: 0,
|
|
56
|
+
accumulated: 0,
|
|
57
|
+
};
|
|
58
|
+
this.tickers.add(entry);
|
|
59
|
+
this.ensureRunning();
|
|
60
|
+
return toDisposable(() => {
|
|
61
|
+
this.tickers.delete(entry);
|
|
62
|
+
this.stopIfIdle();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
tween(options: TweenOptions): Disposable {
|
|
67
|
+
const { from, to, durationMs, onUpdate, onComplete } = options;
|
|
68
|
+
const ease = easingFn(options.easing);
|
|
69
|
+
|
|
70
|
+
if (!this.enabled || durationMs <= 0) {
|
|
71
|
+
onUpdate(to);
|
|
72
|
+
onComplete?.();
|
|
73
|
+
return toDisposable(() => {});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let elapsed = 0;
|
|
77
|
+
const ticker = this.ticker({
|
|
78
|
+
fps: this.maxFps,
|
|
79
|
+
onTick: (_frame, delta) => {
|
|
80
|
+
elapsed += delta;
|
|
81
|
+
const t = Math.min(1, elapsed / durationMs);
|
|
82
|
+
onUpdate(from + (to - from) * ease(t));
|
|
83
|
+
if (t >= 1) {
|
|
84
|
+
ticker.dispose();
|
|
85
|
+
onComplete?.();
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
return ticker;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Advance by hand. The test harness and the static renderer use this. */
|
|
93
|
+
advance(ms: number): void {
|
|
94
|
+
this.tick(ms);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private tick(delta: number): void {
|
|
98
|
+
if (!this.enabled) return;
|
|
99
|
+
for (const ticker of [...this.tickers]) {
|
|
100
|
+
const interval = 1000 / Math.max(1, ticker.fps);
|
|
101
|
+
ticker.accumulated += delta;
|
|
102
|
+
if (ticker.accumulated < interval) continue;
|
|
103
|
+
|
|
104
|
+
const frames = Math.floor(ticker.accumulated / interval);
|
|
105
|
+
ticker.accumulated -= frames * interval;
|
|
106
|
+
ticker.frame += frames;
|
|
107
|
+
try {
|
|
108
|
+
ticker.onTick(ticker.frame, frames * interval);
|
|
109
|
+
} catch {
|
|
110
|
+
// A failing ticker must not take the frame loop down with it.
|
|
111
|
+
this.tickers.delete(ticker);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private ensureRunning(): void {
|
|
117
|
+
if (this.manual || this.timer || !this.enabled || this.tickers.size === 0) return;
|
|
118
|
+
const interval = Math.max(1000 / this.maxFps, 16);
|
|
119
|
+
this.lastTime = Date.now();
|
|
120
|
+
this.timer = setInterval(() => {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const delta = now - this.lastTime;
|
|
123
|
+
this.lastTime = now;
|
|
124
|
+
this.tick(delta);
|
|
125
|
+
}, interval);
|
|
126
|
+
this.timer.unref?.();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private stopIfIdle(): void {
|
|
130
|
+
if (this.tickers.size > 0 || !this.timer) return;
|
|
131
|
+
clearInterval(this.timer);
|
|
132
|
+
this.timer = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
dispose(): void {
|
|
136
|
+
if (this.timer) clearInterval(this.timer);
|
|
137
|
+
this.timer = null;
|
|
138
|
+
this.tickers.clear();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createAnimation(options?: ConstructorParameters<typeof Animation>[0]): Animation {
|
|
143
|
+
return new Animation(options);
|
|
144
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BindingPath } from '../types/graph.js';
|
|
2
|
+
import type { ReactiveStore } from '../types/store.js';
|
|
3
|
+
import type { TerminalAdapter } from '../types/terminal.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The clipboard.
|
|
7
|
+
*
|
|
8
|
+
* A terminal can be *told* what to put on the system clipboard - OSC 52 - and
|
|
9
|
+
* cannot be asked what is on it. The read half of that sequence is a way for
|
|
10
|
+
* anything with a pty to read whatever you last copied, so terminals worth
|
|
11
|
+
* using refuse it, and the ones that answer are the ones you would not want
|
|
12
|
+
* to. So a copy goes two places: out to the system clipboard, where the rest
|
|
13
|
+
* of the desktop can reach it, and into the store, which is the half a paste
|
|
14
|
+
* can read back.
|
|
15
|
+
*
|
|
16
|
+
* That makes the store the clipboard as far as this application is concerned.
|
|
17
|
+
* It is in the session scope because a clipboard is not worth persisting and
|
|
18
|
+
* is worth surviving a screen change.
|
|
19
|
+
*/
|
|
20
|
+
export const CLIPBOARD_PATH = '$/session/clipboard' as BindingPath;
|
|
21
|
+
|
|
22
|
+
export function readClipboard(store: ReactiveStore): string {
|
|
23
|
+
return store.get<string>(CLIPBOARD_PATH) ?? '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Put text on the clipboard.
|
|
28
|
+
*
|
|
29
|
+
* The terminal is optional because the store half is the half that has to
|
|
30
|
+
* work: a session with no OSC 52 - a plain console, a pipe, the test harness -
|
|
31
|
+
* still copies and pastes within the application.
|
|
32
|
+
*/
|
|
33
|
+
export function writeClipboard(
|
|
34
|
+
store: ReactiveStore,
|
|
35
|
+
text: string,
|
|
36
|
+
terminal?: TerminalAdapter | null,
|
|
37
|
+
): void {
|
|
38
|
+
store.set(CLIPBOARD_PATH, text);
|
|
39
|
+
terminal?.writeClipboard?.(text);
|
|
40
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommandContext, CommandDefinition, CommandRegistry, CommandScope,
|
|
3
|
+
} from '../types/command.js';
|
|
4
|
+
import type { Disposable } from '../types/disposable.js';
|
|
5
|
+
import type { ReactiveStore } from '../types/store.js';
|
|
6
|
+
import type { WhenEngine } from '../types/when.js';
|
|
7
|
+
import type { TextUIApp } from '../types/app.js';
|
|
8
|
+
import { toDisposable } from '../util/disposable.js';
|
|
9
|
+
|
|
10
|
+
const SCOPE_ORDER: CommandScope[] = ['component', 'region', 'screen', 'app'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Commands are the only way an action should be spelled.
|
|
14
|
+
*
|
|
15
|
+
* A button that calls an API directly and a palette entry that calls the same
|
|
16
|
+
* API are two implementations that will drift; a button that runs a registered
|
|
17
|
+
* command cannot drift from the command. That is also why resolution walks
|
|
18
|
+
* from the most specific scope outward - `table.search` means whichever table
|
|
19
|
+
* is focused, without every table inventing its own id.
|
|
20
|
+
*/
|
|
21
|
+
export class Commands implements CommandRegistry {
|
|
22
|
+
private byId = new Map<string, CommandDefinition[]>();
|
|
23
|
+
/** Scope instance ids that are currently active, innermost first. */
|
|
24
|
+
private activeScopes: string[] = [];
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private deps: {
|
|
28
|
+
store: ReactiveStore;
|
|
29
|
+
when: WhenEngine;
|
|
30
|
+
app(): TextUIApp | null;
|
|
31
|
+
onError(err: unknown, context: string): void;
|
|
32
|
+
},
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
setActiveScopes(ids: string[]): void {
|
|
36
|
+
this.activeScopes = ids;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
register(def: CommandDefinition): Disposable {
|
|
40
|
+
const list = this.byId.get(def.id) ?? [];
|
|
41
|
+
list.push(def);
|
|
42
|
+
this.byId.set(def.id, list);
|
|
43
|
+
|
|
44
|
+
return toDisposable(() => {
|
|
45
|
+
const current = this.byId.get(def.id);
|
|
46
|
+
if (!current) return;
|
|
47
|
+
const next = current.filter((d) => d !== def);
|
|
48
|
+
if (next.length === 0) this.byId.delete(def.id);
|
|
49
|
+
else this.byId.set(def.id, next);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
unregister(id: string, scopeId?: string): void {
|
|
54
|
+
if (scopeId === undefined) {
|
|
55
|
+
this.byId.delete(id);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const next = (this.byId.get(id) ?? []).filter((d) => d.scopeId !== scopeId);
|
|
59
|
+
if (next.length === 0) this.byId.delete(id);
|
|
60
|
+
else this.byId.set(id, next);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get(id: string): CommandDefinition | undefined {
|
|
64
|
+
return this.resolve(id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
list(options: { scope?: CommandScope; slot?: string; enabledOnly?: boolean } = {}): CommandDefinition[] {
|
|
68
|
+
const out: CommandDefinition[] = [];
|
|
69
|
+
for (const defs of this.byId.values()) {
|
|
70
|
+
for (const def of defs) {
|
|
71
|
+
if (options.scope && (def.scope ?? 'app') !== options.scope) continue;
|
|
72
|
+
if (options.slot && !(def.slots ?? []).includes(options.slot)) continue;
|
|
73
|
+
if (options.enabledOnly && !this.deps.when.evaluate(def.when)) continue;
|
|
74
|
+
out.push(def);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Innermost active scope wins; an app-scoped registration is the floor. */
|
|
81
|
+
resolve(id: string): CommandDefinition | undefined {
|
|
82
|
+
const candidates = (this.byId.get(id) ?? []).filter((def) =>
|
|
83
|
+
this.deps.when.evaluate(def.when),
|
|
84
|
+
);
|
|
85
|
+
if (candidates.length === 0) return undefined;
|
|
86
|
+
if (candidates.length === 1) return candidates[0];
|
|
87
|
+
|
|
88
|
+
for (const scopeId of this.activeScopes) {
|
|
89
|
+
const match = candidates.find((d) => d.scopeId === scopeId);
|
|
90
|
+
if (match) return match;
|
|
91
|
+
}
|
|
92
|
+
for (const scope of SCOPE_ORDER) {
|
|
93
|
+
const match = candidates.find((d) => (d.scope ?? 'app') === scope && !d.scopeId);
|
|
94
|
+
if (match) return match;
|
|
95
|
+
}
|
|
96
|
+
return candidates[candidates.length - 1];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
enabled(id: string): boolean {
|
|
100
|
+
return this.resolve(id) !== undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
isChecked(id: string): boolean | undefined {
|
|
104
|
+
const clause = this.resolve(id)?.checked;
|
|
105
|
+
return clause === undefined ? undefined : this.deps.when.evaluate(clause);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async execute(
|
|
109
|
+
id: string,
|
|
110
|
+
args: Record<string, unknown> = {},
|
|
111
|
+
source: CommandContext['source'] = 'api',
|
|
112
|
+
): Promise<unknown> {
|
|
113
|
+
const def = this.resolve(id);
|
|
114
|
+
if (!def) {
|
|
115
|
+
// Registered but gated is not the same as absent, and it used to be
|
|
116
|
+
// reported as absent: `alt+left` with nothing open said "no command
|
|
117
|
+
// registered as go.previousTab" about a command registered forty lines
|
|
118
|
+
// away, whose `when` simply did not pass.
|
|
119
|
+
//
|
|
120
|
+
// A `when` that does not pass is the command saying "not now", which is
|
|
121
|
+
// what it is for - so this does nothing, and the key does nothing. A
|
|
122
|
+
// typo still throws, because a keybinding pointing at a name that was
|
|
123
|
+
// never registered has to be findable.
|
|
124
|
+
if ((this.byId.get(id) ?? []).length > 0) return undefined;
|
|
125
|
+
throw new Error(`[textui] no command registered as "${id}"`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const missing = (def.args ?? [])
|
|
129
|
+
.filter((a) => a.required && args[a.name] === undefined && a.default === undefined)
|
|
130
|
+
.map((a) => a.name);
|
|
131
|
+
if (missing.length > 0) {
|
|
132
|
+
throw new Error(`[textui] command "${id}" needs: ${missing.join(', ')}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const filled: Record<string, unknown> = { ...args };
|
|
136
|
+
for (const spec of def.args ?? []) {
|
|
137
|
+
if (filled[spec.name] === undefined && spec.default !== undefined) {
|
|
138
|
+
filled[spec.name] = spec.default;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const app = this.deps.app();
|
|
143
|
+
const ctx: CommandContext = {
|
|
144
|
+
app: app as TextUIApp,
|
|
145
|
+
store: this.deps.store,
|
|
146
|
+
scopeId: def.scopeId ?? null,
|
|
147
|
+
source,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Announced, not just run. Which command fired and what asked for it is
|
|
151
|
+
// the first question anyone debugging a keybinding, a menu or a palette
|
|
152
|
+
// has, and reconstructing it from its effects is guesswork.
|
|
153
|
+
app?.events.emit('@/command/run', { id, source, args: filled });
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const result = await def.run(filled, ctx);
|
|
157
|
+
app?.events.emit('@/command/done', { id, source });
|
|
158
|
+
return result;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
app?.events.emit('@/command/error', { id, source, message: String(err) });
|
|
161
|
+
this.deps.onError(err, `command "${id}"`);
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createCommands(deps: ConstructorParameters<typeof Commands>[0]): Commands {
|
|
168
|
+
return new Commands(deps);
|
|
169
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentCategory, ComponentDefinition, ComponentRegistry,
|
|
3
|
+
} from '../types/component-registry.js';
|
|
4
|
+
import type { Disposable } from '../types/disposable.js';
|
|
5
|
+
import { toDisposable, createBag } from '../util/disposable.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Late binding is the mechanism the whole design rests on: a graph names a
|
|
9
|
+
* component, and the name is resolved at mount time. A name that was never
|
|
10
|
+
* registered is a runtime miss rather than a compile error - that is the price
|
|
11
|
+
* of a screen being data, and it is why the miss renders visibly.
|
|
12
|
+
*/
|
|
13
|
+
export class Components implements ComponentRegistry {
|
|
14
|
+
private defs = new Map<string, ComponentDefinition>();
|
|
15
|
+
private resolving = new Map<string, Promise<ComponentDefinition>>();
|
|
16
|
+
|
|
17
|
+
register(def: ComponentDefinition): Disposable {
|
|
18
|
+
this.defs.set(def.component, def);
|
|
19
|
+
return toDisposable(() => {
|
|
20
|
+
if (this.defs.get(def.component) === def) this.defs.delete(def.component);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
registerMany(defs: ComponentDefinition[]): Disposable {
|
|
25
|
+
const bag = createBag();
|
|
26
|
+
for (const def of defs) bag.add(this.register(def));
|
|
27
|
+
return bag;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
unregister(component: string): void {
|
|
31
|
+
this.defs.delete(component);
|
|
32
|
+
this.resolving.delete(component);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get(component: string): ComponentDefinition | undefined {
|
|
36
|
+
return this.defs.get(component);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
has(component: string): boolean {
|
|
40
|
+
return this.defs.has(component);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
list(category?: ComponentCategory): ComponentDefinition[] {
|
|
44
|
+
const all = [...this.defs.values()];
|
|
45
|
+
return category ? all.filter((d) => d.category === category) : all;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async resolve(component: string): Promise<ComponentDefinition> {
|
|
49
|
+
const def = this.defs.get(component);
|
|
50
|
+
if (!def) throw new Error(`[textui] no component registered as "${component}"`);
|
|
51
|
+
if (def.renderer.kind !== 'lazy') return def;
|
|
52
|
+
|
|
53
|
+
const inFlight = this.resolving.get(component);
|
|
54
|
+
if (inFlight) return inFlight;
|
|
55
|
+
|
|
56
|
+
const load = def.renderer.load;
|
|
57
|
+
const promise = Promise.resolve(load()).then((mod) => {
|
|
58
|
+
const render = typeof mod === 'function' ? mod : mod.default;
|
|
59
|
+
const resolved: ComponentDefinition = {
|
|
60
|
+
...def,
|
|
61
|
+
renderer: { kind: 'function', render },
|
|
62
|
+
};
|
|
63
|
+
this.defs.set(component, resolved);
|
|
64
|
+
this.resolving.delete(component);
|
|
65
|
+
return resolved;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.resolving.set(component, promise);
|
|
69
|
+
return promise;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pending(): string[] {
|
|
73
|
+
return [...this.defs.values()]
|
|
74
|
+
.filter((d) => d.renderer.kind === 'lazy')
|
|
75
|
+
.map((d) => d.component);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Which components can display a resource of this kind. The selector is not
|
|
80
|
+
* evaluated here - a caller with a store re-filters through `when`.
|
|
81
|
+
*/
|
|
82
|
+
findOpeners(resourceKind: string): ComponentDefinition[] {
|
|
83
|
+
return [...this.defs.values()]
|
|
84
|
+
.filter((d) => d.opens?.resourceKinds?.some((k) => k === resourceKind || resourceKind.startsWith(k + '.')))
|
|
85
|
+
.sort((a, b) => (b.opens?.priority ?? 0) - (a.opens?.priority ?? 0));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createComponents(): Components {
|
|
90
|
+
return new Components();
|
|
91
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { EventBus } from '../types/store.js';
|
|
2
|
+
import type { EventPath } from '../types/graph.js';
|
|
3
|
+
import type { Disposable } from '../types/disposable.js';
|
|
4
|
+
import { toDisposable } from '../util/disposable.js';
|
|
5
|
+
import { isDescendantKey, matchKey } from '../util/paths.js';
|
|
6
|
+
|
|
7
|
+
interface Listener {
|
|
8
|
+
key: string;
|
|
9
|
+
subtree: boolean;
|
|
10
|
+
wildcard: boolean;
|
|
11
|
+
once: boolean;
|
|
12
|
+
fn: (payload: unknown, path: EventPath) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function eventKey(path: string): string {
|
|
16
|
+
const body = path.startsWith('@/') ? path.slice(2) : path.replace(/^\//, '');
|
|
17
|
+
return body.split('/').filter((s) => s !== '').join('/');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Transient events, addressed like store paths on purpose - `@/dialog/confirm`
|
|
22
|
+
* reads the same way `$/dialog/open` does - but with no value to read back.
|
|
23
|
+
* That difference is the whole reason they are two mechanisms.
|
|
24
|
+
*/
|
|
25
|
+
export class Events implements EventBus {
|
|
26
|
+
private listeners = new Set<Listener>();
|
|
27
|
+
private disposed = false;
|
|
28
|
+
|
|
29
|
+
onError: (err: unknown, context: string) => void = (err, context) => {
|
|
30
|
+
console.error(`[textui/events] ${context}`, err);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
emit(path: EventPath, payload?: unknown): void {
|
|
34
|
+
if (this.disposed) return;
|
|
35
|
+
const key = eventKey(path);
|
|
36
|
+
for (const listener of [...this.listeners]) {
|
|
37
|
+
if (!this.matches(listener, key)) continue;
|
|
38
|
+
if (listener.once) this.listeners.delete(listener);
|
|
39
|
+
try {
|
|
40
|
+
listener.fn(payload, path);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
this.onError(err, `listener of @/${listener.key}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private matches(listener: Listener, key: string): boolean {
|
|
48
|
+
if (listener.wildcard) return matchKey(key, listener.key, listener.subtree);
|
|
49
|
+
if (listener.key === key) return true;
|
|
50
|
+
return listener.subtree && isDescendantKey(key, listener.key);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
on(
|
|
54
|
+
path: EventPath,
|
|
55
|
+
fn: (payload: unknown, path: EventPath) => void,
|
|
56
|
+
options: { subtree?: boolean; once?: boolean } = {},
|
|
57
|
+
): Disposable {
|
|
58
|
+
const key = eventKey(path);
|
|
59
|
+
const listener: Listener = {
|
|
60
|
+
key,
|
|
61
|
+
subtree: options.subtree ?? false,
|
|
62
|
+
wildcard: key.includes('*'),
|
|
63
|
+
once: options.once ?? false,
|
|
64
|
+
fn,
|
|
65
|
+
};
|
|
66
|
+
this.listeners.add(listener);
|
|
67
|
+
return toDisposable(() => this.listeners.delete(listener));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
next(path: EventPath, timeoutMs?: number): Promise<unknown> {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const sub = this.on(path, (payload) => {
|
|
73
|
+
if (timer) clearTimeout(timer);
|
|
74
|
+
resolve(payload);
|
|
75
|
+
}, { once: true });
|
|
76
|
+
|
|
77
|
+
const timer = timeoutMs
|
|
78
|
+
? setTimeout(() => {
|
|
79
|
+
sub.dispose();
|
|
80
|
+
reject(new Error(`timed out waiting for ${path}`));
|
|
81
|
+
}, timeoutMs)
|
|
82
|
+
: null;
|
|
83
|
+
timer?.unref?.();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dispose(): void {
|
|
88
|
+
this.disposed = true;
|
|
89
|
+
this.listeners.clear();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createEvents(): Events {
|
|
94
|
+
return new Events();
|
|
95
|
+
}
|