@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.
Files changed (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/adapters/index.d.ts +12 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +11 -0
  6. package/dist/app/app.d.ts +200 -0
  7. package/dist/app/app.d.ts.map +1 -0
  8. package/dist/app/app.js +980 -0
  9. package/dist/core/animation.d.ts +34 -0
  10. package/dist/core/animation.d.ts.map +1 -0
  11. package/dist/core/animation.js +125 -0
  12. package/dist/core/clipboard.d.ts +29 -0
  13. package/dist/core/clipboard.d.ts.map +1 -0
  14. package/dist/core/clipboard.js +30 -0
  15. package/dist/core/commands.d.ts +42 -0
  16. package/dist/core/commands.d.ts.map +1 -0
  17. package/dist/core/commands.js +146 -0
  18. package/dist/core/components.d.ts +27 -0
  19. package/dist/core/components.d.ts.map +1 -0
  20. package/dist/core/components.js +78 -0
  21. package/dist/core/events.d.ts +23 -0
  22. package/dist/core/events.d.ts.map +1 -0
  23. package/dist/core/events.js +77 -0
  24. package/dist/core/focus.d.ts +69 -0
  25. package/dist/core/focus.d.ts.map +1 -0
  26. package/dist/core/focus.js +336 -0
  27. package/dist/core/i18n.d.ts +27 -0
  28. package/dist/core/i18n.d.ts.map +1 -0
  29. package/dist/core/i18n.js +88 -0
  30. package/dist/core/keybindings.d.ts +53 -0
  31. package/dist/core/keybindings.d.ts.map +1 -0
  32. package/dist/core/keybindings.js +163 -0
  33. package/dist/core/layers.d.ts +28 -0
  34. package/dist/core/layers.d.ts.map +1 -0
  35. package/dist/core/layers.js +84 -0
  36. package/dist/core/manifest.d.ts +22 -0
  37. package/dist/core/manifest.d.ts.map +1 -0
  38. package/dist/core/manifest.js +85 -0
  39. package/dist/core/navigation.d.ts +47 -0
  40. package/dist/core/navigation.d.ts.map +1 -0
  41. package/dist/core/navigation.js +110 -0
  42. package/dist/core/resources.d.ts +115 -0
  43. package/dist/core/resources.d.ts.map +1 -0
  44. package/dist/core/resources.js +321 -0
  45. package/dist/core/services.d.ts +21 -0
  46. package/dist/core/services.d.ts.map +1 -0
  47. package/dist/core/services.js +63 -0
  48. package/dist/core/store.d.ts +60 -0
  49. package/dist/core/store.d.ts.map +1 -0
  50. package/dist/core/store.js +593 -0
  51. package/dist/core/surfaces.d.ts +81 -0
  52. package/dist/core/surfaces.d.ts.map +1 -0
  53. package/dist/core/surfaces.js +237 -0
  54. package/dist/core/syntax.d.ts +49 -0
  55. package/dist/core/syntax.d.ts.map +1 -0
  56. package/dist/core/syntax.js +172 -0
  57. package/dist/core/when.d.ts +15 -0
  58. package/dist/core/when.d.ts.map +1 -0
  59. package/dist/core/when.js +224 -0
  60. package/dist/index.d.ts +49 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +54 -0
  63. package/dist/jsx/factory.d.ts +27 -0
  64. package/dist/jsx/factory.d.ts.map +1 -0
  65. package/dist/jsx/factory.js +102 -0
  66. package/dist/jsx/intrinsics.d.ts +96 -0
  67. package/dist/jsx/intrinsics.d.ts.map +1 -0
  68. package/dist/jsx/intrinsics.js +1 -0
  69. package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
  70. package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
  71. package/dist/jsx/jsx-dev-runtime.js +12 -0
  72. package/dist/jsx/jsx-runtime.d.ts +42 -0
  73. package/dist/jsx/jsx-runtime.d.ts.map +1 -0
  74. package/dist/jsx/jsx-runtime.js +16 -0
  75. package/dist/render/buffer.d.ts +77 -0
  76. package/dist/render/buffer.d.ts.map +1 -0
  77. package/dist/render/buffer.js +275 -0
  78. package/dist/render/color.d.ts +35 -0
  79. package/dist/render/color.d.ts.map +1 -0
  80. package/dist/render/color.js +160 -0
  81. package/dist/render/diff.d.ts +37 -0
  82. package/dist/render/diff.d.ts.map +1 -0
  83. package/dist/render/diff.js +66 -0
  84. package/dist/render/layout.d.ts +72 -0
  85. package/dist/render/layout.d.ts.map +1 -0
  86. package/dist/render/layout.js +633 -0
  87. package/dist/render/static.d.ts +63 -0
  88. package/dist/render/static.d.ts.map +1 -0
  89. package/dist/render/static.js +209 -0
  90. package/dist/runtime/bindings.d.ts +46 -0
  91. package/dist/runtime/bindings.d.ts.map +1 -0
  92. package/dist/runtime/bindings.js +116 -0
  93. package/dist/runtime/hooks.d.ts +284 -0
  94. package/dist/runtime/hooks.d.ts.map +1 -0
  95. package/dist/runtime/hooks.js +846 -0
  96. package/dist/runtime/instance.d.ts +106 -0
  97. package/dist/runtime/instance.d.ts.map +1 -0
  98. package/dist/runtime/instance.js +181 -0
  99. package/dist/runtime/paint.d.ts +27 -0
  100. package/dist/runtime/paint.d.ts.map +1 -0
  101. package/dist/runtime/paint.js +567 -0
  102. package/dist/runtime/reconcile.d.ts +23 -0
  103. package/dist/runtime/reconcile.d.ts.map +1 -0
  104. package/dist/runtime/reconcile.js +260 -0
  105. package/dist/runtime/runtime.d.ts +42 -0
  106. package/dist/runtime/runtime.d.ts.map +1 -0
  107. package/dist/runtime/runtime.js +1 -0
  108. package/dist/runtime/style.d.ts +55 -0
  109. package/dist/runtime/style.d.ts.map +1 -0
  110. package/dist/runtime/style.js +143 -0
  111. package/dist/themes/borders.d.ts +10 -0
  112. package/dist/themes/borders.d.ts.map +1 -0
  113. package/dist/themes/borders.js +88 -0
  114. package/dist/themes/builtin.d.ts +31 -0
  115. package/dist/themes/builtin.d.ts.map +1 -0
  116. package/dist/themes/builtin.js +291 -0
  117. package/dist/themes/glyphs.d.ts +15 -0
  118. package/dist/themes/glyphs.d.ts.map +1 -0
  119. package/dist/themes/glyphs.js +97 -0
  120. package/dist/themes/index.d.ts +5 -0
  121. package/dist/themes/index.d.ts.map +1 -0
  122. package/dist/themes/index.js +4 -0
  123. package/dist/themes/registry.d.ts +20 -0
  124. package/dist/themes/registry.d.ts.map +1 -0
  125. package/dist/themes/registry.js +204 -0
  126. package/dist/types/adapter.d.ts +38 -0
  127. package/dist/types/adapter.d.ts.map +1 -0
  128. package/dist/types/adapter.js +1 -0
  129. package/dist/types/animation.d.ts +31 -0
  130. package/dist/types/animation.d.ts.map +1 -0
  131. package/dist/types/animation.js +1 -0
  132. package/dist/types/app.d.ts +148 -0
  133. package/dist/types/app.d.ts.map +1 -0
  134. package/dist/types/app.js +1 -0
  135. package/dist/types/async.d.ts +30 -0
  136. package/dist/types/async.d.ts.map +1 -0
  137. package/dist/types/async.js +1 -0
  138. package/dist/types/capabilities.d.ts +44 -0
  139. package/dist/types/capabilities.d.ts.map +1 -0
  140. package/dist/types/capabilities.js +33 -0
  141. package/dist/types/cells.d.ts +72 -0
  142. package/dist/types/cells.d.ts.map +1 -0
  143. package/dist/types/cells.js +10 -0
  144. package/dist/types/command.d.ts +127 -0
  145. package/dist/types/command.d.ts.map +1 -0
  146. package/dist/types/command.js +1 -0
  147. package/dist/types/component-registry.d.ts +77 -0
  148. package/dist/types/component-registry.d.ts.map +1 -0
  149. package/dist/types/component-registry.js +1 -0
  150. package/dist/types/disposable.d.ts +8 -0
  151. package/dist/types/disposable.d.ts.map +1 -0
  152. package/dist/types/disposable.js +1 -0
  153. package/dist/types/focus.d.ts +72 -0
  154. package/dist/types/focus.d.ts.map +1 -0
  155. package/dist/types/focus.js +1 -0
  156. package/dist/types/geometry.d.ts +28 -0
  157. package/dist/types/geometry.d.ts.map +1 -0
  158. package/dist/types/geometry.js +14 -0
  159. package/dist/types/graph.d.ts +129 -0
  160. package/dist/types/graph.d.ts.map +1 -0
  161. package/dist/types/graph.js +31 -0
  162. package/dist/types/i18n.d.ts +23 -0
  163. package/dist/types/i18n.d.ts.map +1 -0
  164. package/dist/types/i18n.js +1 -0
  165. package/dist/types/index.d.ts +32 -0
  166. package/dist/types/index.d.ts.map +1 -0
  167. package/dist/types/index.js +31 -0
  168. package/dist/types/input.d.ts +65 -0
  169. package/dist/types/input.d.ts.map +1 -0
  170. package/dist/types/input.js +1 -0
  171. package/dist/types/keybinding.d.ts +49 -0
  172. package/dist/types/keybinding.d.ts.map +1 -0
  173. package/dist/types/keybinding.js +1 -0
  174. package/dist/types/layer.d.ts +54 -0
  175. package/dist/types/layer.d.ts.map +1 -0
  176. package/dist/types/layer.js +1 -0
  177. package/dist/types/manifest.d.ts +114 -0
  178. package/dist/types/manifest.d.ts.map +1 -0
  179. package/dist/types/manifest.js +1 -0
  180. package/dist/types/markdown.d.ts +58 -0
  181. package/dist/types/markdown.d.ts.map +1 -0
  182. package/dist/types/markdown.js +1 -0
  183. package/dist/types/navigation.d.ts +40 -0
  184. package/dist/types/navigation.d.ts.map +1 -0
  185. package/dist/types/navigation.js +1 -0
  186. package/dist/types/render.d.ts +72 -0
  187. package/dist/types/render.d.ts.map +1 -0
  188. package/dist/types/render.js +1 -0
  189. package/dist/types/resource.d.ts +170 -0
  190. package/dist/types/resource.d.ts.map +1 -0
  191. package/dist/types/resource.js +1 -0
  192. package/dist/types/services.d.ts +24 -0
  193. package/dist/types/services.d.ts.map +1 -0
  194. package/dist/types/services.js +3 -0
  195. package/dist/types/shell.d.ts +32 -0
  196. package/dist/types/shell.d.ts.map +1 -0
  197. package/dist/types/shell.js +1 -0
  198. package/dist/types/store.d.ts +124 -0
  199. package/dist/types/store.d.ts.map +1 -0
  200. package/dist/types/store.js +1 -0
  201. package/dist/types/stream.d.ts +18 -0
  202. package/dist/types/stream.d.ts.map +1 -0
  203. package/dist/types/stream.js +1 -0
  204. package/dist/types/style.d.ts +166 -0
  205. package/dist/types/style.d.ts.map +1 -0
  206. package/dist/types/style.js +1 -0
  207. package/dist/types/surface.d.ts +97 -0
  208. package/dist/types/surface.d.ts.map +1 -0
  209. package/dist/types/surface.js +1 -0
  210. package/dist/types/syntax.d.ts +68 -0
  211. package/dist/types/syntax.d.ts.map +1 -0
  212. package/dist/types/syntax.js +5 -0
  213. package/dist/types/terminal.d.ts +52 -0
  214. package/dist/types/terminal.d.ts.map +1 -0
  215. package/dist/types/terminal.js +1 -0
  216. package/dist/types/theme.d.ts +117 -0
  217. package/dist/types/theme.d.ts.map +1 -0
  218. package/dist/types/theme.js +1 -0
  219. package/dist/types/when.d.ts +19 -0
  220. package/dist/types/when.d.ts.map +1 -0
  221. package/dist/types/when.js +1 -0
  222. package/dist/ui/primitives.d.ts +56 -0
  223. package/dist/ui/primitives.d.ts.map +1 -0
  224. package/dist/ui/primitives.js +108 -0
  225. package/dist/ui/screen.d.ts +23 -0
  226. package/dist/ui/screen.d.ts.map +1 -0
  227. package/dist/ui/screen.js +18 -0
  228. package/dist/util/disposable.d.ts +6 -0
  229. package/dist/util/disposable.d.ts.map +1 -0
  230. package/dist/util/disposable.js +49 -0
  231. package/dist/util/markdown.d.ts +24 -0
  232. package/dist/util/markdown.d.ts.map +1 -0
  233. package/dist/util/markdown.js +220 -0
  234. package/dist/util/paths.d.ts +47 -0
  235. package/dist/util/paths.d.ts.map +1 -0
  236. package/dist/util/paths.js +134 -0
  237. package/dist/util/stream.d.ts +32 -0
  238. package/dist/util/stream.d.ts.map +1 -0
  239. package/dist/util/stream.js +206 -0
  240. package/dist/util/text.d.ts +65 -0
  241. package/dist/util/text.d.ts.map +1 -0
  242. package/dist/util/text.js +419 -0
  243. package/package.json +70 -0
  244. package/src/adapters/index.ts +11 -0
  245. package/src/app/app.ts +1096 -0
  246. package/src/core/animation.ts +144 -0
  247. package/src/core/clipboard.ts +40 -0
  248. package/src/core/commands.ts +169 -0
  249. package/src/core/components.ts +91 -0
  250. package/src/core/events.ts +95 -0
  251. package/src/core/focus.ts +358 -0
  252. package/src/core/i18n.ts +107 -0
  253. package/src/core/keybindings.ts +184 -0
  254. package/src/core/layers.ts +94 -0
  255. package/src/core/manifest.ts +84 -0
  256. package/src/core/navigation.ts +135 -0
  257. package/src/core/resources.ts +362 -0
  258. package/src/core/services.ts +69 -0
  259. package/src/core/store.ts +640 -0
  260. package/src/core/surfaces.ts +292 -0
  261. package/src/core/syntax.ts +200 -0
  262. package/src/core/when.ts +238 -0
  263. package/src/index.ts +76 -0
  264. package/src/jsx/factory.ts +124 -0
  265. package/src/jsx/intrinsics.ts +99 -0
  266. package/src/jsx/jsx-dev-runtime.ts +23 -0
  267. package/src/jsx/jsx-runtime.ts +76 -0
  268. package/src/render/buffer.ts +318 -0
  269. package/src/render/color.ts +180 -0
  270. package/src/render/diff.ts +99 -0
  271. package/src/render/layout.ts +764 -0
  272. package/src/render/static.ts +290 -0
  273. package/src/runtime/bindings.ts +150 -0
  274. package/src/runtime/hooks.ts +1057 -0
  275. package/src/runtime/instance.ts +284 -0
  276. package/src/runtime/paint.ts +737 -0
  277. package/src/runtime/reconcile.ts +335 -0
  278. package/src/runtime/runtime.ts +47 -0
  279. package/src/runtime/style.ts +205 -0
  280. package/src/themes/borders.ts +95 -0
  281. package/src/themes/builtin.ts +301 -0
  282. package/src/themes/glyphs.ts +101 -0
  283. package/src/themes/index.ts +4 -0
  284. package/src/themes/registry.ts +224 -0
  285. package/src/types/adapter.ts +41 -0
  286. package/src/types/animation.ts +34 -0
  287. package/src/types/app.ts +149 -0
  288. package/src/types/async.ts +35 -0
  289. package/src/types/capabilities.ts +79 -0
  290. package/src/types/cells.ts +79 -0
  291. package/src/types/command.ts +139 -0
  292. package/src/types/component-registry.ts +82 -0
  293. package/src/types/disposable.ts +8 -0
  294. package/src/types/focus.ts +77 -0
  295. package/src/types/geometry.ts +50 -0
  296. package/src/types/graph.ts +174 -0
  297. package/src/types/i18n.ts +25 -0
  298. package/src/types/index.ts +31 -0
  299. package/src/types/input.ts +81 -0
  300. package/src/types/keybinding.ts +51 -0
  301. package/src/types/layer.ts +46 -0
  302. package/src/types/manifest.ts +101 -0
  303. package/src/types/markdown.ts +47 -0
  304. package/src/types/navigation.ts +42 -0
  305. package/src/types/render.ts +86 -0
  306. package/src/types/resource.ts +185 -0
  307. package/src/types/services.ts +28 -0
  308. package/src/types/shell.ts +30 -0
  309. package/src/types/store.ts +148 -0
  310. package/src/types/stream.ts +24 -0
  311. package/src/types/style.ts +205 -0
  312. package/src/types/surface.ts +119 -0
  313. package/src/types/syntax.ts +93 -0
  314. package/src/types/terminal.ts +58 -0
  315. package/src/types/theme.ts +121 -0
  316. package/src/types/when.ts +21 -0
  317. package/src/ui/primitives.ts +118 -0
  318. package/src/ui/screen.ts +41 -0
  319. package/src/util/disposable.ts +49 -0
  320. package/src/util/markdown.ts +225 -0
  321. package/src/util/paths.ts +138 -0
  322. package/src/util/stream.ts +213 -0
  323. 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
+ }