silvery 0.3.0 → 0.4.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 (88) hide show
  1. package/README.md +41 -145
  2. package/package.json +64 -12
  3. package/src/index.ts +67 -1
  4. package/src/runtime.ts +4 -0
  5. package/src/theme.ts +4 -0
  6. package/src/ui/animation.ts +2 -0
  7. package/src/ui/ansi.ts +2 -0
  8. package/src/ui/cli.ts +2 -0
  9. package/src/ui/display.ts +2 -0
  10. package/src/ui/image.ts +2 -0
  11. package/src/ui/input.ts +2 -0
  12. package/src/ui/progress.ts +2 -0
  13. package/src/ui/react.ts +2 -0
  14. package/src/ui/utils.ts +2 -0
  15. package/src/ui/wrappers.ts +2 -0
  16. package/src/ui.ts +4 -0
  17. package/examples/CLAUDE.md +0 -75
  18. package/examples/_banner.tsx +0 -60
  19. package/examples/cli.ts +0 -228
  20. package/examples/index.md +0 -101
  21. package/examples/inline/inline-nontty.tsx +0 -98
  22. package/examples/inline/inline-progress.tsx +0 -79
  23. package/examples/inline/inline-simple.tsx +0 -63
  24. package/examples/inline/scrollback.tsx +0 -185
  25. package/examples/interactive/_input-debug.tsx +0 -110
  26. package/examples/interactive/_stdin-test.ts +0 -71
  27. package/examples/interactive/_textarea-bare.tsx +0 -45
  28. package/examples/interactive/aichat/components.tsx +0 -468
  29. package/examples/interactive/aichat/index.tsx +0 -207
  30. package/examples/interactive/aichat/script.ts +0 -460
  31. package/examples/interactive/aichat/state.ts +0 -326
  32. package/examples/interactive/aichat/types.ts +0 -19
  33. package/examples/interactive/app-todo.tsx +0 -198
  34. package/examples/interactive/async-data.tsx +0 -208
  35. package/examples/interactive/cli-wizard.tsx +0 -332
  36. package/examples/interactive/clipboard.tsx +0 -183
  37. package/examples/interactive/components.tsx +0 -463
  38. package/examples/interactive/data-explorer.tsx +0 -506
  39. package/examples/interactive/dev-tools.tsx +0 -379
  40. package/examples/interactive/explorer.tsx +0 -747
  41. package/examples/interactive/gallery.tsx +0 -652
  42. package/examples/interactive/inline-bench.tsx +0 -136
  43. package/examples/interactive/kanban.tsx +0 -267
  44. package/examples/interactive/layout-ref.tsx +0 -185
  45. package/examples/interactive/outline.tsx +0 -171
  46. package/examples/interactive/paste-demo.tsx +0 -198
  47. package/examples/interactive/scroll.tsx +0 -77
  48. package/examples/interactive/search-filter.tsx +0 -240
  49. package/examples/interactive/task-list.tsx +0 -279
  50. package/examples/interactive/terminal.tsx +0 -798
  51. package/examples/interactive/textarea.tsx +0 -103
  52. package/examples/interactive/theme.tsx +0 -336
  53. package/examples/interactive/transform.tsx +0 -256
  54. package/examples/interactive/virtual-10k.tsx +0 -413
  55. package/examples/kitty/canvas.tsx +0 -519
  56. package/examples/kitty/generate-samples.ts +0 -236
  57. package/examples/kitty/image-component.tsx +0 -273
  58. package/examples/kitty/images.tsx +0 -604
  59. package/examples/kitty/input.tsx +0 -371
  60. package/examples/kitty/keys.tsx +0 -378
  61. package/examples/kitty/paint.tsx +0 -1017
  62. package/examples/layout/dashboard.tsx +0 -551
  63. package/examples/layout/live-resize.tsx +0 -290
  64. package/examples/layout/overflow.tsx +0 -51
  65. package/examples/playground/README.md +0 -69
  66. package/examples/playground/build.ts +0 -61
  67. package/examples/playground/index.html +0 -420
  68. package/examples/playground/playground-app.tsx +0 -416
  69. package/examples/runtime/elm-counter.tsx +0 -206
  70. package/examples/runtime/hello-runtime.tsx +0 -73
  71. package/examples/runtime/pipe-composition.tsx +0 -184
  72. package/examples/runtime/run-counter.tsx +0 -78
  73. package/examples/runtime/runtime-counter.tsx +0 -197
  74. package/examples/screenshots/generate.tsx +0 -563
  75. package/examples/scrollback-perf.tsx +0 -230
  76. package/examples/viewer.tsx +0 -654
  77. package/examples/web/build.ts +0 -365
  78. package/examples/web/canvas-app.tsx +0 -80
  79. package/examples/web/canvas.html +0 -89
  80. package/examples/web/dom-app.tsx +0 -81
  81. package/examples/web/dom.html +0 -113
  82. package/examples/web/showcase-app.tsx +0 -107
  83. package/examples/web/showcase.html +0 -34
  84. package/examples/web/showcases/index.tsx +0 -56
  85. package/examples/web/viewer-app.tsx +0 -555
  86. package/examples/web/viewer.html +0 -30
  87. package/examples/web/xterm-app.tsx +0 -105
  88. package/examples/web/xterm.html +0 -118
@@ -1,416 +0,0 @@
1
- /**
2
- * Canvas Playground App
3
- *
4
- * Interactive demo showcasing silvery's Canvas adapter with multiple preset examples,
5
- * live resize, and theme controls. Communicates with the host page via window messages.
6
- */
7
-
8
- import React, { useState, useEffect, useCallback } from "react"
9
- import { renderToCanvas, Box, Text, useContentRect } from "../../src/canvas/index.js"
10
-
11
- // ============================================================================
12
- // Shared components
13
- // ============================================================================
14
-
15
- function SizeDisplay() {
16
- const { width, height } = useContentRect()
17
- return (
18
- <Text color="$success">
19
- {Math.round(width)}px x {Math.round(height)}px
20
- </Text>
21
- )
22
- }
23
-
24
- function Divider({ color = "$muted" }: { color?: string }) {
25
- const { width } = useContentRect()
26
- const line = "─".repeat(Math.max(1, Math.floor(width / 8)))
27
- return <Text color={color}>{line}</Text>
28
- }
29
-
30
- // ============================================================================
31
- // Preset: Hello World
32
- // ============================================================================
33
-
34
- function HelloWorld() {
35
- return (
36
- <Box flexDirection="column" padding={1}>
37
- <Box borderStyle="single" borderColor="$info" padding={1}>
38
- <Box flexDirection="column">
39
- <Text bold color="$info">
40
- Hello from silvery!
41
- </Text>
42
- <Text color="$muted">React components rendered to HTML5 Canvas</Text>
43
- <SizeDisplay />
44
- </Box>
45
- </Box>
46
- </Box>
47
- )
48
- }
49
-
50
- // ============================================================================
51
- // Preset: Text Styles
52
- // ============================================================================
53
-
54
- function TextStyles() {
55
- return (
56
- <Box flexDirection="column" padding={1}>
57
- <Text bold color="$warning">
58
- Text Styles
59
- </Text>
60
- <Divider />
61
- <Box flexDirection="column" marginTop={1}>
62
- <Box flexDirection="row" gap={2}>
63
- <Text>Normal</Text>
64
- <Text bold>Bold</Text>
65
- <Text italic>Italic</Text>
66
- <Text bold italic>
67
- Bold Italic
68
- </Text>
69
- </Box>
70
- <Box flexDirection="row" gap={2} marginTop={1}>
71
- <Text underline>Underline</Text>
72
- <Text strikethrough>Strikethrough</Text>
73
- <Text dim>Dim</Text>
74
- </Box>
75
- <Box flexDirection="row" gap={2} marginTop={1}>
76
- <Text underlineStyle="double" underline>
77
- Double
78
- </Text>
79
- <Text underlineStyle="curly" underlineColor="$error" underline>
80
- Curly Red
81
- </Text>
82
- <Text underlineStyle="dotted" underline>
83
- Dotted
84
- </Text>
85
- <Text underlineStyle="dashed" underline>
86
- Dashed
87
- </Text>
88
- </Box>
89
- </Box>
90
- </Box>
91
- )
92
- }
93
-
94
- // ============================================================================
95
- // Preset: Colors & Backgrounds
96
- // ============================================================================
97
-
98
- function ColorsAndBackgrounds() {
99
- const colors = [
100
- { bg: "red", fg: "white", label: "Red" },
101
- { bg: "green", fg: "black", label: "Green" },
102
- { bg: "blue", fg: "white", label: "Blue" },
103
- { bg: "yellow", fg: "black", label: "Yellow" },
104
- { bg: "magenta", fg: "white", label: "Magenta" },
105
- { bg: "cyan", fg: "black", label: "Cyan" },
106
- ]
107
-
108
- return (
109
- <Box flexDirection="column" padding={1}>
110
- <Text bold color="$warning">
111
- Colors and Backgrounds
112
- </Text>
113
- <Divider />
114
- <Box flexDirection="row" gap={1} marginTop={1}>
115
- {colors.map(({ bg, fg, label }) => (
116
- <Box key={bg} backgroundColor={bg} padding={1}>
117
- <Text color={fg}>{label}</Text>
118
- </Box>
119
- ))}
120
- </Box>
121
- <Box flexDirection="column" marginTop={1}>
122
- <Text color="red">Red text</Text>
123
- <Text color="green">Green text</Text>
124
- <Text color="blue">Blue text</Text>
125
- <Text color="brightYellow">Bright yellow text</Text>
126
- <Text color="brightCyan">Bright cyan text</Text>
127
- <Text color="#ff6b6b">Custom hex #ff6b6b</Text>
128
- <Text color="rgb(147, 130, 220)">Custom RGB</Text>
129
- </Box>
130
- </Box>
131
- )
132
- }
133
-
134
- // ============================================================================
135
- // Preset: Flexbox Layout
136
- // ============================================================================
137
-
138
- function FlexboxLayout() {
139
- return (
140
- <Box flexDirection="column" padding={1}>
141
- <Text bold color="$warning">
142
- Flexbox Layout
143
- </Text>
144
- <Divider />
145
-
146
- <Text color="$muted" marginTop={1}>
147
- Row (gap=1):
148
- </Text>
149
- <Box flexDirection="row" gap={1}>
150
- <Box borderStyle="single" borderColor="$error" padding={1} flexGrow={1}>
151
- <Text color="$error">Col 1</Text>
152
- </Box>
153
- <Box borderStyle="single" borderColor="$success" padding={1} flexGrow={2}>
154
- <Text color="$success">Col 2 (grow=2)</Text>
155
- </Box>
156
- <Box borderStyle="single" borderColor="$primary" padding={1} flexGrow={1}>
157
- <Text color="$primary">Col 3</Text>
158
- </Box>
159
- </Box>
160
-
161
- <Text color="$muted" marginTop={1}>
162
- Nested columns:
163
- </Text>
164
- <Box flexDirection="row" gap={1}>
165
- <Box borderStyle="round" borderColor="$accent" padding={1} flexGrow={1} flexDirection="column">
166
- <Text bold color="$accent">
167
- Panel A
168
- </Text>
169
- <Text>Item 1</Text>
170
- <Text>Item 2</Text>
171
- <Text>Item 3</Text>
172
- </Box>
173
- <Box borderStyle="round" borderColor="$info" padding={1} flexGrow={1} flexDirection="column">
174
- <Text bold color="$info">
175
- Panel B
176
- </Text>
177
- <Text>Item A</Text>
178
- <Text>Item B</Text>
179
- </Box>
180
- </Box>
181
- </Box>
182
- )
183
- }
184
-
185
- // ============================================================================
186
- // Preset: Border Styles
187
- // ============================================================================
188
-
189
- function BorderStyles() {
190
- const styles: Array<{ style: string; color: string }> = [
191
- { style: "single", color: "$info" },
192
- { style: "double", color: "$warning" },
193
- { style: "round", color: "$success" },
194
- { style: "bold", color: "$accent" },
195
- ]
196
-
197
- return (
198
- <Box flexDirection="column" padding={1}>
199
- <Text bold color="$warning">
200
- Border Styles
201
- </Text>
202
- <Divider />
203
- <Box flexDirection="row" gap={1} marginTop={1}>
204
- {styles.map(({ style, color }) => (
205
- <Box key={style} borderStyle={style as any} borderColor={color} padding={1} flexGrow={1}>
206
- <Box flexDirection="column">
207
- <Text bold color={color}>
208
- {style}
209
- </Text>
210
- <Text>border</Text>
211
- </Box>
212
- </Box>
213
- ))}
214
- </Box>
215
- </Box>
216
- )
217
- }
218
-
219
- // ============================================================================
220
- // Preset: Dashboard
221
- // ============================================================================
222
-
223
- function Dashboard() {
224
- return (
225
- <Box flexDirection="column" padding={1}>
226
- <Box borderStyle="single" borderColor="$info" padding={1}>
227
- <Text bold color="$info">
228
- System Dashboard
229
- </Text>
230
- </Box>
231
- <Box flexDirection="row" gap={1} marginTop={1}>
232
- <Box borderStyle="round" borderColor="$success" padding={1} flexGrow={1} flexDirection="column">
233
- <Text bold color="$success">
234
- CPU
235
- </Text>
236
- <Text color="$success">|||||||....</Text>
237
- <Text>65%</Text>
238
- </Box>
239
- <Box borderStyle="round" borderColor="$warning" padding={1} flexGrow={1} flexDirection="column">
240
- <Text bold color="$warning">
241
- Memory
242
- </Text>
243
- <Text color="$warning">|||||||||..</Text>
244
- <Text>82%</Text>
245
- </Box>
246
- <Box borderStyle="round" borderColor="$error" padding={1} flexGrow={1} flexDirection="column">
247
- <Text bold color="$error">
248
- Disk
249
- </Text>
250
- <Text color="$error">||||||||||.</Text>
251
- <Text>91%</Text>
252
- </Box>
253
- </Box>
254
- <Box borderStyle="single" borderColor="$muted" padding={1} marginTop={1} flexDirection="column">
255
- <Text bold>Recent Events</Text>
256
- <Text color="$success"> OK api-server healthy</Text>
257
- <Text color="$success"> OK database connected</Text>
258
- <Text color="$warning"> WARN cache miss rate high</Text>
259
- <Text color="$error"> ERR disk space low on /var</Text>
260
- </Box>
261
- </Box>
262
- )
263
- }
264
-
265
- // ============================================================================
266
- // Preset: Responsive
267
- // ============================================================================
268
-
269
- function Responsive() {
270
- const { width } = useContentRect()
271
- const isWide = width > 350
272
-
273
- return (
274
- <Box flexDirection="column" padding={1}>
275
- <Text bold color="$warning">
276
- Responsive Layout
277
- </Text>
278
- <Text color="$muted">Resize the canvas to see layout adapt ({Math.round(width)}px wide)</Text>
279
- <Divider />
280
- <Box flexDirection={isWide ? "row" : "column"} gap={1} marginTop={1}>
281
- <Box borderStyle="single" borderColor="$info" padding={1} flexGrow={1} flexDirection="column">
282
- <Text bold color="$info">
283
- Main Content
284
- </Text>
285
- <Text>This panel takes available space.</Text>
286
- <Text>Layout: {isWide ? "horizontal" : "vertical"}</Text>
287
- <SizeDisplay />
288
- </Box>
289
- <Box
290
- borderStyle="single"
291
- borderColor="$accent"
292
- padding={1}
293
- flexDirection="column"
294
- {...(isWide ? { width: 180 } : {})}
295
- >
296
- <Text bold color="$accent">
297
- Sidebar
298
- </Text>
299
- <Text>Fixed width when wide,</Text>
300
- <Text>full width when narrow.</Text>
301
- </Box>
302
- </Box>
303
- </Box>
304
- )
305
- }
306
-
307
- // ============================================================================
308
- // Presets Registry
309
- // ============================================================================
310
-
311
- const PRESETS: Record<string, { label: string; component: React.FC }> = {
312
- hello: { label: "Hello World", component: HelloWorld },
313
- text: { label: "Text Styles", component: TextStyles },
314
- colors: { label: "Colors", component: ColorsAndBackgrounds },
315
- flexbox: { label: "Flexbox", component: FlexboxLayout },
316
- borders: { label: "Borders", component: BorderStyles },
317
- dashboard: { label: "Dashboard", component: Dashboard },
318
- responsive: { label: "Responsive", component: Responsive },
319
- }
320
-
321
- // ============================================================================
322
- // Root App
323
- // ============================================================================
324
-
325
- function App({ preset: initialPreset }: { preset: string }) {
326
- const [currentPreset, setPreset] = useState(initialPreset)
327
-
328
- useEffect(() => {
329
- // Listen for preset changes from host page
330
- function handleMessage(e: MessageEvent) {
331
- if (e.data?.type === "set-preset" && PRESETS[e.data.preset]) {
332
- setPreset(e.data.preset)
333
- }
334
- }
335
- window.addEventListener("message", handleMessage)
336
- return () => window.removeEventListener("message", handleMessage)
337
- }, [])
338
-
339
- const Component = PRESETS[currentPreset]?.component ?? HelloWorld
340
- return <Component />
341
- }
342
-
343
- // ============================================================================
344
- // Mount
345
- // ============================================================================
346
-
347
- const canvas = document.getElementById("canvas") as HTMLCanvasElement
348
- if (canvas) {
349
- // Read initial preset from URL hash
350
- const hash = window.location.hash.slice(1)
351
- const initialPreset = PRESETS[hash] ? hash : "hello"
352
-
353
- let instance = renderToCanvas(<App preset={initialPreset} />, canvas, {
354
- fontSize: 14,
355
- fontFamily: "monospace",
356
- })
357
-
358
- // Handle resize
359
- function handleResize() {
360
- const container = canvas.parentElement
361
- if (!container) return
362
- const rect = container.getBoundingClientRect()
363
- const newWidth = Math.floor(rect.width)
364
- const newHeight = Math.max(300, Math.floor(rect.height))
365
- if (canvas.width !== newWidth || canvas.height !== newHeight) {
366
- canvas.width = newWidth
367
- canvas.height = newHeight
368
- // Re-render with new dimensions
369
- instance.unmount()
370
- const hash = window.location.hash.slice(1)
371
- const preset = PRESETS[hash] ? hash : "hello"
372
- instance = renderToCanvas(<App preset={preset} />, canvas, {
373
- fontSize: 14,
374
- fontFamily: "monospace",
375
- width: newWidth,
376
- height: newHeight,
377
- })
378
- }
379
- }
380
-
381
- // Listen for preset changes and re-render
382
- window.addEventListener("message", (e) => {
383
- if (e.data?.type === "set-preset" && PRESETS[e.data.preset]) {
384
- window.location.hash = e.data.preset
385
- instance.unmount()
386
- const container = canvas.parentElement
387
- const w = container ? Math.floor(container.getBoundingClientRect().width) : canvas.width
388
- const h = container ? Math.max(300, Math.floor(container.getBoundingClientRect().height)) : canvas.height
389
- canvas.width = w
390
- canvas.height = h
391
- instance = renderToCanvas(<App preset={e.data.preset} />, canvas, {
392
- fontSize: 14,
393
- fontFamily: "monospace",
394
- width: w,
395
- height: h,
396
- })
397
- }
398
- })
399
-
400
- const resizeObserver = new ResizeObserver(handleResize)
401
- const container = canvas.parentElement
402
- if (container) resizeObserver.observe(container)
403
-
404
- // Initial size
405
- handleResize()
406
-
407
- // Expose for debugging
408
- ;(window as any).silveryInstance = instance
409
- ;(window as any).PRESETS = Object.keys(PRESETS)
410
- }
411
-
412
- // Export presets list for the host page
413
- ;(window as any).PRESET_LIST = Object.entries(PRESETS).map(([id, { label }]) => ({
414
- id,
415
- label,
416
- }))
@@ -1,206 +0,0 @@
1
- /**
2
- * Elm Counter - Layer 1 Example
3
- *
4
- * Demonstrates the Elm-style architecture:
5
- * - User drives the event loop
6
- * - Pure reducer for state updates
7
- * - Pure view function for rendering
8
- *
9
- * Usage: bun examples/mode3-counter.tsx
10
- */
11
-
12
- import React from "react"
13
- import { Box, Text, H3, Muted } from "../../src/index.js"
14
- import {
15
- layout,
16
- diff,
17
- ensureLayoutEngine,
18
- createTick,
19
- merge,
20
- map,
21
- takeUntil,
22
- type Buffer,
23
- type Dims,
24
- } from "@silvery/term/runtime"
25
- import type { ExampleMeta } from "../_banner.js"
26
-
27
- export const meta: ExampleMeta = {
28
- name: "Elm Counter",
29
- description: "Pure functional Elm-style: reducer + view, no hooks",
30
- features: ["createRuntime()", "Elm architecture", "layout() + diff()"],
31
- }
32
-
33
- // ============================================================================
34
- // State
35
- // ============================================================================
36
-
37
- interface State {
38
- count: number
39
- running: boolean
40
- }
41
-
42
- // ============================================================================
43
- // Events
44
- // ============================================================================
45
-
46
- type Event = { type: "tick" } | { type: "key"; key: string } | { type: "quit" }
47
-
48
- // ============================================================================
49
- // Reducer (pure function)
50
- // ============================================================================
51
-
52
- function reducer(state: State, event: Event): State {
53
- switch (event.type) {
54
- case "tick":
55
- return { ...state, count: state.count + 1 }
56
- case "key":
57
- if (event.key === "q" || event.key === "\x03") {
58
- // q or ctrl+c
59
- return { ...state, running: false }
60
- }
61
- if (event.key === "r") {
62
- return { ...state, count: 0 }
63
- }
64
- return state
65
- case "quit":
66
- return { ...state, running: false }
67
- default:
68
- return state
69
- }
70
- }
71
-
72
- // ============================================================================
73
- // View (pure function)
74
- // ============================================================================
75
-
76
- function view(state: State): React.ReactElement {
77
- return (
78
- <Box flexDirection="column" padding={1}>
79
- <H3>Elm Counter Example</H3>
80
- <Text> </Text>
81
- <Text>
82
- Count: <Text color="green">{state.count}</Text>
83
- </Text>
84
- <Text> </Text>
85
- <Muted>Press 'r' to reset, 'q' to quit</Muted>
86
- </Box>
87
- )
88
- }
89
-
90
- // ============================================================================
91
- // Event Sources
92
- // ============================================================================
93
-
94
- /**
95
- * Create a keyboard event source from stdin.
96
- */
97
- function createKeyboardSource(signal: AbortSignal): AsyncIterable<Event> {
98
- return {
99
- async *[Symbol.asyncIterator]() {
100
- const stdin = process.stdin
101
- stdin.setRawMode(true)
102
- stdin.resume()
103
- stdin.setEncoding("utf8")
104
-
105
- try {
106
- while (!signal.aborted) {
107
- const key = await new Promise<string | null>((resolve) => {
108
- const onData = (data: string) => {
109
- stdin.off("data", onData)
110
- resolve(data)
111
- }
112
- const onAbort = () => {
113
- stdin.off("data", onData)
114
- resolve(null)
115
- }
116
- stdin.on("data", onData)
117
- signal.addEventListener("abort", onAbort, { once: true })
118
- })
119
-
120
- if (key === null || signal.aborted) break
121
- yield { type: "key" as const, key }
122
- }
123
- } finally {
124
- stdin.setRawMode(false)
125
- stdin.pause()
126
- }
127
- },
128
- }
129
- }
130
-
131
- // ============================================================================
132
- // Main Loop
133
- // ============================================================================
134
-
135
- async function main() {
136
- // Initialize layout engine
137
- await ensureLayoutEngine()
138
-
139
- // Get terminal dimensions
140
- const dims: Dims = {
141
- cols: process.stdout.columns || 80,
142
- rows: process.stdout.rows || 24,
143
- }
144
-
145
- // Initial state
146
- let state: State = { count: 0, running: true }
147
-
148
- // Abort controller for cleanup
149
- const controller = new AbortController()
150
-
151
- // Create event sources
152
- const ticks = map(createTick(100, controller.signal), () => ({
153
- type: "tick" as const,
154
- }))
155
- const keys = createKeyboardSource(controller.signal)
156
-
157
- // Merge all event sources
158
- const events = takeUntil(merge(ticks, keys), controller.signal)
159
-
160
- // Previous buffer for diffing
161
- let prevBuffer: Buffer | null = null
162
-
163
- // Clear screen and hide cursor
164
- process.stdout.write("\x1b[2J\x1b[H\x1b[?25l")
165
-
166
- try {
167
- // Initial render
168
- const buffer = layout(view(state), dims)
169
- const output = diff(null, buffer)
170
- process.stdout.write(output)
171
- prevBuffer = buffer
172
-
173
- // Event loop
174
- for await (const event of events) {
175
- // Update state
176
- const newState = reducer(state, event)
177
-
178
- // Check if we should exit
179
- if (!newState.running) {
180
- break
181
- }
182
-
183
- // Only re-render if state changed
184
- if (newState !== state) {
185
- state = newState
186
-
187
- // Render
188
- const buffer = layout(view(state), dims)
189
- const output = diff(prevBuffer, buffer)
190
- process.stdout.write(output)
191
- prevBuffer = buffer
192
- }
193
- }
194
- } finally {
195
- // Cleanup
196
- controller.abort()
197
-
198
- // Show cursor and reset
199
- process.stdout.write("\x1b[?25h\x1b[0m\n")
200
- }
201
- }
202
-
203
- // Run
204
- if (import.meta.main) {
205
- main().catch(console.error)
206
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * Hello Runtime - Minimal createRuntime Example
3
- *
4
- * The simplest possible example using the silvery-loop runtime.
5
- * Shows basic setup, rendering, and cleanup.
6
- *
7
- * Usage: bun examples/hello-runtime.tsx
8
- */
9
-
10
- import React from "react"
11
- import { Box, Text, H1, Muted } from "../../src/index.js"
12
- import { createRuntime, ensureLayoutEngine, layout, type Dims, type RenderTarget } from "@silvery/term/runtime"
13
- import type { ExampleMeta } from "../_banner.js"
14
-
15
- export const meta: ExampleMeta = {
16
- name: "Hello Runtime",
17
- description: "Simplest Layer 1 API: createRuntime(), layout(), Symbol.dispose",
18
- features: ["createRuntime()", "layout()", "renderString()"],
19
- }
20
-
21
- // Simple terminal target
22
- const termTarget: RenderTarget = {
23
- write: (frame) => process.stdout.write(frame),
24
- getDims: () => ({
25
- cols: process.stdout.columns || 80,
26
- rows: process.stdout.rows || 24,
27
- }),
28
- }
29
-
30
- // Simple view
31
- function HelloView({ name }: { name: string }): React.ReactElement {
32
- return (
33
- <Box flexDirection="column" padding={1}>
34
- <H1 color="green">Hello, {name}!</H1>
35
- <Muted>Welcome to silvery-loop</Muted>
36
- </Box>
37
- )
38
- }
39
-
40
- async function main() {
41
- // Initialize layout engine (required once)
42
- await ensureLayoutEngine()
43
-
44
- // Create runtime
45
- const runtime = createRuntime({ target: termTarget })
46
-
47
- // Render
48
- const buffer = layout(<HelloView name="World" />, runtime.getDims())
49
- runtime.render(buffer)
50
-
51
- // Wait a moment to see the output
52
- await new Promise((resolve) => setTimeout(resolve, 1000))
53
-
54
- // Update with new content
55
- const buffer2 = layout(<HelloView name="silvery-loop" />, runtime.getDims())
56
- runtime.render(buffer2)
57
-
58
- // Wait again
59
- await new Promise((resolve) => setTimeout(resolve, 1000))
60
-
61
- // Cleanup
62
- runtime[Symbol.dispose]()
63
-
64
- console.log("\nDone!")
65
- }
66
-
67
- if (import.meta.main) {
68
- try {
69
- await main()
70
- } catch (e) {
71
- console.error(e)
72
- }
73
- }