@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/explorer.tsx DELETED
@@ -1,731 +0,0 @@
1
- /**
2
- * Explorer — Log Viewer & Process Explorer
3
- *
4
- * A tabbed data exploration demo combining:
5
- * - Streaming log viewer with ~2000 lines, severity-level coloring, and level toggles
6
- * - Sortable process table with ~50 processes, live CPU/MEM jitter, and responsive columns
7
- * - Shared TextInput search bar with useDeferredValue for non-blocking filtering
8
- * - ListView with interactive scrolling for both tabs
9
- *
10
- * Usage: bun vendor/silvery/examples/apps/explorer.tsx
11
- *
12
- * Controls:
13
- * Tab/h/l - Switch tabs (Logs / Processes)
14
- * j/k or Up/Dn - Navigate rows
15
- * d/u - Half-page down/up
16
- * g/G - Jump to first/last
17
- * / - Focus search bar
18
- * 1-4 - Toggle log levels (Logs tab)
19
- * s - Cycle sort column (Processes tab)
20
- * Esc - Exit search / quit
21
- * q - Quit (when not searching)
22
- */
23
-
24
- import React, { useState, useCallback, useMemo, useDeferredValue, useEffect, useRef } from "react"
25
- import {
26
- render,
27
- Box,
28
- Text,
29
- ListView,
30
- TextInput,
31
- Tabs,
32
- TabList,
33
- Tab,
34
- Divider,
35
- useBoxRect,
36
- useInput,
37
- useApp,
38
- createTerm,
39
- Kbd,
40
- Muted,
41
- type Key,
42
- } from "silvery"
43
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
44
-
45
- export const meta: ExampleMeta = {
46
- name: "Explorer",
47
- description: "Log viewer and process explorer with ListView search",
48
- demo: true,
49
- features: ["ListView", "TextInput", "useBoxRect()", "useDeferredValue", "2000+ rows"],
50
- }
51
-
52
- // ============================================================================
53
- // Shared Types & Utilities
54
- // ============================================================================
55
-
56
- function seededRandom(seed: number): () => number {
57
- let s = seed
58
- return () => {
59
- s = (s * 1664525 + 1013904223) & 0x7fffffff
60
- return s / 0x7fffffff
61
- }
62
- }
63
-
64
- // ============================================================================
65
- // Log Data
66
- // ============================================================================
67
-
68
- type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR"
69
-
70
- interface LogEntry {
71
- id: number
72
- timestamp: string
73
- service: string
74
- level: LogLevel
75
- message: string
76
- }
77
-
78
- const SERVICES = ["api", "auth", "db", "cache", "worker", "gateway", "scheduler", "metrics", "queue", "ws"]
79
-
80
- const LOG_TEMPLATES: Record<LogLevel, string[]> = {
81
- DEBUG: [
82
- "Cache miss for key user:session:{id}",
83
- "Query plan: sequential scan on events ({n} rows)",
84
- "WebSocket frame received: {n} bytes",
85
- "GC pause: {n}ms (minor collection)",
86
- "Connection pool stats: {n} active, {n} idle",
87
- "Route matched: GET /api/v2/resources/{id}",
88
- "DNS resolution took {n}ms for upstream.svc",
89
- "Retry backoff: sleeping {n}ms before attempt",
90
- ],
91
- INFO: [
92
- "Request completed: 200 OK ({n}ms)",
93
- "User {id} authenticated via OAuth",
94
- "Background job processed: email_dispatch #{id}",
95
- "Server listening on port {n}",
96
- "Database migration applied: v{n}",
97
- "Health check passed (latency: {n}ms)",
98
- "Deployed version 2.{n}.0 to production",
99
- "Cache warmed: {n} entries loaded in {n}ms",
100
- ],
101
- WARN: [
102
- "Slow query detected: {n}ms (threshold: 200ms)",
103
- "Rate limit approaching: {n}/1000 requests",
104
- "Memory usage: {n}% of allocated heap",
105
- "Retry attempt {n}/3 for external API call",
106
- "Certificate expires in {n} days",
107
- "Connection pool near capacity: {n}/100",
108
- "Request body exceeds {n}KB soft limit",
109
- "Stale cache entry served for key products:{id}",
110
- ],
111
- ERROR: [
112
- "Unhandled exception in request handler: TypeError",
113
- "Database connection refused: ECONNREFUSED",
114
- "Authentication failed for user {id}: invalid token",
115
- "Timeout after {n}ms waiting for upstream service",
116
- "Disk usage critical: {n}% on /var/data",
117
- "Failed to process message from queue: malformed payload",
118
- "OOM kill triggered for worker process PID {id}",
119
- "TLS handshake failed: certificate chain incomplete",
120
- ],
121
- }
122
-
123
- const LEVEL_COLORS: Record<LogLevel, string> = {
124
- DEBUG: "$muted",
125
- INFO: "$success",
126
- WARN: "$warning",
127
- ERROR: "$error",
128
- }
129
-
130
- const LEVEL_BADGES: Record<LogLevel, string> = {
131
- DEBUG: "DBG",
132
- INFO: "INF",
133
- WARN: "WRN",
134
- ERROR: "ERR",
135
- }
136
-
137
- function generateLogs(count: number): LogEntry[] {
138
- const rng = seededRandom(42)
139
- const levels: LogLevel[] = ["DEBUG", "INFO", "INFO", "INFO", "INFO", "WARN", "WARN", "ERROR"]
140
- const entries: LogEntry[] = []
141
-
142
- // Start time: spread over 30 minutes
143
- const baseHour = 14
144
- const baseMinute = 30
145
-
146
- for (let i = 0; i < count; i++) {
147
- const level = levels[Math.floor(rng() * levels.length)]!
148
- const templates = LOG_TEMPLATES[level]
149
- const template = templates[Math.floor(rng() * templates.length)]!
150
- const message = template
151
- .replace(/\{id\}/g, () => String(Math.floor(rng() * 99999)))
152
- .replace(/\{n\}/g, () => String(Math.floor(rng() * 999)))
153
-
154
- const totalSeconds = (i / count) * 1800 // 30 min spread
155
- const h = baseHour + Math.floor((baseMinute * 60 + totalSeconds) / 3600)
156
- const m = Math.floor(((baseMinute * 60 + totalSeconds) % 3600) / 60)
157
- const s = Math.floor(totalSeconds % 60)
158
- const ms = Math.floor(rng() * 1000)
159
-
160
- entries.push({
161
- id: i,
162
- timestamp: `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(ms).padStart(3, "0")}`,
163
- service: SERVICES[Math.floor(rng() * SERVICES.length)]!,
164
- level,
165
- message,
166
- })
167
- }
168
-
169
- return entries
170
- }
171
-
172
- const ALL_LOGS = generateLogs(2000)
173
-
174
- // ============================================================================
175
- // Process Data
176
- // ============================================================================
177
-
178
- type SortColumn = "pid" | "name" | "cpu" | "mem" | "status"
179
-
180
- interface ProcessInfo {
181
- pid: number
182
- name: string
183
- cpu: number
184
- mem: number
185
- status: "running" | "sleeping" | "stopped" | "zombie"
186
- }
187
-
188
- const PROCESS_NAMES = [
189
- "node",
190
- "bun",
191
- "postgres",
192
- "redis-server",
193
- "nginx",
194
- "docker",
195
- "sshd",
196
- "containerd",
197
- "kubelet",
198
- "etcd",
199
- "coredns",
200
- "prometheus",
201
- "grafana",
202
- "elasticsearch",
203
- "rabbitmq",
204
- "kafka",
205
- "consul",
206
- "vault",
207
- "haproxy",
208
- "traefik",
209
- "envoy",
210
- "mysql",
211
- "mongo",
212
- "clickhouse",
213
- "influxdb",
214
- "jenkins",
215
- "cadvisor",
216
- "telegraf",
217
- "deno",
218
- "esbuild",
219
- "python3",
220
- "ruby",
221
- "java",
222
- "go",
223
- "rustc",
224
- "webpack",
225
- "vite",
226
- "swc",
227
- "chrome",
228
- "code",
229
- "tmux",
230
- "zsh",
231
- "cron",
232
- "systemd",
233
- "rsyslogd",
234
- "logstash",
235
- "kibana",
236
- "alertmanager",
237
- "buildkitd",
238
- "registry",
239
- ]
240
-
241
- const PROCESS_STATUSES: ProcessInfo["status"][] = ["running", "sleeping", "stopped", "zombie"]
242
-
243
- function generateProcesses(count: number): ProcessInfo[] {
244
- const rng = seededRandom(123)
245
- const procs: ProcessInfo[] = []
246
-
247
- for (let i = 0; i < count; i++) {
248
- const nameBase = PROCESS_NAMES[Math.floor(rng() * PROCESS_NAMES.length)]!
249
- const hasInstance = rng() > 0.7
250
- const status = rng() < 0.65 ? "running" : PROCESS_STATUSES[Math.floor(rng() * PROCESS_STATUSES.length)]!
251
-
252
- procs.push({
253
- pid: 1000 + Math.floor(rng() * 60000),
254
- name: hasInstance ? `${nameBase}:${Math.floor(rng() * 16)}` : nameBase,
255
- cpu: status === "running" ? Math.round(rng() * 1000) / 10 : 0,
256
- mem: Math.round(rng() * 800) / 10,
257
- status,
258
- })
259
- }
260
-
261
- return procs
262
- }
263
-
264
- const INITIAL_PROCESSES = generateProcesses(50)
265
-
266
- const STATUS_COLORS: Record<ProcessInfo["status"], string> = {
267
- running: "$success",
268
- sleeping: "$muted",
269
- stopped: "$warning",
270
- zombie: "$error",
271
- }
272
-
273
- const STATUS_ICONS: Record<ProcessInfo["status"], string> = {
274
- running: "\u25b6",
275
- sleeping: "\u25cc",
276
- stopped: "\u25a0",
277
- zombie: "\u2620",
278
- }
279
-
280
- const SORT_COLUMNS: SortColumn[] = ["cpu", "mem", "pid", "name", "status"]
281
-
282
- // ============================================================================
283
- // Log Components
284
- // ============================================================================
285
-
286
- function LogRow({ entry, isSelected }: { entry: LogEntry; isSelected: boolean }) {
287
- return (
288
- <Box paddingX={1} backgroundColor={isSelected ? "$mutedbg" : undefined}>
289
- <Muted>{entry.timestamp} </Muted>
290
- <Text color={LEVEL_COLORS[entry.level]} bold>
291
- {LEVEL_BADGES[entry.level]}
292
- </Text>
293
- <Muted> [{entry.service.padEnd(9)}] </Muted>
294
- <Text>{entry.message}</Text>
295
- </Box>
296
- )
297
- }
298
-
299
- function LogListArea({ entries, cursor }: { entries: LogEntry[]; cursor: number }) {
300
- const { height } = useBoxRect()
301
-
302
- return (
303
- <ListView
304
- items={entries}
305
- height={height}
306
- estimateHeight={1}
307
- scrollTo={cursor}
308
- overscan={5}
309
- renderItem={(entry, index) => <LogRow key={entry.id} entry={entry} isSelected={index === cursor} />}
310
- />
311
- )
312
- }
313
-
314
- function LevelToggles({
315
- levels,
316
- onToggle,
317
- }: {
318
- levels: Record<LogLevel, boolean>
319
- onToggle: (level: LogLevel) => void
320
- }) {
321
- const allLevels: LogLevel[] = ["DEBUG", "INFO", "WARN", "ERROR"]
322
- return (
323
- <Box gap={1}>
324
- {allLevels.map((level, i) => {
325
- const active = levels[level]
326
- return (
327
- <Box key={level} gap={0}>
328
- <Text color="$muted" dim>
329
- {i + 1}:
330
- </Text>
331
- <Text color={active ? LEVEL_COLORS[level] : "$muted"} bold={active} dim={!active} strikethrough={!active}>
332
- {LEVEL_BADGES[level]}
333
- </Text>
334
- </Box>
335
- )
336
- })}
337
- </Box>
338
- )
339
- }
340
-
341
- // ============================================================================
342
- // Process Components
343
- // ============================================================================
344
-
345
- function useColumns(totalWidth: number) {
346
- return useMemo(() => {
347
- const pidW = 7
348
- const cpuW = 8
349
- const memW = 8
350
- const statusW = 11
351
- const fixed = pidW + cpuW + memW + statusW + 4 // gaps
352
- const nameW = Math.max(12, totalWidth - fixed)
353
- return { pidW, nameW, cpuW, memW, statusW }
354
- }, [totalWidth])
355
- }
356
-
357
- function ProcessHeader({ width }: { width: number }) {
358
- const cols = useColumns(width)
359
- return (
360
- <Box paddingX={1}>
361
- <Text bold color="$muted">
362
- {"PID".padEnd(cols.pidW)}
363
- {"NAME".padEnd(cols.nameW)}
364
- {"CPU%".padStart(cols.cpuW)}
365
- {"MEM%".padStart(cols.memW)}
366
- {" "}
367
- {"STATUS".padEnd(cols.statusW)}
368
- </Text>
369
- </Box>
370
- )
371
- }
372
-
373
- function ProcessRow({ proc, isSelected, width }: { proc: ProcessInfo; isSelected: boolean; width: number }) {
374
- const cols = useColumns(width)
375
- const cpuColor = proc.cpu > 80 ? "$error" : proc.cpu > 40 ? "$warning" : "$success"
376
- const displayName = proc.name.length > cols.nameW - 1 ? proc.name.slice(0, cols.nameW - 2) + "\u2026" : proc.name
377
-
378
- return (
379
- <Box paddingX={1} backgroundColor={isSelected ? "$mutedbg" : undefined}>
380
- <Text color="$muted">{String(proc.pid).padEnd(cols.pidW)}</Text>
381
- <Text bold={isSelected}>{displayName.padEnd(cols.nameW)}</Text>
382
- <Text color={cpuColor}>{proc.cpu.toFixed(1).padStart(cols.cpuW - 1)}%</Text>
383
- <Text color={proc.mem > 40 ? "$warning" : "$muted"}>{proc.mem.toFixed(1).padStart(cols.memW - 1)}%</Text>
384
- <Text>{" "}</Text>
385
- <Text color={STATUS_COLORS[proc.status]}>
386
- {STATUS_ICONS[proc.status]} {proc.status.padEnd(cols.statusW - 2)}
387
- </Text>
388
- </Box>
389
- )
390
- }
391
-
392
- function ProcessListArea({ processes, cursor, width }: { processes: ProcessInfo[]; cursor: number; width: number }) {
393
- const { height } = useBoxRect()
394
-
395
- return (
396
- <ListView
397
- items={processes}
398
- height={height}
399
- estimateHeight={1}
400
- scrollTo={cursor}
401
- overscan={5}
402
- renderItem={(proc, index) => (
403
- <ProcessRow key={proc.pid} proc={proc} isSelected={index === cursor} width={width} />
404
- )}
405
- />
406
- )
407
- }
408
-
409
- // ============================================================================
410
- // Main App
411
- // ============================================================================
412
-
413
- export function Explorer() {
414
- const { exit } = useApp()
415
- const { width } = useBoxRect()
416
-
417
- // Tab state
418
- const [activeTab, setActiveTab] = useState("logs")
419
-
420
- // Search state (shared)
421
- const [searchMode, setSearchMode] = useState(false)
422
- const [query, setQuery] = useState("")
423
- const deferredQuery = useDeferredValue(query)
424
-
425
- // Log state
426
- const [logCursor, setLogCursor] = useState(0)
427
- const [logLevels, setLogLevels] = useState<Record<LogLevel, boolean>>({
428
- DEBUG: true,
429
- INFO: true,
430
- WARN: true,
431
- ERROR: true,
432
- })
433
-
434
- // Process state
435
- const [procCursor, setProcCursor] = useState(0)
436
- const [sortCol, setSortCol] = useState<SortColumn>("cpu")
437
- const [processes, setProcesses] = useState(INITIAL_PROCESSES)
438
-
439
- // Live jitter on CPU/MEM values
440
- const jitterRef = useRef<ReturnType<typeof setInterval> | null>(null)
441
- useEffect(() => {
442
- const rng = seededRandom(Date.now())
443
- jitterRef.current = setInterval(() => {
444
- setProcesses((prev) =>
445
- prev.map((p) => {
446
- if (p.status !== "running") return p
447
- const cpuDelta = (rng() - 0.5) * 6
448
- const memDelta = (rng() - 0.5) * 2
449
- return {
450
- ...p,
451
- cpu: Math.max(0, Math.min(100, Math.round((p.cpu + cpuDelta) * 10) / 10)),
452
- mem: Math.max(0, Math.min(100, Math.round((p.mem + memDelta) * 10) / 10)),
453
- }
454
- }),
455
- )
456
- }, 2000)
457
- return () => {
458
- if (jitterRef.current) clearInterval(jitterRef.current)
459
- }
460
- }, [])
461
-
462
- // Filtered logs
463
- const filteredLogs = useMemo(() => {
464
- let logs = ALL_LOGS.filter((e) => logLevels[e.level])
465
- if (deferredQuery) {
466
- const q = deferredQuery.toLowerCase()
467
- logs = logs.filter(
468
- (e) =>
469
- e.message.toLowerCase().includes(q) ||
470
- e.service.toLowerCase().includes(q) ||
471
- e.level.toLowerCase().includes(q),
472
- )
473
- }
474
- return logs
475
- }, [deferredQuery, logLevels])
476
-
477
- // Filtered + sorted processes
478
- const filteredProcesses = useMemo(() => {
479
- let procs = processes
480
- if (deferredQuery) {
481
- const q = deferredQuery.toLowerCase()
482
- procs = procs.filter((p) => p.name.toLowerCase().includes(q) || p.status.includes(q) || String(p.pid).includes(q))
483
- }
484
- return [...procs].sort((a, b) => {
485
- switch (sortCol) {
486
- case "cpu":
487
- return b.cpu - a.cpu
488
- case "mem":
489
- return b.mem - a.mem
490
- case "pid":
491
- return a.pid - b.pid
492
- case "name":
493
- return a.name.localeCompare(b.name)
494
- case "status":
495
- return a.status.localeCompare(b.status)
496
- }
497
- })
498
- }, [processes, deferredQuery, sortCol])
499
-
500
- // Current list length for navigation
501
- const currentItems = activeTab === "logs" ? filteredLogs : filteredProcesses
502
- const cursor = activeTab === "logs" ? logCursor : procCursor
503
- const setCursor = activeTab === "logs" ? setLogCursor : setProcCursor
504
- const halfPage = Math.max(1, Math.floor(20 / 2))
505
-
506
- // Clamp cursors when filter changes
507
- const effectiveLogCursor = Math.min(logCursor, Math.max(0, filteredLogs.length - 1))
508
- const effectiveProcCursor = Math.min(procCursor, Math.max(0, filteredProcesses.length - 1))
509
-
510
- const handleSearchSubmit = useCallback(() => {
511
- setSearchMode(false)
512
- }, [])
513
-
514
- const toggleLevel = useCallback((level: LogLevel) => {
515
- setLogLevels((prev) => ({ ...prev, [level]: !prev[level] }))
516
- setLogCursor(0)
517
- }, [])
518
-
519
- useInput(
520
- useCallback(
521
- (input: string, key: Key) => {
522
- // Search mode: only handle Esc
523
- if (searchMode) {
524
- if (key.escape) {
525
- setSearchMode(false)
526
- return
527
- }
528
- return
529
- }
530
-
531
- // Quit
532
- if (input === "q" || key.escape) {
533
- exit()
534
- return
535
- }
536
-
537
- // Search
538
- if (input === "/") {
539
- setSearchMode(true)
540
- return
541
- }
542
-
543
- // Tab switching (Tab key handled by Tabs component via h/l)
544
-
545
- // Log level toggles (logs tab only)
546
- if (activeTab === "logs") {
547
- const levelMap: Record<string, LogLevel> = {
548
- "1": "DEBUG",
549
- "2": "INFO",
550
- "3": "WARN",
551
- "4": "ERROR",
552
- }
553
- if (levelMap[input]) {
554
- toggleLevel(levelMap[input])
555
- return
556
- }
557
- }
558
-
559
- // Sort cycling (processes tab only)
560
- if (activeTab === "processes" && input === "s") {
561
- setSortCol((prev) => {
562
- const idx = SORT_COLUMNS.indexOf(prev)
563
- return SORT_COLUMNS[(idx + 1) % SORT_COLUMNS.length]!
564
- })
565
- return
566
- }
567
-
568
- // Navigation
569
- if (input === "j" || key.downArrow) {
570
- setCursor((c: number) => Math.min(currentItems.length - 1, c + 1))
571
- }
572
- if (input === "k" || key.upArrow) {
573
- setCursor((c: number) => Math.max(0, c - 1))
574
- }
575
- if (input === "d" || key.pageDown) {
576
- setCursor((c: number) => Math.min(currentItems.length - 1, c + halfPage))
577
- }
578
- if (input === "u" || key.pageUp) {
579
- setCursor((c: number) => Math.max(0, c - halfPage))
580
- }
581
- if (input === "g" || key.home) {
582
- setCursor(0)
583
- }
584
- if (input === "G" || key.end) {
585
- setCursor(currentItems.length - 1)
586
- }
587
-
588
- // Clear filter
589
- if (key.backspace && query) {
590
- setQuery("")
591
- setCursor(0)
592
- }
593
- },
594
- [searchMode, exit, activeTab, currentItems.length, halfPage, query, toggleLevel, setCursor],
595
- ),
596
- )
597
-
598
- return (
599
- <Box flexDirection="column" flexGrow={1}>
600
- {/* Search bar */}
601
- <Box paddingX={1}>
602
- {searchMode ? (
603
- <Box flexGrow={1}>
604
- <Text color="$primary" bold>
605
- /{" "}
606
- </Text>
607
- <TextInput
608
- value={query}
609
- onChange={(v) => {
610
- setQuery(v)
611
- setLogCursor(0)
612
- setProcCursor(0)
613
- }}
614
- onSubmit={handleSearchSubmit}
615
- prompt=""
616
- isActive={searchMode}
617
- />
618
- </Box>
619
- ) : query ? (
620
- <Muted>
621
- filter: <Text bold>{query}</Text> (<Kbd>backspace</Kbd> clear, <Kbd>/</Kbd> edit)
622
- </Muted>
623
- ) : (
624
- <Muted>
625
- <Kbd>/</Kbd> search
626
- </Muted>
627
- )}
628
- </Box>
629
-
630
- {/* Tab bar */}
631
- <Tabs value={activeTab} onChange={setActiveTab} isActive={!searchMode}>
632
- <Box paddingX={1}>
633
- <TabList>
634
- <Tab value="logs">Logs ({filteredLogs.length.toLocaleString()})</Tab>
635
- <Tab value="processes">Processes ({filteredProcesses.length})</Tab>
636
- </TabList>
637
- </Box>
638
- </Tabs>
639
-
640
- {/* Tab content — outside Tabs so flexGrow works */}
641
- {activeTab === "logs" && (
642
- <>
643
- <Box paddingX={1} justifyContent="space-between">
644
- <LevelToggles levels={logLevels} onToggle={toggleLevel} />
645
- <Muted>
646
- {effectiveLogCursor + 1}/{filteredLogs.length.toLocaleString()}
647
- </Muted>
648
- </Box>
649
- <Box flexGrow={1} flexDirection="column">
650
- {filteredLogs.length > 0 ? (
651
- <LogListArea entries={filteredLogs} cursor={effectiveLogCursor} />
652
- ) : (
653
- <Box paddingX={1} justifyContent="center">
654
- <Muted>No logs match the current filter</Muted>
655
- </Box>
656
- )}
657
- </Box>
658
- </>
659
- )}
660
-
661
- {activeTab === "processes" && (
662
- <>
663
- <Box paddingX={1} justifyContent="space-between">
664
- <Box gap={1}>
665
- <Muted>sort:</Muted>
666
- <Text bold color="$primary">
667
- {sortCol.toUpperCase()}
668
- </Text>
669
- <Muted>
670
- (<Kbd>s</Kbd> cycle)
671
- </Muted>
672
- </Box>
673
- <Muted>
674
- {effectiveProcCursor + 1}/{filteredProcesses.length}
675
- </Muted>
676
- </Box>
677
- <ProcessHeader width={width} />
678
- <Box paddingX={1}>
679
- <Divider />
680
- </Box>
681
- <Box flexGrow={1} flexDirection="column">
682
- {filteredProcesses.length > 0 ? (
683
- <ProcessListArea processes={filteredProcesses} cursor={effectiveProcCursor} width={width} />
684
- ) : (
685
- <Box paddingX={1} justifyContent="center">
686
- <Muted>No processes match the current filter</Muted>
687
- </Box>
688
- )}
689
- </Box>
690
- </>
691
- )}
692
-
693
- {/* Help bar */}
694
- <Box paddingX={1} justifyContent="space-between">
695
- <Muted>
696
- <Kbd>h/l</Kbd> tab <Kbd>j/k</Kbd> navigate <Kbd>d/u</Kbd> page <Kbd>/</Kbd> search{" "}
697
- {activeTab === "logs" && (
698
- <>
699
- <Kbd>1-4</Kbd> levels{" "}
700
- </>
701
- )}
702
- {activeTab === "processes" && (
703
- <>
704
- <Kbd>s</Kbd> sort{" "}
705
- </>
706
- )}
707
- <Kbd>q</Kbd> quit
708
- </Muted>
709
- </Box>
710
- </Box>
711
- )
712
- }
713
-
714
- // ============================================================================
715
- // Main
716
- // ============================================================================
717
-
718
- export async function main() {
719
- using term = createTerm()
720
- const { waitUntilExit } = await render(
721
- <ExampleBanner meta={meta} controls="h/l tab j/k navigate d/u page / search 1-4 levels s sort q quit">
722
- <Explorer />
723
- </ExampleBanner>,
724
- term,
725
- )
726
- await waitUntilExit()
727
- }
728
-
729
- if (import.meta.main) {
730
- main().catch(console.error)
731
- }