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,378 +0,0 @@
1
- /**
2
- * Key Explorer
3
- *
4
- * Interactive key chord tester — press any key combination to see exactly
5
- * how the terminal reports it. Color-coded modifiers, live event log,
6
- * and a visual modifier dashboard make it easy to understand what your
7
- * terminal can do.
8
- *
9
- * Features:
10
- * - Legacy vs Kitty parsing differences
11
- * - All modifier fields (ctrl, alt, shift, super, hyper, capsLock, numLock)
12
- * - Event types (press/repeat/release)
13
- * - shiftedKey, baseLayoutKey, associatedText
14
- * - macOS symbols in the display (⌘ ⌥ ⌃ ⇧ ✦)
15
- * - Kitty auto-detection on startup
16
- *
17
- * Run: bun vendor/silvery/examples/kitty/key-explorer.tsx
18
- */
19
-
20
- import React, { useState, useRef, useEffect } from "react"
21
- import {
22
- render,
23
- Box,
24
- Text,
25
- useInput,
26
- useApp,
27
- createTerm,
28
- parseKeypress,
29
- KittyFlags,
30
- enableKittyKeyboard,
31
- disableKittyKeyboard,
32
- detectKittyFromStdio,
33
- type Key,
34
- type ParsedKeypress,
35
- } from "../../src/index.js"
36
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
37
-
38
- export const meta: ExampleMeta = {
39
- name: "Key Events",
40
- description: "Interactive key chord tester with color-coded modifiers",
41
- features: ["parseKeypress()", "detectKittySupport()", "⌘ ⌥ ⌃ ⇧ ✦ symbols", "KittyFlags"],
42
- }
43
-
44
- // eventType is already a string ("press" | "repeat" | "release")
45
-
46
- interface KeyEvent {
47
- index: number
48
- input: string
49
- key: Key
50
- parsed: ParsedKeypress
51
- raw: string
52
- }
53
-
54
- /** Modifier definition with display name, symbol, and color */
55
- interface ModDef {
56
- symbol: string
57
- label: string
58
- color: string
59
- }
60
-
61
- const MODIFIER_DEFS: ModDef[] = [
62
- { symbol: "⌃", label: "Ctrl", color: "red" },
63
- { symbol: "⇧", label: "Shift", color: "yellow" },
64
- { symbol: "⌥", label: "Alt", color: "blue" },
65
- { symbol: "⌘", label: "Super", color: "green" },
66
- { symbol: "✦", label: "Hyper", color: "magenta" },
67
- ]
68
-
69
- function KeyExplorer({ kittySupported }: { kittySupported: boolean }): JSX.Element {
70
- const { exit } = useApp()
71
- const [events, setEvents] = useState<KeyEvent[]>([])
72
- const [latest, setLatest] = useState<KeyEvent | null>(null)
73
- const counterRef = useRef(0)
74
- const stdin = process.stdin
75
-
76
- // Listen to raw stdin for full ParsedKeypress info
77
- useEffect(() => {
78
- const onData = (data: Buffer) => {
79
- const raw = data.toString()
80
- // Skip mouse sequences
81
- if (raw.startsWith("\x1b[<")) return
82
-
83
- const parsed = parseKeypress(raw)
84
- // Don't log the quit key
85
- if (parsed.name === "escape" || (raw === "q" && !parsed.ctrl && !parsed.meta)) return
86
-
87
- counterRef.current++
88
- const [input, key] = parseInputKey(raw)
89
- const event: KeyEvent = {
90
- index: counterRef.current,
91
- input,
92
- key,
93
- parsed,
94
- raw,
95
- }
96
- setLatest(event)
97
- setEvents((prev) => [...prev.slice(-14), event])
98
- }
99
-
100
- stdin.on("data", onData)
101
- return () => {
102
- stdin.off("data", onData)
103
- }
104
- }, [stdin])
105
-
106
- useInput((input: string, key: Key) => {
107
- if (input === "q" || key.escape) {
108
- exit()
109
- }
110
- })
111
-
112
- return (
113
- <Box flexDirection="column" padding={1}>
114
- {/* Status bar */}
115
- <Box gap={2} marginBottom={1}>
116
- <Text>
117
- <Text bold color="cyan">
118
- Protocol:
119
- </Text>{" "}
120
- {kittySupported ? (
121
- <Text color="green">Kitty keyboard enabled</Text>
122
- ) : (
123
- <Text color="yellow">Legacy mode (terminal does not support Kitty)</Text>
124
- )}
125
- </Text>
126
- </Box>
127
-
128
- <Box gap={4}>
129
- {/* Left panel: Current key details */}
130
- <Box flexDirection="column" width={50}>
131
- <Text bold color="cyan">
132
- Last Key Pressed
133
- </Text>
134
- <Box height={1} />
135
- {latest ? (
136
- <KeyDetails event={latest} />
137
- ) : (
138
- <Box flexDirection="column">
139
- <Text color="cyan">Try pressing some key combinations:</Text>
140
- <Box height={1} />
141
- <Text> Ctrl+A, Shift+Tab, Alt+Enter...</Text>
142
- {kittySupported && <Text> Cmd+S, Hyper+X (Kitty-only)</Text>}
143
- <Box height={1} />
144
- <Text dim>Each keypress shows its full breakdown here.</Text>
145
- </Box>
146
- )}
147
- </Box>
148
-
149
- {/* Right panel: Event log */}
150
- <Box flexDirection="column" width={42}>
151
- <Text bold color="cyan">
152
- Event Log
153
- </Text>
154
- <Text dim>
155
- {counterRef.current} {counterRef.current === 1 ? "event" : "events"} captured
156
- </Text>
157
- <Box height={1} />
158
- {events.length === 0 ? (
159
- <Text dim>Waiting for input...</Text>
160
- ) : (
161
- events.map((e, i) => (
162
- <Text key={i} dimColor={i < events.length - 1}>
163
- <Text dim>#{String(e.index).padStart(3)}</Text> {formatEventSummary(e)}
164
- </Text>
165
- ))
166
- )}
167
- </Box>
168
- </Box>
169
- </Box>
170
- )
171
- }
172
-
173
- function KeyDetails({ event }: { event: KeyEvent }): JSX.Element {
174
- const { parsed, raw } = event
175
-
176
- // Determine which modifiers are active
177
- const modActive: boolean[] = [parsed.ctrl, parsed.shift, parsed.meta || parsed.option, parsed.super, parsed.hyper]
178
-
179
- return (
180
- <Box flexDirection="column">
181
- {/* Key name - big and prominent */}
182
- <Text>
183
- <Text bold>Name:</Text>{" "}
184
- <Text bold color="white">
185
- {parsed.name || "(none)"}
186
- </Text>
187
- </Text>
188
- <Text>
189
- <Text bold>Input:</Text> {JSON.stringify(event.input)}
190
- </Text>
191
-
192
- {/* Color-coded modifier dashboard */}
193
- <Box marginTop={1}>
194
- <Box gap={1}>
195
- {MODIFIER_DEFS.map((mod, i) => (
196
- <ModBadge key={mod.symbol} mod={mod} active={modActive[i]!} />
197
- ))}
198
- </Box>
199
- </Box>
200
-
201
- {/* Event type (Kitty-only) */}
202
- {parsed.eventType && (
203
- <Box marginTop={1}>
204
- <Text>
205
- <Text bold>Event type:</Text> <Text color="magenta">{parsed.eventType}</Text>
206
- </Text>
207
- </Box>
208
- )}
209
-
210
- {/* Kitty-specific fields */}
211
- <Box flexDirection="column" marginTop={1}>
212
- <Text bold dim>
213
- Kitty Extensions
214
- </Text>
215
- <KeyField label="shiftedKey" value={parsed.shiftedKey} />
216
- <KeyField label="baseLayoutKey" value={parsed.baseLayoutKey} />
217
- <KeyField label="associatedText" value={parsed.associatedText} />
218
- <KeyField label="capsLock" value={parsed.capsLock} />
219
- <KeyField label="numLock" value={parsed.numLock} />
220
- </Box>
221
-
222
- {/* Raw sequence */}
223
- <Box marginTop={1}>
224
- <Text>
225
- <Text bold>Raw:</Text>{" "}
226
- <Text dim>
227
- {[...raw]
228
- .map((c) =>
229
- c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127
230
- ? `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`
231
- : c,
232
- )
233
- .join("")}
234
- </Text>
235
- </Text>
236
- </Box>
237
- <Text>
238
- <Text bold>Sequence:</Text> <Text dim>{JSON.stringify(parsed.sequence)}</Text>
239
- </Text>
240
- </Box>
241
- )
242
- }
243
-
244
- function ModBadge({ mod, active }: { mod: ModDef; active: boolean }): JSX.Element {
245
- if (active) {
246
- return (
247
- <Text backgroundColor={mod.color as any} color="white" bold>
248
- {` ${mod.symbol} ${mod.label} `}
249
- </Text>
250
- )
251
- }
252
- return (
253
- <Text dim color="gray">
254
- {` ${mod.symbol} `}
255
- </Text>
256
- )
257
- }
258
-
259
- function KeyField({ label, value }: { label: string; value: string | boolean | undefined }): JSX.Element {
260
- if (value === undefined) {
261
- return <Text dim>{label}: --</Text>
262
- }
263
- return (
264
- <Text>
265
- {label}: <Text color="yellow">{String(value)}</Text>
266
- </Text>
267
- )
268
- }
269
-
270
- function formatEventSummary(event: KeyEvent): string {
271
- const parts: string[] = []
272
- const { parsed } = event
273
- if (parsed.ctrl) parts.push("⌃")
274
- if (parsed.shift) parts.push("⇧")
275
- if (parsed.meta || parsed.option) parts.push("⌥")
276
- if (parsed.super) parts.push("⌘")
277
- if (parsed.hyper) parts.push("✦")
278
- parts.push(parsed.name || JSON.stringify(event.input))
279
- if (parsed.eventType) parts.push(`(${parsed.eventType})`)
280
- return parts.join("")
281
- }
282
-
283
- /** Parse raw input into [input, Key] using the same logic as the runtime */
284
- function parseInputKey(raw: string): [string, Key] {
285
- const parsed = parseKeypress(raw)
286
- const key: Key = {
287
- upArrow: parsed.name === "up",
288
- downArrow: parsed.name === "down",
289
- leftArrow: parsed.name === "left",
290
- rightArrow: parsed.name === "right",
291
- pageDown: parsed.name === "pagedown",
292
- pageUp: parsed.name === "pageup",
293
- home: parsed.name === "home",
294
- end: parsed.name === "end",
295
- return: parsed.name === "return",
296
- escape: parsed.name === "escape",
297
- ctrl: parsed.ctrl,
298
- shift: parsed.shift,
299
- tab: parsed.name === "tab",
300
- backspace: parsed.name === "backspace",
301
- delete: parsed.name === "delete",
302
- meta: parsed.meta || parsed.option,
303
- super: parsed.super,
304
- hyper: parsed.hyper,
305
- eventType: parsed.eventType,
306
- }
307
-
308
- // For printable chars, input is the character itself
309
- const input = parsed.name.length === 1 ? parsed.name : ""
310
- return [input, key]
311
- }
312
-
313
- async function main() {
314
- const cleanup = () => {
315
- const stdout = process.stdout
316
- stdout.write("\x1b[?1003l\x1b[?1006l") // Disable mouse
317
- stdout.write("\x1b[?25h") // Show cursor
318
- stdout.write("\x1b[?1049l") // Exit alternate screen
319
- stdout.write("\x1b[0m") // Reset colors
320
- if (process.stdin.isTTY && process.stdin.isRaw) {
321
- try {
322
- process.stdin.setRawMode(false)
323
- } catch {}
324
- }
325
- }
326
- process.on("uncaughtException", (err) => {
327
- cleanup()
328
- throw err
329
- })
330
-
331
- // Detect Kitty support before starting the app
332
- const kittyResult = await detectKittyFromStdio(process.stdout, process.stdin)
333
-
334
- // Enable Kitty with all reporting flags if supported
335
- if (kittyResult.supported) {
336
- const flags =
337
- KittyFlags.DISAMBIGUATE |
338
- KittyFlags.REPORT_EVENTS |
339
- KittyFlags.REPORT_ALTERNATE |
340
- KittyFlags.REPORT_ALL_KEYS |
341
- KittyFlags.REPORT_TEXT
342
- process.stdout.write(enableKittyKeyboard(flags))
343
- }
344
-
345
- using term = createTerm()
346
- const { waitUntilExit } = await render(
347
- <ExampleBanner meta={meta} controls="q/Esc quit">
348
- <KeyExplorer kittySupported={kittyResult.supported} />
349
- </ExampleBanner>,
350
- term,
351
- )
352
- await waitUntilExit()
353
-
354
- // Cleanup: disable Kitty protocol
355
- if (kittyResult.supported) {
356
- process.stdout.write(disableKittyKeyboard())
357
- }
358
- }
359
-
360
- if (import.meta.main) {
361
- try {
362
- await main()
363
- } catch (err) {
364
- // Restore terminal on crash
365
- const stdout = process.stdout
366
- stdout.write("\x1b[?1003l\x1b[?1006l") // Disable mouse
367
- stdout.write("\x1b[?25h") // Show cursor
368
- stdout.write("\x1b[?1049l") // Exit alternate screen
369
- stdout.write("\x1b[0m") // Reset colors
370
- if (process.stdin.isTTY && process.stdin.isRaw) {
371
- try {
372
- process.stdin.setRawMode(false)
373
- } catch {}
374
- }
375
- console.error(err)
376
- process.exit(1)
377
- }
378
- }