silvery 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +41 -145
  2. package/package.json +64 -12
  3. package/src/index.ts +67 -1
  4. package/src/runtime.ts +4 -0
  5. package/src/theme.ts +4 -0
  6. package/src/ui/animation.ts +2 -0
  7. package/src/ui/ansi.ts +2 -0
  8. package/src/ui/cli.ts +2 -0
  9. package/src/ui/display.ts +2 -0
  10. package/src/ui/image.ts +2 -0
  11. package/src/ui/input.ts +2 -0
  12. package/src/ui/progress.ts +2 -0
  13. package/src/ui/react.ts +2 -0
  14. package/src/ui/utils.ts +2 -0
  15. package/src/ui/wrappers.ts +2 -0
  16. package/src/ui.ts +4 -0
  17. package/examples/CLAUDE.md +0 -75
  18. package/examples/_banner.tsx +0 -60
  19. package/examples/cli.ts +0 -228
  20. package/examples/index.md +0 -101
  21. package/examples/inline/inline-nontty.tsx +0 -98
  22. package/examples/inline/inline-progress.tsx +0 -79
  23. package/examples/inline/inline-simple.tsx +0 -63
  24. package/examples/inline/scrollback.tsx +0 -185
  25. package/examples/interactive/_input-debug.tsx +0 -110
  26. package/examples/interactive/_stdin-test.ts +0 -71
  27. package/examples/interactive/_textarea-bare.tsx +0 -45
  28. package/examples/interactive/aichat/components.tsx +0 -468
  29. package/examples/interactive/aichat/index.tsx +0 -207
  30. package/examples/interactive/aichat/script.ts +0 -460
  31. package/examples/interactive/aichat/state.ts +0 -326
  32. package/examples/interactive/aichat/types.ts +0 -19
  33. package/examples/interactive/app-todo.tsx +0 -198
  34. package/examples/interactive/async-data.tsx +0 -208
  35. package/examples/interactive/cli-wizard.tsx +0 -332
  36. package/examples/interactive/clipboard.tsx +0 -183
  37. package/examples/interactive/components.tsx +0 -463
  38. package/examples/interactive/data-explorer.tsx +0 -506
  39. package/examples/interactive/dev-tools.tsx +0 -379
  40. package/examples/interactive/explorer.tsx +0 -747
  41. package/examples/interactive/gallery.tsx +0 -652
  42. package/examples/interactive/inline-bench.tsx +0 -136
  43. package/examples/interactive/kanban.tsx +0 -267
  44. package/examples/interactive/layout-ref.tsx +0 -185
  45. package/examples/interactive/outline.tsx +0 -171
  46. package/examples/interactive/paste-demo.tsx +0 -198
  47. package/examples/interactive/scroll.tsx +0 -77
  48. package/examples/interactive/search-filter.tsx +0 -240
  49. package/examples/interactive/task-list.tsx +0 -279
  50. package/examples/interactive/terminal.tsx +0 -798
  51. package/examples/interactive/textarea.tsx +0 -103
  52. package/examples/interactive/theme.tsx +0 -336
  53. package/examples/interactive/transform.tsx +0 -256
  54. package/examples/interactive/virtual-10k.tsx +0 -413
  55. package/examples/kitty/canvas.tsx +0 -519
  56. package/examples/kitty/generate-samples.ts +0 -236
  57. package/examples/kitty/image-component.tsx +0 -273
  58. package/examples/kitty/images.tsx +0 -604
  59. package/examples/kitty/input.tsx +0 -371
  60. package/examples/kitty/keys.tsx +0 -378
  61. package/examples/kitty/paint.tsx +0 -1017
  62. package/examples/layout/dashboard.tsx +0 -551
  63. package/examples/layout/live-resize.tsx +0 -290
  64. package/examples/layout/overflow.tsx +0 -51
  65. package/examples/playground/README.md +0 -69
  66. package/examples/playground/build.ts +0 -61
  67. package/examples/playground/index.html +0 -420
  68. package/examples/playground/playground-app.tsx +0 -416
  69. package/examples/runtime/elm-counter.tsx +0 -206
  70. package/examples/runtime/hello-runtime.tsx +0 -73
  71. package/examples/runtime/pipe-composition.tsx +0 -184
  72. package/examples/runtime/run-counter.tsx +0 -78
  73. package/examples/runtime/runtime-counter.tsx +0 -197
  74. package/examples/screenshots/generate.tsx +0 -563
  75. package/examples/scrollback-perf.tsx +0 -230
  76. package/examples/viewer.tsx +0 -654
  77. package/examples/web/build.ts +0 -365
  78. package/examples/web/canvas-app.tsx +0 -80
  79. package/examples/web/canvas.html +0 -89
  80. package/examples/web/dom-app.tsx +0 -81
  81. package/examples/web/dom.html +0 -113
  82. package/examples/web/showcase-app.tsx +0 -107
  83. package/examples/web/showcase.html +0 -34
  84. package/examples/web/showcases/index.tsx +0 -56
  85. package/examples/web/viewer-app.tsx +0 -555
  86. package/examples/web/viewer.html +0 -30
  87. package/examples/web/xterm-app.tsx +0 -105
  88. package/examples/web/xterm.html +0 -118
@@ -1,506 +0,0 @@
1
- /**
2
- * Data Explorer — Process Table Example
3
- *
4
- * A process explorer with a searchable, scrollable table demonstrating:
5
- * - Table-like display with responsive column widths via useContentRect()
6
- * - TextInput for live search/filter with useDeferredValue
7
- * - VirtualList for smooth scrolling through 500+ rows
8
- * - Keyboard navigation with j/k and vim-style jumps
9
- * - Color-coded status indicators
10
- *
11
- * Usage: bun run examples/interactive/data-explorer.tsx
12
- *
13
- * Controls:
14
- * j/k or Up/Down - Navigate rows
15
- * d/u - Half-page down/up
16
- * g/G - Jump to first/last
17
- * / - Toggle search mode
18
- * Esc - Exit search / quit
19
- * q - Quit (when not searching)
20
- */
21
-
22
- import React, { useState, useCallback, useMemo, useDeferredValue } from "react"
23
- import {
24
- render,
25
- Box,
26
- Text,
27
- VirtualList,
28
- TextInput,
29
- Divider,
30
- useContentRect,
31
- useInput,
32
- useApp,
33
- createTerm,
34
- Kbd,
35
- Muted,
36
- Lead,
37
- type Key,
38
- } from "../../src/index.js"
39
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
40
-
41
- export const meta: ExampleMeta = {
42
- name: "Data Explorer",
43
- description: "Process explorer table with search, VirtualList, and responsive column widths",
44
- features: ["useContentRect()", "TextInput", "useInput()", "responsive layout", "useDeferredValue"],
45
- }
46
-
47
- // ============================================================================
48
- // Types
49
- // ============================================================================
50
-
51
- type ProcessStatus = "running" | "sleeping" | "stopped" | "zombie"
52
-
53
- interface ProcessInfo {
54
- pid: number
55
- name: string
56
- cpu: number
57
- mem: number
58
- status: ProcessStatus
59
- user: string
60
- threads: number
61
- uptime: string
62
- }
63
-
64
- // ============================================================================
65
- // Data Generation
66
- // ============================================================================
67
-
68
- const PROCESS_NAMES = [
69
- "node",
70
- "python3",
71
- "nginx",
72
- "redis-server",
73
- "postgres",
74
- "docker",
75
- "sshd",
76
- "systemd",
77
- "cron",
78
- "rsyslogd",
79
- "webpack",
80
- "vite",
81
- "chrome",
82
- "firefox",
83
- "code",
84
- "vim",
85
- "tmux",
86
- "bash",
87
- "zsh",
88
- "containerd",
89
- "kubelet",
90
- "etcd",
91
- "coredns",
92
- "flannel",
93
- "prometheus",
94
- "grafana",
95
- "elasticsearch",
96
- "kibana",
97
- "logstash",
98
- "rabbitmq",
99
- "kafka",
100
- "zookeeper",
101
- "consul",
102
- "vault",
103
- "haproxy",
104
- "traefik",
105
- "envoy",
106
- "istio-proxy",
107
- "jaeger",
108
- "mysql",
109
- "mongo",
110
- "cassandra",
111
- "clickhouse",
112
- "influxdb",
113
- "jenkins",
114
- "gitlab-runner",
115
- "buildkitd",
116
- "registry",
117
- "cadvisor",
118
- "node-exporter",
119
- "alertmanager",
120
- "telegraf",
121
- "bun",
122
- "deno",
123
- "esbuild",
124
- "swc",
125
- "turbo",
126
- "pnpm",
127
- ]
128
-
129
- const USERS = ["root", "www-data", "postgres", "redis", "node", "admin", "deploy", "monitor"]
130
- const STATUSES: ProcessStatus[] = ["running", "sleeping", "stopped", "zombie"]
131
-
132
- function seededRandom(seed: number): () => number {
133
- let s = seed
134
- return () => {
135
- s = (s * 1664525 + 1013904223) & 0x7fffffff
136
- return s / 0x7fffffff
137
- }
138
- }
139
-
140
- function formatUptime(seconds: number): string {
141
- if (seconds < 60) return `${seconds}s`
142
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m`
143
- if (seconds < 86400) return `${Math.floor(seconds / 3600)}h${Math.floor((seconds % 3600) / 60)}m`
144
- return `${Math.floor(seconds / 86400)}d${Math.floor((seconds % 86400) / 3600)}h`
145
- }
146
-
147
- function generateProcesses(count: number): ProcessInfo[] {
148
- const rng = seededRandom(42)
149
- const processes: ProcessInfo[] = []
150
-
151
- for (let i = 0; i < count; i++) {
152
- const nameBase = PROCESS_NAMES[Math.floor(rng() * PROCESS_NAMES.length)]!
153
- const hasInstance = rng() > 0.6
154
- const name = hasInstance ? `${nameBase}:${Math.floor(rng() * 20)}` : nameBase
155
- const status = rng() < 0.7 ? "running" : STATUSES[Math.floor(rng() * STATUSES.length)]!
156
-
157
- processes.push({
158
- pid: 1000 + i,
159
- name,
160
- cpu: status === "running" ? Math.round(rng() * 1000) / 10 : 0,
161
- mem: Math.round(rng() * 500) / 10,
162
- status,
163
- user: USERS[Math.floor(rng() * USERS.length)]!,
164
- threads: 1 + Math.floor(rng() * 64),
165
- uptime: formatUptime(Math.floor(rng() * 864000)),
166
- })
167
- }
168
-
169
- // Sort by CPU descending initially
170
- return processes.sort((a, b) => b.cpu - a.cpu)
171
- }
172
-
173
- const TOTAL_PROCESSES = 600
174
- const ALL_PROCESSES = generateProcesses(TOTAL_PROCESSES)
175
-
176
- // ============================================================================
177
- // Constants
178
- // ============================================================================
179
-
180
- const STATUS_COLORS: Record<ProcessStatus, string> = {
181
- running: "$success",
182
- sleeping: "$muted",
183
- stopped: "$warning",
184
- zombie: "$error",
185
- }
186
-
187
- const STATUS_ICONS: Record<ProcessStatus, string> = {
188
- running: "\u25b6",
189
- sleeping: "\u25cc",
190
- stopped: "\u25a0",
191
- zombie: "\u2620",
192
- }
193
-
194
- // ============================================================================
195
- // Components
196
- // ============================================================================
197
-
198
- /** Column layout helper -- computes column widths based on available space */
199
- function useColumns(totalWidth: number) {
200
- return useMemo(() => {
201
- // Fixed columns
202
- const pidW = 6
203
- const cpuW = 7
204
- const memW = 7
205
- const statusW = 10
206
- const threadsW = 5
207
- const uptimeW = 8
208
- const userW = 10
209
- const fixed = pidW + cpuW + memW + statusW + threadsW + uptimeW + userW + 8 // gaps
210
-
211
- // Name gets the rest
212
- const nameW = Math.max(10, totalWidth - fixed)
213
-
214
- return { pidW, nameW, cpuW, memW, statusW, userW, threadsW, uptimeW }
215
- }, [totalWidth])
216
- }
217
-
218
- function TableHeader({ width }: { width: number }): JSX.Element {
219
- const cols = useColumns(width)
220
-
221
- return (
222
- <Box paddingX={1}>
223
- <Text bold color="$muted">
224
- {"PID".padEnd(cols.pidW)}
225
- {"NAME".padEnd(cols.nameW)}
226
- {"CPU%".padStart(cols.cpuW)}
227
- {"MEM%".padStart(cols.memW)}
228
- {" "}
229
- {"STATUS".padEnd(cols.statusW)}
230
- {"USER".padEnd(cols.userW)}
231
- {"THR".padStart(cols.threadsW)}
232
- {" "}
233
- {"UPTIME".padStart(cols.uptimeW)}
234
- </Text>
235
- </Box>
236
- )
237
- }
238
-
239
- function ProcessRow({
240
- proc,
241
- isSelected,
242
- width,
243
- }: {
244
- proc: ProcessInfo
245
- isSelected: boolean
246
- width: number
247
- }): JSX.Element {
248
- const cols = useColumns(width)
249
- const cpuColor = proc.cpu > 80 ? "$error" : proc.cpu > 40 ? "$warning" : "$success"
250
- const memColor = proc.mem > 40 ? "$warning" : "$muted"
251
-
252
- // Truncate name to fit column
253
- const displayName = proc.name.length > cols.nameW - 1 ? proc.name.slice(0, cols.nameW - 2) + "\u2026" : proc.name
254
-
255
- return (
256
- <Box paddingX={1} backgroundColor={isSelected ? "$primary" : undefined}>
257
- <Text color={isSelected ? "white" : "$muted"}>{String(proc.pid).padEnd(cols.pidW)}</Text>
258
- <Text bold={isSelected} color={isSelected ? "white" : undefined}>
259
- {displayName.padEnd(cols.nameW)}
260
- </Text>
261
- <Text color={isSelected ? "white" : cpuColor}>{proc.cpu.toFixed(1).padStart(cols.cpuW - 1)}%</Text>
262
- <Text color={isSelected ? "white" : memColor}>{proc.mem.toFixed(1).padStart(cols.memW - 1)}%</Text>
263
- <Text>{" "}</Text>
264
- <Text color={isSelected ? "white" : STATUS_COLORS[proc.status]}>
265
- {STATUS_ICONS[proc.status]} {proc.status.padEnd(cols.statusW - 2)}
266
- </Text>
267
- <Text color={isSelected ? "white" : "$muted"}>{proc.user.padEnd(cols.userW)}</Text>
268
- <Text color={isSelected ? "white" : "$muted"}>{String(proc.threads).padStart(cols.threadsW)}</Text>
269
- <Text>{" "}</Text>
270
- <Text color={isSelected ? "white" : "$muted"}>{proc.uptime.padStart(cols.uptimeW)}</Text>
271
- </Box>
272
- )
273
- }
274
-
275
- function SummaryBar({ processes, query }: { processes: ProcessInfo[]; query: string }): JSX.Element {
276
- const stats = useMemo(() => {
277
- let running = 0
278
- let totalCpu = 0
279
- let totalMem = 0
280
- for (const p of processes) {
281
- if (p.status === "running") running++
282
- totalCpu += p.cpu
283
- totalMem += p.mem
284
- }
285
- return { running, totalCpu: totalCpu.toFixed(1), totalMem: totalMem.toFixed(1) }
286
- }, [processes])
287
-
288
- return (
289
- <Box paddingX={1} gap={2}>
290
- <Text bold>{processes.length}</Text>
291
- <Muted>processes</Muted>
292
- <Text color="$success" bold>
293
- {stats.running}
294
- </Text>
295
- <Muted>running</Muted>
296
- <Muted>|</Muted>
297
- <Text color="$primary">CPU: {stats.totalCpu}%</Text>
298
- <Text color="$warning">MEM: {stats.totalMem}%</Text>
299
- {query && (
300
- <>
301
- <Muted>|</Muted>
302
- <Text dim>filter: &quot;{query}&quot;</Text>
303
- </>
304
- )}
305
- </Box>
306
- )
307
- }
308
-
309
- /** Inner component that reads the flex container's height */
310
- function ProcessListArea({
311
- processes,
312
- cursor,
313
- width,
314
- }: {
315
- processes: ProcessInfo[]
316
- cursor: number
317
- width: number
318
- }): JSX.Element {
319
- const { height } = useContentRect()
320
-
321
- return (
322
- <VirtualList
323
- items={processes}
324
- height={height}
325
- itemHeight={1}
326
- scrollTo={cursor}
327
- overscan={5}
328
- renderItem={(proc, index) => (
329
- <ProcessRow key={proc.pid} proc={proc} isSelected={index === cursor} width={width} />
330
- )}
331
- />
332
- )
333
- }
334
-
335
- // ============================================================================
336
- // Main App
337
- // ============================================================================
338
-
339
- export function DataExplorer(): JSX.Element {
340
- const { exit } = useApp()
341
- const { width } = useContentRect()
342
- const [cursor, setCursor] = useState(0)
343
- const [searchMode, setSearchMode] = useState(false)
344
- const [query, setQuery] = useState("")
345
- const deferredQuery = useDeferredValue(query)
346
-
347
- // Filter processes based on deferred query
348
- const filtered = useMemo(() => {
349
- if (!deferredQuery) return ALL_PROCESSES
350
- const q = deferredQuery.toLowerCase()
351
- return ALL_PROCESSES.filter(
352
- (p) =>
353
- p.name.toLowerCase().includes(q) ||
354
- p.user.toLowerCase().includes(q) ||
355
- p.status.includes(q) ||
356
- String(p.pid).includes(q),
357
- )
358
- }, [deferredQuery])
359
-
360
- const listHeight = useMemo(() => Math.max(5, filtered.length), [filtered.length])
361
- const halfPage = Math.max(1, Math.floor(listHeight / 4))
362
-
363
- // Clamp cursor when filter changes
364
- const effectiveCursor = Math.min(cursor, Math.max(0, filtered.length - 1))
365
-
366
- const handleSearchSubmit = useCallback(() => {
367
- setSearchMode(false)
368
- }, [])
369
-
370
- useInput(
371
- useCallback(
372
- (input: string, key: Key) => {
373
- // In search mode, only handle Esc to exit
374
- if (searchMode) {
375
- if (key.escape) {
376
- setSearchMode(false)
377
- return
378
- }
379
- // TextInput handles all other input
380
- return
381
- }
382
-
383
- // Normal mode
384
- if (input === "q" || key.escape) {
385
- exit()
386
- return
387
- }
388
-
389
- if (input === "/") {
390
- setSearchMode(true)
391
- return
392
- }
393
-
394
- // Navigation
395
- if (input === "j" || key.downArrow) {
396
- setCursor((c) => Math.min(filtered.length - 1, c + 1))
397
- }
398
- if (input === "k" || key.upArrow) {
399
- setCursor((c) => Math.max(0, c - 1))
400
- }
401
- if (input === "d" || key.pageDown) {
402
- setCursor((c) => Math.min(filtered.length - 1, c + halfPage))
403
- }
404
- if (input === "u" || key.pageUp) {
405
- setCursor((c) => Math.max(0, c - halfPage))
406
- }
407
- if (input === "g" || key.home) {
408
- setCursor(0)
409
- }
410
- if (input === "G" || key.end) {
411
- setCursor(filtered.length - 1)
412
- }
413
-
414
- // Clear filter
415
- if (key.backspace && query) {
416
- setQuery("")
417
- setCursor(0)
418
- }
419
- },
420
- [searchMode, exit, filtered.length, halfPage, query],
421
- ),
422
- )
423
-
424
- return (
425
- <Box flexDirection="column" flexGrow={1}>
426
- {/* Summary bar */}
427
- <SummaryBar processes={filtered} query={deferredQuery} />
428
-
429
- {/* Search bar */}
430
- <Box paddingX={1}>
431
- {searchMode ? (
432
- <Box>
433
- <Text color="$primary" bold>
434
- /{" "}
435
- </Text>
436
- <TextInput
437
- value={query}
438
- onChange={(v) => {
439
- setQuery(v)
440
- setCursor(0)
441
- }}
442
- onSubmit={handleSearchSubmit}
443
- prompt=""
444
- isActive={searchMode}
445
- />
446
- </Box>
447
- ) : query ? (
448
- <Muted>
449
- filter: <Text bold>{query}</Text> (backspace to clear, / to edit)
450
- </Muted>
451
- ) : (
452
- <Muted>
453
- Press <Kbd>/</Kbd> to search
454
- </Muted>
455
- )}
456
- </Box>
457
-
458
- {/* Table header */}
459
- <TableHeader width={width} />
460
- <Box paddingX={1}>
461
- <Divider />
462
- </Box>
463
-
464
- {/* Process list */}
465
- <Box flexGrow={1} flexDirection="column">
466
- {filtered.length > 0 ? (
467
- <ProcessListArea processes={filtered} cursor={effectiveCursor} width={width} />
468
- ) : (
469
- <Box paddingX={1} justifyContent="center">
470
- <Lead>No processes match &quot;{deferredQuery}&quot;</Lead>
471
- </Box>
472
- )}
473
- </Box>
474
-
475
- {/* Scroll indicator + help */}
476
- <Box paddingX={1} justifyContent="space-between">
477
- <Muted>
478
- <Kbd>j/k</Kbd> navigate <Kbd>d/u</Kbd> half-page <Kbd>g/G</Kbd> start/end <Kbd>/</Kbd> search <Kbd>Esc/q</Kbd>{" "}
479
- quit
480
- </Muted>
481
- <Muted>
482
- {effectiveCursor + 1}/{filtered.length}
483
- </Muted>
484
- </Box>
485
- </Box>
486
- )
487
- }
488
-
489
- // ============================================================================
490
- // Main
491
- // ============================================================================
492
-
493
- async function main() {
494
- using term = createTerm()
495
- const { waitUntilExit } = await render(
496
- <ExampleBanner meta={meta} controls="j/k navigate d/u half-page g/G start/end / search Esc/q quit">
497
- <DataExplorer />
498
- </ExampleBanner>,
499
- term,
500
- )
501
- await waitUntilExit()
502
- }
503
-
504
- if (import.meta.main) {
505
- main().catch(console.error)
506
- }