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,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
- }