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,798 +0,0 @@
1
- /**
2
- * Terminal Kitchensink
3
- *
4
- * A tabbed demo showcasing terminal interaction capabilities:
5
- * keyboard events, mouse tracking, clipboard (OSC 52), and
6
- * terminal focus detection.
7
- *
8
- * Features:
9
- * - Key event tester with color-coded modifier badges
10
- * - Mouse position tracking, button state, scroll events
11
- * - OSC 52 clipboard copy/paste
12
- * - Terminal focus/blur tracking with event log
13
- * - Kitty keyboard protocol auto-detection
14
- *
15
- * Run: bun vendor/silvery/examples/interactive/terminal.tsx
16
- */
17
-
18
- import React, { useState, useRef, useEffect } from "react"
19
- import {
20
- render,
21
- Box,
22
- Text,
23
- H2,
24
- Muted,
25
- Small,
26
- Kbd,
27
- Tabs,
28
- TabList,
29
- Tab,
30
- TabPanel,
31
- useInput,
32
- useApp,
33
- useStdout,
34
- createTerm,
35
- parseKeypress,
36
- copyToClipboard,
37
- requestClipboard,
38
- parseClipboardResponse,
39
- enableMouse,
40
- disableMouse,
41
- isMouseSequence,
42
- parseMouseSequence,
43
- KittyFlags,
44
- enableKittyKeyboard,
45
- disableKittyKeyboard,
46
- detectKittyFromStdio,
47
- enableFocusReporting,
48
- disableFocusReporting,
49
- parseFocusEvent,
50
- type Key,
51
- type ParsedKeypress,
52
- } from "../../src/index.js"
53
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
54
-
55
- export const meta: ExampleMeta = {
56
- name: "Terminal",
57
- description: "Keyboard, mouse, clipboard, focus, and terminal capabilities",
58
- demo: true,
59
- features: ["useInput", "useMouse", "clipboard", "focus", "Kitty protocol"],
60
- }
61
-
62
- // ============================================================================
63
- // Types
64
- // ============================================================================
65
-
66
- interface KeyEvent {
67
- index: number
68
- input: string
69
- key: Key
70
- parsed: ParsedKeypress
71
- raw: string
72
- }
73
-
74
- interface MouseLogEntry {
75
- index: number
76
- action: string
77
- button: string
78
- x: number
79
- y: number
80
- mods: string
81
- timestamp: string
82
- }
83
-
84
- interface FocusEvent {
85
- index: number
86
- focused: boolean
87
- timestamp: string
88
- }
89
-
90
- /** Modifier definition with display name, symbol, and color */
91
- interface ModDef {
92
- symbol: string
93
- label: string
94
- color: string
95
- }
96
-
97
- const MODIFIER_DEFS: ModDef[] = [
98
- { symbol: "\u2303", label: "Ctrl", color: "$color1" },
99
- { symbol: "\u21E7", label: "Shift", color: "$color3" },
100
- { symbol: "\u2325", label: "Alt", color: "$color4" },
101
- { symbol: "\u2318", label: "Super", color: "$color2" },
102
- { symbol: "\u2726", label: "Hyper", color: "$color5" },
103
- ]
104
-
105
- // ============================================================================
106
- // Shared utilities
107
- // ============================================================================
108
-
109
- function now(): string {
110
- const d = new Date()
111
- return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}:${d.getSeconds().toString().padStart(2, "0")}`
112
- }
113
-
114
- // ============================================================================
115
- // Keys Tab
116
- // ============================================================================
117
-
118
- function KeysTab({ kittySupported }: { kittySupported: boolean }): JSX.Element {
119
- const [events, setEvents] = useState<KeyEvent[]>([])
120
- const [latest, setLatest] = useState<KeyEvent | null>(null)
121
- const counterRef = useRef(0)
122
- const stdin = process.stdin
123
-
124
- useEffect(() => {
125
- const onData = (data: Buffer) => {
126
- const raw = data.toString()
127
- if (raw.startsWith("\x1b[<")) return // skip mouse
128
- if (raw.startsWith("\x1b[I") || raw.startsWith("\x1b[O")) return // skip focus
129
-
130
- const parsed = parseKeypress(raw)
131
- // Don't log quit/tab-switch keys
132
- if (parsed.name === "escape") return
133
- if (raw === "q" && !parsed.ctrl && !parsed.meta) return
134
- if (parsed.name === "left" || parsed.name === "right") return
135
- if (raw === "h" || raw === "l") return
136
-
137
- counterRef.current++
138
- const key: Key = {
139
- upArrow: parsed.name === "up",
140
- downArrow: parsed.name === "down",
141
- leftArrow: parsed.name === "left",
142
- rightArrow: parsed.name === "right",
143
- pageDown: parsed.name === "pagedown",
144
- pageUp: parsed.name === "pageup",
145
- home: parsed.name === "home",
146
- end: parsed.name === "end",
147
- return: parsed.name === "return",
148
- escape: parsed.name === "escape",
149
- ctrl: parsed.ctrl,
150
- shift: parsed.shift,
151
- tab: parsed.name === "tab",
152
- backspace: parsed.name === "backspace",
153
- delete: parsed.name === "delete",
154
- meta: parsed.meta || parsed.option,
155
- super: parsed.super,
156
- hyper: parsed.hyper,
157
- eventType: parsed.eventType,
158
- }
159
- const input = parsed.name.length === 1 ? parsed.name : ""
160
- const event: KeyEvent = { index: counterRef.current, input, key, parsed, raw }
161
- setLatest(event)
162
- setEvents((prev) => [...prev.slice(-11), event])
163
- }
164
-
165
- stdin.on("data", onData)
166
- return () => {
167
- stdin.off("data", onData)
168
- }
169
- }, [stdin])
170
-
171
- return (
172
- <Box gap={3} paddingX={1} paddingTop={1}>
173
- {/* Left: Current key details */}
174
- <Box flexDirection="column" width={46}>
175
- <H2>Last Key Pressed</H2>
176
- <Box height={1} />
177
- {latest ? <KeyDetails event={latest} /> : <KeyPlaceholder kittySupported={kittySupported} />}
178
- </Box>
179
-
180
- {/* Right: Event log */}
181
- <Box flexDirection="column" flexGrow={1}>
182
- <H2>
183
- Event Log{" "}
184
- <Small>
185
- ({counterRef.current} {counterRef.current === 1 ? "event" : "events"})
186
- </Small>
187
- </H2>
188
- <Box height={1} />
189
- {events.length === 0 ? (
190
- <Muted>Waiting for input...</Muted>
191
- ) : (
192
- <Box flexDirection="column" overflow="scroll" scrollTo={events.length - 1}>
193
- {events.map((e, i) => (
194
- <Text key={e.index} dimColor={i < events.length - 1}>
195
- <Text color="$muted">#{String(e.index).padStart(3)}</Text> {formatKeyEventSummary(e)}
196
- </Text>
197
- ))}
198
- </Box>
199
- )}
200
- </Box>
201
- </Box>
202
- )
203
- }
204
-
205
- function KeyPlaceholder({ kittySupported }: { kittySupported: boolean }): JSX.Element {
206
- return (
207
- <Box flexDirection="column">
208
- <Text>Try pressing some key combinations:</Text>
209
- <Box height={1} />
210
- <Text> Ctrl+A, Shift+Tab, Alt+Enter...</Text>
211
- {kittySupported && <Text> Cmd+S, Hyper+X (Kitty-only)</Text>}
212
- <Box height={1} />
213
- <Muted>Each keypress shows its full breakdown here.</Muted>
214
- </Box>
215
- )
216
- }
217
-
218
- function KeyDetails({ event }: { event: KeyEvent }): JSX.Element {
219
- const { parsed, raw } = event
220
- const modActive: boolean[] = [parsed.ctrl, parsed.shift, parsed.meta || parsed.option, parsed.super, parsed.hyper]
221
-
222
- return (
223
- <Box flexDirection="column">
224
- <Text>
225
- <Text bold>Name:</Text>{" "}
226
- <Text bold color="$primary">
227
- {parsed.name || "(none)"}
228
- </Text>
229
- </Text>
230
- <Text>
231
- <Text bold>Input:</Text> {JSON.stringify(event.input)}
232
- </Text>
233
-
234
- {/* Modifier badges */}
235
- <Box marginTop={1} gap={1}>
236
- {MODIFIER_DEFS.map((mod, i) => (
237
- <ModBadge key={mod.symbol} mod={mod} active={modActive[i]!} />
238
- ))}
239
- </Box>
240
-
241
- {/* Event type (Kitty-only) */}
242
- {parsed.eventType && (
243
- <Box marginTop={1}>
244
- <Text>
245
- <Text bold>Event type:</Text> <Text color="$accent">{parsed.eventType}</Text>
246
- </Text>
247
- </Box>
248
- )}
249
-
250
- {/* Kitty extensions */}
251
- <Box flexDirection="column" marginTop={1}>
252
- <Text bold color="$muted">
253
- Kitty Extensions
254
- </Text>
255
- <KittyField label="shiftedKey" value={parsed.shiftedKey} />
256
- <KittyField label="baseLayoutKey" value={parsed.baseLayoutKey} />
257
- <KittyField label="associatedText" value={parsed.associatedText} />
258
- <KittyField label="capsLock" value={parsed.capsLock} />
259
- <KittyField label="numLock" value={parsed.numLock} />
260
- </Box>
261
-
262
- {/* Raw sequence */}
263
- <Box marginTop={1}>
264
- <Text>
265
- <Text bold>Raw:</Text>{" "}
266
- <Muted>
267
- {[...raw]
268
- .map((c) =>
269
- c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127
270
- ? `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`
271
- : c,
272
- )
273
- .join("")}
274
- </Muted>
275
- </Text>
276
- </Box>
277
- </Box>
278
- )
279
- }
280
-
281
- function ModBadge({ mod, active }: { mod: ModDef; active: boolean }): JSX.Element {
282
- if (active) {
283
- return (
284
- <Text backgroundColor={mod.color} color="$inversebg" bold>
285
- {` ${mod.symbol} ${mod.label} `}
286
- </Text>
287
- )
288
- }
289
- return <Text color="$muted">{` ${mod.symbol} `}</Text>
290
- }
291
-
292
- function KittyField({ label, value }: { label: string; value: string | boolean | undefined }): JSX.Element {
293
- if (value === undefined) {
294
- return (
295
- <Muted>
296
- {label}: {"--"}
297
- </Muted>
298
- )
299
- }
300
- return (
301
- <Text>
302
- {label}: <Text color="$warning">{String(value)}</Text>
303
- </Text>
304
- )
305
- }
306
-
307
- function formatKeyEventSummary(event: KeyEvent): string {
308
- const parts: string[] = []
309
- const { parsed } = event
310
- if (parsed.ctrl) parts.push("\u2303")
311
- if (parsed.shift) parts.push("\u21E7")
312
- if (parsed.meta || parsed.option) parts.push("\u2325")
313
- if (parsed.super) parts.push("\u2318")
314
- if (parsed.hyper) parts.push("\u2726")
315
- parts.push(parsed.name || JSON.stringify(event.input))
316
- if (parsed.eventType) parts.push(` (${parsed.eventType})`)
317
- return parts.join("")
318
- }
319
-
320
- // ============================================================================
321
- // Mouse Tab
322
- // ============================================================================
323
-
324
- function MouseTab(): JSX.Element {
325
- const [mousePos, setMousePos] = useState<{ x: number; y: number } | null>(null)
326
- const [events, setEvents] = useState<MouseLogEntry[]>([])
327
- const [clicks, setClicks] = useState({ left: 0, middle: 0, right: 0 })
328
- const [scrollTotal, setScrollTotal] = useState(0)
329
- const counterRef = useRef(0)
330
- const stdin = process.stdin
331
-
332
- useEffect(() => {
333
- const onData = (data: Buffer) => {
334
- const raw = data.toString()
335
- if (!isMouseSequence(raw)) return
336
-
337
- const parsed = parseMouseSequence(raw)
338
- if (!parsed) return
339
-
340
- setMousePos({ x: parsed.x, y: parsed.y })
341
-
342
- const mods: string[] = []
343
- if (parsed.ctrl) mods.push("Ctrl")
344
- if (parsed.shift) mods.push("Shift")
345
- if (parsed.meta) mods.push("Alt")
346
- const modStr = mods.join("+")
347
-
348
- if (parsed.action === "down") {
349
- const btn = ["Left", "Middle", "Right"][parsed.button] ?? `Btn${parsed.button}`
350
- counterRef.current++
351
- setEvents((prev) => [
352
- ...prev.slice(-11),
353
- {
354
- index: counterRef.current,
355
- action: "click",
356
- button: btn,
357
- x: parsed.x,
358
- y: parsed.y,
359
- mods: modStr,
360
- timestamp: now(),
361
- },
362
- ])
363
- if (parsed.button === 0) setClicks((c) => ({ ...c, left: c.left + 1 }))
364
- else if (parsed.button === 1) setClicks((c) => ({ ...c, middle: c.middle + 1 }))
365
- else if (parsed.button === 2) setClicks((c) => ({ ...c, right: c.right + 1 }))
366
- } else if (parsed.action === "wheel") {
367
- counterRef.current++
368
- const dir = parsed.delta! < 0 ? "up" : "down"
369
- setEvents((prev) => [
370
- ...prev.slice(-11),
371
- {
372
- index: counterRef.current,
373
- action: `scroll ${dir}`,
374
- button: "wheel",
375
- x: parsed.x,
376
- y: parsed.y,
377
- mods: modStr,
378
- timestamp: now(),
379
- },
380
- ])
381
- setScrollTotal((s) => s + 1)
382
- } else if (parsed.action === "move") {
383
- // Just update position, don't flood the log
384
- }
385
- }
386
-
387
- stdin.on("data", onData)
388
- return () => {
389
- stdin.off("data", onData)
390
- }
391
- }, [stdin])
392
-
393
- return (
394
- <Box gap={3} paddingX={1} paddingTop={1}>
395
- {/* Left: Position + stats */}
396
- <Box flexDirection="column" width={36}>
397
- <H2>Position</H2>
398
- <Box marginTop={1}>
399
- {mousePos ? (
400
- <Box flexDirection="column">
401
- <Text>
402
- <Text bold>X:</Text>{" "}
403
- <Text color="$primary" bold>
404
- {String(mousePos.x).padStart(4)}
405
- </Text>
406
- </Text>
407
- <Text>
408
- <Text bold>Y:</Text>{" "}
409
- <Text color="$primary" bold>
410
- {String(mousePos.y).padStart(4)}
411
- </Text>
412
- </Text>
413
- </Box>
414
- ) : (
415
- <Muted>Move mouse to track position</Muted>
416
- )}
417
- </Box>
418
-
419
- <Box marginTop={1} flexDirection="column">
420
- <H2>Click Counts</H2>
421
- <Box marginTop={1} flexDirection="column">
422
- <Text>
423
- <Text bold>Left:</Text> <Text color="$info">{clicks.left}</Text>
424
- </Text>
425
- <Text>
426
- <Text bold>Middle:</Text> <Text color="$info">{clicks.middle}</Text>
427
- </Text>
428
- <Text>
429
- <Text bold>Right:</Text> <Text color="$info">{clicks.right}</Text>
430
- </Text>
431
- <Text>
432
- <Text bold>Scroll:</Text> <Text color="$info">{scrollTotal}</Text>
433
- </Text>
434
- </Box>
435
- </Box>
436
- </Box>
437
-
438
- {/* Right: Event log */}
439
- <Box flexDirection="column" flexGrow={1}>
440
- <H2>
441
- Mouse Events <Small>({counterRef.current})</Small>
442
- </H2>
443
- <Box height={1} />
444
- {events.length === 0 ? (
445
- <Muted>Click or scroll to see events...</Muted>
446
- ) : (
447
- <Box flexDirection="column" overflow="scroll" scrollTo={events.length - 1}>
448
- {events.map((e, i) => (
449
- <Text key={e.index} dimColor={i < events.length - 1}>
450
- <Small>{e.timestamp}</Small>{" "}
451
- <Text color={e.action.startsWith("scroll") ? "$accent" : "$primary"} bold>
452
- {e.action}
453
- </Text>{" "}
454
- {e.button !== "wheel" && <Text>{e.button} </Text>}
455
- <Muted>
456
- ({e.x},{e.y})
457
- </Muted>
458
- {e.mods ? <Text color="$warning"> +{e.mods}</Text> : null}
459
- </Text>
460
- ))}
461
- </Box>
462
- )}
463
- </Box>
464
- </Box>
465
- )
466
- }
467
-
468
- // ============================================================================
469
- // Clipboard Tab
470
- // ============================================================================
471
-
472
- function ClipboardTab(): JSX.Element {
473
- const { stdout } = useStdout()
474
- const [selectedIndex, setSelectedIndex] = useState(0)
475
- const [lastCopied, setLastCopied] = useState<string | null>(null)
476
- const [lastPasted, setLastPasted] = useState<string | null>(null)
477
- const [history, setHistory] = useState<Array<{ action: string; text: string; time: string }>>([])
478
-
479
- const snippets = [
480
- "Hello, world!",
481
- "The quick brown fox jumps over the lazy dog",
482
- "OSC 52 clipboard protocol",
483
- "npx silvery examples",
484
- "console.log('silvery')",
485
- "https://silvery.dev",
486
- ]
487
-
488
- useInput((input: string, key: Key) => {
489
- // Navigation
490
- if (key.upArrow || input === "k") {
491
- setSelectedIndex((i) => Math.max(0, i - 1))
492
- }
493
- if (key.downArrow || input === "j") {
494
- setSelectedIndex((i) => Math.min(snippets.length - 1, i + 1))
495
- }
496
-
497
- // Copy selected item
498
- if (input === "c") {
499
- const text = snippets[selectedIndex]!
500
- copyToClipboard(stdout, text)
501
- setLastCopied(text)
502
- setHistory((h) => [...h.slice(-7), { action: "copy", text, time: now() }])
503
- }
504
-
505
- // Request clipboard
506
- if (input === "v") {
507
- requestClipboard(stdout)
508
- setHistory((h) => [...h.slice(-7), { action: "request", text: "(paste requested)", time: now() }])
509
- }
510
-
511
- // Parse clipboard response from raw input
512
- const parsed = parseClipboardResponse(input)
513
- if (parsed) {
514
- setLastPasted(parsed)
515
- setHistory((h) => [...h.slice(-7), { action: "paste", text: parsed, time: now() }])
516
- }
517
- })
518
-
519
- return (
520
- <Box flexDirection="column" paddingX={1} paddingTop={1} gap={1}>
521
- {/* Snippet list */}
522
- <Box flexDirection="column">
523
- <H2>
524
- Snippets{" "}
525
- <Small>
526
- {selectedIndex + 1}/{snippets.length}
527
- </Small>
528
- </H2>
529
- <Box flexDirection="column" marginTop={1} overflow="scroll" scrollTo={selectedIndex}>
530
- {snippets.map((text, i) => (
531
- <Box key={i} paddingX={1}>
532
- <Text
533
- color={i === selectedIndex ? "$bg" : undefined}
534
- backgroundColor={i === selectedIndex ? "$primary" : undefined}
535
- bold={i === selectedIndex}
536
- >
537
- {i === selectedIndex ? " > " : " "}
538
- {text}
539
- </Text>
540
- </Box>
541
- ))}
542
- </Box>
543
- </Box>
544
-
545
- {/* Status */}
546
- <Box gap={4}>
547
- <Box flexDirection="column">
548
- <Text bold>Last Copied:</Text>
549
- {lastCopied ? (
550
- <Text color="$success">
551
- {"✓ "}
552
- {lastCopied}
553
- </Text>
554
- ) : (
555
- <Muted>nothing</Muted>
556
- )}
557
- </Box>
558
- <Box flexDirection="column">
559
- <Text bold>Last Pasted:</Text>
560
- {lastPasted ? <Text color="$warning">{lastPasted}</Text> : <Muted>nothing</Muted>}
561
- </Box>
562
- </Box>
563
-
564
- {/* History */}
565
- {history.length > 0 && (
566
- <Box flexDirection="column">
567
- <H2>History</H2>
568
- <Box flexDirection="column" overflow="scroll" scrollTo={history.length - 1}>
569
- {history.map((h, i) => (
570
- <Text key={i} dimColor={i < history.length - 1}>
571
- <Small>{h.time}</Small>{" "}
572
- <Text color={h.action === "copy" ? "$success" : h.action === "paste" ? "$warning" : "$muted"} bold>
573
- {h.action}
574
- </Text>{" "}
575
- <Text>{h.text.length > 40 ? h.text.slice(0, 37) + "..." : h.text}</Text>
576
- </Text>
577
- ))}
578
- </Box>
579
- </Box>
580
- )}
581
-
582
- <Muted>
583
- <Kbd>j/k</Kbd> navigate <Kbd>c</Kbd> copy <Kbd>v</Kbd> paste (OSC 52)
584
- </Muted>
585
- </Box>
586
- )
587
- }
588
-
589
- // ============================================================================
590
- // Focus Tab
591
- // ============================================================================
592
-
593
- function FocusTab(): JSX.Element {
594
- const [focused, setFocused] = useState(true)
595
- const [events, setEvents] = useState<FocusEvent[]>([])
596
- const counterRef = useRef(0)
597
- const stdin = process.stdin
598
-
599
- // Parse focus events directly from stdin (CSI I / CSI O)
600
- useEffect(() => {
601
- const onData = (data: Buffer) => {
602
- const raw = data.toString()
603
- const focusEvt = parseFocusEvent(raw)
604
- if (!focusEvt) return
605
-
606
- const isFocused = focusEvt.type === "focus-in"
607
- setFocused(isFocused)
608
- counterRef.current++
609
- setEvents((prev) => [
610
- ...prev.slice(-14),
611
- {
612
- index: counterRef.current,
613
- focused: isFocused,
614
- timestamp: now(),
615
- },
616
- ])
617
- }
618
-
619
- stdin.on("data", onData)
620
- return () => {
621
- stdin.off("data", onData)
622
- }
623
- }, [stdin])
624
-
625
- return (
626
- <Box gap={3} paddingX={1} paddingTop={1}>
627
- {/* Left: Focus indicator */}
628
- <Box flexDirection="column" width={36}>
629
- <H2>Terminal Focus</H2>
630
- <Box marginTop={1} flexDirection="column" alignItems="center" gap={1}>
631
- <Text bold color={focused ? "$success" : "$error"}>
632
- {focused ? " FOCUSED " : " UNFOCUSED "}
633
- </Text>
634
- <Text color={focused ? "$success" : "$error"}>
635
- {focused ? "Terminal window is active" : "Terminal window lost focus"}
636
- </Text>
637
- </Box>
638
-
639
- <Box marginTop={2} flexDirection="column">
640
- <Muted>
641
- Switch to another window and back to see focus events. Uses CSI I/O terminal focus reporting protocol.
642
- </Muted>
643
- </Box>
644
-
645
- <Box marginTop={1}>
646
- <Text>
647
- <Text bold>Protocol:</Text> <Text color="$info">CSI ?1004h (DECRPM focus events)</Text>
648
- </Text>
649
- </Box>
650
- </Box>
651
-
652
- {/* Right: Event log */}
653
- <Box flexDirection="column" flexGrow={1}>
654
- <H2>
655
- Focus Events <Small>({counterRef.current})</Small>
656
- </H2>
657
- <Box height={1} />
658
- {events.length === 0 ? (
659
- <Muted>Switch windows to generate focus events...</Muted>
660
- ) : (
661
- <Box flexDirection="column" overflow="scroll" scrollTo={events.length - 1}>
662
- {events.map((e, i) => (
663
- <Text key={e.index} dimColor={i < events.length - 1}>
664
- <Small>{e.timestamp}</Small>{" "}
665
- <Text color={e.focused ? "$success" : "$error"} bold>
666
- {e.focused ? "focus-in " : "focus-out"}
667
- </Text>{" "}
668
- <Text color={e.focused ? "$success" : "$error"}>
669
- {e.focused ? "Terminal gained focus" : "Terminal lost focus"}
670
- </Text>
671
- </Text>
672
- ))}
673
- </Box>
674
- )}
675
- </Box>
676
- </Box>
677
- )
678
- }
679
-
680
- // ============================================================================
681
- // Main App
682
- // ============================================================================
683
-
684
- export function TerminalDemo({ kittySupported }: { kittySupported: boolean }): JSX.Element {
685
- const { exit } = useApp()
686
-
687
- useInput((input: string, key: Key) => {
688
- if (input === "q" || key.escape) {
689
- exit()
690
- }
691
- })
692
-
693
- return (
694
- <Box flexDirection="column" flexGrow={1}>
695
- {/* Status bar */}
696
- <Box paddingX={1} gap={2}>
697
- <Text>
698
- <Text bold>Kitty:</Text>{" "}
699
- {kittySupported ? <Text color="$success">enabled</Text> : <Text color="$warning">legacy mode</Text>}
700
- </Text>
701
- </Box>
702
-
703
- {/* Tabbed content */}
704
- <Tabs defaultValue="keys">
705
- <TabList>
706
- <Tab value="keys">Keys</Tab>
707
- <Tab value="mouse">Mouse</Tab>
708
- <Tab value="clipboard">Clipboard</Tab>
709
- <Tab value="focus">Focus</Tab>
710
- </TabList>
711
-
712
- <TabPanel value="keys">
713
- <KeysTab kittySupported={kittySupported} />
714
- </TabPanel>
715
-
716
- <TabPanel value="mouse">
717
- <MouseTab />
718
- </TabPanel>
719
-
720
- <TabPanel value="clipboard">
721
- <ClipboardTab />
722
- </TabPanel>
723
-
724
- <TabPanel value="focus">
725
- <FocusTab />
726
- </TabPanel>
727
- </Tabs>
728
-
729
- <Box paddingX={1}>
730
- <Muted>
731
- <Kbd>h/l</Kbd> switch tabs <Kbd>Esc/q</Kbd> quit
732
- </Muted>
733
- </Box>
734
- </Box>
735
- )
736
- }
737
-
738
- // ============================================================================
739
- // Main
740
- // ============================================================================
741
-
742
- async function main() {
743
- // Detect Kitty support before starting the app
744
- const kittyResult = await detectKittyFromStdio(process.stdout, process.stdin)
745
-
746
- // Enable Kitty with all reporting flags if supported
747
- if (kittyResult.supported) {
748
- const flags =
749
- KittyFlags.DISAMBIGUATE |
750
- KittyFlags.REPORT_EVENTS |
751
- KittyFlags.REPORT_ALTERNATE |
752
- KittyFlags.REPORT_ALL_KEYS |
753
- KittyFlags.REPORT_TEXT
754
- process.stdout.write(enableKittyKeyboard(flags))
755
- }
756
-
757
- using term = createTerm()
758
-
759
- // Enable mouse tracking and focus reporting
760
- process.stdout.write(enableMouse())
761
- enableFocusReporting((s) => process.stdout.write(s))
762
-
763
- const { waitUntilExit } = await render(
764
- <ExampleBanner meta={meta} controls="h/l tabs Esc/q quit">
765
- <TerminalDemo kittySupported={kittyResult.supported} />
766
- </ExampleBanner>,
767
- term,
768
- )
769
-
770
- await waitUntilExit()
771
-
772
- // Cleanup
773
- process.stdout.write(disableMouse())
774
- disableFocusReporting((s) => process.stdout.write(s))
775
- if (kittyResult.supported) {
776
- process.stdout.write(disableKittyKeyboard())
777
- }
778
- }
779
-
780
- export { main }
781
-
782
- if (import.meta.main) {
783
- main().catch((err) => {
784
- const stdout = process.stdout
785
- stdout.write(disableMouse())
786
- disableFocusReporting((s) => stdout.write(s))
787
- stdout.write("\x1b[?25h")
788
- stdout.write("\x1b[?1049l")
789
- stdout.write("\x1b[0m")
790
- if (process.stdin.isTTY && process.stdin.isRaw) {
791
- try {
792
- process.stdin.setRawMode(false)
793
- } catch {}
794
- }
795
- console.error(err)
796
- process.exit(1)
797
- })
798
- }