silvery 0.3.0 → 0.4.1
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.
- package/README.md +41 -145
- package/dist/chalk.js +3 -0
- package/dist/chalk.js.map +11 -0
- package/dist/index.js +340 -0
- package/dist/index.js.map +282 -0
- package/dist/ink.js +129 -0
- package/dist/ink.js.map +140 -0
- package/dist/runtime.js +394 -0
- package/dist/runtime.js.map +286 -0
- package/dist/theme.js +343 -0
- package/dist/theme.js.map +286 -0
- package/dist/ui/animation.js +3 -0
- package/dist/ui/animation.js.map +15 -0
- package/dist/ui/ansi.js +3 -0
- package/dist/ui/ansi.js.map +10 -0
- package/dist/ui/cli.js +8 -0
- package/dist/ui/cli.js.map +14 -0
- package/dist/ui/display.js +4 -0
- package/dist/ui/display.js.map +10 -0
- package/dist/ui/image.js +4 -0
- package/dist/ui/image.js.map +15 -0
- package/dist/ui/input.js +3 -0
- package/dist/ui/input.js.map +11 -0
- package/dist/ui/progress.js +8 -0
- package/dist/ui/progress.js.map +20 -0
- package/dist/ui/react.js +3 -0
- package/dist/ui/react.js.map +15 -0
- package/dist/ui/utils.js +3 -0
- package/dist/ui/utils.js.map +10 -0
- package/dist/ui/wrappers.js +14 -0
- package/dist/ui/wrappers.js.map +19 -0
- package/dist/ui.js +17 -0
- package/dist/ui.js.map +20 -0
- package/package.json +67 -15
- package/src/index.ts +67 -1
- package/src/runtime.ts +4 -0
- package/src/theme.ts +4 -0
- package/src/ui/animation.ts +2 -0
- package/src/ui/ansi.ts +2 -0
- package/src/ui/cli.ts +2 -0
- package/src/ui/display.ts +2 -0
- package/src/ui/image.ts +2 -0
- package/src/ui/input.ts +2 -0
- package/src/ui/progress.ts +2 -0
- package/src/ui/react.ts +2 -0
- package/src/ui/utils.ts +2 -0
- package/src/ui/wrappers.ts +2 -0
- package/src/ui.ts +4 -0
- package/examples/CLAUDE.md +0 -75
- package/examples/_banner.tsx +0 -60
- package/examples/cli.ts +0 -228
- package/examples/index.md +0 -101
- package/examples/inline/inline-nontty.tsx +0 -98
- package/examples/inline/inline-progress.tsx +0 -79
- package/examples/inline/inline-simple.tsx +0 -63
- package/examples/inline/scrollback.tsx +0 -185
- package/examples/interactive/_input-debug.tsx +0 -110
- package/examples/interactive/_stdin-test.ts +0 -71
- package/examples/interactive/_textarea-bare.tsx +0 -45
- package/examples/interactive/aichat/components.tsx +0 -468
- package/examples/interactive/aichat/index.tsx +0 -207
- package/examples/interactive/aichat/script.ts +0 -460
- package/examples/interactive/aichat/state.ts +0 -326
- package/examples/interactive/aichat/types.ts +0 -19
- package/examples/interactive/app-todo.tsx +0 -198
- package/examples/interactive/async-data.tsx +0 -208
- package/examples/interactive/cli-wizard.tsx +0 -332
- package/examples/interactive/clipboard.tsx +0 -183
- package/examples/interactive/components.tsx +0 -463
- package/examples/interactive/data-explorer.tsx +0 -506
- package/examples/interactive/dev-tools.tsx +0 -379
- package/examples/interactive/explorer.tsx +0 -747
- package/examples/interactive/gallery.tsx +0 -652
- package/examples/interactive/inline-bench.tsx +0 -136
- package/examples/interactive/kanban.tsx +0 -267
- package/examples/interactive/layout-ref.tsx +0 -185
- package/examples/interactive/outline.tsx +0 -171
- package/examples/interactive/paste-demo.tsx +0 -198
- package/examples/interactive/scroll.tsx +0 -77
- package/examples/interactive/search-filter.tsx +0 -240
- package/examples/interactive/task-list.tsx +0 -279
- package/examples/interactive/terminal.tsx +0 -798
- package/examples/interactive/textarea.tsx +0 -103
- package/examples/interactive/theme.tsx +0 -336
- package/examples/interactive/transform.tsx +0 -256
- package/examples/interactive/virtual-10k.tsx +0 -413
- package/examples/kitty/canvas.tsx +0 -519
- package/examples/kitty/generate-samples.ts +0 -236
- package/examples/kitty/image-component.tsx +0 -273
- package/examples/kitty/images.tsx +0 -604
- package/examples/kitty/input.tsx +0 -371
- package/examples/kitty/keys.tsx +0 -378
- package/examples/kitty/paint.tsx +0 -1017
- package/examples/layout/dashboard.tsx +0 -551
- package/examples/layout/live-resize.tsx +0 -290
- package/examples/layout/overflow.tsx +0 -51
- package/examples/playground/README.md +0 -69
- package/examples/playground/build.ts +0 -61
- package/examples/playground/index.html +0 -420
- package/examples/playground/playground-app.tsx +0 -416
- package/examples/runtime/elm-counter.tsx +0 -206
- package/examples/runtime/hello-runtime.tsx +0 -73
- package/examples/runtime/pipe-composition.tsx +0 -184
- package/examples/runtime/run-counter.tsx +0 -78
- package/examples/runtime/runtime-counter.tsx +0 -197
- package/examples/screenshots/generate.tsx +0 -563
- package/examples/scrollback-perf.tsx +0 -230
- package/examples/viewer.tsx +0 -654
- package/examples/web/build.ts +0 -365
- package/examples/web/canvas-app.tsx +0 -80
- package/examples/web/canvas.html +0 -89
- package/examples/web/dom-app.tsx +0 -81
- package/examples/web/dom.html +0 -113
- package/examples/web/showcase-app.tsx +0 -107
- package/examples/web/showcase.html +0 -34
- package/examples/web/showcases/index.tsx +0 -56
- package/examples/web/viewer-app.tsx +0 -555
- package/examples/web/viewer.html +0 -30
- package/examples/web/xterm-app.tsx +0 -105
- package/examples/web/xterm.html +0 -118
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pipe Composition - Plugin System Example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates the full pipe() composition pattern — silvery's
|
|
5
|
-
* plugin system for building apps from composable pieces.
|
|
6
|
-
*
|
|
7
|
-
* Each plugin is a function (app) => enhancedApp that adds one
|
|
8
|
-
* capability. pipe() chains them left-to-right:
|
|
9
|
-
*
|
|
10
|
-
* pipe(base, p1, p2, p3) = p3(p2(p1(base)))
|
|
11
|
-
*
|
|
12
|
-
* This example builds a selectable list using:
|
|
13
|
-
* - createApp() — Zustand store for list state
|
|
14
|
-
* - withReact() — binds the React element
|
|
15
|
-
* - withTerminal() — binds stdin/stdout for terminal I/O
|
|
16
|
-
*
|
|
17
|
-
* Usage: bun examples/runtime/pipe-composition.tsx
|
|
18
|
-
*
|
|
19
|
-
* Controls:
|
|
20
|
-
* j/k - Move selection down/up
|
|
21
|
-
* Space/x - Toggle item
|
|
22
|
-
* a - Add new item
|
|
23
|
-
* Esc/q - Quit
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import React from "react"
|
|
27
|
-
import { Box, Text, H3, Muted, Small } from "../../src/index.js"
|
|
28
|
-
import { createApp, useApp } from "@silvery/term/runtime"
|
|
29
|
-
import { pipe, withReact, withTerminal } from "@silvery/tea/plugins"
|
|
30
|
-
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
31
|
-
|
|
32
|
-
export const meta: ExampleMeta = {
|
|
33
|
-
name: "Pipe Composition",
|
|
34
|
-
description: "Plugin system: pipe() composes createApp + withReact + withTerminal",
|
|
35
|
-
features: ["pipe()", "withReact()", "withTerminal()", "createApp()"],
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ============================================================================
|
|
39
|
-
// Types
|
|
40
|
-
// ============================================================================
|
|
41
|
-
|
|
42
|
-
interface ListItem {
|
|
43
|
-
id: number
|
|
44
|
-
label: string
|
|
45
|
-
selected: boolean
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface State {
|
|
49
|
-
items: ListItem[]
|
|
50
|
-
cursor: number
|
|
51
|
-
nextId: number
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ============================================================================
|
|
55
|
-
// Components
|
|
56
|
-
// ============================================================================
|
|
57
|
-
|
|
58
|
-
function Item({ item, isCursor }: { item: ListItem; isCursor: boolean }) {
|
|
59
|
-
const check = item.selected ? "◉" : "○"
|
|
60
|
-
return (
|
|
61
|
-
<Box>
|
|
62
|
-
<Text color={isCursor ? "$primary" : "$muted"}>{isCursor ? "❯ " : " "}</Text>
|
|
63
|
-
<Text color={item.selected ? "$success" : undefined}>
|
|
64
|
-
{check} {item.label}
|
|
65
|
-
</Text>
|
|
66
|
-
</Box>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function SelectableList() {
|
|
71
|
-
const items = useApp((s: State) => s.items)
|
|
72
|
-
const cursor = useApp((s: State) => s.cursor)
|
|
73
|
-
|
|
74
|
-
const selectedCount = items.filter((i) => i.selected).length
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<Box flexDirection="column" padding={1}>
|
|
78
|
-
<H3>Select items:</H3>
|
|
79
|
-
<Text> </Text>
|
|
80
|
-
{items.map((item, i) => (
|
|
81
|
-
<Item key={item.id} item={item} isCursor={i === cursor} />
|
|
82
|
-
))}
|
|
83
|
-
{items.length === 0 && <Muted>No items. Press 'a' to add one.</Muted>}
|
|
84
|
-
<Text> </Text>
|
|
85
|
-
<Muted>
|
|
86
|
-
{selectedCount}/{items.length} selected
|
|
87
|
-
</Muted>
|
|
88
|
-
<Text> </Text>
|
|
89
|
-
<Small>j/k: move • space/x: toggle • a: add • Esc/q: quit</Small>
|
|
90
|
-
</Box>
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ============================================================================
|
|
95
|
-
// App — the pipe() composition pattern
|
|
96
|
-
// ============================================================================
|
|
97
|
-
|
|
98
|
-
// Step 1: Define the app with createApp()
|
|
99
|
-
//
|
|
100
|
-
// createApp() takes a store factory and event handlers.
|
|
101
|
-
// It returns an AppDefinition with a run(element, options) method.
|
|
102
|
-
const baseApp = createApp<Record<string, unknown>, State>(
|
|
103
|
-
// Store factory: (providers) => zustand StateCreator
|
|
104
|
-
() => (set) => ({
|
|
105
|
-
items: [
|
|
106
|
-
{ id: 1, label: "Read the docs", selected: false },
|
|
107
|
-
{ id: 2, label: "Try pipe() composition", selected: true },
|
|
108
|
-
{ id: 3, label: "Build something", selected: false },
|
|
109
|
-
{ id: 4, label: "Ship it", selected: false },
|
|
110
|
-
],
|
|
111
|
-
cursor: 0,
|
|
112
|
-
nextId: 5,
|
|
113
|
-
}),
|
|
114
|
-
|
|
115
|
-
// Event handlers: 'provider:event' → handler
|
|
116
|
-
{
|
|
117
|
-
"term:key": (
|
|
118
|
-
data: unknown,
|
|
119
|
-
{ get, set }: { get: () => State; set: (fn: (s: State) => Partial<State>) => void },
|
|
120
|
-
) => {
|
|
121
|
-
const { input: k, key } = data as { input: string; key: { escape: boolean } }
|
|
122
|
-
const { items, cursor, nextId } = get()
|
|
123
|
-
|
|
124
|
-
if (key.escape || k === "q") return "exit"
|
|
125
|
-
|
|
126
|
-
switch (k) {
|
|
127
|
-
case "j":
|
|
128
|
-
set(() => ({ cursor: Math.min(cursor + 1, items.length - 1) }))
|
|
129
|
-
break
|
|
130
|
-
case "k":
|
|
131
|
-
set(() => ({ cursor: Math.max(cursor - 1, 0) }))
|
|
132
|
-
break
|
|
133
|
-
case " ":
|
|
134
|
-
case "x":
|
|
135
|
-
set(() => ({
|
|
136
|
-
items: items.map((item, i) => (i === cursor ? { ...item, selected: !item.selected } : item)),
|
|
137
|
-
}))
|
|
138
|
-
break
|
|
139
|
-
case "a":
|
|
140
|
-
set(() => ({
|
|
141
|
-
items: [...items, { id: nextId, label: `Item ${nextId}`, selected: false }],
|
|
142
|
-
nextId: nextId + 1,
|
|
143
|
-
}))
|
|
144
|
-
break
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
// Step 2: Compose with pipe()
|
|
151
|
-
//
|
|
152
|
-
// pipe() chains plugins left-to-right. Each plugin wraps run():
|
|
153
|
-
// - withReact(<App />) → binds the element, so run() needs no JSX
|
|
154
|
-
// - withTerminal(process) → binds stdin/stdout, so run() needs no options
|
|
155
|
-
//
|
|
156
|
-
// The result is an app where run() takes no arguments.
|
|
157
|
-
const app = pipe(
|
|
158
|
-
baseApp,
|
|
159
|
-
withReact(
|
|
160
|
-
<ExampleBanner meta={meta} controls="j/k move space/x toggle a add Esc/q quit">
|
|
161
|
-
<SelectableList />
|
|
162
|
-
</ExampleBanner>,
|
|
163
|
-
),
|
|
164
|
-
withTerminal(process),
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
// ============================================================================
|
|
168
|
-
// Main
|
|
169
|
-
// ============================================================================
|
|
170
|
-
|
|
171
|
-
async function main() {
|
|
172
|
-
// Step 3: Run — no arguments needed, everything is composed
|
|
173
|
-
const handle = await app.run()
|
|
174
|
-
|
|
175
|
-
await handle.waitUntilExit()
|
|
176
|
-
|
|
177
|
-
const { items } = handle.store.getState()
|
|
178
|
-
const selected = items.filter((i) => i.selected)
|
|
179
|
-
console.log(`\nSelected ${selected.length} items:`, selected.map((i) => i.label).join(", "))
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (import.meta.main) {
|
|
183
|
-
main().catch(console.error)
|
|
184
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Run Counter - Layer 2 Example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates run() with React hooks (useState, useEffect)
|
|
5
|
-
* and useInput for keyboard handling.
|
|
6
|
-
*
|
|
7
|
-
* This is the simplest way to build an interactive TUI app.
|
|
8
|
-
* Under the hood, run() creates a createApp() with an empty store
|
|
9
|
-
* and renders the element — it's sugar over the Layer 3 pipe() pattern:
|
|
10
|
-
*
|
|
11
|
-
* run(<App />)
|
|
12
|
-
* // is equivalent to:
|
|
13
|
-
* pipe(createApp(() => () => ({})), withReact(<App />), withTerminal(process)).run()
|
|
14
|
-
*
|
|
15
|
-
* Use run() when component-local state (useState) is sufficient.
|
|
16
|
-
* Use pipe() + createApp() when you need shared state (Zustand store).
|
|
17
|
-
*
|
|
18
|
-
* Usage: bun examples/runtime/run-counter.tsx
|
|
19
|
-
*
|
|
20
|
-
* Controls:
|
|
21
|
-
* j/k - Increment/decrement counter
|
|
22
|
-
* r - Reset to 0
|
|
23
|
-
* Esc/q - Quit
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import React, { useState, useCallback } from "react"
|
|
27
|
-
import { Box, Text, Small } from "../../src/index.js"
|
|
28
|
-
import { run, useInput, type Key } from "@silvery/term/runtime"
|
|
29
|
-
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
30
|
-
|
|
31
|
-
export const meta: ExampleMeta = {
|
|
32
|
-
name: "Run Counter",
|
|
33
|
-
description: "Layer 2: run() with React hooks and useRuntimeInput",
|
|
34
|
-
features: ["run()", "useState", "useInput"],
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function Counter() {
|
|
38
|
-
const [count, setCount] = useState(0)
|
|
39
|
-
|
|
40
|
-
useInput(
|
|
41
|
-
useCallback((input: string, key: Key) => {
|
|
42
|
-
if (input === "j") setCount((c) => c + 1)
|
|
43
|
-
if (input === "k") setCount((c) => c - 1)
|
|
44
|
-
if (input === "r") setCount(0)
|
|
45
|
-
if (input === "q" || key.escape) return "exit"
|
|
46
|
-
}, []),
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<Box flexDirection="column" padding={1}>
|
|
51
|
-
<Box>
|
|
52
|
-
<Text>Count: </Text>
|
|
53
|
-
<Text bold color={count >= 0 ? "green" : "red"}>
|
|
54
|
-
{count}
|
|
55
|
-
</Text>
|
|
56
|
-
</Box>
|
|
57
|
-
<Text> </Text>
|
|
58
|
-
<Small>j/k: increment/decrement • r: reset • Esc/q: quit</Small>
|
|
59
|
-
</Box>
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function main() {
|
|
64
|
-
const handle = await run(
|
|
65
|
-
<ExampleBanner meta={meta} controls="j/k inc/dec r reset Esc/q quit">
|
|
66
|
-
<Counter />
|
|
67
|
-
</ExampleBanner>,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
// Wait until user presses q
|
|
71
|
-
await handle.waitUntilExit()
|
|
72
|
-
|
|
73
|
-
console.log("\nGoodbye!")
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (import.meta.main) {
|
|
77
|
-
main().catch(console.error)
|
|
78
|
-
}
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime Counter Example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates the createRuntime() API (Layer 1):
|
|
5
|
-
* - Runtime handles diffing internally
|
|
6
|
-
* - events() provides AsyncIterable event stream
|
|
7
|
-
* - schedule() for async effects
|
|
8
|
-
* - Clean disposal with Symbol.dispose
|
|
9
|
-
*
|
|
10
|
-
* This is the recommended way to build custom event loops.
|
|
11
|
-
*
|
|
12
|
-
* Usage: bun examples/runtime-counter.tsx
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import React from "react"
|
|
16
|
-
import { Box, Text, H3, Small } from "../../src/index.js"
|
|
17
|
-
import {
|
|
18
|
-
createRuntime,
|
|
19
|
-
ensureLayoutEngine,
|
|
20
|
-
layout,
|
|
21
|
-
type Dims,
|
|
22
|
-
type Event,
|
|
23
|
-
type RenderTarget,
|
|
24
|
-
} from "@silvery/term/runtime"
|
|
25
|
-
import type { ExampleMeta } from "../_banner.js"
|
|
26
|
-
|
|
27
|
-
export const meta: ExampleMeta = {
|
|
28
|
-
name: "Runtime Counter",
|
|
29
|
-
description: "Layer 1 event loop: events() AsyncIterable + schedule()",
|
|
30
|
-
features: ["createRuntime()", "event loop", "dispatch()"],
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ============================================================================
|
|
34
|
-
// State & Types
|
|
35
|
-
// ============================================================================
|
|
36
|
-
|
|
37
|
-
interface State {
|
|
38
|
-
count: number
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ============================================================================
|
|
42
|
-
// Reducer (pure function)
|
|
43
|
-
// ============================================================================
|
|
44
|
-
|
|
45
|
-
function reducer(state: State, event: Event): State {
|
|
46
|
-
switch (event.type) {
|
|
47
|
-
case "resize":
|
|
48
|
-
// Just trigger re-render, state unchanged
|
|
49
|
-
return state
|
|
50
|
-
case "effect":
|
|
51
|
-
// Effect completed - increment count
|
|
52
|
-
// Note: event.id is auto-generated ("effect-0", "effect-1", ...),
|
|
53
|
-
// event.result is whatever schedule() callback returned
|
|
54
|
-
if (event.result === "increment") {
|
|
55
|
-
return { ...state, count: state.count + 1 }
|
|
56
|
-
}
|
|
57
|
-
return state
|
|
58
|
-
default:
|
|
59
|
-
return state
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ============================================================================
|
|
64
|
-
// View (pure function)
|
|
65
|
-
// ============================================================================
|
|
66
|
-
|
|
67
|
-
function view(state: State, dims: Dims): React.ReactElement {
|
|
68
|
-
return (
|
|
69
|
-
<Box flexDirection="column" padding={1}>
|
|
70
|
-
<H3>Runtime Counter Example</H3>
|
|
71
|
-
<Text> </Text>
|
|
72
|
-
<Text>
|
|
73
|
-
Count: <Text color="green">{state.count}</Text>
|
|
74
|
-
</Text>
|
|
75
|
-
<Text> </Text>
|
|
76
|
-
<Small>
|
|
77
|
-
Terminal: {dims.cols}x{dims.rows}
|
|
78
|
-
</Small>
|
|
79
|
-
<Small>Press Ctrl+C to quit</Small>
|
|
80
|
-
</Box>
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ============================================================================
|
|
85
|
-
// Terminal Target
|
|
86
|
-
// ============================================================================
|
|
87
|
-
|
|
88
|
-
function createTerminalTarget(): RenderTarget & { cleanup: () => void } {
|
|
89
|
-
const resizeHandlers = new Set<(dims: Dims) => void>()
|
|
90
|
-
|
|
91
|
-
const onResizeHandler = () => {
|
|
92
|
-
const dims = {
|
|
93
|
-
cols: process.stdout.columns || 80,
|
|
94
|
-
rows: process.stdout.rows || 24,
|
|
95
|
-
}
|
|
96
|
-
for (const handler of resizeHandlers) {
|
|
97
|
-
handler(dims)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
process.stdout.on("resize", onResizeHandler)
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
write(frame: string): void {
|
|
105
|
-
process.stdout.write(frame)
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
getDims(): Dims {
|
|
109
|
-
return {
|
|
110
|
-
cols: process.stdout.columns || 80,
|
|
111
|
-
rows: process.stdout.rows || 24,
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
onResize(handler: (dims: Dims) => void): () => void {
|
|
116
|
-
resizeHandlers.add(handler)
|
|
117
|
-
return () => resizeHandlers.delete(handler)
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
cleanup(): void {
|
|
121
|
-
process.stdout.off("resize", onResizeHandler)
|
|
122
|
-
},
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ============================================================================
|
|
127
|
-
// Main
|
|
128
|
-
// ============================================================================
|
|
129
|
-
|
|
130
|
-
async function main() {
|
|
131
|
-
// Initialize layout engine
|
|
132
|
-
await ensureLayoutEngine()
|
|
133
|
-
|
|
134
|
-
// Create terminal target
|
|
135
|
-
const target = createTerminalTarget()
|
|
136
|
-
|
|
137
|
-
// Create runtime with abort signal for cleanup
|
|
138
|
-
const controller = new AbortController()
|
|
139
|
-
const runtime = createRuntime({ target, signal: controller.signal })
|
|
140
|
-
|
|
141
|
-
// Handle Ctrl+C
|
|
142
|
-
process.on("SIGINT", () => {
|
|
143
|
-
controller.abort()
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
// Initial state
|
|
147
|
-
let state: State = { count: 0 }
|
|
148
|
-
|
|
149
|
-
// Clear screen and hide cursor
|
|
150
|
-
process.stdout.write("\x1b[2J\x1b[H\x1b[?25l")
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
// Initial render
|
|
154
|
-
runtime.render(layout(view(state, runtime.getDims()), runtime.getDims()))
|
|
155
|
-
|
|
156
|
-
// Schedule periodic increments using effects
|
|
157
|
-
const scheduleIncrement = () => {
|
|
158
|
-
runtime.schedule(async () => {
|
|
159
|
-
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
160
|
-
return "increment"
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Start first increment
|
|
165
|
-
scheduleIncrement()
|
|
166
|
-
|
|
167
|
-
// Event loop
|
|
168
|
-
for await (const event of runtime.events()) {
|
|
169
|
-
// Update state
|
|
170
|
-
const newState = reducer(state, event)
|
|
171
|
-
|
|
172
|
-
// Re-render if state changed or resize occurred
|
|
173
|
-
if (newState !== state || event.type === "resize") {
|
|
174
|
-
state = newState
|
|
175
|
-
runtime.render(layout(view(state, runtime.getDims()), runtime.getDims()))
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Schedule next increment after effect completes
|
|
179
|
-
if (event.type === "effect" && event.id.startsWith("effect-")) {
|
|
180
|
-
scheduleIncrement()
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
} finally {
|
|
184
|
-
// Cleanup
|
|
185
|
-
runtime[Symbol.dispose]()
|
|
186
|
-
target.cleanup()
|
|
187
|
-
|
|
188
|
-
// Show cursor and reset
|
|
189
|
-
process.stdout.write("\x1b[?25h\x1b[0m\n")
|
|
190
|
-
console.log("Final count:", state.count)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Run
|
|
195
|
-
if (import.meta.main) {
|
|
196
|
-
main().catch(console.error)
|
|
197
|
-
}
|