@silvery/examples 0.5.6 → 0.17.4

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