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,371 +0,0 @@
1
- /**
2
- * Rich Input
3
- *
4
- * Combined keyboard + mouse showcase demonstrating all rich terminal input
5
- * features: Kitty protocol modifiers, mouse tracking, mode switching, and
6
- * a live event log.
7
- *
8
- * Features:
9
- * - Keybinding display using parseHotkey with ⌘ ⌥ ⌃ ⇧ ✦ symbols
10
- * - Mouse-clickable UI elements
11
- * - Mode switching (normal/insert)
12
- * - Event log showing all input events
13
- * - Kitty auto-detection
14
- *
15
- * Run: bun vendor/silvery/examples/kitty/rich-input.tsx
16
- */
17
-
18
- import React, { useState, useRef, useEffect } from "react"
19
- import {
20
- render,
21
- Box,
22
- Text,
23
- useInput,
24
- useApp,
25
- createTerm,
26
- parseHotkey,
27
- parseMouseSequence,
28
- isMouseSequence,
29
- KittyFlags,
30
- enableKittyKeyboard,
31
- disableKittyKeyboard,
32
- enableMouse,
33
- disableMouse,
34
- detectKittyFromStdio,
35
- type Key,
36
- type ParsedMouse,
37
- } from "../../src/index.js"
38
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
39
-
40
- export const meta: ExampleMeta = {
41
- name: "Mouse & Keys",
42
- description: "Combined keyboard + mouse input showcase with Kitty protocol",
43
- features: ["parseHotkey()", "parseMouseSequence()", "⌘ ⌥ ⌃ ⇧ ✦"],
44
- }
45
-
46
- type Mode = "normal" | "insert"
47
- type EventEntry = {
48
- index: number
49
- type: "key" | "mouse"
50
- summary: string
51
- color?: string
52
- }
53
-
54
- // Keybinding definitions with macOS symbols
55
- const KEYBINDINGS = [
56
- { hotkey: "i", action: "Enter insert mode", mode: "normal" as const },
57
- { hotkey: "Escape", action: "Return to normal mode", mode: "insert" as const },
58
- { hotkey: "⌃c", action: "Quit", mode: "both" as const },
59
- { hotkey: "j", action: "Move down", mode: "normal" as const },
60
- { hotkey: "k", action: "Move up", mode: "normal" as const },
61
- { hotkey: "⇧J", action: "Move item down", mode: "normal" as const },
62
- { hotkey: "⇧K", action: "Move item up", mode: "normal" as const },
63
- { hotkey: "⌘s", action: "Save (Kitty only)", mode: "both" as const },
64
- { hotkey: "✦⌘x", action: "Special action (Kitty only)", mode: "normal" as const },
65
- { hotkey: "q", action: "Quit", mode: "normal" as const },
66
- ]
67
-
68
- const ITEMS = ["Inbox", "Today", "Upcoming", "Projects", "Archive", "Trash"]
69
-
70
- function RichInputDemo({ kittySupported }: { kittySupported: boolean }): JSX.Element {
71
- const { exit } = useApp()
72
- const stdin = process.stdin
73
- const [mode, setMode] = useState<Mode>("normal")
74
- const [cursor, setCursor] = useState(0)
75
- const [events, setEvents] = useState<EventEntry[]>([])
76
- const [insertText, setInsertText] = useState("")
77
- const [mousePos, setMousePos] = useState<{ x: number; y: number } | null>(null)
78
- const counterRef = useRef(0)
79
-
80
- // Enable mouse tracking
81
- useEffect(() => {
82
- process.stdout.write(enableMouse())
83
- return () => {
84
- process.stdout.write(disableMouse())
85
- }
86
- }, [])
87
-
88
- function addEvent(type: "key" | "mouse", summary: string, color?: string) {
89
- counterRef.current++
90
- setEvents((prev) => [...prev.slice(-18), { index: counterRef.current, type, summary, color }])
91
- }
92
-
93
- // Listen to raw stdin for mouse events + Kitty key details
94
- useEffect(() => {
95
- const onData = (data: Buffer) => {
96
- const raw = data.toString()
97
-
98
- if (isMouseSequence(raw)) {
99
- const parsed = parseMouseSequence(raw)
100
- if (!parsed) return
101
-
102
- setMousePos({ x: parsed.x, y: parsed.y })
103
-
104
- const mods: string[] = []
105
- if (parsed.ctrl) mods.push("⌃")
106
- if (parsed.shift) mods.push("⇧")
107
- if (parsed.meta) mods.push("⌥")
108
- const modStr = mods.length > 0 ? mods.join("") + " " : ""
109
-
110
- if (parsed.action === "down") {
111
- const btn = ["Left", "Middle", "Right"][parsed.button] ?? `Btn${parsed.button}`
112
- addEvent("mouse", `${modStr}${btn} click at (${parsed.x},${parsed.y})`, "blue")
113
- } else if (parsed.action === "wheel") {
114
- addEvent(
115
- "mouse",
116
- `${modStr}Scroll ${parsed.delta! < 0 ? "up" : "down"} at (${parsed.x},${parsed.y})`,
117
- "magenta",
118
- )
119
- }
120
- }
121
- }
122
-
123
- stdin.on("data", onData)
124
- return () => {
125
- stdin.off("data", onData)
126
- }
127
- }, [stdin])
128
-
129
- useInput((input: string, key: Key) => {
130
- // Always: Ctrl+C or q (in normal mode) to quit
131
- if (key.ctrl && input === "c") {
132
- addEvent("key", "⌃C Quit", "red")
133
- exit()
134
- return
135
- }
136
-
137
- if (mode === "normal") {
138
- if (input === "q") {
139
- addEvent("key", "q Quit", "red")
140
- exit()
141
- return
142
- }
143
- if (input === "i") {
144
- setMode("insert")
145
- setInsertText("")
146
- addEvent("key", "i Enter insert mode", "green")
147
- return
148
- }
149
- if (input === "j" || key.downArrow) {
150
- setCursor((c) => Math.min(c + 1, ITEMS.length - 1))
151
- addEvent("key", `${input === "j" ? "j" : "Arrow"} Move down`)
152
- return
153
- }
154
- if (input === "k" || key.upArrow) {
155
- setCursor((c) => Math.max(c - 1, 0))
156
- addEvent("key", `${input === "k" ? "k" : "Arrow"} Move up`)
157
- return
158
- }
159
- if (input === "J" && key.shift) {
160
- addEvent("key", "⇧J Move item down", "yellow")
161
- return
162
- }
163
- if (input === "K" && key.shift) {
164
- addEvent("key", "⇧K Move item up", "yellow")
165
- return
166
- }
167
- // Kitty-only: Super modifier
168
- if (key.super && input === "s") {
169
- addEvent("key", "⌘S Save", "green")
170
- return
171
- }
172
- if (key.hyper && key.super && input === "x") {
173
- addEvent("key", "✦⌘X Special action!", "magenta")
174
- return
175
- }
176
-
177
- // Log unhandled keys
178
- if (input && input >= " ") {
179
- addEvent("key", `${input} (unbound)`, "gray")
180
- }
181
- } else if (mode === "insert") {
182
- if (key.escape) {
183
- setMode("normal")
184
- addEvent("key", "Esc Normal mode", "green")
185
- return
186
- }
187
- if (key.backspace) {
188
- setInsertText((t) => t.slice(0, -1))
189
- addEvent("key", "Backspace", "gray")
190
- return
191
- }
192
- if (input && input >= " ") {
193
- setInsertText((t) => t + input)
194
- addEvent("key", `'${input}'`, "gray")
195
- return
196
- }
197
- }
198
- })
199
-
200
- return (
201
- <Box flexDirection="column" padding={1}>
202
- {/* Top bar */}
203
- <Box gap={2} marginBottom={1}>
204
- <Text>
205
- <Text bold>Mode:</Text>{" "}
206
- <Text color={mode === "normal" ? "cyan" : "green"} bold>
207
- {mode.toUpperCase()}
208
- </Text>
209
- </Text>
210
- <Text>
211
- <Text bold>Kitty:</Text> {kittySupported ? <Text color="green">yes</Text> : <Text color="yellow">no</Text>}
212
- </Text>
213
- {mousePos && (
214
- <Text>
215
- <Text bold>Mouse:</Text> ({mousePos.x},{mousePos.y})
216
- </Text>
217
- )}
218
- </Box>
219
-
220
- <Box gap={2}>
221
- {/* Left: List + keybindings */}
222
- <Box flexDirection="column" width={35}>
223
- {/* Interactive list */}
224
- <Text bold color="cyan">
225
- Items
226
- </Text>
227
- <Box flexDirection="column" marginTop={1}>
228
- {ITEMS.map((item, i) => (
229
- <Text key={i}>
230
- <Text color={i === cursor ? "cyan" : "white"} bold={i === cursor}>
231
- {i === cursor ? ">" : " "} {item}
232
- </Text>
233
- </Text>
234
- ))}
235
- </Box>
236
-
237
- {/* Insert mode text */}
238
- {mode === "insert" && (
239
- <Box marginTop={1} borderStyle="single" borderColor="green" paddingX={1}>
240
- <Text>
241
- <Text bold color="green">
242
- Input:
243
- </Text>{" "}
244
- {insertText}
245
- <Text color="green">|</Text>
246
- </Text>
247
- </Box>
248
- )}
249
-
250
- {/* Keybinding reference */}
251
- <Box flexDirection="column" marginTop={1}>
252
- <Text bold dim>
253
- Keybindings
254
- </Text>
255
- {KEYBINDINGS.filter((kb) => kb.mode === "both" || kb.mode === mode).map((kb, i) => {
256
- const parsed = parseHotkey(kb.hotkey)
257
- const hotkeyDisplay = formatHotkey(kb.hotkey, parsed)
258
- const needsKitty = kb.hotkey.includes("⌘") || kb.hotkey.includes("✦")
259
- return (
260
- <Text key={i} dimColor={needsKitty && !kittySupported}>
261
- <Text bold color="yellow">
262
- {hotkeyDisplay.padEnd(10)}
263
- </Text>{" "}
264
- {kb.action}
265
- {needsKitty && !kittySupported ? <Text dim> (needs Kitty)</Text> : ""}
266
- </Text>
267
- )
268
- })}
269
- </Box>
270
- </Box>
271
-
272
- {/* Right: Event log */}
273
- <Box flexDirection="column" width={45}>
274
- <Text bold color="cyan">
275
- All Events ({counterRef.current})
276
- </Text>
277
- <Box height={1} />
278
- {events.length === 0 ? (
279
- <Text dim>Interact to see events...</Text>
280
- ) : (
281
- events.map((e, i) => (
282
- <Text key={i} dimColor={i < events.length - 1}>
283
- <Text color={e.type === "key" ? "cyan" : "blue"}>{e.type === "key" ? "KEY" : "PTR"}</Text>{" "}
284
- <Text color={(e.color ?? "white") as any}>{e.summary}</Text>
285
- </Text>
286
- ))
287
- )}
288
- </Box>
289
- </Box>
290
- </Box>
291
- )
292
- }
293
-
294
- function formatHotkey(raw: string, parsed: ReturnType<typeof parseHotkey>): string {
295
- // Use the raw string if it already uses symbols
296
- if (/[⌘⌥⌃⇧✦]/.test(raw)) return raw
297
- // Otherwise build from parsed
298
- const parts: string[] = []
299
- if (parsed.ctrl) parts.push("⌃")
300
- if (parsed.shift) parts.push("⇧")
301
- if (parsed.alt) parts.push("⌥")
302
- if (parsed.super) parts.push("⌘")
303
- if (parsed.hyper) parts.push("✦")
304
- parts.push(parsed.key)
305
- return parts.join("")
306
- }
307
-
308
- async function main() {
309
- const cleanup = () => {
310
- const stdout = process.stdout
311
- stdout.write("\x1b[?1003l\x1b[?1006l") // Disable mouse
312
- stdout.write("\x1b[?25h") // Show cursor
313
- stdout.write("\x1b[?1049l") // Exit alternate screen
314
- stdout.write("\x1b[0m") // Reset colors
315
- if (process.stdin.isTTY && process.stdin.isRaw) {
316
- try {
317
- process.stdin.setRawMode(false)
318
- } catch {}
319
- }
320
- }
321
- process.on("uncaughtException", (err) => {
322
- cleanup()
323
- throw err
324
- })
325
-
326
- // Detect Kitty support
327
- const kittyResult = await detectKittyFromStdio(process.stdout, process.stdin)
328
-
329
- // Enable Kitty with full flags if supported
330
- if (kittyResult.supported) {
331
- const flags =
332
- KittyFlags.DISAMBIGUATE |
333
- KittyFlags.REPORT_EVENTS |
334
- KittyFlags.REPORT_ALTERNATE |
335
- KittyFlags.REPORT_ALL_KEYS |
336
- KittyFlags.REPORT_TEXT
337
- process.stdout.write(enableKittyKeyboard(flags))
338
- }
339
-
340
- using term = createTerm()
341
- const { waitUntilExit } = await render(
342
- <ExampleBanner meta={meta} controls="i insert Esc normal j/k navigate q quit">
343
- <RichInputDemo kittySupported={kittyResult.supported} />
344
- </ExampleBanner>,
345
- term,
346
- )
347
- await waitUntilExit()
348
-
349
- // Cleanup
350
- if (kittyResult.supported) {
351
- process.stdout.write(disableKittyKeyboard())
352
- }
353
- }
354
-
355
- if (import.meta.main) {
356
- main().catch((err) => {
357
- // Restore terminal on crash
358
- const stdout = process.stdout
359
- stdout.write("\x1b[?1003l\x1b[?1006l") // Disable mouse
360
- stdout.write("\x1b[?25h") // Show cursor
361
- stdout.write("\x1b[?1049l") // Exit alternate screen
362
- stdout.write("\x1b[0m") // Reset colors
363
- if (process.stdin.isTTY && process.stdin.isRaw) {
364
- try {
365
- process.stdin.setRawMode(false)
366
- } catch {}
367
- }
368
- console.error(err)
369
- process.exit(1)
370
- })
371
- }