@flyingrobots/bijou-tui 0.1.0 → 0.3.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 (60) hide show
  1. package/README.md +212 -30
  2. package/dist/animate.d.ts +60 -0
  3. package/dist/animate.d.ts.map +1 -0
  4. package/dist/animate.js +98 -0
  5. package/dist/animate.js.map +1 -0
  6. package/dist/commands.d.ts.map +1 -1
  7. package/dist/commands.js +2 -2
  8. package/dist/commands.js.map +1 -1
  9. package/dist/eventbus.d.ts +69 -0
  10. package/dist/eventbus.d.ts.map +1 -0
  11. package/dist/eventbus.js +120 -0
  12. package/dist/eventbus.js.map +1 -0
  13. package/dist/flex.d.ts +64 -0
  14. package/dist/flex.d.ts.map +1 -0
  15. package/dist/flex.js +261 -0
  16. package/dist/flex.js.map +1 -0
  17. package/dist/help.d.ts +58 -0
  18. package/dist/help.d.ts.map +1 -0
  19. package/dist/help.js +104 -0
  20. package/dist/help.js.map +1 -0
  21. package/dist/index.d.ts +11 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +18 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/inputstack.d.ts +79 -0
  26. package/dist/inputstack.d.ts.map +1 -0
  27. package/dist/inputstack.js +81 -0
  28. package/dist/inputstack.js.map +1 -0
  29. package/dist/keybindings.d.ts +83 -0
  30. package/dist/keybindings.d.ts.map +1 -0
  31. package/dist/keybindings.js +184 -0
  32. package/dist/keybindings.js.map +1 -0
  33. package/dist/layout.d.ts +0 -1
  34. package/dist/layout.d.ts.map +1 -1
  35. package/dist/layout.js +3 -4
  36. package/dist/layout.js.map +1 -1
  37. package/dist/runtime.d.ts +3 -0
  38. package/dist/runtime.d.ts.map +1 -1
  39. package/dist/runtime.js +24 -37
  40. package/dist/runtime.js.map +1 -1
  41. package/dist/screen.d.ts +10 -4
  42. package/dist/screen.d.ts.map +1 -1
  43. package/dist/screen.js +13 -11
  44. package/dist/screen.js.map +1 -1
  45. package/dist/spring.d.ts +139 -0
  46. package/dist/spring.d.ts.map +1 -0
  47. package/dist/spring.js +106 -0
  48. package/dist/spring.js.map +1 -0
  49. package/dist/timeline.d.ts +127 -0
  50. package/dist/timeline.d.ts.map +1 -0
  51. package/dist/timeline.js +298 -0
  52. package/dist/timeline.js.map +1 -0
  53. package/dist/types.d.ts +11 -2
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js.map +1 -1
  56. package/dist/viewport.d.ts +64 -0
  57. package/dist/viewport.d.ts.map +1 -0
  58. package/dist/viewport.js +162 -0
  59. package/dist/viewport.js.map +1 -0
  60. package/package.json +2 -2
package/README.md CHANGED
@@ -1,35 +1,41 @@
1
1
  # @flyingrobots/bijou-tui
2
2
 
3
- TEA runtime for terminal UIs — model/update/view with keyboard input, alt screen, and layout helpers.
3
+ TEA runtime for terminal UIs — model/update/view with physics-based animation, flexbox layout, declarative keybindings, and a centralized event bus.
4
4
 
5
- Inspired by [Bubble Tea](https://github.com/charmbracelet/bubbletea) (Go), bijou-tui brings The Elm Architecture to TypeScript terminals.
5
+ Inspired by [Bubble Tea](https://github.com/charmbracelet/bubbletea) (Go) and [GSAP](https://gsap.com/) animation.
6
+
7
+ ## What's New in 0.3.0?
8
+
9
+ - Lock-step version bump with `@flyingrobots/bijou` v0.3.0 (DAG renderer)
10
+
11
+ See the [CHANGELOG](https://github.com/flyingrobots/bijou/blob/main/docs/CHANGELOG.md) for the full release history.
6
12
 
7
13
  ## Install
8
14
 
9
15
  ```bash
10
- npm install @flyingrobots/bijou @flyingrobots/bijou-tui
16
+ npm install @flyingrobots/bijou @flyingrobots/bijou-node @flyingrobots/bijou-tui
11
17
  ```
12
18
 
13
19
  ## Quick Start
14
20
 
15
21
  ```typescript
22
+ import { initDefaultContext } from '@flyingrobots/bijou-node';
16
23
  import { run, quit, tick, type App, type KeyMsg } from '@flyingrobots/bijou-tui';
17
24
 
25
+ initDefaultContext();
26
+
18
27
  type Model = { count: number };
19
28
 
20
29
  const app: App<Model> = {
21
- init: () => [{ count: 0 }, tick(1000)],
30
+ init: () => [{ count: 0 }, []],
22
31
 
23
32
  update: (msg, model) => {
24
33
  if (msg.type === 'key') {
25
- if (msg.key === 'q') return [model, quit()];
26
- if (msg.key === '+') return [{ count: model.count + 1 }, null];
27
- if (msg.key === '-') return [{ count: model.count - 1 }, null];
28
- }
29
- if (msg.type === 'tick') {
30
- return [model, tick(1000)];
34
+ if (msg.key === 'q') return [model, [quit()]];
35
+ if (msg.key === '+') return [{ count: model.count + 1 }, []];
36
+ if (msg.key === '-') return [{ count: model.count - 1 }, []];
31
37
  }
32
- return [model, null];
38
+ return [model, []];
33
39
  },
34
40
 
35
41
  view: (model) => `Count: ${model.count}\n\nPress +/- to change, q to quit`,
@@ -38,35 +44,211 @@ const app: App<Model> = {
38
44
  run(app);
39
45
  ```
40
46
 
41
- ## API
47
+ ## Animation
48
+
49
+ ### Spring Physics
50
+
51
+ ```typescript
52
+ import { animate, SPRING_PRESETS } from '@flyingrobots/bijou-tui';
53
+
54
+ // Physics-based (default) — runs until the spring settles
55
+ const cmd = animate({
56
+ from: 0,
57
+ to: 100,
58
+ spring: 'wobbly', // or 'default', 'gentle', 'stiff', 'slow', 'molasses'
59
+ onFrame: (v) => ({ type: 'scroll', y: v }),
60
+ });
61
+
62
+ // Duration-based with easing
63
+ const fade = animate({
64
+ type: 'tween',
65
+ from: 0,
66
+ to: 1,
67
+ duration: 300,
68
+ ease: EASINGS.easeOutCubic,
69
+ onFrame: (v) => ({ type: 'fade', opacity: v }),
70
+ });
71
+
72
+ // Skip animation (reduced motion)
73
+ const jump = animate({
74
+ from: 0, to: 100,
75
+ immediate: true,
76
+ onFrame: (v) => ({ type: 'scroll', y: v }),
77
+ });
78
+ ```
79
+
80
+ ### Timeline
42
81
 
43
- ### Core Types
82
+ GSAP-style orchestration — pure state machine, no timers:
44
83
 
45
- - **`App<M>`** — defines `init`, `update(msg, model)`, and `view(model)` functions
46
- - **`KeyMsg`** keyboard input message with `key`, `ctrl`, `shift`, `alt` fields
47
- - **`Cmd`** — side-effect commands returned from `update`
48
- - **`QUIT`** — sentinel value to signal app termination
84
+ ```typescript
85
+ import { timeline } from '@flyingrobots/bijou-tui';
49
86
 
50
- ### Commands
87
+ const tl = timeline()
88
+ .add('slideIn', { type: 'tween', from: -100, to: 0, duration: 300 })
89
+ .add('fadeIn', { type: 'tween', from: 0, to: 1, duration: 200 }, '-=100')
90
+ .label('settled')
91
+ .add('bounce', { from: 0, to: 10, spring: 'wobbly' }, 'settled')
92
+ .call('onReady', 'settled+=50')
93
+ .build();
51
94
 
52
- - **`quit()`** exit the app
53
- - **`tick(ms)`** — schedule a tick after `ms` milliseconds
54
- - **`batch(...cmds)`** combine multiple commands
95
+ // Drive from TEA update:
96
+ let tlState = tl.init();
97
+ // on each frame:
98
+ tlState = tl.step(tlState, 1/60);
99
+ const { slideIn, fadeIn, bounce } = tl.values(tlState);
100
+ const fired = tl.firedCallbacks(prev, tlState); // ['onReady']
101
+ ```
102
+
103
+ Position syntax: `'<'` (parallel), `'+=N'` (gap), `'-=N'` (overlap), `'<+=N'` (offset from previous start), absolute ms, `'label'`, `'label+=N'`.
55
104
 
56
- ### Screen Control
105
+ ## Layout
57
106
 
58
- - **`enterScreen()` / `exitScreen()`** — alt screen buffer management
59
- - **`clearAndHome()`** — clear screen and move cursor to top-left
60
- - **`renderFrame(content)`** — efficient frame rendering
107
+ ### Flexbox
61
108
 
62
- ### Layout
109
+ ```typescript
110
+ import { flex } from '@flyingrobots/bijou-tui';
63
111
 
64
- - **`vstack(...lines)`** vertical stack (join with newlines)
65
- - **`hstack(...cols)`** horizontal stack (side-by-side columns)
112
+ // Sidebar + main content, responsive to terminal width
113
+ flex({ direction: 'row', width: cols, height: rows, gap: 1 },
114
+ { basis: 20, content: sidebarText },
115
+ { flex: 1, content: (w, h) => renderMain(w, h) },
116
+ );
66
117
 
67
- ### Key Parsing
118
+ // Header + body + footer
119
+ flex({ direction: 'column', width: cols, height: rows },
120
+ { basis: 1, content: headerLine },
121
+ { flex: 1, content: (w, h) => renderBody(w, h) },
122
+ { basis: 1, content: statusLine },
123
+ );
124
+ ```
125
+
126
+ Children can be **render functions** `(width, height) => string` — they receive their allocated space and reflow automatically when the terminal resizes.
127
+
128
+ ### Viewport
129
+
130
+ ```typescript
131
+ import { viewport, createScrollState, scrollBy, pageDown } from '@flyingrobots/bijou-tui';
132
+
133
+ let scroll = createScrollState(content, viewportHeight);
134
+
135
+ // Render visible window with scrollbar
136
+ const view = viewport({ width: 60, height: 20, content, scrollY: scroll.y });
137
+
138
+ // Handle scroll keys
139
+ scroll = scrollBy(scroll, 1); // down one line
140
+ scroll = pageDown(scroll); // down one page
141
+ ```
142
+
143
+ ### Basic Layout
144
+
145
+ ```typescript
146
+ import { vstack, hstack } from '@flyingrobots/bijou-tui';
147
+
148
+ vstack(header, content, footer); // vertical stack
149
+ hstack(2, leftPanel, rightPanel); // side-by-side with gap
150
+ ```
151
+
152
+ ## Resize Handling
153
+
154
+ Terminal resize events are dispatched automatically as `ResizeMsg`:
155
+
156
+ ```typescript
157
+ update(msg, model) {
158
+ if (msg.type === 'resize') {
159
+ return [{ ...model, cols: msg.columns, rows: msg.rows }, []];
160
+ }
161
+ // ...
162
+ }
163
+
164
+ view(model) {
165
+ return flex(
166
+ { direction: 'row', width: model.cols, height: model.rows },
167
+ { basis: 20, content: sidebar },
168
+ { flex: 1, content: (w, h) => mainContent(w, h) },
169
+ );
170
+ }
171
+ ```
172
+
173
+ ## Event Bus
174
+
175
+ The runtime uses an `EventBus` internally. You can also create your own for custom event sources:
176
+
177
+ ```typescript
178
+ import { createEventBus } from '@flyingrobots/bijou-tui';
179
+
180
+ const bus = createEventBus<MyMsg>();
181
+ bus.connectIO(ctx.io); // keyboard + resize
182
+ bus.on((msg) => { /* ... */ }); // single subscription
183
+ bus.emit(customMsg); // synthetic events
184
+ bus.runCmd(someCommand); // command results re-emitted
185
+ bus.dispose(); // clean shutdown
186
+ ```
187
+
188
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full event flow and [GUIDE.md](./GUIDE.md) for detailed usage patterns.
189
+
190
+ ## Keybinding Manager
191
+
192
+ Declarative key binding with modifier support, named groups, and runtime enable/disable:
193
+
194
+ ```typescript
195
+ import { createKeyMap, type KeyMsg } from '@flyingrobots/bijou-tui';
196
+
197
+ type Msg = { type: 'quit' } | { type: 'help' } | { type: 'move'; dir: string };
198
+
199
+ const kb = createKeyMap<Msg>()
200
+ .bind('q', 'Quit', { type: 'quit' })
201
+ .bind('?', 'Help', { type: 'help' })
202
+ .bind('ctrl+c', 'Force quit', { type: 'quit' })
203
+ .group('Navigation', (g) => g
204
+ .bind('j', 'Down', { type: 'move', dir: 'down' })
205
+ .bind('k', 'Up', { type: 'move', dir: 'up' })
206
+ );
207
+
208
+ // In TEA update:
209
+ const action = kb.handle(keyMsg);
210
+ if (action !== undefined) return [model, [/* ... */]];
211
+
212
+ // Runtime enable/disable
213
+ kb.disableGroup('Navigation');
214
+ kb.enable('Quit');
215
+ ```
216
+
217
+ ### Help Generation
218
+
219
+ Auto-generate help text from registered bindings:
220
+
221
+ ```typescript
222
+ import { helpView, helpShort, helpFor } from '@flyingrobots/bijou-tui';
223
+
224
+ helpView(kb); // full grouped multi-line help
225
+ helpShort(kb); // "q Quit • ? Help • Ctrl+c Force quit • j Down • k Up"
226
+ helpFor(kb, 'Nav'); // only Navigation group
227
+ ```
228
+
229
+ ### Input Stack
230
+
231
+ Layered input dispatch for modal UIs — push/pop handlers with opaque or passthrough behavior:
232
+
233
+ ```typescript
234
+ import { createInputStack, type KeyMsg } from '@flyingrobots/bijou-tui';
235
+
236
+ const stack = createInputStack<KeyMsg, Msg>();
237
+
238
+ // Base layer — global keys, lets unmatched events fall through
239
+ stack.push(appKeys, { passthrough: true });
240
+
241
+ // Modal opens — captures all input (opaque by default)
242
+ const modalId = stack.push(modalKeys);
243
+
244
+ // Dispatch returns first matched action, top-down
245
+ const action = stack.dispatch(keyMsg);
246
+
247
+ // Modal closes
248
+ stack.remove(modalId);
249
+ ```
68
250
 
69
- - **`parseKey(data)`** parse raw stdin bytes into a `KeyMsg`
251
+ `KeyMap` implements `InputHandler`, so it plugs directly into the input stack.
70
252
 
71
253
  ## Related Packages
72
254
 
@@ -0,0 +1,60 @@
1
+ /**
2
+ * TEA-integrated animation commands.
3
+ *
4
+ * GSAP-style API: `animate()` returns a `Cmd` that fires `onFrame`
5
+ * messages as the animation progresses, fitting naturally into the
6
+ * TEA update cycle.
7
+ *
8
+ * Two animation modes:
9
+ * - **spring**: Physics-based (default). No fixed duration — runs until
10
+ * the spring settles. Use for organic, responsive motion.
11
+ * - **tween**: Duration-based with easing curves. Use for predictable,
12
+ * timed transitions.
13
+ *
14
+ * Both modes support `immediate: true` to skip animation and jump to
15
+ * the target value in a single frame.
16
+ */
17
+ import type { Cmd } from './types.js';
18
+ import { type SpringConfig, type SpringPreset, type EasingFn } from './spring.js';
19
+ interface AnimateBase<M> {
20
+ /** Starting value. */
21
+ readonly from: number;
22
+ /** Target value. */
23
+ readonly to: number;
24
+ /** Frames per second. Default: 60. */
25
+ readonly fps?: number;
26
+ /** Skip animation — jump to target in one frame. Default: false. */
27
+ readonly immediate?: boolean;
28
+ /** Called each frame with the interpolated value. Return a message for TEA. */
29
+ readonly onFrame: (value: number) => M;
30
+ /** Optional message to emit when the animation is fully complete. */
31
+ readonly onComplete?: () => M;
32
+ }
33
+ export interface SpringAnimateOptions<M> extends AnimateBase<M> {
34
+ readonly type?: 'spring';
35
+ /** Spring config — preset name or custom values. */
36
+ readonly spring?: Partial<SpringConfig> | SpringPreset;
37
+ }
38
+ export interface TweenAnimateOptions<M> extends AnimateBase<M> {
39
+ readonly type: 'tween';
40
+ /** Duration in milliseconds. */
41
+ readonly duration: number;
42
+ /** Easing function. Default: easeOutCubic. */
43
+ readonly ease?: EasingFn;
44
+ }
45
+ export type AnimateOptions<M> = SpringAnimateOptions<M> | TweenAnimateOptions<M>;
46
+ /**
47
+ * Create a TEA command that drives an animation.
48
+ *
49
+ * Spring mode (default):
50
+ * ```ts
51
+ * animate({ from: 0, to: 100, spring: 'wobbly', onFrame: (v) => ({ type: 'scroll', y: v }) })
52
+ * ```
53
+ */
54
+ export declare function animate<M>(options: AnimateOptions<M>): Cmd<M>;
55
+ /**
56
+ * Run animations in sequence. Each animation completes before the next starts.
57
+ */
58
+ export declare function sequence<M>(...cmds: Cmd<M>[]): Cmd<M>;
59
+ export {};
60
+ //# sourceMappingURL=animate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animate.d.ts","sourceRoot":"","sources":["../src/animate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,QAAQ,EAQd,MAAM,aAAa,CAAC;AAMrB,UAAU,WAAW,CAAC,CAAC;IACrB,sBAAsB;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC,CAAE,SAAQ,WAAW,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;IACzB,oDAAoD;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC;CACxD;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC,CAAE,SAAQ,WAAW,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,gCAAgC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;CAC1B;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,oBAAoB,CAAC,CAAC,CAAC,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAMjF;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAiB7D;AAsED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAMrD"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * TEA-integrated animation commands.
3
+ *
4
+ * GSAP-style API: `animate()` returns a `Cmd` that fires `onFrame`
5
+ * messages as the animation progresses, fitting naturally into the
6
+ * TEA update cycle.
7
+ *
8
+ * Two animation modes:
9
+ * - **spring**: Physics-based (default). No fixed duration — runs until
10
+ * the spring settles. Use for organic, responsive motion.
11
+ * - **tween**: Duration-based with easing curves. Use for predictable,
12
+ * timed transitions.
13
+ *
14
+ * Both modes support `immediate: true` to skip animation and jump to
15
+ * the target value in a single frame.
16
+ */
17
+ import { springStep, createSpringState, resolveSpringConfig, tweenStep, createTweenState, resolveTweenConfig, EASINGS, } from './spring.js';
18
+ // ---------------------------------------------------------------------------
19
+ // animate() — the main API
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Create a TEA command that drives an animation.
23
+ *
24
+ * Spring mode (default):
25
+ * ```ts
26
+ * animate({ from: 0, to: 100, spring: 'wobbly', onFrame: (v) => ({ type: 'scroll', y: v }) })
27
+ * ```
28
+ */
29
+ export function animate(options) {
30
+ const { from, to, fps = 60, immediate = false, onFrame, onComplete } = options;
31
+ // Immediate mode — single frame, no physics
32
+ if (immediate) {
33
+ return async (emit) => {
34
+ emit(onFrame(to));
35
+ if (onComplete)
36
+ emit(onComplete());
37
+ };
38
+ }
39
+ if (options.type === 'tween') {
40
+ return createTweenCmd(from, to, options.duration, options.ease ?? EASINGS.easeOutCubic, fps, onFrame, onComplete);
41
+ }
42
+ const config = resolveSpringConfig(options.spring);
43
+ return createSpringCmd(from, to, config, fps, onFrame, onComplete);
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // Internal: spring command
47
+ // ---------------------------------------------------------------------------
48
+ function createSpringCmd(from, to, config, fps, onFrame, onComplete) {
49
+ return (emit) => new Promise((resolve) => {
50
+ let state = createSpringState(from);
51
+ const dt = 1 / fps;
52
+ const intervalMs = Math.round(1000 / fps);
53
+ const id = setInterval(() => {
54
+ state = springStep(state, to, config, dt);
55
+ emit(onFrame(state.value));
56
+ if (state.done) {
57
+ clearInterval(id);
58
+ if (onComplete)
59
+ emit(onComplete());
60
+ resolve();
61
+ }
62
+ }, intervalMs);
63
+ });
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Internal: tween command
67
+ // ---------------------------------------------------------------------------
68
+ function createTweenCmd(from, to, duration, ease, fps, onFrame, onComplete) {
69
+ const config = resolveTweenConfig({ from, to, duration, ease });
70
+ return (emit) => new Promise((resolve) => {
71
+ let state = createTweenState(from);
72
+ const intervalMs = Math.round(1000 / fps);
73
+ const id = setInterval(() => {
74
+ state = tweenStep(state, config, intervalMs);
75
+ emit(onFrame(state.value));
76
+ if (state.done) {
77
+ clearInterval(id);
78
+ if (onComplete)
79
+ emit(onComplete());
80
+ resolve();
81
+ }
82
+ }, intervalMs);
83
+ });
84
+ }
85
+ // ---------------------------------------------------------------------------
86
+ // sequence() — chain animations like a GSAP timeline
87
+ // ---------------------------------------------------------------------------
88
+ /**
89
+ * Run animations in sequence. Each animation completes before the next starts.
90
+ */
91
+ export function sequence(...cmds) {
92
+ return async (emit) => {
93
+ for (const cmd of cmds) {
94
+ await cmd(emit);
95
+ }
96
+ };
97
+ }
98
+ //# sourceMappingURL=animate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animate.js","sourceRoot":"","sources":["../src/animate.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,GAAG,EAAmB,MAAM,YAAY,CAAC;AAE7D,gDAAgD;AAChD,wBAAgB,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAEhC;AAED,gEAAgE;AAChE,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAIlD;AAED,0DAA0D;AAC1D,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAEpD"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,GAAG,EAAmB,MAAM,YAAY,CAAC;AAE7D,gDAAgD;AAChD,wBAAgB,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAEhC;AAED,gEAAgE;AAChE,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAKlD;AAED,0DAA0D;AAC1D,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAEpD"}
package/dist/commands.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { QUIT } from './types.js';
2
2
  /** Command that signals the runtime to quit. */
3
3
  export function quit() {
4
- return () => Promise.resolve(QUIT);
4
+ return async (_emit) => QUIT;
5
5
  }
6
6
  /** Command that delivers a message after a delay (one-shot). */
7
7
  export function tick(ms, msg) {
8
- return () => new Promise((resolve) => {
8
+ return (_emit) => new Promise((resolve) => {
9
9
  setTimeout(() => resolve(msg), ms);
10
10
  });
11
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAA6B,MAAM,YAAY,CAAC;AAE7D,gDAAgD;AAChD,MAAM,UAAU,IAAI;IAClB,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAkB,CAAC,CAAC;AACnD,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,IAAI,CAAI,EAAU,EAAE,GAAM;IACxC,OAAO,GAAG,EAAE,CAAC,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,EAAE;QACtC,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,KAAK,CAAI,GAAG,IAAc;IACxC,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAA6B,MAAM,YAAY,CAAC;AAE7D,gDAAgD;AAChD,MAAM,UAAU,IAAI;IAClB,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,IAAkB,CAAC;AAC7C,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,IAAI,CAAI,EAAU,EAAE,GAAM;IACxC,OAAO,CAAC,KAAK,EAAE,EAAE,CACf,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,EAAE;QACzB,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACP,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,KAAK,CAAI,GAAG,IAAc;IACxC,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Centralized event bus for TUI applications.
3
+ *
4
+ * Unifies all input sources (keyboard, resize, commands, custom) into
5
+ * a single typed event stream. The TEA runtime subscribes to the bus
6
+ * instead of manually wiring callbacks.
7
+ *
8
+ * ```ts
9
+ * const bus = createEventBus<Msg>();
10
+ *
11
+ * // Connect I/O sources (keyboard + resize)
12
+ * bus.connectIO(ctx.io);
13
+ *
14
+ * // Subscribe to all events
15
+ * bus.on((msg) => {
16
+ * const [model, cmds] = app.update(msg, model);
17
+ * cmds.forEach(cmd => bus.runCmd(cmd));
18
+ * });
19
+ *
20
+ * // Emit custom events
21
+ * bus.emit({ type: 'tick' });
22
+ *
23
+ * // In tests — emit directly, no I/O needed
24
+ * bus.emit({ type: 'key', key: 'a', ctrl: false, alt: false, shift: false });
25
+ * ```
26
+ */
27
+ import type { IOPort } from '@flyingrobots/bijou';
28
+ import type { Cmd, KeyMsg, ResizeMsg } from './types.js';
29
+ /** Any message the bus can carry — built-in or app-defined. */
30
+ export type BusMsg<M> = KeyMsg | ResizeMsg | M;
31
+ export interface EventBus<M> {
32
+ /**
33
+ * Subscribe to all events. Returns a dispose function.
34
+ * Multiple subscribers are supported — all receive every event.
35
+ */
36
+ on(handler: (msg: BusMsg<M>) => void): Disposable;
37
+ /** Emit a message to all subscribers. */
38
+ emit(msg: BusMsg<M>): void;
39
+ /**
40
+ * Connect keyboard and resize sources from an IOPort.
41
+ * Raw stdin bytes are parsed into KeyMsg automatically.
42
+ * Returns a dispose function that disconnects both.
43
+ */
44
+ connectIO(io: IOPort): Disposable;
45
+ /**
46
+ * Run a command. The command receives the bus's `emit` function to
47
+ * dispatch intermediate messages during execution. When it resolves:
48
+ * - QUIT signal → fires onQuit handlers
49
+ * - Message → emitted to all subscribers
50
+ * - void/undefined → ignored
51
+ *
52
+ * Rejected commands are logged to stderr via `console.error`.
53
+ */
54
+ runCmd(cmd: Cmd<M>): void;
55
+ /**
56
+ * Register a quit handler. Called when a command resolves to QUIT.
57
+ * Separate from `on()` so the runtime can handle shutdown without
58
+ * the app needing to filter for it.
59
+ */
60
+ onQuit(handler: () => void): Disposable;
61
+ /** Disconnect all sources and remove all subscribers. */
62
+ dispose(): void;
63
+ }
64
+ interface Disposable {
65
+ dispose(): void;
66
+ }
67
+ export declare function createEventBus<M>(): EventBus<M>;
68
+ export {};
69
+ //# sourceMappingURL=eventbus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventbus.d.ts","sourceRoot":"","sources":["../src/eventbus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAQzD,+DAA+D;AAC/D,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,MAAM,GAAG,SAAS,GAAG,CAAC,CAAC;AAE/C,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB;;;OAGG;IACH,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,UAAU,CAAC;IAElD,yCAAyC;IACzC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAE3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,CAAC;IAElC;;;;;;;;OAQG;IACH,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAE1B;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,UAAU,CAAC;IAExC,yDAAyD;IACzD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,UAAU,UAAU;IAClB,OAAO,IAAI,IAAI,CAAC;CACjB;AAMD,wBAAgB,cAAc,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CA0F/C"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Centralized event bus for TUI applications.
3
+ *
4
+ * Unifies all input sources (keyboard, resize, commands, custom) into
5
+ * a single typed event stream. The TEA runtime subscribes to the bus
6
+ * instead of manually wiring callbacks.
7
+ *
8
+ * ```ts
9
+ * const bus = createEventBus<Msg>();
10
+ *
11
+ * // Connect I/O sources (keyboard + resize)
12
+ * bus.connectIO(ctx.io);
13
+ *
14
+ * // Subscribe to all events
15
+ * bus.on((msg) => {
16
+ * const [model, cmds] = app.update(msg, model);
17
+ * cmds.forEach(cmd => bus.runCmd(cmd));
18
+ * });
19
+ *
20
+ * // Emit custom events
21
+ * bus.emit({ type: 'tick' });
22
+ *
23
+ * // In tests — emit directly, no I/O needed
24
+ * bus.emit({ type: 'key', key: 'a', ctrl: false, alt: false, shift: false });
25
+ * ```
26
+ */
27
+ import { QUIT } from './types.js';
28
+ import { parseKey } from './keys.js';
29
+ // ---------------------------------------------------------------------------
30
+ // Implementation
31
+ // ---------------------------------------------------------------------------
32
+ export function createEventBus() {
33
+ const subscribers = new Set();
34
+ const quitHandlers = new Set();
35
+ const disposables = [];
36
+ let disposed = false;
37
+ function emit(msg) {
38
+ if (disposed)
39
+ return;
40
+ for (const handler of subscribers) {
41
+ handler(msg);
42
+ }
43
+ }
44
+ return {
45
+ on(handler) {
46
+ subscribers.add(handler);
47
+ return {
48
+ dispose() {
49
+ subscribers.delete(handler);
50
+ },
51
+ };
52
+ },
53
+ emit,
54
+ connectIO(io) {
55
+ // Keyboard input
56
+ const inputHandle = io.rawInput((raw) => {
57
+ if (disposed)
58
+ return;
59
+ const keyMsg = parseKey(raw);
60
+ // Skip unknown sequences (mouse events, etc.)
61
+ if (keyMsg.key === 'unknown')
62
+ return;
63
+ emit(keyMsg);
64
+ });
65
+ // Resize events
66
+ const resizeHandle = io.onResize((columns, rows) => {
67
+ if (disposed)
68
+ return;
69
+ const msg = { type: 'resize', columns, rows };
70
+ emit(msg);
71
+ });
72
+ const handle = {
73
+ dispose() {
74
+ inputHandle.dispose();
75
+ resizeHandle.dispose();
76
+ },
77
+ };
78
+ disposables.push(handle);
79
+ return handle;
80
+ },
81
+ runCmd(cmd) {
82
+ if (disposed)
83
+ return;
84
+ void cmd(emit).then((result) => {
85
+ if (disposed)
86
+ return;
87
+ if (result === QUIT) {
88
+ for (const handler of quitHandlers) {
89
+ handler();
90
+ }
91
+ return;
92
+ }
93
+ if (result !== undefined) {
94
+ emit(result);
95
+ }
96
+ }).catch((err) => {
97
+ // Surface command rejections instead of leaving unhandled promise rejections.
98
+ console.error('[EventBus] Command rejected:', err);
99
+ });
100
+ },
101
+ onQuit(handler) {
102
+ quitHandlers.add(handler);
103
+ return {
104
+ dispose() {
105
+ quitHandlers.delete(handler);
106
+ },
107
+ };
108
+ },
109
+ dispose() {
110
+ disposed = true;
111
+ for (const d of disposables) {
112
+ d.dispose();
113
+ }
114
+ disposables.length = 0;
115
+ subscribers.clear();
116
+ quitHandlers.clear();
117
+ },
118
+ };
119
+ }
120
+ //# sourceMappingURL=eventbus.js.map