procm-cli 0.0.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.
- package/CLAUDE.md +106 -0
- package/README.md +56 -0
- package/bun.lock +213 -0
- package/index.ts +547 -0
- package/lib.test.ts +258 -0
- package/lib.ts +122 -0
- package/package.json +31 -0
- package/tsconfig.json +29 -0
package/index.ts
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import {
|
|
3
|
+
createCliRenderer,
|
|
4
|
+
BoxRenderable,
|
|
5
|
+
TextRenderable,
|
|
6
|
+
TextAttributes,
|
|
7
|
+
type KeyEvent,
|
|
8
|
+
} from "@opentui/core"
|
|
9
|
+
import {
|
|
10
|
+
type Process,
|
|
11
|
+
parseProcessLine,
|
|
12
|
+
c,
|
|
13
|
+
pad,
|
|
14
|
+
rpad,
|
|
15
|
+
cpuColor,
|
|
16
|
+
memColor,
|
|
17
|
+
statLabel,
|
|
18
|
+
statColor,
|
|
19
|
+
sortFields,
|
|
20
|
+
type SortField,
|
|
21
|
+
sortProcs,
|
|
22
|
+
} from "./lib"
|
|
23
|
+
|
|
24
|
+
// ── Process data ───────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
async function getProcesses(): Promise<Process[]> {
|
|
27
|
+
const proc = Bun.spawn(
|
|
28
|
+
["ps", "-eo", "pid,ppid,pcpu,pmem,stat,tty,lstart,command"],
|
|
29
|
+
{ stdout: "pipe", stderr: "ignore" },
|
|
30
|
+
)
|
|
31
|
+
const text = await new Response(proc.stdout).text()
|
|
32
|
+
const lines = text.split("\n")
|
|
33
|
+
|
|
34
|
+
const processes: Process[] = []
|
|
35
|
+
const selfPid = process.pid
|
|
36
|
+
|
|
37
|
+
for (let i = 1; i < lines.length; i++) {
|
|
38
|
+
const p = parseProcessLine(lines[i])
|
|
39
|
+
if (p && p.pid !== selfPid) processes.push(p)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return processes
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Columns ────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const cols = [
|
|
48
|
+
{ label: "PID", w: 7, right: true, val: (p: Process) => String(p.pid), color: (_: Process) => c.cyan },
|
|
49
|
+
{ label: "PPID", w: 7, right: true, val: (p: Process) => String(p.ppid), color: (_: Process) => c.dim },
|
|
50
|
+
{ label: "CPU%", w: 7, right: true, val: (p: Process) => p.cpu.toFixed(1), color: (p: Process) => cpuColor(p.cpu) },
|
|
51
|
+
{ label: "MEM%", w: 7, right: true, val: (p: Process) => p.mem.toFixed(1), color: (p: Process) => memColor(p.mem) },
|
|
52
|
+
{ label: "STATUS", w: 10, right: false, val: (p: Process) => statLabel(p.stat), color: (p: Process) => statColor(p.stat) },
|
|
53
|
+
{ label: "COMMAND", w: 0, right: false, val: (p: Process) => p.command, color: (_: Process) => c.text },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
// ── Sort state ─────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
let sortBy: SortField = "cpu"
|
|
59
|
+
let sortAsc = false
|
|
60
|
+
|
|
61
|
+
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
interface RowHandle {
|
|
64
|
+
box: BoxRenderable
|
|
65
|
+
cells: TextRenderable[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true })
|
|
70
|
+
|
|
71
|
+
let displayed: Process[] = []
|
|
72
|
+
let selected = 0
|
|
73
|
+
let scrollOffset = 0
|
|
74
|
+
let filter = ""
|
|
75
|
+
let filtering = false
|
|
76
|
+
let confirm: { pid: number; cmd: string; action: string } | null = null
|
|
77
|
+
let refreshing = false
|
|
78
|
+
let destroyed = false
|
|
79
|
+
|
|
80
|
+
// Chrome rows: title(1) + sort(1) + header(1) + filter(1) + footer(1) = 5
|
|
81
|
+
const CHROME = 5
|
|
82
|
+
let viewportH = renderer.height - CHROME
|
|
83
|
+
|
|
84
|
+
// ── Layout ─────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const root = renderer.root
|
|
87
|
+
root.backgroundColor = c.bg
|
|
88
|
+
root.flexDirection = "column"
|
|
89
|
+
|
|
90
|
+
// Title bar
|
|
91
|
+
const titleBar = new BoxRenderable(renderer, {
|
|
92
|
+
id: "title-bar",
|
|
93
|
+
flexDirection: "row",
|
|
94
|
+
justifyContent: "space-between",
|
|
95
|
+
alignItems: "center",
|
|
96
|
+
height: 1,
|
|
97
|
+
paddingLeft: 1,
|
|
98
|
+
paddingRight: 1,
|
|
99
|
+
backgroundColor: c.blue,
|
|
100
|
+
})
|
|
101
|
+
const titleText = new TextRenderable(renderer, {
|
|
102
|
+
id: "title",
|
|
103
|
+
content: " PROCM ",
|
|
104
|
+
fg: c.bg,
|
|
105
|
+
attributes: TextAttributes.BOLD,
|
|
106
|
+
})
|
|
107
|
+
const statsText = new TextRenderable(renderer, {
|
|
108
|
+
id: "stats",
|
|
109
|
+
content: "",
|
|
110
|
+
fg: c.bg,
|
|
111
|
+
})
|
|
112
|
+
titleBar.add(titleText)
|
|
113
|
+
titleBar.add(statsText)
|
|
114
|
+
|
|
115
|
+
// Sort bar
|
|
116
|
+
const sortBar = new BoxRenderable(renderer, {
|
|
117
|
+
id: "sort-bar",
|
|
118
|
+
height: 1,
|
|
119
|
+
paddingLeft: 1,
|
|
120
|
+
backgroundColor: c.bgAlt,
|
|
121
|
+
})
|
|
122
|
+
const sortText = new TextRenderable(renderer, {
|
|
123
|
+
id: "sort-text",
|
|
124
|
+
content: "",
|
|
125
|
+
fg: c.muted,
|
|
126
|
+
})
|
|
127
|
+
sortBar.add(sortText)
|
|
128
|
+
|
|
129
|
+
// Column headers
|
|
130
|
+
const headerRow = new BoxRenderable(renderer, {
|
|
131
|
+
id: "hdr",
|
|
132
|
+
flexDirection: "row",
|
|
133
|
+
height: 1,
|
|
134
|
+
paddingLeft: 1,
|
|
135
|
+
paddingRight: 1,
|
|
136
|
+
backgroundColor: c.bgAlt,
|
|
137
|
+
gap: 1,
|
|
138
|
+
})
|
|
139
|
+
for (const col of cols) {
|
|
140
|
+
headerRow.add(
|
|
141
|
+
new TextRenderable(renderer, {
|
|
142
|
+
id: `hdr-${col.label}`,
|
|
143
|
+
content: col.w > 0
|
|
144
|
+
? (col.right ? rpad(col.label, col.w) : pad(col.label, col.w))
|
|
145
|
+
: col.label,
|
|
146
|
+
fg: c.yellow,
|
|
147
|
+
attributes: TextAttributes.BOLD,
|
|
148
|
+
...(col.w > 0 ? { width: col.w } : { flexGrow: 1 }),
|
|
149
|
+
}),
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Virtual list area (plain Box, not ScrollBox)
|
|
154
|
+
const listBox = new BoxRenderable(renderer, {
|
|
155
|
+
id: "list",
|
|
156
|
+
flexGrow: 1,
|
|
157
|
+
flexDirection: "column",
|
|
158
|
+
backgroundColor: c.bg,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Filter bar
|
|
162
|
+
const filterBar = new BoxRenderable(renderer, {
|
|
163
|
+
id: "filter-bar",
|
|
164
|
+
height: 1,
|
|
165
|
+
paddingLeft: 1,
|
|
166
|
+
backgroundColor: c.bgAlt,
|
|
167
|
+
})
|
|
168
|
+
const filterText = new TextRenderable(renderer, {
|
|
169
|
+
id: "filter-text",
|
|
170
|
+
content: "",
|
|
171
|
+
fg: c.text,
|
|
172
|
+
})
|
|
173
|
+
filterBar.add(filterText)
|
|
174
|
+
|
|
175
|
+
// Confirm bar
|
|
176
|
+
const confirmBar = new BoxRenderable(renderer, {
|
|
177
|
+
id: "confirm-bar",
|
|
178
|
+
height: 1,
|
|
179
|
+
paddingLeft: 1,
|
|
180
|
+
backgroundColor: c.red,
|
|
181
|
+
visible: false,
|
|
182
|
+
})
|
|
183
|
+
const confirmText = new TextRenderable(renderer, {
|
|
184
|
+
id: "confirm-text",
|
|
185
|
+
content: "",
|
|
186
|
+
fg: c.bg,
|
|
187
|
+
attributes: TextAttributes.BOLD,
|
|
188
|
+
})
|
|
189
|
+
confirmBar.add(confirmText)
|
|
190
|
+
|
|
191
|
+
// Footer
|
|
192
|
+
const footerBar = new BoxRenderable(renderer, {
|
|
193
|
+
id: "footer",
|
|
194
|
+
flexDirection: "row",
|
|
195
|
+
height: 1,
|
|
196
|
+
paddingLeft: 1,
|
|
197
|
+
backgroundColor: c.bgAlt,
|
|
198
|
+
gap: 2,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
for (const [key, label] of [
|
|
202
|
+
["j/k", "Nav"],
|
|
203
|
+
["/", "Filter"],
|
|
204
|
+
["s", "Sort"],
|
|
205
|
+
["K", "Kill"],
|
|
206
|
+
["t", "Term"],
|
|
207
|
+
["r", "Restart"],
|
|
208
|
+
["q", "Quit"],
|
|
209
|
+
]) {
|
|
210
|
+
const kb = new BoxRenderable(renderer, {
|
|
211
|
+
id: `kb-${key}`,
|
|
212
|
+
flexDirection: "row",
|
|
213
|
+
})
|
|
214
|
+
kb.add(
|
|
215
|
+
new TextRenderable(renderer, {
|
|
216
|
+
id: `kk-${key}`,
|
|
217
|
+
content: ` ${key} `,
|
|
218
|
+
fg: c.bg,
|
|
219
|
+
bg: c.border,
|
|
220
|
+
attributes: TextAttributes.BOLD,
|
|
221
|
+
}),
|
|
222
|
+
)
|
|
223
|
+
kb.add(
|
|
224
|
+
new TextRenderable(renderer, {
|
|
225
|
+
id: `kl-${key}`,
|
|
226
|
+
content: ` ${label}`,
|
|
227
|
+
fg: c.dim,
|
|
228
|
+
}),
|
|
229
|
+
)
|
|
230
|
+
footerBar.add(kb)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
root.add(titleBar)
|
|
234
|
+
root.add(sortBar)
|
|
235
|
+
root.add(headerRow)
|
|
236
|
+
root.add(listBox)
|
|
237
|
+
root.add(filterBar)
|
|
238
|
+
root.add(confirmBar)
|
|
239
|
+
root.add(footerBar)
|
|
240
|
+
|
|
241
|
+
// ── Virtual row pool (viewport-sized) ─────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
const rowPool: RowHandle[] = []
|
|
244
|
+
|
|
245
|
+
function createRow(i: number): RowHandle {
|
|
246
|
+
const box = new BoxRenderable(renderer, {
|
|
247
|
+
id: `r-${i}`,
|
|
248
|
+
flexDirection: "row",
|
|
249
|
+
height: 1,
|
|
250
|
+
paddingLeft: 1,
|
|
251
|
+
paddingRight: 1,
|
|
252
|
+
backgroundColor: c.bg,
|
|
253
|
+
gap: 1,
|
|
254
|
+
})
|
|
255
|
+
const cells: TextRenderable[] = []
|
|
256
|
+
for (const col of cols) {
|
|
257
|
+
const cell = new TextRenderable(renderer, {
|
|
258
|
+
id: `c-${i}-${col.label}`,
|
|
259
|
+
content: "",
|
|
260
|
+
fg: c.text,
|
|
261
|
+
...(col.w > 0 ? { width: col.w } : { flexGrow: 1 }),
|
|
262
|
+
})
|
|
263
|
+
box.add(cell)
|
|
264
|
+
cells.push(cell)
|
|
265
|
+
}
|
|
266
|
+
listBox.add(box)
|
|
267
|
+
return { box, cells }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function ensurePoolSize(n: number) {
|
|
271
|
+
while (rowPool.length < n) {
|
|
272
|
+
rowPool.push(createRow(rowPool.length))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Pre-allocate rows for initial viewport
|
|
277
|
+
ensurePoolSize(viewportH)
|
|
278
|
+
|
|
279
|
+
// ── Scroll helpers ────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
function clampScroll() {
|
|
282
|
+
const maxOffset = Math.max(0, displayed.length - viewportH)
|
|
283
|
+
if (scrollOffset > maxOffset) scrollOffset = maxOffset
|
|
284
|
+
if (scrollOffset < 0) scrollOffset = 0
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function ensureVisible() {
|
|
288
|
+
if (selected < scrollOffset) scrollOffset = selected
|
|
289
|
+
else if (selected >= scrollOffset + viewportH)
|
|
290
|
+
scrollOffset = selected - viewportH + 1
|
|
291
|
+
clampScroll()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Render helpers ─────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
function updateUI() {
|
|
297
|
+
const arrow = sortAsc ? "▲" : "▼"
|
|
298
|
+
sortText.content = `Sort: ${sortBy.toUpperCase()} ${arrow} (s to cycle, S to flip)`
|
|
299
|
+
|
|
300
|
+
if (filtering) {
|
|
301
|
+
filterText.content = `Filter: ${filter}█`
|
|
302
|
+
filterText.fg = c.yellow
|
|
303
|
+
} else if (filter) {
|
|
304
|
+
filterText.content = `Filter: ${filter} (/ to edit, Esc to clear)`
|
|
305
|
+
filterText.fg = c.muted
|
|
306
|
+
} else {
|
|
307
|
+
filterText.content = ""
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const pos = displayed.length > 0
|
|
311
|
+
? `${selected + 1}/${displayed.length}`
|
|
312
|
+
: "0"
|
|
313
|
+
statsText.content = `${pos} | ${new Date().toLocaleTimeString()}`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function renderRows() {
|
|
317
|
+
ensureVisible()
|
|
318
|
+
const visibleCount = Math.min(viewportH, displayed.length - scrollOffset)
|
|
319
|
+
ensurePoolSize(viewportH)
|
|
320
|
+
|
|
321
|
+
for (let vi = 0; vi < viewportH; vi++) {
|
|
322
|
+
const row = rowPool[vi]
|
|
323
|
+
if (vi >= visibleCount) {
|
|
324
|
+
row.box.visible = false
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const di = scrollOffset + vi // data index
|
|
329
|
+
const p = displayed[di]
|
|
330
|
+
const sel = di === selected
|
|
331
|
+
const bg = sel ? c.bgHl : di % 2 === 0 ? c.bg : c.bgAlt
|
|
332
|
+
|
|
333
|
+
row.box.visible = true
|
|
334
|
+
row.box.backgroundColor = bg
|
|
335
|
+
|
|
336
|
+
for (let j = 0; j < cols.length; j++) {
|
|
337
|
+
const col = cols[j]
|
|
338
|
+
const raw = col.val(p)
|
|
339
|
+
const txt = col.w > 0
|
|
340
|
+
? (col.right ? rpad(raw, col.w) : pad(raw, col.w))
|
|
341
|
+
: raw
|
|
342
|
+
|
|
343
|
+
row.cells[j].content = txt
|
|
344
|
+
row.cells[j].fg = sel ? "#ffffff" : col.color(p)
|
|
345
|
+
row.cells[j].attributes = sel ? TextAttributes.BOLD : 0
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function refresh() {
|
|
351
|
+
if (refreshing || destroyed) return
|
|
352
|
+
refreshing = true
|
|
353
|
+
try {
|
|
354
|
+
const procs = await getProcesses()
|
|
355
|
+
const sorted = sortProcs(procs, sortBy, sortAsc)
|
|
356
|
+
|
|
357
|
+
if (filter) {
|
|
358
|
+
const f = filter.toLowerCase()
|
|
359
|
+
displayed = sorted.filter(
|
|
360
|
+
(p) =>
|
|
361
|
+
p.command.toLowerCase().includes(f) ||
|
|
362
|
+
String(p.pid).includes(f) ||
|
|
363
|
+
statLabel(p.stat).toLowerCase().includes(f),
|
|
364
|
+
)
|
|
365
|
+
} else {
|
|
366
|
+
displayed = sorted
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (selected >= displayed.length) {
|
|
370
|
+
selected = Math.max(0, displayed.length - 1)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
updateUI()
|
|
374
|
+
renderRows()
|
|
375
|
+
} finally {
|
|
376
|
+
refreshing = false
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Keyboard ───────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
renderer.keyInput.on("keypress", async (key: KeyEvent) => {
|
|
383
|
+
// Confirm mode
|
|
384
|
+
if (confirm) {
|
|
385
|
+
if (key.name === "y") {
|
|
386
|
+
try {
|
|
387
|
+
const sig = confirm.action === "kill" ? "-9" : "-15"
|
|
388
|
+
Bun.spawn(["kill", sig, String(confirm.pid)], {
|
|
389
|
+
stdout: "ignore",
|
|
390
|
+
stderr: "ignore",
|
|
391
|
+
})
|
|
392
|
+
if (confirm.action === "restart") {
|
|
393
|
+
await Bun.sleep(500)
|
|
394
|
+
Bun.spawn(["sh", "-c", confirm.cmd], {
|
|
395
|
+
stdout: "ignore",
|
|
396
|
+
stderr: "ignore",
|
|
397
|
+
stdin: "ignore",
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
} catch {}
|
|
401
|
+
confirm = null
|
|
402
|
+
confirmBar.visible = false
|
|
403
|
+
await refresh()
|
|
404
|
+
} else {
|
|
405
|
+
confirm = null
|
|
406
|
+
confirmBar.visible = false
|
|
407
|
+
}
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Filter mode
|
|
412
|
+
if (filtering) {
|
|
413
|
+
if (key.name === "return") {
|
|
414
|
+
filtering = false
|
|
415
|
+
updateUI()
|
|
416
|
+
await refresh()
|
|
417
|
+
} else if (key.name === "escape") {
|
|
418
|
+
filtering = false
|
|
419
|
+
filter = ""
|
|
420
|
+
updateUI()
|
|
421
|
+
await refresh()
|
|
422
|
+
} else if (key.name === "backspace") {
|
|
423
|
+
filter = filter.slice(0, -1)
|
|
424
|
+
updateUI()
|
|
425
|
+
await refresh()
|
|
426
|
+
} else if (key.sequence?.length === 1 && !key.ctrl && !key.meta) {
|
|
427
|
+
filter += key.sequence
|
|
428
|
+
updateUI()
|
|
429
|
+
await refresh()
|
|
430
|
+
}
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Normal mode
|
|
435
|
+
switch (key.name) {
|
|
436
|
+
case "q":
|
|
437
|
+
destroyed = true
|
|
438
|
+
renderer.destroy()
|
|
439
|
+
process.exit(0)
|
|
440
|
+
break
|
|
441
|
+
|
|
442
|
+
case "j":
|
|
443
|
+
case "down":
|
|
444
|
+
if (selected < displayed.length - 1) {
|
|
445
|
+
selected++
|
|
446
|
+
updateUI()
|
|
447
|
+
renderRows()
|
|
448
|
+
}
|
|
449
|
+
break
|
|
450
|
+
|
|
451
|
+
case "k":
|
|
452
|
+
case "up":
|
|
453
|
+
if (selected > 0) {
|
|
454
|
+
selected--
|
|
455
|
+
updateUI()
|
|
456
|
+
renderRows()
|
|
457
|
+
}
|
|
458
|
+
break
|
|
459
|
+
|
|
460
|
+
case "d":
|
|
461
|
+
selected = Math.min(displayed.length - 1, selected + Math.floor(viewportH / 2))
|
|
462
|
+
updateUI()
|
|
463
|
+
renderRows()
|
|
464
|
+
break
|
|
465
|
+
|
|
466
|
+
case "u":
|
|
467
|
+
selected = Math.max(0, selected - Math.floor(viewportH / 2))
|
|
468
|
+
updateUI()
|
|
469
|
+
renderRows()
|
|
470
|
+
break
|
|
471
|
+
|
|
472
|
+
case "g":
|
|
473
|
+
selected = 0
|
|
474
|
+
updateUI()
|
|
475
|
+
renderRows()
|
|
476
|
+
break
|
|
477
|
+
|
|
478
|
+
case "G":
|
|
479
|
+
selected = Math.max(0, displayed.length - 1)
|
|
480
|
+
updateUI()
|
|
481
|
+
renderRows()
|
|
482
|
+
break
|
|
483
|
+
|
|
484
|
+
case "/":
|
|
485
|
+
filtering = true
|
|
486
|
+
updateUI()
|
|
487
|
+
break
|
|
488
|
+
|
|
489
|
+
case "s": {
|
|
490
|
+
const i = sortFields.indexOf(sortBy)
|
|
491
|
+
sortBy = sortFields[(i + 1) % sortFields.length]
|
|
492
|
+
sortAsc = sortBy === "pid" || sortBy === "command"
|
|
493
|
+
await refresh()
|
|
494
|
+
break
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
case "S":
|
|
498
|
+
sortAsc = !sortAsc
|
|
499
|
+
await refresh()
|
|
500
|
+
break
|
|
501
|
+
|
|
502
|
+
case "K": {
|
|
503
|
+
const p = displayed[selected]
|
|
504
|
+
if (p) {
|
|
505
|
+
confirm = { pid: p.pid, cmd: p.command, action: "kill" }
|
|
506
|
+
confirmText.content = `SIGKILL PID ${p.pid} (${p.command.slice(0, 50)})? [y/n]`
|
|
507
|
+
confirmBar.visible = true
|
|
508
|
+
}
|
|
509
|
+
break
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
case "t": {
|
|
513
|
+
const p = displayed[selected]
|
|
514
|
+
if (p) {
|
|
515
|
+
confirm = { pid: p.pid, cmd: p.command, action: "term" }
|
|
516
|
+
confirmText.content = `SIGTERM PID ${p.pid} (${p.command.slice(0, 50)})? [y/n]`
|
|
517
|
+
confirmBar.visible = true
|
|
518
|
+
}
|
|
519
|
+
break
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
case "r": {
|
|
523
|
+
const p = displayed[selected]
|
|
524
|
+
if (p) {
|
|
525
|
+
confirm = { pid: p.pid, cmd: p.command, action: "restart" }
|
|
526
|
+
confirmText.content = `Restart PID ${p.pid} (${p.command.slice(0, 50)})? [y/n]`
|
|
527
|
+
confirmBar.visible = true
|
|
528
|
+
}
|
|
529
|
+
break
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
// ── Start ──────────────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
renderer.on("destroy", () => { destroyed = true })
|
|
537
|
+
|
|
538
|
+
await refresh()
|
|
539
|
+
setInterval(async () => {
|
|
540
|
+
if (!filtering && !confirm && !destroyed) await refresh()
|
|
541
|
+
}, 2000)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
main().catch((err) => {
|
|
545
|
+
console.error(err)
|
|
546
|
+
process.exit(1)
|
|
547
|
+
})
|