silvery 0.0.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -11
  3. package/bin/silvery.ts +258 -0
  4. package/examples/CLAUDE.md +75 -0
  5. package/examples/_banner.tsx +60 -0
  6. package/examples/cli.ts +228 -0
  7. package/examples/index.md +101 -0
  8. package/examples/inline/inline-nontty.tsx +98 -0
  9. package/examples/inline/inline-progress.tsx +79 -0
  10. package/examples/inline/inline-simple.tsx +63 -0
  11. package/examples/inline/scrollback.tsx +185 -0
  12. package/examples/interactive/_input-debug.tsx +110 -0
  13. package/examples/interactive/_stdin-test.ts +71 -0
  14. package/examples/interactive/_textarea-bare.tsx +45 -0
  15. package/examples/interactive/aichat/components.tsx +468 -0
  16. package/examples/interactive/aichat/index.tsx +207 -0
  17. package/examples/interactive/aichat/script.ts +460 -0
  18. package/examples/interactive/aichat/state.ts +326 -0
  19. package/examples/interactive/aichat/types.ts +19 -0
  20. package/examples/interactive/app-todo.tsx +198 -0
  21. package/examples/interactive/async-data.tsx +208 -0
  22. package/examples/interactive/cli-wizard.tsx +332 -0
  23. package/examples/interactive/clipboard.tsx +183 -0
  24. package/examples/interactive/components.tsx +463 -0
  25. package/examples/interactive/data-explorer.tsx +506 -0
  26. package/examples/interactive/dev-tools.tsx +379 -0
  27. package/examples/interactive/explorer.tsx +747 -0
  28. package/examples/interactive/gallery.tsx +652 -0
  29. package/examples/interactive/inline-bench.tsx +136 -0
  30. package/examples/interactive/kanban.tsx +267 -0
  31. package/examples/interactive/layout-ref.tsx +185 -0
  32. package/examples/interactive/outline.tsx +171 -0
  33. package/examples/interactive/paste-demo.tsx +198 -0
  34. package/examples/interactive/scroll.tsx +77 -0
  35. package/examples/interactive/search-filter.tsx +240 -0
  36. package/examples/interactive/task-list.tsx +279 -0
  37. package/examples/interactive/terminal.tsx +798 -0
  38. package/examples/interactive/textarea.tsx +103 -0
  39. package/examples/interactive/theme.tsx +336 -0
  40. package/examples/interactive/transform.tsx +256 -0
  41. package/examples/interactive/virtual-10k.tsx +413 -0
  42. package/examples/kitty/canvas.tsx +519 -0
  43. package/examples/kitty/generate-samples.ts +236 -0
  44. package/examples/kitty/image-component.tsx +273 -0
  45. package/examples/kitty/images.tsx +604 -0
  46. package/examples/kitty/input.tsx +371 -0
  47. package/examples/kitty/keys.tsx +378 -0
  48. package/examples/kitty/paint.tsx +1017 -0
  49. package/examples/layout/dashboard.tsx +551 -0
  50. package/examples/layout/live-resize.tsx +290 -0
  51. package/examples/layout/overflow.tsx +51 -0
  52. package/examples/playground/README.md +69 -0
  53. package/examples/playground/build.ts +61 -0
  54. package/examples/playground/index.html +420 -0
  55. package/examples/playground/playground-app.tsx +416 -0
  56. package/examples/runtime/elm-counter.tsx +206 -0
  57. package/examples/runtime/hello-runtime.tsx +73 -0
  58. package/examples/runtime/pipe-composition.tsx +184 -0
  59. package/examples/runtime/run-counter.tsx +78 -0
  60. package/examples/runtime/runtime-counter.tsx +197 -0
  61. package/examples/screenshots/generate.tsx +563 -0
  62. package/examples/scrollback-perf.tsx +230 -0
  63. package/examples/viewer.tsx +654 -0
  64. package/examples/web/build.ts +365 -0
  65. package/examples/web/canvas-app.tsx +80 -0
  66. package/examples/web/canvas.html +89 -0
  67. package/examples/web/dom-app.tsx +81 -0
  68. package/examples/web/dom.html +113 -0
  69. package/examples/web/showcase-app.tsx +107 -0
  70. package/examples/web/showcase.html +34 -0
  71. package/examples/web/showcases/index.tsx +56 -0
  72. package/examples/web/viewer-app.tsx +555 -0
  73. package/examples/web/viewer.html +30 -0
  74. package/examples/web/xterm-app.tsx +105 -0
  75. package/examples/web/xterm.html +118 -0
  76. package/package.json +106 -5
  77. package/src/index.ts +5 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Scrollback Mode — REPL
3
+ *
4
+ * Interactive expression evaluator demonstrating useScrollback + VirtualList virtualized.
5
+ * Completed results freeze into terminal scrollback; the active prompt stays at bottom.
6
+ *
7
+ * Controls:
8
+ * Type expression + Enter - Evaluate
9
+ * q (when input empty) - Quit
10
+ */
11
+
12
+ import React, { useState, useCallback } from "react"
13
+ import { Box, Text, Divider, VirtualList, useInput, type Key } from "../../src/index.js"
14
+ import { run, useExit } from "@silvery/term/runtime"
15
+ import { useScrollback } from "../../src/hooks/useScrollback.js"
16
+ import { ExampleBanner, type ExampleMeta } from "../_banner.js"
17
+
18
+ export const meta: ExampleMeta = {
19
+ name: "Scrollback",
20
+ description: "REPL with useScrollback + VirtualList virtualized for terminal scrollback",
21
+ features: ["useScrollback()", "VirtualList virtualized", "inline mode"],
22
+ }
23
+
24
+ // =============================================================================
25
+ // Data
26
+ // =============================================================================
27
+
28
+ interface Result {
29
+ id: number
30
+ expr: string
31
+ value: string
32
+ frozen: boolean
33
+ }
34
+
35
+ let nextId = 0
36
+
37
+ function evaluate(expr: string): string {
38
+ try {
39
+ // eslint-disable-next-line no-eval
40
+ return String(eval(expr))
41
+ } catch (e: unknown) {
42
+ return `Error: ${e instanceof Error ? e.message : String(e)}`
43
+ }
44
+ }
45
+
46
+ // =============================================================================
47
+ // Component
48
+ // =============================================================================
49
+
50
+ export function Repl() {
51
+ const exit = useExit()
52
+ const [results, setResults] = useState<Result[]>([])
53
+ const [input, setInput] = useState("")
54
+ const [cursor, setCursor] = useState(0)
55
+
56
+ // Push frozen results to terminal scrollback
57
+ const frozenCount = useScrollback(results, {
58
+ frozen: (r) => r.frozen,
59
+ render: (r) => `$ ${r.expr}\n→ ${r.value}`,
60
+ })
61
+
62
+ const submit = useCallback(() => {
63
+ const expr = input.trim()
64
+ if (!expr) return
65
+
66
+ const value = evaluate(expr)
67
+ const id = nextId++
68
+
69
+ // Mark all existing results as frozen, add new one unfrozen
70
+ setResults((prev) => [...prev.map((r) => ({ ...r, frozen: true })), { id, expr, value, frozen: false }])
71
+ setInput("")
72
+ setCursor(0)
73
+ }, [input])
74
+
75
+ useInput((ch: string, key: Key) => {
76
+ if (key.return) {
77
+ submit()
78
+ return
79
+ }
80
+ if (key.escape || (ch === "q" && input === "")) {
81
+ exit()
82
+ return
83
+ }
84
+ if (key.backspace) {
85
+ if (cursor > 0) {
86
+ setInput((v) => v.slice(0, cursor - 1) + v.slice(cursor))
87
+ setCursor((c) => c - 1)
88
+ }
89
+ return
90
+ }
91
+ if (key.leftArrow) {
92
+ setCursor((c) => Math.max(0, c - 1))
93
+ return
94
+ }
95
+ if (key.rightArrow) {
96
+ setCursor((c) => Math.min(input.length, c + 1))
97
+ return
98
+ }
99
+ // Ctrl+A: beginning of line
100
+ if (key.ctrl && ch === "a") {
101
+ setCursor(0)
102
+ return
103
+ }
104
+ // Ctrl+E: end of line
105
+ if (key.ctrl && ch === "e") {
106
+ setCursor(input.length)
107
+ return
108
+ }
109
+ // Ctrl+U: clear line
110
+ if (key.ctrl && ch === "u") {
111
+ setInput("")
112
+ setCursor(0)
113
+ return
114
+ }
115
+ if (ch >= " ") {
116
+ setInput((v) => v.slice(0, cursor) + ch + v.slice(cursor))
117
+ setCursor((c) => c + 1)
118
+ }
119
+ })
120
+
121
+ const activeCount = results.length - frozenCount
122
+ const beforeCursor = input.slice(0, cursor)
123
+ const atCursor = input[cursor] ?? " "
124
+ const afterCursor = input.slice(cursor + 1)
125
+
126
+ return (
127
+ <Box flexDirection="column">
128
+ {/* Active (non-virtualized) results via VirtualList */}
129
+ {activeCount > 0 && (
130
+ <VirtualList
131
+ items={results}
132
+ virtualized={(r) => r.frozen}
133
+ height={activeCount * 2}
134
+ itemHeight={2}
135
+ scrollTo={0}
136
+ renderItem={(r) => (
137
+ <Box key={r.id} flexDirection="column">
138
+ <Text>
139
+ <Text color="gray">{"$ "}</Text>
140
+ <Text>{r.expr}</Text>
141
+ </Text>
142
+ <Text>
143
+ <Text color="cyan">{"→ "}</Text>
144
+ <Text>{r.value}</Text>
145
+ </Text>
146
+ </Box>
147
+ )}
148
+ />
149
+ )}
150
+
151
+ {/* Separator */}
152
+ <Divider />
153
+
154
+ {/* Input prompt */}
155
+ <Text>
156
+ <Text color="yellow">{"› "}</Text>
157
+ <Text>{beforeCursor}</Text>
158
+ <Text inverse>{atCursor}</Text>
159
+ <Text>{afterCursor}</Text>
160
+ </Text>
161
+
162
+ {/* Status */}
163
+ <Text dim>
164
+ {results.length} result{results.length !== 1 ? "s" : ""} | Esc/q to quit
165
+ </Text>
166
+ </Box>
167
+ )
168
+ }
169
+
170
+ // =============================================================================
171
+ // Main
172
+ // =============================================================================
173
+
174
+ async function main() {
175
+ await run(
176
+ <ExampleBanner meta={meta} controls="Type expr + Enter Esc/q quit">
177
+ <Repl />
178
+ </ExampleBanner>,
179
+ { mode: "inline" },
180
+ )
181
+ }
182
+
183
+ if (import.meta.main) {
184
+ main().catch(console.error)
185
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Input Debug Tool
3
+ *
4
+ * Minimal diagnostic to find where keypresses are lost.
5
+ * Shows every event received by useInput + TextArea side by side.
6
+ *
7
+ * Run: bun vendor/silvery/examples/interactive/_input-debug.tsx
8
+ */
9
+
10
+ import React, { useState, useRef } from "react"
11
+ import { render, Box, Text, TextArea, useInput, useApp, createTerm, type Key } from "../../src/index.js"
12
+
13
+ function InputDebug(): JSX.Element {
14
+ const { exit } = useApp()
15
+
16
+ // Track raw useInput events
17
+ const [rawEvents, setRawEvents] = useState<string[]>([])
18
+ const rawCountRef = useRef(0)
19
+
20
+ // Track TextArea value
21
+ const [textValue, setTextValue] = useState("")
22
+ const textChangeCountRef = useRef(0)
23
+
24
+ // Raw useInput handler — logs EVERY event
25
+ useInput((input: string, key: Key) => {
26
+ if (key.escape) {
27
+ exit()
28
+ return
29
+ }
30
+ rawCountRef.current++
31
+ const desc = describeKey(input, key)
32
+ setRawEvents((prev) => [...prev.slice(-15), `#${rawCountRef.current} ${desc}`])
33
+ })
34
+
35
+ // TextArea onChange handler
36
+ function handleChange(value: string) {
37
+ textChangeCountRef.current++
38
+ setTextValue(value)
39
+ }
40
+
41
+ return (
42
+ <Box flexDirection="column" padding={1}>
43
+ <Text bold color="cyan">
44
+ Input Pipeline Diagnostic
45
+ </Text>
46
+ <Text dim>Type slowly (1 char/2sec). Compare left (raw events) vs right (TextArea value).</Text>
47
+ <Text dim>Press Esc to quit.</Text>
48
+ <Box height={1} />
49
+
50
+ <Box gap={4}>
51
+ {/* Left: Raw useInput events */}
52
+ <Box flexDirection="column" width={40}>
53
+ <Text bold color="yellow">
54
+ useInput events: {rawCountRef.current}
55
+ </Text>
56
+ {rawEvents.map((e, i) => (
57
+ <Text key={i} dimColor={i < rawEvents.length - 1}>
58
+ {e}
59
+ </Text>
60
+ ))}
61
+ </Box>
62
+
63
+ {/* Right: TextArea */}
64
+ <Box flexDirection="column" width={40}>
65
+ <Text bold color="green">
66
+ TextArea value ({textValue.length} chars, {textChangeCountRef.current} changes):
67
+ </Text>
68
+ <Box borderStyle="single" borderColor="green">
69
+ <Box paddingX={1}>
70
+ <TextArea value={textValue} onChange={handleChange} height={4} placeholder="Type here..." />
71
+ </Box>
72
+ </Box>
73
+ <Box marginTop={1}>
74
+ <Text>Value: {JSON.stringify(textValue)}</Text>
75
+ </Box>
76
+ </Box>
77
+ </Box>
78
+ </Box>
79
+ )
80
+ }
81
+
82
+ function describeKey(input: string, key: Key): string {
83
+ const parts: string[] = []
84
+ if (key.ctrl) parts.push("Ctrl")
85
+ if (key.meta) parts.push("Meta")
86
+ if (key.shift) parts.push("Shift")
87
+
88
+ if (key.return) parts.push("Enter")
89
+ else if (key.escape) parts.push("Esc")
90
+ else if (key.backspace) parts.push("BS")
91
+ else if (key.delete) parts.push("Del")
92
+ else if (key.upArrow) parts.push("Up")
93
+ else if (key.downArrow) parts.push("Down")
94
+ else if (key.leftArrow) parts.push("Left")
95
+ else if (key.rightArrow) parts.push("Right")
96
+ else if (key.tab) parts.push("Tab")
97
+ else if (input.length === 1 && input >= " ") parts.push(`'${input}'`)
98
+ else if (input) parts.push(`raw:${JSON.stringify(input)}`)
99
+ else parts.push("(empty)")
100
+
101
+ return parts.join("+")
102
+ }
103
+
104
+ async function main() {
105
+ using term = createTerm()
106
+ const { waitUntilExit } = await render(<InputDebug />, term)
107
+ await waitUntilExit()
108
+ }
109
+
110
+ main().catch(console.error)
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Bare stdin test — no React, no silvery.
4
+ *
5
+ * Tests whether Bun's stdin "readable" events deliver all keypresses.
6
+ * Run in a real terminal (not through a tool): bun vendor/silvery/examples/interactive/_stdin-test.ts
7
+ *
8
+ * Type characters slowly. Each keypress should appear immediately.
9
+ * Press Ctrl+C to exit.
10
+ */
11
+
12
+ const fs = await import("node:fs")
13
+
14
+ const log = fs.createWriteStream("/tmp/stdin-test.log", { flags: "w" })
15
+ function logMsg(msg: string) {
16
+ const ts = new Date().toISOString().slice(11, 23)
17
+ log.write(`[${ts}] ${msg}\n`)
18
+ process.stdout.write(msg + "\n")
19
+ }
20
+
21
+ logMsg("=== Bare stdin readable test ===")
22
+ logMsg("Type characters slowly. Press Ctrl+C to exit.")
23
+ logMsg("")
24
+
25
+ process.stdin.setEncoding("utf8")
26
+ process.stdin.setRawMode(true)
27
+ process.stdin.ref()
28
+
29
+ let count = 0
30
+
31
+ function handleReadable() {
32
+ const chunk = process.stdin.read() as string | null
33
+ if (chunk === null) {
34
+ logMsg(` readable event but stdin.read() returned null`)
35
+ return
36
+ }
37
+
38
+ for (let i = 0; i < chunk.length; i++) {
39
+ count++
40
+ const ch = chunk[i]!
41
+ const code = ch.charCodeAt(0)
42
+
43
+ if (code === 3) {
44
+ // Ctrl+C
45
+ logMsg(`\n=== Total: ${count} keypresses ===`)
46
+ log.end()
47
+ process.stdin.setRawMode(false)
48
+ process.stdin.unref()
49
+ process.exit(0)
50
+ }
51
+
52
+ if (code >= 32 && code < 127) {
53
+ logMsg(`#${count} char='${ch}' code=${code} chunkLen=${chunk.length}`)
54
+ } else {
55
+ logMsg(
56
+ `#${count} code=0x${code.toString(16).padStart(2, "0")} chunkLen=${chunk.length} raw=${JSON.stringify(chunk.slice(i))}`,
57
+ )
58
+ }
59
+ }
60
+
61
+ // Try reading again in case more data
62
+ const more = process.stdin.read() as string | null
63
+ if (more !== null) {
64
+ logMsg(` EXTRA data in buffer: ${JSON.stringify(more)}`)
65
+ }
66
+ }
67
+
68
+ process.stdin.on("readable", handleReadable)
69
+
70
+ logMsg("Listening for stdin readable events...")
71
+ logMsg("")
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Bare TextArea Test — single useInput hook, no parent useInput.
3
+ *
4
+ * Tests whether the bug is from two useInput hooks competing.
5
+ * Run: bun vendor/silvery/examples/interactive/_textarea-bare.tsx
6
+ *
7
+ * If chars are STILL eaten → bug is in TextArea or input pipeline
8
+ * If chars are NOT eaten → bug is from two useInput hooks
9
+ */
10
+
11
+ import React, { useState } from "react"
12
+ import { render, Box, Text, TextArea, createTerm } from "../../src/index.js"
13
+
14
+ function BareTextArea(): JSX.Element {
15
+ const [value, setValue] = useState("")
16
+
17
+ // NO parent useInput — only TextArea's internal one
18
+ return (
19
+ <Box flexDirection="column" padding={1}>
20
+ <Text bold color="cyan">
21
+ Bare TextArea (single useInput hook)
22
+ </Text>
23
+ <Text dim>Type slowly. Ctrl+C to exit.</Text>
24
+ <Box height={1} />
25
+
26
+ <Box borderStyle="single" borderColor="cyan" paddingX={1}>
27
+ <TextArea value={value} onChange={setValue} height={4} placeholder="Type here..." />
28
+ </Box>
29
+
30
+ <Box marginTop={1}>
31
+ <Text>
32
+ Value ({value.length} chars): {JSON.stringify(value)}
33
+ </Text>
34
+ </Box>
35
+ </Box>
36
+ )
37
+ }
38
+
39
+ async function main() {
40
+ using term = createTerm()
41
+ const { waitUntilExit } = await render(<BareTextArea />, term)
42
+ await waitUntilExit()
43
+ }
44
+
45
+ main().catch(console.error)