@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/data-explorer.tsx
DELETED
|
@@ -1,490 +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 useBoxRect()
|
|
6
|
-
* - TextInput for live search/filter with useDeferredValue
|
|
7
|
-
* - ListView 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/apps/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
|
-
ListView,
|
|
28
|
-
TextInput,
|
|
29
|
-
Divider,
|
|
30
|
-
useBoxRect,
|
|
31
|
-
useInput,
|
|
32
|
-
useApp,
|
|
33
|
-
createTerm,
|
|
34
|
-
Kbd,
|
|
35
|
-
Muted,
|
|
36
|
-
Lead,
|
|
37
|
-
type Key,
|
|
38
|
-
} from "silvery"
|
|
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, ListView, and responsive column widths",
|
|
44
|
-
features: ["useBoxRect()", "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 }) {
|
|
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({ proc, isSelected, width }: { proc: ProcessInfo; isSelected: boolean; width: number }) {
|
|
240
|
-
const cols = useColumns(width)
|
|
241
|
-
const cpuColor = proc.cpu > 80 ? "$error" : proc.cpu > 40 ? "$warning" : "$success"
|
|
242
|
-
const memColor = proc.mem > 40 ? "$warning" : "$muted"
|
|
243
|
-
|
|
244
|
-
// Truncate name to fit column
|
|
245
|
-
const displayName = proc.name.length > cols.nameW - 1 ? proc.name.slice(0, cols.nameW - 2) + "\u2026" : proc.name
|
|
246
|
-
|
|
247
|
-
return (
|
|
248
|
-
<Box paddingX={1} backgroundColor={isSelected ? "$primary" : undefined}>
|
|
249
|
-
<Text color={isSelected ? "$primary-fg" : "$muted"}>{String(proc.pid).padEnd(cols.pidW)}</Text>
|
|
250
|
-
<Text bold={isSelected} color={isSelected ? "$primary-fg" : undefined}>
|
|
251
|
-
{displayName.padEnd(cols.nameW)}
|
|
252
|
-
</Text>
|
|
253
|
-
<Text color={isSelected ? "$primary-fg" : cpuColor}>{proc.cpu.toFixed(1).padStart(cols.cpuW - 1)}%</Text>
|
|
254
|
-
<Text color={isSelected ? "$primary-fg" : memColor}>{proc.mem.toFixed(1).padStart(cols.memW - 1)}%</Text>
|
|
255
|
-
<Text>{" "}</Text>
|
|
256
|
-
<Text color={isSelected ? "$primary-fg" : STATUS_COLORS[proc.status]}>
|
|
257
|
-
{STATUS_ICONS[proc.status]} {proc.status.padEnd(cols.statusW - 2)}
|
|
258
|
-
</Text>
|
|
259
|
-
<Text color={isSelected ? "$primary-fg" : "$muted"}>{proc.user.padEnd(cols.userW)}</Text>
|
|
260
|
-
<Text color={isSelected ? "$primary-fg" : "$muted"}>{String(proc.threads).padStart(cols.threadsW)}</Text>
|
|
261
|
-
<Text>{" "}</Text>
|
|
262
|
-
<Text color={isSelected ? "$primary-fg" : "$muted"}>{proc.uptime.padStart(cols.uptimeW)}</Text>
|
|
263
|
-
</Box>
|
|
264
|
-
)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function SummaryBar({ processes, query }: { processes: ProcessInfo[]; query: string }) {
|
|
268
|
-
const stats = useMemo(() => {
|
|
269
|
-
let running = 0
|
|
270
|
-
let totalCpu = 0
|
|
271
|
-
let totalMem = 0
|
|
272
|
-
for (const p of processes) {
|
|
273
|
-
if (p.status === "running") running++
|
|
274
|
-
totalCpu += p.cpu
|
|
275
|
-
totalMem += p.mem
|
|
276
|
-
}
|
|
277
|
-
return { running, totalCpu: totalCpu.toFixed(1), totalMem: totalMem.toFixed(1) }
|
|
278
|
-
}, [processes])
|
|
279
|
-
|
|
280
|
-
return (
|
|
281
|
-
<Box paddingX={1} gap={2}>
|
|
282
|
-
<Text bold>{processes.length}</Text>
|
|
283
|
-
<Muted>processes</Muted>
|
|
284
|
-
<Text color="$success" bold>
|
|
285
|
-
{stats.running}
|
|
286
|
-
</Text>
|
|
287
|
-
<Muted>running</Muted>
|
|
288
|
-
<Muted>|</Muted>
|
|
289
|
-
<Text color="$primary">CPU: {stats.totalCpu}%</Text>
|
|
290
|
-
<Text color="$warning">MEM: {stats.totalMem}%</Text>
|
|
291
|
-
{query && (
|
|
292
|
-
<>
|
|
293
|
-
<Muted>|</Muted>
|
|
294
|
-
<Text dim>filter: "{query}"</Text>
|
|
295
|
-
</>
|
|
296
|
-
)}
|
|
297
|
-
</Box>
|
|
298
|
-
)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** Inner component that reads the flex container's height */
|
|
302
|
-
function ProcessListArea({ processes, cursor, width }: { processes: ProcessInfo[]; cursor: number; width: number }) {
|
|
303
|
-
const { height } = useBoxRect()
|
|
304
|
-
|
|
305
|
-
return (
|
|
306
|
-
<ListView
|
|
307
|
-
items={processes}
|
|
308
|
-
height={height}
|
|
309
|
-
estimateHeight={1}
|
|
310
|
-
scrollTo={cursor}
|
|
311
|
-
overscan={5}
|
|
312
|
-
renderItem={(proc, index) => (
|
|
313
|
-
<ProcessRow key={proc.pid} proc={proc} isSelected={index === cursor} width={width} />
|
|
314
|
-
)}
|
|
315
|
-
/>
|
|
316
|
-
)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ============================================================================
|
|
320
|
-
// Main App
|
|
321
|
-
// ============================================================================
|
|
322
|
-
|
|
323
|
-
export function DataExplorer() {
|
|
324
|
-
const { exit } = useApp()
|
|
325
|
-
const { width } = useBoxRect()
|
|
326
|
-
const [cursor, setCursor] = useState(0)
|
|
327
|
-
const [searchMode, setSearchMode] = useState(false)
|
|
328
|
-
const [query, setQuery] = useState("")
|
|
329
|
-
const deferredQuery = useDeferredValue(query)
|
|
330
|
-
|
|
331
|
-
// Filter processes based on deferred query
|
|
332
|
-
const filtered = useMemo(() => {
|
|
333
|
-
if (!deferredQuery) return ALL_PROCESSES
|
|
334
|
-
const q = deferredQuery.toLowerCase()
|
|
335
|
-
return ALL_PROCESSES.filter(
|
|
336
|
-
(p) =>
|
|
337
|
-
p.name.toLowerCase().includes(q) ||
|
|
338
|
-
p.user.toLowerCase().includes(q) ||
|
|
339
|
-
p.status.includes(q) ||
|
|
340
|
-
String(p.pid).includes(q),
|
|
341
|
-
)
|
|
342
|
-
}, [deferredQuery])
|
|
343
|
-
|
|
344
|
-
const listHeight = useMemo(() => Math.max(5, filtered.length), [filtered.length])
|
|
345
|
-
const halfPage = Math.max(1, Math.floor(listHeight / 4))
|
|
346
|
-
|
|
347
|
-
// Clamp cursor when filter changes
|
|
348
|
-
const effectiveCursor = Math.min(cursor, Math.max(0, filtered.length - 1))
|
|
349
|
-
|
|
350
|
-
const handleSearchSubmit = useCallback(() => {
|
|
351
|
-
setSearchMode(false)
|
|
352
|
-
}, [])
|
|
353
|
-
|
|
354
|
-
useInput(
|
|
355
|
-
useCallback(
|
|
356
|
-
(input: string, key: Key) => {
|
|
357
|
-
// In search mode, only handle Esc to exit
|
|
358
|
-
if (searchMode) {
|
|
359
|
-
if (key.escape) {
|
|
360
|
-
setSearchMode(false)
|
|
361
|
-
return
|
|
362
|
-
}
|
|
363
|
-
// TextInput handles all other input
|
|
364
|
-
return
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Normal mode
|
|
368
|
-
if (input === "q" || key.escape) {
|
|
369
|
-
exit()
|
|
370
|
-
return
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (input === "/") {
|
|
374
|
-
setSearchMode(true)
|
|
375
|
-
return
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Navigation
|
|
379
|
-
if (input === "j" || key.downArrow) {
|
|
380
|
-
setCursor((c) => Math.min(filtered.length - 1, c + 1))
|
|
381
|
-
}
|
|
382
|
-
if (input === "k" || key.upArrow) {
|
|
383
|
-
setCursor((c) => Math.max(0, c - 1))
|
|
384
|
-
}
|
|
385
|
-
if (input === "d" || key.pageDown) {
|
|
386
|
-
setCursor((c) => Math.min(filtered.length - 1, c + halfPage))
|
|
387
|
-
}
|
|
388
|
-
if (input === "u" || key.pageUp) {
|
|
389
|
-
setCursor((c) => Math.max(0, c - halfPage))
|
|
390
|
-
}
|
|
391
|
-
if (input === "g" || key.home) {
|
|
392
|
-
setCursor(0)
|
|
393
|
-
}
|
|
394
|
-
if (input === "G" || key.end) {
|
|
395
|
-
setCursor(filtered.length - 1)
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Clear filter
|
|
399
|
-
if (key.backspace && query) {
|
|
400
|
-
setQuery("")
|
|
401
|
-
setCursor(0)
|
|
402
|
-
}
|
|
403
|
-
},
|
|
404
|
-
[searchMode, exit, filtered.length, halfPage, query],
|
|
405
|
-
),
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
return (
|
|
409
|
-
<Box flexDirection="column" flexGrow={1}>
|
|
410
|
-
{/* Summary bar */}
|
|
411
|
-
<SummaryBar processes={filtered} query={deferredQuery} />
|
|
412
|
-
|
|
413
|
-
{/* Search bar */}
|
|
414
|
-
<Box paddingX={1}>
|
|
415
|
-
{searchMode ? (
|
|
416
|
-
<Box>
|
|
417
|
-
<Text color="$primary" bold>
|
|
418
|
-
/{" "}
|
|
419
|
-
</Text>
|
|
420
|
-
<TextInput
|
|
421
|
-
value={query}
|
|
422
|
-
onChange={(v) => {
|
|
423
|
-
setQuery(v)
|
|
424
|
-
setCursor(0)
|
|
425
|
-
}}
|
|
426
|
-
onSubmit={handleSearchSubmit}
|
|
427
|
-
prompt=""
|
|
428
|
-
isActive={searchMode}
|
|
429
|
-
/>
|
|
430
|
-
</Box>
|
|
431
|
-
) : query ? (
|
|
432
|
-
<Muted>
|
|
433
|
-
filter: <Text bold>{query}</Text> (backspace to clear, / to edit)
|
|
434
|
-
</Muted>
|
|
435
|
-
) : (
|
|
436
|
-
<Muted>
|
|
437
|
-
Press <Kbd>/</Kbd> to search
|
|
438
|
-
</Muted>
|
|
439
|
-
)}
|
|
440
|
-
</Box>
|
|
441
|
-
|
|
442
|
-
{/* Table header */}
|
|
443
|
-
<TableHeader width={width} />
|
|
444
|
-
<Box paddingX={1}>
|
|
445
|
-
<Divider />
|
|
446
|
-
</Box>
|
|
447
|
-
|
|
448
|
-
{/* Process list */}
|
|
449
|
-
<Box flexGrow={1} flexDirection="column">
|
|
450
|
-
{filtered.length > 0 ? (
|
|
451
|
-
<ProcessListArea processes={filtered} cursor={effectiveCursor} width={width} />
|
|
452
|
-
) : (
|
|
453
|
-
<Box paddingX={1} justifyContent="center">
|
|
454
|
-
<Lead>No processes match "{deferredQuery}"</Lead>
|
|
455
|
-
</Box>
|
|
456
|
-
)}
|
|
457
|
-
</Box>
|
|
458
|
-
|
|
459
|
-
{/* Scroll indicator + help */}
|
|
460
|
-
<Box paddingX={1} justifyContent="space-between">
|
|
461
|
-
<Muted>
|
|
462
|
-
<Kbd>j/k</Kbd> navigate <Kbd>d/u</Kbd> half-page <Kbd>g/G</Kbd> start/end <Kbd>/</Kbd> search <Kbd>Esc/q</Kbd>{" "}
|
|
463
|
-
quit
|
|
464
|
-
</Muted>
|
|
465
|
-
<Muted>
|
|
466
|
-
{effectiveCursor + 1}/{filtered.length}
|
|
467
|
-
</Muted>
|
|
468
|
-
</Box>
|
|
469
|
-
</Box>
|
|
470
|
-
)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// ============================================================================
|
|
474
|
-
// Main
|
|
475
|
-
// ============================================================================
|
|
476
|
-
|
|
477
|
-
export async function main() {
|
|
478
|
-
using term = createTerm()
|
|
479
|
-
const { waitUntilExit } = await render(
|
|
480
|
-
<ExampleBanner meta={meta} controls="j/k navigate d/u half-page g/G start/end / search Esc/q quit">
|
|
481
|
-
<DataExplorer />
|
|
482
|
-
</ExampleBanner>,
|
|
483
|
-
term,
|
|
484
|
-
)
|
|
485
|
-
await waitUntilExit()
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (import.meta.main) {
|
|
489
|
-
await main()
|
|
490
|
-
}
|