@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/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
|
-
await main()
|
|
731
|
-
}
|