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.
Files changed (120) hide show
  1. package/README.md +41 -145
  2. package/dist/chalk.js +3 -0
  3. package/dist/chalk.js.map +11 -0
  4. package/dist/index.js +340 -0
  5. package/dist/index.js.map +282 -0
  6. package/dist/ink.js +129 -0
  7. package/dist/ink.js.map +140 -0
  8. package/dist/runtime.js +394 -0
  9. package/dist/runtime.js.map +286 -0
  10. package/dist/theme.js +343 -0
  11. package/dist/theme.js.map +286 -0
  12. package/dist/ui/animation.js +3 -0
  13. package/dist/ui/animation.js.map +15 -0
  14. package/dist/ui/ansi.js +3 -0
  15. package/dist/ui/ansi.js.map +10 -0
  16. package/dist/ui/cli.js +8 -0
  17. package/dist/ui/cli.js.map +14 -0
  18. package/dist/ui/display.js +4 -0
  19. package/dist/ui/display.js.map +10 -0
  20. package/dist/ui/image.js +4 -0
  21. package/dist/ui/image.js.map +15 -0
  22. package/dist/ui/input.js +3 -0
  23. package/dist/ui/input.js.map +11 -0
  24. package/dist/ui/progress.js +8 -0
  25. package/dist/ui/progress.js.map +20 -0
  26. package/dist/ui/react.js +3 -0
  27. package/dist/ui/react.js.map +15 -0
  28. package/dist/ui/utils.js +3 -0
  29. package/dist/ui/utils.js.map +10 -0
  30. package/dist/ui/wrappers.js +14 -0
  31. package/dist/ui/wrappers.js.map +19 -0
  32. package/dist/ui.js +17 -0
  33. package/dist/ui.js.map +20 -0
  34. package/package.json +67 -15
  35. package/src/index.ts +67 -1
  36. package/src/runtime.ts +4 -0
  37. package/src/theme.ts +4 -0
  38. package/src/ui/animation.ts +2 -0
  39. package/src/ui/ansi.ts +2 -0
  40. package/src/ui/cli.ts +2 -0
  41. package/src/ui/display.ts +2 -0
  42. package/src/ui/image.ts +2 -0
  43. package/src/ui/input.ts +2 -0
  44. package/src/ui/progress.ts +2 -0
  45. package/src/ui/react.ts +2 -0
  46. package/src/ui/utils.ts +2 -0
  47. package/src/ui/wrappers.ts +2 -0
  48. package/src/ui.ts +4 -0
  49. package/examples/CLAUDE.md +0 -75
  50. package/examples/_banner.tsx +0 -60
  51. package/examples/cli.ts +0 -228
  52. package/examples/index.md +0 -101
  53. package/examples/inline/inline-nontty.tsx +0 -98
  54. package/examples/inline/inline-progress.tsx +0 -79
  55. package/examples/inline/inline-simple.tsx +0 -63
  56. package/examples/inline/scrollback.tsx +0 -185
  57. package/examples/interactive/_input-debug.tsx +0 -110
  58. package/examples/interactive/_stdin-test.ts +0 -71
  59. package/examples/interactive/_textarea-bare.tsx +0 -45
  60. package/examples/interactive/aichat/components.tsx +0 -468
  61. package/examples/interactive/aichat/index.tsx +0 -207
  62. package/examples/interactive/aichat/script.ts +0 -460
  63. package/examples/interactive/aichat/state.ts +0 -326
  64. package/examples/interactive/aichat/types.ts +0 -19
  65. package/examples/interactive/app-todo.tsx +0 -198
  66. package/examples/interactive/async-data.tsx +0 -208
  67. package/examples/interactive/cli-wizard.tsx +0 -332
  68. package/examples/interactive/clipboard.tsx +0 -183
  69. package/examples/interactive/components.tsx +0 -463
  70. package/examples/interactive/data-explorer.tsx +0 -506
  71. package/examples/interactive/dev-tools.tsx +0 -379
  72. package/examples/interactive/explorer.tsx +0 -747
  73. package/examples/interactive/gallery.tsx +0 -652
  74. package/examples/interactive/inline-bench.tsx +0 -136
  75. package/examples/interactive/kanban.tsx +0 -267
  76. package/examples/interactive/layout-ref.tsx +0 -185
  77. package/examples/interactive/outline.tsx +0 -171
  78. package/examples/interactive/paste-demo.tsx +0 -198
  79. package/examples/interactive/scroll.tsx +0 -77
  80. package/examples/interactive/search-filter.tsx +0 -240
  81. package/examples/interactive/task-list.tsx +0 -279
  82. package/examples/interactive/terminal.tsx +0 -798
  83. package/examples/interactive/textarea.tsx +0 -103
  84. package/examples/interactive/theme.tsx +0 -336
  85. package/examples/interactive/transform.tsx +0 -256
  86. package/examples/interactive/virtual-10k.tsx +0 -413
  87. package/examples/kitty/canvas.tsx +0 -519
  88. package/examples/kitty/generate-samples.ts +0 -236
  89. package/examples/kitty/image-component.tsx +0 -273
  90. package/examples/kitty/images.tsx +0 -604
  91. package/examples/kitty/input.tsx +0 -371
  92. package/examples/kitty/keys.tsx +0 -378
  93. package/examples/kitty/paint.tsx +0 -1017
  94. package/examples/layout/dashboard.tsx +0 -551
  95. package/examples/layout/live-resize.tsx +0 -290
  96. package/examples/layout/overflow.tsx +0 -51
  97. package/examples/playground/README.md +0 -69
  98. package/examples/playground/build.ts +0 -61
  99. package/examples/playground/index.html +0 -420
  100. package/examples/playground/playground-app.tsx +0 -416
  101. package/examples/runtime/elm-counter.tsx +0 -206
  102. package/examples/runtime/hello-runtime.tsx +0 -73
  103. package/examples/runtime/pipe-composition.tsx +0 -184
  104. package/examples/runtime/run-counter.tsx +0 -78
  105. package/examples/runtime/runtime-counter.tsx +0 -197
  106. package/examples/screenshots/generate.tsx +0 -563
  107. package/examples/scrollback-perf.tsx +0 -230
  108. package/examples/viewer.tsx +0 -654
  109. package/examples/web/build.ts +0 -365
  110. package/examples/web/canvas-app.tsx +0 -80
  111. package/examples/web/canvas.html +0 -89
  112. package/examples/web/dom-app.tsx +0 -81
  113. package/examples/web/dom.html +0 -113
  114. package/examples/web/showcase-app.tsx +0 -107
  115. package/examples/web/showcase.html +0 -34
  116. package/examples/web/showcases/index.tsx +0 -56
  117. package/examples/web/viewer-app.tsx +0 -555
  118. package/examples/web/viewer.html +0 -30
  119. package/examples/web/xterm-app.tsx +0 -105
  120. 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
- }