@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,34 @@
1
+ import type { AnimationDriver, TickerOptions, TweenOptions } from '../types/animation.js';
2
+ import type { Disposable } from '../types/disposable.js';
3
+ /**
4
+ * One driver for everything that moves.
5
+ *
6
+ * Spinners, progress, value transitions and appearing elements all run off the
7
+ * same clock, so `enabled = false` is a single flag that stops all of it, and
8
+ * a slow link can drop the frame rate for all of it at once. The clock can
9
+ * also be driven by hand, which is how the test harness advances time.
10
+ */
11
+ export declare class Animation implements AnimationDriver {
12
+ enabled: boolean;
13
+ maxFps: number;
14
+ private tickers;
15
+ private timer;
16
+ private lastTime;
17
+ private manual;
18
+ constructor(options?: {
19
+ enabled?: boolean;
20
+ maxFps?: number;
21
+ manual?: boolean;
22
+ });
23
+ get disabled(): boolean;
24
+ ticker(options: TickerOptions): Disposable;
25
+ tween(options: TweenOptions): Disposable;
26
+ /** Advance by hand. The test harness and the static renderer use this. */
27
+ advance(ms: number): void;
28
+ private tick;
29
+ private ensureRunning;
30
+ private stopIfIdle;
31
+ dispose(): void;
32
+ }
33
+ export declare function createAnimation(options?: ConstructorParameters<typeof Animation>[0]): Animation;
34
+ //# sourceMappingURL=animation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animation.d.ts","sourceRoot":"","sources":["../../src/core/animation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAU,aAAa,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAClG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAsBzD;;;;;;;GAOG;AACH,qBAAa,SAAU,YAAW,eAAe;IAC/C,OAAO,UAAQ;IACf,MAAM,SAAM;IAEZ,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,MAAM,CAAU;gBAEZ,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO;IAMlF,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,UAAU;IAe1C,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,UAAU;IA0BxC,0EAA0E;IAC1E,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAIzB,OAAO,CAAC,IAAI;IAmBZ,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,UAAU;IAMlB,OAAO,IAAI,IAAI;CAKhB;AAED,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,CAE/F"}
@@ -0,0 +1,125 @@
1
+ import { toDisposable } from '../util/disposable.js';
2
+ const EASINGS = {
3
+ linear: (t) => t,
4
+ easeIn: (t) => t * t,
5
+ easeOut: (t) => t * (2 - t),
6
+ easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
7
+ };
8
+ function easingFn(easing) {
9
+ if (typeof easing === 'function')
10
+ return easing;
11
+ return EASINGS[easing ?? 'easeOut'];
12
+ }
13
+ /**
14
+ * One driver for everything that moves.
15
+ *
16
+ * Spinners, progress, value transitions and appearing elements all run off the
17
+ * same clock, so `enabled = false` is a single flag that stops all of it, and
18
+ * a slow link can drop the frame rate for all of it at once. The clock can
19
+ * also be driven by hand, which is how the test harness advances time.
20
+ */
21
+ export class Animation {
22
+ enabled = true;
23
+ maxFps = 30;
24
+ tickers = new Set();
25
+ timer = null;
26
+ lastTime = 0;
27
+ manual;
28
+ constructor(options = {}) {
29
+ this.enabled = options.enabled ?? true;
30
+ this.maxFps = options.maxFps ?? 30;
31
+ this.manual = options.manual ?? false;
32
+ }
33
+ get disabled() {
34
+ return !this.enabled;
35
+ }
36
+ ticker(options) {
37
+ const entry = {
38
+ fps: Math.min(options.fps ?? this.maxFps, this.maxFps),
39
+ onTick: options.onTick,
40
+ frame: 0,
41
+ accumulated: 0,
42
+ };
43
+ this.tickers.add(entry);
44
+ this.ensureRunning();
45
+ return toDisposable(() => {
46
+ this.tickers.delete(entry);
47
+ this.stopIfIdle();
48
+ });
49
+ }
50
+ tween(options) {
51
+ const { from, to, durationMs, onUpdate, onComplete } = options;
52
+ const ease = easingFn(options.easing);
53
+ if (!this.enabled || durationMs <= 0) {
54
+ onUpdate(to);
55
+ onComplete?.();
56
+ return toDisposable(() => { });
57
+ }
58
+ let elapsed = 0;
59
+ const ticker = this.ticker({
60
+ fps: this.maxFps,
61
+ onTick: (_frame, delta) => {
62
+ elapsed += delta;
63
+ const t = Math.min(1, elapsed / durationMs);
64
+ onUpdate(from + (to - from) * ease(t));
65
+ if (t >= 1) {
66
+ ticker.dispose();
67
+ onComplete?.();
68
+ }
69
+ },
70
+ });
71
+ return ticker;
72
+ }
73
+ /** Advance by hand. The test harness and the static renderer use this. */
74
+ advance(ms) {
75
+ this.tick(ms);
76
+ }
77
+ tick(delta) {
78
+ if (!this.enabled)
79
+ return;
80
+ for (const ticker of [...this.tickers]) {
81
+ const interval = 1000 / Math.max(1, ticker.fps);
82
+ ticker.accumulated += delta;
83
+ if (ticker.accumulated < interval)
84
+ continue;
85
+ const frames = Math.floor(ticker.accumulated / interval);
86
+ ticker.accumulated -= frames * interval;
87
+ ticker.frame += frames;
88
+ try {
89
+ ticker.onTick(ticker.frame, frames * interval);
90
+ }
91
+ catch {
92
+ // A failing ticker must not take the frame loop down with it.
93
+ this.tickers.delete(ticker);
94
+ }
95
+ }
96
+ }
97
+ ensureRunning() {
98
+ if (this.manual || this.timer || !this.enabled || this.tickers.size === 0)
99
+ return;
100
+ const interval = Math.max(1000 / this.maxFps, 16);
101
+ this.lastTime = Date.now();
102
+ this.timer = setInterval(() => {
103
+ const now = Date.now();
104
+ const delta = now - this.lastTime;
105
+ this.lastTime = now;
106
+ this.tick(delta);
107
+ }, interval);
108
+ this.timer.unref?.();
109
+ }
110
+ stopIfIdle() {
111
+ if (this.tickers.size > 0 || !this.timer)
112
+ return;
113
+ clearInterval(this.timer);
114
+ this.timer = null;
115
+ }
116
+ dispose() {
117
+ if (this.timer)
118
+ clearInterval(this.timer);
119
+ this.timer = null;
120
+ this.tickers.clear();
121
+ }
122
+ }
123
+ export function createAnimation(options) {
124
+ return new Animation(options);
125
+ }
@@ -0,0 +1,29 @@
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
+ * The clipboard.
6
+ *
7
+ * A terminal can be *told* what to put on the system clipboard - OSC 52 - and
8
+ * cannot be asked what is on it. The read half of that sequence is a way for
9
+ * anything with a pty to read whatever you last copied, so terminals worth
10
+ * using refuse it, and the ones that answer are the ones you would not want
11
+ * to. So a copy goes two places: out to the system clipboard, where the rest
12
+ * of the desktop can reach it, and into the store, which is the half a paste
13
+ * can read back.
14
+ *
15
+ * That makes the store the clipboard as far as this application is concerned.
16
+ * It is in the session scope because a clipboard is not worth persisting and
17
+ * is worth surviving a screen change.
18
+ */
19
+ export declare const CLIPBOARD_PATH: BindingPath;
20
+ export declare function readClipboard(store: ReactiveStore): string;
21
+ /**
22
+ * Put text on the clipboard.
23
+ *
24
+ * The terminal is optional because the store half is the half that has to
25
+ * work: a session with no OSC 52 - a plain console, a pipe, the test harness -
26
+ * still copies and pastes within the application.
27
+ */
28
+ export declare function writeClipboard(store: ReactiveStore, text: string, terminal?: TerminalAdapter | null): void;
29
+ //# sourceMappingURL=clipboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/core/clipboard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,EAA4B,WAAW,CAAC;AAEnE,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAE1D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,GAChC,IAAI,CAGN"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The clipboard.
3
+ *
4
+ * A terminal can be *told* what to put on the system clipboard - OSC 52 - and
5
+ * cannot be asked what is on it. The read half of that sequence is a way for
6
+ * anything with a pty to read whatever you last copied, so terminals worth
7
+ * using refuse it, and the ones that answer are the ones you would not want
8
+ * to. So a copy goes two places: out to the system clipboard, where the rest
9
+ * of the desktop can reach it, and into the store, which is the half a paste
10
+ * can read back.
11
+ *
12
+ * That makes the store the clipboard as far as this application is concerned.
13
+ * It is in the session scope because a clipboard is not worth persisting and
14
+ * is worth surviving a screen change.
15
+ */
16
+ export const CLIPBOARD_PATH = '$/session/clipboard';
17
+ export function readClipboard(store) {
18
+ return store.get(CLIPBOARD_PATH) ?? '';
19
+ }
20
+ /**
21
+ * Put text on the clipboard.
22
+ *
23
+ * The terminal is optional because the store half is the half that has to
24
+ * work: a session with no OSC 52 - a plain console, a pipe, the test harness -
25
+ * still copies and pastes within the application.
26
+ */
27
+ export function writeClipboard(store, text, terminal) {
28
+ store.set(CLIPBOARD_PATH, text);
29
+ terminal?.writeClipboard?.(text);
30
+ }
@@ -0,0 +1,42 @@
1
+ import type { CommandContext, CommandDefinition, CommandRegistry, CommandScope } from '../types/command.js';
2
+ import type { Disposable } from '../types/disposable.js';
3
+ import type { ReactiveStore } from '../types/store.js';
4
+ import type { WhenEngine } from '../types/when.js';
5
+ import type { TextUIApp } from '../types/app.js';
6
+ /**
7
+ * Commands are the only way an action should be spelled.
8
+ *
9
+ * A button that calls an API directly and a palette entry that calls the same
10
+ * API are two implementations that will drift; a button that runs a registered
11
+ * command cannot drift from the command. That is also why resolution walks
12
+ * from the most specific scope outward - `table.search` means whichever table
13
+ * is focused, without every table inventing its own id.
14
+ */
15
+ export declare class Commands implements CommandRegistry {
16
+ private deps;
17
+ private byId;
18
+ /** Scope instance ids that are currently active, innermost first. */
19
+ private activeScopes;
20
+ constructor(deps: {
21
+ store: ReactiveStore;
22
+ when: WhenEngine;
23
+ app(): TextUIApp | null;
24
+ onError(err: unknown, context: string): void;
25
+ });
26
+ setActiveScopes(ids: string[]): void;
27
+ register(def: CommandDefinition): Disposable;
28
+ unregister(id: string, scopeId?: string): void;
29
+ get(id: string): CommandDefinition | undefined;
30
+ list(options?: {
31
+ scope?: CommandScope;
32
+ slot?: string;
33
+ enabledOnly?: boolean;
34
+ }): CommandDefinition[];
35
+ /** Innermost active scope wins; an app-scoped registration is the floor. */
36
+ resolve(id: string): CommandDefinition | undefined;
37
+ enabled(id: string): boolean;
38
+ isChecked(id: string): boolean | undefined;
39
+ execute(id: string, args?: Record<string, unknown>, source?: CommandContext['source']): Promise<unknown>;
40
+ }
41
+ export declare function createCommands(deps: ConstructorParameters<typeof Commands>[0]): Commands;
42
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/core/commands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,YAAY,EACjE,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKjD;;;;;;;;GAQG;AACH,qBAAa,QAAS,YAAW,eAAe;IAM5C,OAAO,CAAC,IAAI;IALd,OAAO,CAAC,IAAI,CAA0C;IACtD,qEAAqE;IACrE,OAAO,CAAC,YAAY,CAAgB;gBAG1B,IAAI,EAAE;QACZ,KAAK,EAAE,aAAa,CAAC;QACrB,IAAI,EAAE,UAAU,CAAC;QACjB,GAAG,IAAI,SAAS,GAAG,IAAI,CAAC;QACxB,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;KAC9C;IAGH,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI;IAIpC,QAAQ,CAAC,GAAG,EAAE,iBAAiB,GAAG,UAAU;IAc5C,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAU9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAI9C,IAAI,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,YAAY,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,iBAAiB,EAAE;IAavG,4EAA4E;IAC5E,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAkBlD,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAI5B,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAKpC,OAAO,CACX,EAAE,EAAE,MAAM,EACV,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EAClC,MAAM,GAAE,cAAc,CAAC,QAAQ,CAAS,GACvC,OAAO,CAAC,OAAO,CAAC;CAqDpB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,qBAAqB,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,CAExF"}
@@ -0,0 +1,146 @@
1
+ import { toDisposable } from '../util/disposable.js';
2
+ const SCOPE_ORDER = ['component', 'region', 'screen', 'app'];
3
+ /**
4
+ * Commands are the only way an action should be spelled.
5
+ *
6
+ * A button that calls an API directly and a palette entry that calls the same
7
+ * API are two implementations that will drift; a button that runs a registered
8
+ * command cannot drift from the command. That is also why resolution walks
9
+ * from the most specific scope outward - `table.search` means whichever table
10
+ * is focused, without every table inventing its own id.
11
+ */
12
+ export class Commands {
13
+ deps;
14
+ byId = new Map();
15
+ /** Scope instance ids that are currently active, innermost first. */
16
+ activeScopes = [];
17
+ constructor(deps) {
18
+ this.deps = deps;
19
+ }
20
+ setActiveScopes(ids) {
21
+ this.activeScopes = ids;
22
+ }
23
+ register(def) {
24
+ const list = this.byId.get(def.id) ?? [];
25
+ list.push(def);
26
+ this.byId.set(def.id, list);
27
+ return toDisposable(() => {
28
+ const current = this.byId.get(def.id);
29
+ if (!current)
30
+ return;
31
+ const next = current.filter((d) => d !== def);
32
+ if (next.length === 0)
33
+ this.byId.delete(def.id);
34
+ else
35
+ this.byId.set(def.id, next);
36
+ });
37
+ }
38
+ unregister(id, scopeId) {
39
+ if (scopeId === undefined) {
40
+ this.byId.delete(id);
41
+ return;
42
+ }
43
+ const next = (this.byId.get(id) ?? []).filter((d) => d.scopeId !== scopeId);
44
+ if (next.length === 0)
45
+ this.byId.delete(id);
46
+ else
47
+ this.byId.set(id, next);
48
+ }
49
+ get(id) {
50
+ return this.resolve(id);
51
+ }
52
+ list(options = {}) {
53
+ const out = [];
54
+ for (const defs of this.byId.values()) {
55
+ for (const def of defs) {
56
+ if (options.scope && (def.scope ?? 'app') !== options.scope)
57
+ continue;
58
+ if (options.slot && !(def.slots ?? []).includes(options.slot))
59
+ continue;
60
+ if (options.enabledOnly && !this.deps.when.evaluate(def.when))
61
+ continue;
62
+ out.push(def);
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+ /** Innermost active scope wins; an app-scoped registration is the floor. */
68
+ resolve(id) {
69
+ const candidates = (this.byId.get(id) ?? []).filter((def) => this.deps.when.evaluate(def.when));
70
+ if (candidates.length === 0)
71
+ return undefined;
72
+ if (candidates.length === 1)
73
+ return candidates[0];
74
+ for (const scopeId of this.activeScopes) {
75
+ const match = candidates.find((d) => d.scopeId === scopeId);
76
+ if (match)
77
+ return match;
78
+ }
79
+ for (const scope of SCOPE_ORDER) {
80
+ const match = candidates.find((d) => (d.scope ?? 'app') === scope && !d.scopeId);
81
+ if (match)
82
+ return match;
83
+ }
84
+ return candidates[candidates.length - 1];
85
+ }
86
+ enabled(id) {
87
+ return this.resolve(id) !== undefined;
88
+ }
89
+ isChecked(id) {
90
+ const clause = this.resolve(id)?.checked;
91
+ return clause === undefined ? undefined : this.deps.when.evaluate(clause);
92
+ }
93
+ async execute(id, args = {}, source = 'api') {
94
+ const def = this.resolve(id);
95
+ if (!def) {
96
+ // Registered but gated is not the same as absent, and it used to be
97
+ // reported as absent: `alt+left` with nothing open said "no command
98
+ // registered as go.previousTab" about a command registered forty lines
99
+ // away, whose `when` simply did not pass.
100
+ //
101
+ // A `when` that does not pass is the command saying "not now", which is
102
+ // what it is for - so this does nothing, and the key does nothing. A
103
+ // typo still throws, because a keybinding pointing at a name that was
104
+ // never registered has to be findable.
105
+ if ((this.byId.get(id) ?? []).length > 0)
106
+ return undefined;
107
+ throw new Error(`[textui] no command registered as "${id}"`);
108
+ }
109
+ const missing = (def.args ?? [])
110
+ .filter((a) => a.required && args[a.name] === undefined && a.default === undefined)
111
+ .map((a) => a.name);
112
+ if (missing.length > 0) {
113
+ throw new Error(`[textui] command "${id}" needs: ${missing.join(', ')}`);
114
+ }
115
+ const filled = { ...args };
116
+ for (const spec of def.args ?? []) {
117
+ if (filled[spec.name] === undefined && spec.default !== undefined) {
118
+ filled[spec.name] = spec.default;
119
+ }
120
+ }
121
+ const app = this.deps.app();
122
+ const ctx = {
123
+ app: app,
124
+ store: this.deps.store,
125
+ scopeId: def.scopeId ?? null,
126
+ source,
127
+ };
128
+ // Announced, not just run. Which command fired and what asked for it is
129
+ // the first question anyone debugging a keybinding, a menu or a palette
130
+ // has, and reconstructing it from its effects is guesswork.
131
+ app?.events.emit('@/command/run', { id, source, args: filled });
132
+ try {
133
+ const result = await def.run(filled, ctx);
134
+ app?.events.emit('@/command/done', { id, source });
135
+ return result;
136
+ }
137
+ catch (err) {
138
+ app?.events.emit('@/command/error', { id, source, message: String(err) });
139
+ this.deps.onError(err, `command "${id}"`);
140
+ throw err;
141
+ }
142
+ }
143
+ }
144
+ export function createCommands(deps) {
145
+ return new Commands(deps);
146
+ }
@@ -0,0 +1,27 @@
1
+ import type { ComponentCategory, ComponentDefinition, ComponentRegistry } from '../types/component-registry.js';
2
+ import type { Disposable } from '../types/disposable.js';
3
+ /**
4
+ * Late binding is the mechanism the whole design rests on: a graph names a
5
+ * component, and the name is resolved at mount time. A name that was never
6
+ * registered is a runtime miss rather than a compile error - that is the price
7
+ * of a screen being data, and it is why the miss renders visibly.
8
+ */
9
+ export declare class Components implements ComponentRegistry {
10
+ private defs;
11
+ private resolving;
12
+ register(def: ComponentDefinition): Disposable;
13
+ registerMany(defs: ComponentDefinition[]): Disposable;
14
+ unregister(component: string): void;
15
+ get(component: string): ComponentDefinition | undefined;
16
+ has(component: string): boolean;
17
+ list(category?: ComponentCategory): ComponentDefinition[];
18
+ resolve(component: string): Promise<ComponentDefinition>;
19
+ pending(): string[];
20
+ /**
21
+ * Which components can display a resource of this kind. The selector is not
22
+ * evaluated here - a caller with a store re-filters through `when`.
23
+ */
24
+ findOpeners(resourceKind: string): ComponentDefinition[];
25
+ }
26
+ export declare function createComponents(): Components;
27
+ //# sourceMappingURL=components.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/core/components.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAC1D,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAGzD;;;;;GAKG;AACH,qBAAa,UAAW,YAAW,iBAAiB;IAClD,OAAO,CAAC,IAAI,CAA0C;IACtD,OAAO,CAAC,SAAS,CAAmD;IAEpE,QAAQ,CAAC,GAAG,EAAE,mBAAmB,GAAG,UAAU;IAO9C,YAAY,CAAC,IAAI,EAAE,mBAAmB,EAAE,GAAG,UAAU;IAMrD,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKnC,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAIvD,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI/B,IAAI,CAAC,QAAQ,CAAC,EAAE,iBAAiB,GAAG,mBAAmB,EAAE;IAKnD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAwB9D,OAAO,IAAI,MAAM,EAAE;IAMnB;;;OAGG;IACH,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,mBAAmB,EAAE;CAKzD;AAED,wBAAgB,gBAAgB,IAAI,UAAU,CAE7C"}
@@ -0,0 +1,78 @@
1
+ import { toDisposable, createBag } from '../util/disposable.js';
2
+ /**
3
+ * Late binding is the mechanism the whole design rests on: a graph names a
4
+ * component, and the name is resolved at mount time. A name that was never
5
+ * registered is a runtime miss rather than a compile error - that is the price
6
+ * of a screen being data, and it is why the miss renders visibly.
7
+ */
8
+ export class Components {
9
+ defs = new Map();
10
+ resolving = new Map();
11
+ register(def) {
12
+ this.defs.set(def.component, def);
13
+ return toDisposable(() => {
14
+ if (this.defs.get(def.component) === def)
15
+ this.defs.delete(def.component);
16
+ });
17
+ }
18
+ registerMany(defs) {
19
+ const bag = createBag();
20
+ for (const def of defs)
21
+ bag.add(this.register(def));
22
+ return bag;
23
+ }
24
+ unregister(component) {
25
+ this.defs.delete(component);
26
+ this.resolving.delete(component);
27
+ }
28
+ get(component) {
29
+ return this.defs.get(component);
30
+ }
31
+ has(component) {
32
+ return this.defs.has(component);
33
+ }
34
+ list(category) {
35
+ const all = [...this.defs.values()];
36
+ return category ? all.filter((d) => d.category === category) : all;
37
+ }
38
+ async resolve(component) {
39
+ const def = this.defs.get(component);
40
+ if (!def)
41
+ throw new Error(`[textui] no component registered as "${component}"`);
42
+ if (def.renderer.kind !== 'lazy')
43
+ return def;
44
+ const inFlight = this.resolving.get(component);
45
+ if (inFlight)
46
+ return inFlight;
47
+ const load = def.renderer.load;
48
+ const promise = Promise.resolve(load()).then((mod) => {
49
+ const render = typeof mod === 'function' ? mod : mod.default;
50
+ const resolved = {
51
+ ...def,
52
+ renderer: { kind: 'function', render },
53
+ };
54
+ this.defs.set(component, resolved);
55
+ this.resolving.delete(component);
56
+ return resolved;
57
+ });
58
+ this.resolving.set(component, promise);
59
+ return promise;
60
+ }
61
+ pending() {
62
+ return [...this.defs.values()]
63
+ .filter((d) => d.renderer.kind === 'lazy')
64
+ .map((d) => d.component);
65
+ }
66
+ /**
67
+ * Which components can display a resource of this kind. The selector is not
68
+ * evaluated here - a caller with a store re-filters through `when`.
69
+ */
70
+ findOpeners(resourceKind) {
71
+ return [...this.defs.values()]
72
+ .filter((d) => d.opens?.resourceKinds?.some((k) => k === resourceKind || resourceKind.startsWith(k + '.')))
73
+ .sort((a, b) => (b.opens?.priority ?? 0) - (a.opens?.priority ?? 0));
74
+ }
75
+ }
76
+ export function createComponents() {
77
+ return new Components();
78
+ }
@@ -0,0 +1,23 @@
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
+ /**
5
+ * Transient events, addressed like store paths on purpose - `@/dialog/confirm`
6
+ * reads the same way `$/dialog/open` does - but with no value to read back.
7
+ * That difference is the whole reason they are two mechanisms.
8
+ */
9
+ export declare class Events implements EventBus {
10
+ private listeners;
11
+ private disposed;
12
+ onError: (err: unknown, context: string) => void;
13
+ emit(path: EventPath, payload?: unknown): void;
14
+ private matches;
15
+ on(path: EventPath, fn: (payload: unknown, path: EventPath) => void, options?: {
16
+ subtree?: boolean;
17
+ once?: boolean;
18
+ }): Disposable;
19
+ next(path: EventPath, timeoutMs?: number): Promise<unknown>;
20
+ dispose(): void;
21
+ }
22
+ export declare function createEvents(): Events;
23
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/core/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAiBzD;;;;GAIG;AACH,qBAAa,MAAO,YAAW,QAAQ;IACrC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAE9C;IAEF,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI;IAc9C,OAAO,CAAC,OAAO;IAMf,EAAE,CACA,IAAI,EAAE,SAAS,EACf,EAAE,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,KAAK,IAAI,EAC/C,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAO,GAClD,UAAU;IAab,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB3D,OAAO,IAAI,IAAI;CAIhB;AAED,wBAAgB,YAAY,IAAI,MAAM,CAErC"}
@@ -0,0 +1,77 @@
1
+ import { toDisposable } from '../util/disposable.js';
2
+ import { isDescendantKey, matchKey } from '../util/paths.js';
3
+ function eventKey(path) {
4
+ const body = path.startsWith('@/') ? path.slice(2) : path.replace(/^\//, '');
5
+ return body.split('/').filter((s) => s !== '').join('/');
6
+ }
7
+ /**
8
+ * Transient events, addressed like store paths on purpose - `@/dialog/confirm`
9
+ * reads the same way `$/dialog/open` does - but with no value to read back.
10
+ * That difference is the whole reason they are two mechanisms.
11
+ */
12
+ export class Events {
13
+ listeners = new Set();
14
+ disposed = false;
15
+ onError = (err, context) => {
16
+ console.error(`[textui/events] ${context}`, err);
17
+ };
18
+ emit(path, payload) {
19
+ if (this.disposed)
20
+ return;
21
+ const key = eventKey(path);
22
+ for (const listener of [...this.listeners]) {
23
+ if (!this.matches(listener, key))
24
+ continue;
25
+ if (listener.once)
26
+ this.listeners.delete(listener);
27
+ try {
28
+ listener.fn(payload, path);
29
+ }
30
+ catch (err) {
31
+ this.onError(err, `listener of @/${listener.key}`);
32
+ }
33
+ }
34
+ }
35
+ matches(listener, key) {
36
+ if (listener.wildcard)
37
+ return matchKey(key, listener.key, listener.subtree);
38
+ if (listener.key === key)
39
+ return true;
40
+ return listener.subtree && isDescendantKey(key, listener.key);
41
+ }
42
+ on(path, fn, options = {}) {
43
+ const key = eventKey(path);
44
+ const listener = {
45
+ key,
46
+ subtree: options.subtree ?? false,
47
+ wildcard: key.includes('*'),
48
+ once: options.once ?? false,
49
+ fn,
50
+ };
51
+ this.listeners.add(listener);
52
+ return toDisposable(() => this.listeners.delete(listener));
53
+ }
54
+ next(path, timeoutMs) {
55
+ return new Promise((resolve, reject) => {
56
+ const sub = this.on(path, (payload) => {
57
+ if (timer)
58
+ clearTimeout(timer);
59
+ resolve(payload);
60
+ }, { once: true });
61
+ const timer = timeoutMs
62
+ ? setTimeout(() => {
63
+ sub.dispose();
64
+ reject(new Error(`timed out waiting for ${path}`));
65
+ }, timeoutMs)
66
+ : null;
67
+ timer?.unref?.();
68
+ });
69
+ }
70
+ dispose() {
71
+ this.disposed = true;
72
+ this.listeners.clear();
73
+ }
74
+ }
75
+ export function createEvents() {
76
+ return new Events();
77
+ }