@silvery/examples 0.17.3 → 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.
- package/dist/UPNG-Cy7ViL8f.mjs +5074 -0
- package/dist/__vite-browser-external-2447137e-BML7CYau.mjs +4 -0
- package/dist/_banner-DLPxCqVy.mjs +44 -0
- package/dist/ansi-CCE2pVS0.mjs +16397 -0
- package/dist/apng-HhhBjRGt.mjs +68 -0
- package/dist/apng-mwUQbTTF.mjs +3 -0
- package/dist/apps/aichat/index.mjs +1299 -0
- package/dist/apps/app-todo.mjs +139 -0
- package/dist/apps/async-data.mjs +204 -0
- package/dist/apps/cli-wizard.mjs +339 -0
- package/dist/apps/clipboard.mjs +198 -0
- package/dist/apps/components.mjs +864 -0
- package/dist/apps/data-explorer.mjs +483 -0
- package/dist/apps/dev-tools.mjs +397 -0
- package/dist/apps/explorer.mjs +698 -0
- package/dist/apps/gallery.mjs +766 -0
- package/dist/apps/inline-bench.mjs +115 -0
- package/dist/apps/kanban.mjs +280 -0
- package/dist/apps/layout-ref.mjs +187 -0
- package/dist/apps/outline.mjs +203 -0
- package/dist/apps/paste-demo.mjs +189 -0
- package/dist/apps/scroll.mjs +86 -0
- package/dist/apps/search-filter.mjs +287 -0
- package/dist/apps/selection.mjs +355 -0
- package/dist/apps/spatial-focus-demo.mjs +388 -0
- package/dist/apps/task-list.mjs +258 -0
- package/dist/apps/terminal-caps-demo.mjs +315 -0
- package/dist/apps/terminal.mjs +872 -0
- package/dist/apps/text-selection-demo.mjs +254 -0
- package/dist/apps/textarea.mjs +178 -0
- package/dist/apps/theme.mjs +661 -0
- package/dist/apps/transform.mjs +215 -0
- package/dist/apps/virtual-10k.mjs +422 -0
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/backends-Bahh9mKN.mjs +1179 -0
- package/dist/backends-CCtCDQ94.mjs +3 -0
- package/dist/{cli.mjs → bin/cli.mjs} +15 -19
- package/dist/chunk-BSw8zbkd.mjs +37 -0
- package/dist/components/counter.mjs +48 -0
- package/dist/components/hello.mjs +31 -0
- package/dist/components/progress-bar.mjs +59 -0
- package/dist/components/select-list.mjs +85 -0
- package/dist/components/spinner.mjs +57 -0
- package/dist/components/text-input.mjs +62 -0
- package/dist/components/virtual-list.mjs +51 -0
- package/dist/flexily-zero-adapter-UB-ra8fR.mjs +3374 -0
- package/dist/gif-BZaqPPVX.mjs +3 -0
- package/dist/gif-BtnXuxLF.mjs +71 -0
- package/dist/gifenc-CLRW41dk.mjs +728 -0
- package/dist/jsx-runtime-dMs_8fNu.mjs +241 -0
- package/dist/key-mapping-5oYQdAQE.mjs +3 -0
- package/dist/key-mapping-D4LR1go6.mjs +130 -0
- package/dist/layout/dashboard.mjs +1204 -0
- package/dist/layout/live-resize.mjs +303 -0
- package/dist/layout/overflow.mjs +70 -0
- package/dist/layout/text-layout.mjs +335 -0
- package/dist/node-NuJ94BWl.mjs +1083 -0
- package/dist/plugins-D1KtkT4a.mjs +3057 -0
- package/dist/resvg-js-C_8Wps1F.mjs +201 -0
- package/dist/src-BTEVGpd9.mjs +23538 -0
- package/dist/src-CUUOuRH6.mjs +5322 -0
- package/dist/src-CzfRafCQ.mjs +814 -0
- package/dist/usingCtx-CsEf0xO3.mjs +57 -0
- package/dist/yoga-adapter-BVtQ5OJR.mjs +237 -0
- package/package.json +18 -13
- package/_banner.tsx +0 -60
- package/apps/aichat/components.tsx +0 -469
- package/apps/aichat/index.tsx +0 -220
- package/apps/aichat/script.ts +0 -460
- package/apps/aichat/state.ts +0 -325
- package/apps/aichat/types.ts +0 -19
- package/apps/app-todo.tsx +0 -201
- package/apps/async-data.tsx +0 -196
- package/apps/cli-wizard.tsx +0 -332
- package/apps/clipboard.tsx +0 -183
- package/apps/components.tsx +0 -658
- package/apps/data-explorer.tsx +0 -490
- package/apps/dev-tools.tsx +0 -395
- package/apps/explorer.tsx +0 -731
- package/apps/gallery.tsx +0 -653
- package/apps/inline-bench.tsx +0 -138
- package/apps/kanban.tsx +0 -265
- package/apps/layout-ref.tsx +0 -173
- package/apps/outline.tsx +0 -160
- package/apps/panes/index.tsx +0 -203
- package/apps/paste-demo.tsx +0 -185
- package/apps/scroll.tsx +0 -80
- package/apps/search-filter.tsx +0 -240
- package/apps/selection.tsx +0 -346
- package/apps/spatial-focus-demo.tsx +0 -372
- package/apps/task-list.tsx +0 -271
- package/apps/terminal-caps-demo.tsx +0 -317
- package/apps/terminal.tsx +0 -784
- package/apps/text-selection-demo.tsx +0 -193
- package/apps/textarea.tsx +0 -155
- package/apps/theme.tsx +0 -515
- package/apps/transform.tsx +0 -229
- package/apps/virtual-10k.tsx +0 -405
- package/apps/vterm-demo/index.tsx +0 -216
- package/components/counter.tsx +0 -49
- package/components/hello.tsx +0 -38
- package/components/progress-bar.tsx +0 -52
- package/components/select-list.tsx +0 -54
- package/components/spinner.tsx +0 -44
- package/components/text-input.tsx +0 -61
- package/components/virtual-list.tsx +0 -56
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs.map +0 -1
- package/layout/dashboard.tsx +0 -953
- package/layout/live-resize.tsx +0 -282
- package/layout/overflow.tsx +0 -51
- package/layout/text-layout.tsx +0 -283
package/apps/terminal.tsx
DELETED
|
@@ -1,784 +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
|
-
export 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
|
-
if (import.meta.main) {
|
|
783
|
-
await main()
|
|
784
|
-
}
|