@flyingrobots/bijou-tui 0.1.0 → 0.2.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 +221 -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,50 @@
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.2.0?
8
+
9
+ - **Industrial-Grade Renderer** — Flicker-free, scroll-safe rendering loop with `WRAP_DISABLE` and `CLEAR_LINE_TO_END` support.
10
+ - **Spring animation engine** — physics-based springs with 6 presets, plus multi-frame emission for 60fps+ fluidity.
11
+ - **`animate()`** — GSAP-style animation commands for TEA, with `onComplete` signals and `immediate: true` for reduced-motion.
12
+ - **`viewport()`** — scrollable content pane with proportional scrollbar and ANSI-aware clipping.
13
+ - **`flex()`** — flexbox layout with grow/basis/min/max, true horizontal centering, and auto-reflow.
14
+ - **`ResizeMsg`** — terminal resize events auto-dispatched by the runtime.
15
+ - **`EventBus`** — centralized typed event emitter unifying keyboard, resize, and multi-message commands.
16
+ - **Keybinding manager** — `createKeyMap()` for declarative key binding with modifiers, named groups, runtime enable/disable.
17
+ - **Help generator** — `helpView()`, `helpShort()`, `helpFor()` auto-generated from registered keybindings.
18
+ - **Input stack** — `createInputStack()` for layered input dispatch with opaque (modal) and passthrough layers.
19
+
20
+ See the [CHANGELOG](https://github.com/flyingrobots/bijou/blob/main/CHANGELOG.md) for the full release history.
6
21
 
7
22
  ## Install
8
23
 
9
24
  ```bash
10
- npm install @flyingrobots/bijou @flyingrobots/bijou-tui
25
+ npm install @flyingrobots/bijou @flyingrobots/bijou-node @flyingrobots/bijou-tui
11
26
  ```
12
27
 
13
28
  ## Quick Start
14
29
 
15
30
  ```typescript
31
+ import { initDefaultContext } from '@flyingrobots/bijou-node';
16
32
  import { run, quit, tick, type App, type KeyMsg } from '@flyingrobots/bijou-tui';
17
33
 
34
+ initDefaultContext();
35
+
18
36
  type Model = { count: number };
19
37
 
20
38
  const app: App<Model> = {
21
- init: () => [{ count: 0 }, tick(1000)],
39
+ init: () => [{ count: 0 }, []],
22
40
 
23
41
  update: (msg, model) => {
24
42
  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)];
43
+ if (msg.key === 'q') return [model, [quit()]];
44
+ if (msg.key === '+') return [{ count: model.count + 1 }, []];
45
+ if (msg.key === '-') return [{ count: model.count - 1 }, []];
31
46
  }
32
- return [model, null];
47
+ return [model, []];
33
48
  },
34
49
 
35
50
  view: (model) => `Count: ${model.count}\n\nPress +/- to change, q to quit`,
@@ -38,35 +53,211 @@ const app: App<Model> = {
38
53
  run(app);
39
54
  ```
40
55
 
41
- ## API
56
+ ## Animation
57
+
58
+ ### Spring Physics
59
+
60
+ ```typescript
61
+ import { animate, SPRING_PRESETS } from '@flyingrobots/bijou-tui';
62
+
63
+ // Physics-based (default) — runs until the spring settles
64
+ const cmd = animate({
65
+ from: 0,
66
+ to: 100,
67
+ spring: 'wobbly', // or 'default', 'gentle', 'stiff', 'slow', 'molasses'
68
+ onFrame: (v) => ({ type: 'scroll', y: v }),
69
+ });
70
+
71
+ // Duration-based with easing
72
+ const fade = animate({
73
+ type: 'tween',
74
+ from: 0,
75
+ to: 1,
76
+ duration: 300,
77
+ ease: EASINGS.easeOutCubic,
78
+ onFrame: (v) => ({ type: 'fade', opacity: v }),
79
+ });
80
+
81
+ // Skip animation (reduced motion)
82
+ const jump = animate({
83
+ from: 0, to: 100,
84
+ immediate: true,
85
+ onFrame: (v) => ({ type: 'scroll', y: v }),
86
+ });
87
+ ```
88
+
89
+ ### Timeline
42
90
 
43
- ### Core Types
91
+ GSAP-style orchestration — pure state machine, no timers:
44
92
 
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
93
+ ```typescript
94
+ import { timeline } from '@flyingrobots/bijou-tui';
49
95
 
50
- ### Commands
96
+ const tl = timeline()
97
+ .add('slideIn', { type: 'tween', from: -100, to: 0, duration: 300 })
98
+ .add('fadeIn', { type: 'tween', from: 0, to: 1, duration: 200 }, '-=100')
99
+ .label('settled')
100
+ .add('bounce', { from: 0, to: 10, spring: 'wobbly' }, 'settled')
101
+ .call('onReady', 'settled+=50')
102
+ .build();
51
103
 
52
- - **`quit()`** exit the app
53
- - **`tick(ms)`** — schedule a tick after `ms` milliseconds
54
- - **`batch(...cmds)`** combine multiple commands
104
+ // Drive from TEA update:
105
+ let tlState = tl.init();
106
+ // on each frame:
107
+ tlState = tl.step(tlState, 1/60);
108
+ const { slideIn, fadeIn, bounce } = tl.values(tlState);
109
+ const fired = tl.firedCallbacks(prev, tlState); // ['onReady']
110
+ ```
111
+
112
+ Position syntax: `'<'` (parallel), `'+=N'` (gap), `'-=N'` (overlap), `'<+=N'` (offset from previous start), absolute ms, `'label'`, `'label+=N'`.
55
113
 
56
- ### Screen Control
114
+ ## Layout
57
115
 
58
- - **`enterScreen()` / `exitScreen()`** — alt screen buffer management
59
- - **`clearAndHome()`** — clear screen and move cursor to top-left
60
- - **`renderFrame(content)`** — efficient frame rendering
116
+ ### Flexbox
61
117
 
62
- ### Layout
118
+ ```typescript
119
+ import { flex } from '@flyingrobots/bijou-tui';
63
120
 
64
- - **`vstack(...lines)`** vertical stack (join with newlines)
65
- - **`hstack(...cols)`** horizontal stack (side-by-side columns)
121
+ // Sidebar + main content, responsive to terminal width
122
+ flex({ direction: 'row', width: cols, height: rows, gap: 1 },
123
+ { basis: 20, content: sidebarText },
124
+ { flex: 1, content: (w, h) => renderMain(w, h) },
125
+ );
66
126
 
67
- ### Key Parsing
127
+ // Header + body + footer
128
+ flex({ direction: 'column', width: cols, height: rows },
129
+ { basis: 1, content: headerLine },
130
+ { flex: 1, content: (w, h) => renderBody(w, h) },
131
+ { basis: 1, content: statusLine },
132
+ );
133
+ ```
134
+
135
+ Children can be **render functions** `(width, height) => string` — they receive their allocated space and reflow automatically when the terminal resizes.
136
+
137
+ ### Viewport
138
+
139
+ ```typescript
140
+ import { viewport, createScrollState, scrollBy, pageDown } from '@flyingrobots/bijou-tui';
141
+
142
+ let scroll = createScrollState(content, viewportHeight);
143
+
144
+ // Render visible window with scrollbar
145
+ const view = viewport({ width: 60, height: 20, content, scrollY: scroll.y });
146
+
147
+ // Handle scroll keys
148
+ scroll = scrollBy(scroll, 1); // down one line
149
+ scroll = pageDown(scroll); // down one page
150
+ ```
151
+
152
+ ### Basic Layout
153
+
154
+ ```typescript
155
+ import { vstack, hstack } from '@flyingrobots/bijou-tui';
156
+
157
+ vstack(header, content, footer); // vertical stack
158
+ hstack(2, leftPanel, rightPanel); // side-by-side with gap
159
+ ```
160
+
161
+ ## Resize Handling
162
+
163
+ Terminal resize events are dispatched automatically as `ResizeMsg`:
164
+
165
+ ```typescript
166
+ update(msg, model) {
167
+ if (msg.type === 'resize') {
168
+ return [{ ...model, cols: msg.columns, rows: msg.rows }, []];
169
+ }
170
+ // ...
171
+ }
172
+
173
+ view(model) {
174
+ return flex(
175
+ { direction: 'row', width: model.cols, height: model.rows },
176
+ { basis: 20, content: sidebar },
177
+ { flex: 1, content: (w, h) => mainContent(w, h) },
178
+ );
179
+ }
180
+ ```
181
+
182
+ ## Event Bus
183
+
184
+ The runtime uses an `EventBus` internally. You can also create your own for custom event sources:
185
+
186
+ ```typescript
187
+ import { createEventBus } from '@flyingrobots/bijou-tui';
188
+
189
+ const bus = createEventBus<MyMsg>();
190
+ bus.connectIO(ctx.io); // keyboard + resize
191
+ bus.on((msg) => { /* ... */ }); // single subscription
192
+ bus.emit(customMsg); // synthetic events
193
+ bus.runCmd(someCommand); // command results re-emitted
194
+ bus.dispose(); // clean shutdown
195
+ ```
196
+
197
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full event flow and [GUIDE.md](./GUIDE.md) for detailed usage patterns.
198
+
199
+ ## Keybinding Manager
200
+
201
+ Declarative key binding with modifier support, named groups, and runtime enable/disable:
202
+
203
+ ```typescript
204
+ import { createKeyMap, type KeyMsg } from '@flyingrobots/bijou-tui';
205
+
206
+ type Msg = { type: 'quit' } | { type: 'help' } | { type: 'move'; dir: string };
207
+
208
+ const kb = createKeyMap<Msg>()
209
+ .bind('q', 'Quit', { type: 'quit' })
210
+ .bind('?', 'Help', { type: 'help' })
211
+ .bind('ctrl+c', 'Force quit', { type: 'quit' })
212
+ .group('Navigation', (g) => g
213
+ .bind('j', 'Down', { type: 'move', dir: 'down' })
214
+ .bind('k', 'Up', { type: 'move', dir: 'up' })
215
+ );
216
+
217
+ // In TEA update:
218
+ const action = kb.handle(keyMsg);
219
+ if (action !== undefined) return [model, [/* ... */]];
220
+
221
+ // Runtime enable/disable
222
+ kb.disableGroup('Navigation');
223
+ kb.enable('Quit');
224
+ ```
225
+
226
+ ### Help Generation
227
+
228
+ Auto-generate help text from registered bindings:
229
+
230
+ ```typescript
231
+ import { helpView, helpShort, helpFor } from '@flyingrobots/bijou-tui';
232
+
233
+ helpView(kb); // full grouped multi-line help
234
+ helpShort(kb); // "q Quit • ? Help • Ctrl+c Force quit • j Down • k Up"
235
+ helpFor(kb, 'Nav'); // only Navigation group
236
+ ```
237
+
238
+ ### Input Stack
239
+
240
+ Layered input dispatch for modal UIs — push/pop handlers with opaque or passthrough behavior:
241
+
242
+ ```typescript
243
+ import { createInputStack, type KeyMsg } from '@flyingrobots/bijou-tui';
244
+
245
+ const stack = createInputStack<KeyMsg, Msg>();
246
+
247
+ // Base layer — global keys, lets unmatched events fall through
248
+ stack.push(appKeys, { passthrough: true });
249
+
250
+ // Modal opens — captures all input (opaque by default)
251
+ const modalId = stack.push(modalKeys);
252
+
253
+ // Dispatch returns first matched action, top-down
254
+ const action = stack.dispatch(keyMsg);
255
+
256
+ // Modal closes
257
+ stack.remove(modalId);
258
+ ```
68
259
 
69
- - **`parseKey(data)`** parse raw stdin bytes into a `KeyMsg`
260
+ `KeyMap` implements `InputHandler`, so it plugs directly into the input stack.
70
261
 
71
262
  ## Related Packages
72
263
 
@@ -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"}