@aliou/pi-processes 0.4.7 → 0.6.0
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/README.md +83 -13
- package/package.json +10 -4
- package/src/commands/clear/command.ts +14 -0
- package/src/commands/clear/index.ts +1 -0
- package/src/commands/completions.ts +38 -0
- package/src/commands/dock/command.ts +29 -0
- package/src/commands/dock/index.ts +1 -0
- package/src/commands/index.ts +26 -327
- package/src/commands/kill/command.ts +69 -0
- package/src/commands/kill/index.ts +1 -0
- package/src/commands/logs/command.ts +47 -0
- package/src/commands/logs/index.ts +1 -0
- package/src/commands/pick-process.ts +34 -0
- package/src/commands/pin/command.ts +33 -0
- package/src/commands/pin/index.ts +1 -0
- package/src/commands/processes/command.ts +39 -0
- package/src/commands/processes/index.ts +1 -0
- package/src/commands/settings/apply-setting-change.ts +72 -0
- package/src/commands/settings/build-sections.ts +160 -0
- package/src/commands/settings/command.ts +20 -0
- package/src/commands/settings/index.ts +1 -0
- package/src/components/log-dock-component.ts +238 -0
- package/src/components/log-file-viewer.ts +317 -0
- package/src/components/log-overlay-component.ts +555 -0
- package/src/components/process-picker-component.ts +14 -2
- package/src/components/processes-component.ts +41 -21
- package/src/config.ts +38 -1
- package/src/constants/index.ts +1 -0
- package/src/constants/types.ts +7 -1
- package/src/hooks/background-blocker.ts +66 -0
- package/src/hooks/index.ts +15 -3
- package/src/hooks/process-end.ts +3 -30
- package/src/hooks/widget/index.ts +2 -0
- package/src/hooks/widget/setup.ts +168 -0
- package/src/hooks/{widget.ts → widget/status-widget.ts} +27 -68
- package/src/hooks/widget/types.ts +21 -0
- package/src/index.ts +9 -3
- package/src/manager.ts +61 -9
- package/src/tools/actions/index.ts +5 -0
- package/src/tools/actions/write.ts +87 -0
- package/src/tools/index.ts +82 -14
- package/src/utils/command-executor.test.ts +2 -1
- package/src/utils/command-executor.ts +1 -1
- package/src/utils/keybindings.ts +76 -0
- package/src/utils/shell-utils.ts +133 -0
- package/src/commands/settings-command.ts +0 -157
- package/src/components/log-stream-component.ts +0 -149
- package/src/test/test-exit-crash.sh +0 -19
- package/src/test/test-exit-failure.sh +0 -17
- package/src/test/test-exit-success.sh +0 -16
- package/src/test/test-output.sh +0 -28
package/src/manager.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "node:fs";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
+
import type { Writable } from "node:stream";
|
|
12
13
|
|
|
13
14
|
import {
|
|
14
15
|
type KillResult,
|
|
@@ -17,12 +18,15 @@ import {
|
|
|
17
18
|
type ProcessInfo,
|
|
18
19
|
type ProcessStatus,
|
|
19
20
|
type StartOptions,
|
|
21
|
+
type WriteResult,
|
|
20
22
|
} from "./constants";
|
|
21
23
|
import { isProcessGroupAlive, killProcessGroup } from "./utils";
|
|
22
24
|
import { spawnCommand } from "./utils/command-executor";
|
|
23
25
|
|
|
24
26
|
interface ManagedProcess extends ProcessInfo {
|
|
25
27
|
process: ChildProcess;
|
|
28
|
+
stdin: Writable | null;
|
|
29
|
+
stdinClosed: boolean;
|
|
26
30
|
lastSignalSent: NodeJS.Signals | null;
|
|
27
31
|
combinedFile: string;
|
|
28
32
|
}
|
|
@@ -57,15 +61,8 @@ export class ProcessManager {
|
|
|
57
61
|
|
|
58
62
|
private transition(managed: ManagedProcess, next: ProcessStatus): void {
|
|
59
63
|
if (managed.status === next) return;
|
|
60
|
-
const prev = managed.status;
|
|
61
64
|
managed.status = next;
|
|
62
65
|
|
|
63
|
-
this.emit({
|
|
64
|
-
type: "process_status_changed",
|
|
65
|
-
info: this.toProcessInfo(managed),
|
|
66
|
-
prev,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
66
|
if (next === "exited" || next === "killed") {
|
|
70
67
|
this.emit({ type: "process_ended", info: this.toProcessInfo(managed) });
|
|
71
68
|
}
|
|
@@ -159,6 +156,8 @@ export class ProcessManager {
|
|
|
159
156
|
alertOnFailure: options?.alertOnFailure ?? true,
|
|
160
157
|
alertOnKill: options?.alertOnKill ?? false,
|
|
161
158
|
process: child,
|
|
159
|
+
stdin: child.stdin,
|
|
160
|
+
stdinClosed: false,
|
|
162
161
|
lastSignalSent: null,
|
|
163
162
|
};
|
|
164
163
|
|
|
@@ -319,12 +318,15 @@ export class ProcessManager {
|
|
|
319
318
|
}
|
|
320
319
|
}
|
|
321
320
|
|
|
322
|
-
getLogFiles(
|
|
321
|
+
getLogFiles(
|
|
322
|
+
id: string,
|
|
323
|
+
): { stdoutFile: string; stderrFile: string; combinedFile: string } | null {
|
|
323
324
|
const managed = this.processes.get(id);
|
|
324
325
|
if (!managed) return null;
|
|
325
326
|
return {
|
|
326
327
|
stdoutFile: managed.stdoutFile,
|
|
327
328
|
stderrFile: managed.stderrFile,
|
|
329
|
+
combinedFile: managed.combinedFile,
|
|
328
330
|
};
|
|
329
331
|
}
|
|
330
332
|
|
|
@@ -407,6 +409,50 @@ export class ProcessManager {
|
|
|
407
409
|
return { ok: true, info: this.toProcessInfo(managed) };
|
|
408
410
|
}
|
|
409
411
|
|
|
412
|
+
writeToStdin(
|
|
413
|
+
id: string,
|
|
414
|
+
data: string,
|
|
415
|
+
opts?: { end?: boolean },
|
|
416
|
+
): WriteResult {
|
|
417
|
+
const managed = this.processes.get(id);
|
|
418
|
+
if (!managed) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
reason: "not_found",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!LIVE_STATUSES.has(managed.status)) {
|
|
426
|
+
return {
|
|
427
|
+
ok: false,
|
|
428
|
+
reason: "process_exited",
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (managed.stdinClosed || !managed.stdin) {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
reason: "stdin_closed",
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
managed.stdin.write(data);
|
|
441
|
+
|
|
442
|
+
if (opts?.end) {
|
|
443
|
+
managed.stdin.end();
|
|
444
|
+
managed.stdinClosed = true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return { ok: true };
|
|
448
|
+
} catch {
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
reason: "write_error",
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
410
456
|
clearFinished(): number {
|
|
411
457
|
let cleared = 0;
|
|
412
458
|
for (const [id, managed] of this.processes) {
|
|
@@ -519,4 +565,10 @@ export class ProcessManager {
|
|
|
519
565
|
}
|
|
520
566
|
}
|
|
521
567
|
|
|
522
|
-
export type {
|
|
568
|
+
export type {
|
|
569
|
+
ProcessInfo,
|
|
570
|
+
ProcessStatus,
|
|
571
|
+
ManagerEvent,
|
|
572
|
+
KillResult,
|
|
573
|
+
WriteResult,
|
|
574
|
+
};
|
|
@@ -7,12 +7,15 @@ import { executeList } from "./list";
|
|
|
7
7
|
import { executeLogs } from "./logs";
|
|
8
8
|
import { executeOutput } from "./output";
|
|
9
9
|
import { executeStart } from "./start";
|
|
10
|
+
import { executeWrite } from "./write";
|
|
10
11
|
|
|
11
12
|
interface ActionParams {
|
|
12
13
|
action: string;
|
|
13
14
|
command?: string;
|
|
14
15
|
name?: string;
|
|
15
16
|
id?: string;
|
|
17
|
+
input?: string;
|
|
18
|
+
end?: boolean;
|
|
16
19
|
alertOnSuccess?: boolean;
|
|
17
20
|
alertOnFailure?: boolean;
|
|
18
21
|
alertOnKill?: boolean;
|
|
@@ -36,6 +39,8 @@ export async function executeAction(
|
|
|
36
39
|
return executeKill(params, manager);
|
|
37
40
|
case "clear":
|
|
38
41
|
return executeClear(manager);
|
|
42
|
+
case "write":
|
|
43
|
+
return executeWrite(params, manager);
|
|
39
44
|
default:
|
|
40
45
|
return {
|
|
41
46
|
content: [{ type: "text", text: `Unknown action: ${params.action}` }],
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ExecuteResult } from "../../constants";
|
|
2
|
+
import type { ProcessManager } from "../../manager";
|
|
3
|
+
|
|
4
|
+
interface WriteParams {
|
|
5
|
+
id?: string;
|
|
6
|
+
input?: string;
|
|
7
|
+
end?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function executeWrite(
|
|
11
|
+
params: WriteParams,
|
|
12
|
+
manager: ProcessManager,
|
|
13
|
+
): ExecuteResult {
|
|
14
|
+
const { id, input, end } = params;
|
|
15
|
+
|
|
16
|
+
if (!id) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: "Missing required parameter: id" }],
|
|
19
|
+
details: {
|
|
20
|
+
action: "write",
|
|
21
|
+
success: false,
|
|
22
|
+
message: "Missing required parameter: id",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (input === undefined) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: "Missing required parameter: input" }],
|
|
30
|
+
details: {
|
|
31
|
+
action: "write",
|
|
32
|
+
success: false,
|
|
33
|
+
message: "Missing required parameter: input",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const process = manager.find(id);
|
|
39
|
+
if (!process) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: `Process not found: ${id}` }],
|
|
42
|
+
details: {
|
|
43
|
+
action: "write",
|
|
44
|
+
success: false,
|
|
45
|
+
message: `Process not found: ${id}`,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = manager.writeToStdin(process.id, input, { end });
|
|
51
|
+
|
|
52
|
+
if (!result.ok) {
|
|
53
|
+
const messages: Record<string, string> = {
|
|
54
|
+
not_found: `Process not found: ${process.id}`,
|
|
55
|
+
process_exited: `Process has already exited: ${process.id}`,
|
|
56
|
+
stdin_closed: `Stdin already closed for process: ${process.id}`,
|
|
57
|
+
write_error: `Failed to write to stdin for process: ${process.id}`,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const message =
|
|
61
|
+
messages[result.reason] || `Unknown error: ${result.reason}`;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: message }],
|
|
65
|
+
details: {
|
|
66
|
+
action: "write",
|
|
67
|
+
success: false,
|
|
68
|
+
message,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const suffix = end ? " (stdin closed)" : "";
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `Wrote ${input.length} bytes to ${process.id}${suffix}`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
details: {
|
|
82
|
+
action: "write",
|
|
83
|
+
success: true,
|
|
84
|
+
message: `Wrote ${input.length} bytes to process stdin${suffix}`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -15,10 +15,10 @@ import { executeAction } from "./actions";
|
|
|
15
15
|
|
|
16
16
|
const ProcessesParams = Type.Object({
|
|
17
17
|
action: StringEnum(
|
|
18
|
-
["start", "list", "output", "logs", "kill", "clear"] as const,
|
|
18
|
+
["start", "list", "output", "logs", "kill", "clear", "write"] as const,
|
|
19
19
|
{
|
|
20
20
|
description:
|
|
21
|
-
"Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate), clear (remove finished)",
|
|
21
|
+
"Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate), clear (remove finished), write (write to stdin)",
|
|
22
22
|
},
|
|
23
23
|
),
|
|
24
24
|
command: Type.Optional(
|
|
@@ -33,7 +33,18 @@ const ProcessesParams = Type.Object({
|
|
|
33
33
|
id: Type.Optional(
|
|
34
34
|
Type.String({
|
|
35
35
|
description:
|
|
36
|
-
"Process ID or name to match (required for output/kill/logs). Can be proc_N or friendly name.",
|
|
36
|
+
"Process ID or name to match (required for output/kill/logs/write). Can be proc_N or friendly name.",
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
input: Type.Optional(
|
|
40
|
+
Type.String({
|
|
41
|
+
description: "Data to write to process stdin (required for write action)",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
end: Type.Optional(
|
|
45
|
+
Type.Boolean({
|
|
46
|
+
description:
|
|
47
|
+
"Close stdin after writing (optional for write action, use for programs reading until EOF)",
|
|
37
48
|
}),
|
|
38
49
|
),
|
|
39
50
|
alertOnSuccess: Type.Optional(
|
|
@@ -72,6 +83,7 @@ export function setupProcessesTools(pi: ExtensionAPI, manager: ProcessManager) {
|
|
|
72
83
|
- logs: Get log file paths to inspect with read tool (requires 'id')
|
|
73
84
|
- kill: Terminate a process (requires 'id' - can be proc_N or name match like "backend")
|
|
74
85
|
- clear: Remove all finished processes from the list
|
|
86
|
+
- write: Write to process stdin (requires 'id' and 'input', optional 'end' to close stdin)
|
|
75
87
|
|
|
76
88
|
Important: You DON'T need to poll or wait for processes. Notifications arrive automatically based on your preferences. Start processes and continue with other work - you'll be informed if something requires attention.
|
|
77
89
|
|
|
@@ -107,12 +119,20 @@ Note: User always sees process updates in the UI. The notify flags control wheth
|
|
|
107
119
|
if (
|
|
108
120
|
(args.action === "output" ||
|
|
109
121
|
args.action === "kill" ||
|
|
110
|
-
args.action === "logs"
|
|
122
|
+
args.action === "logs" ||
|
|
123
|
+
args.action === "write") &&
|
|
111
124
|
args.id
|
|
112
125
|
) {
|
|
113
126
|
mainArg = args.id;
|
|
114
127
|
}
|
|
115
128
|
|
|
129
|
+
if (args.action === "write" && args.input) {
|
|
130
|
+
optionArgs.push({ label: "input", value: args.input });
|
|
131
|
+
if (args.end) {
|
|
132
|
+
optionArgs.push({ label: "end", value: "true" });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
116
136
|
return new ToolCallHeader(
|
|
117
137
|
{
|
|
118
138
|
toolName: "Process",
|
|
@@ -204,6 +224,23 @@ Note: User always sees process updates in the UI. The notify flags control wheth
|
|
|
204
224
|
}
|
|
205
225
|
|
|
206
226
|
fields.push(new Text(lines.join("\n"), 0, 0));
|
|
227
|
+
|
|
228
|
+
// Collapsed summary
|
|
229
|
+
const previewSource =
|
|
230
|
+
details.output.stdout.length > 0
|
|
231
|
+
? details.output.stdout
|
|
232
|
+
: details.output.stderr;
|
|
233
|
+
const preview = previewSource
|
|
234
|
+
.slice(-2)
|
|
235
|
+
.map((l) => stripAnsi(l))
|
|
236
|
+
.join("\n");
|
|
237
|
+
fields.push({
|
|
238
|
+
label: "Output",
|
|
239
|
+
value: preview
|
|
240
|
+
? `${theme.fg("muted", preview)}`
|
|
241
|
+
: theme.fg("muted", "(empty)"),
|
|
242
|
+
showCollapsed: true,
|
|
243
|
+
});
|
|
207
244
|
} else if (
|
|
208
245
|
details.action === "list" &&
|
|
209
246
|
details.processes &&
|
|
@@ -243,6 +280,31 @@ Note: User always sees process updates in the UI. The notify flags control wheth
|
|
|
243
280
|
}
|
|
244
281
|
|
|
245
282
|
fields.push(new Text(lines.join("\n"), 0, 0));
|
|
283
|
+
|
|
284
|
+
// Collapsed summary: first 3 processes
|
|
285
|
+
const summary = details.processes
|
|
286
|
+
.slice(0, 3)
|
|
287
|
+
.map((p) => {
|
|
288
|
+
const s =
|
|
289
|
+
p.status === "running"
|
|
290
|
+
? theme.fg("accent", "running")
|
|
291
|
+
: p.status === "exited" && p.success
|
|
292
|
+
? theme.fg("success", "exit(0)")
|
|
293
|
+
: p.status === "exited"
|
|
294
|
+
? theme.fg("error", `exit(${p.exitCode ?? "?"})`)
|
|
295
|
+
: theme.fg("muted", p.status);
|
|
296
|
+
return `${theme.fg("accent", `"${p.name}"`)} [${s}]`;
|
|
297
|
+
})
|
|
298
|
+
.join(", ");
|
|
299
|
+
const more =
|
|
300
|
+
details.processes.length > 3
|
|
301
|
+
? theme.fg("muted", ` +${details.processes.length - 3} more`)
|
|
302
|
+
: "";
|
|
303
|
+
fields.push({
|
|
304
|
+
label: "Processes",
|
|
305
|
+
value: summary + more,
|
|
306
|
+
showCollapsed: true,
|
|
307
|
+
});
|
|
246
308
|
} else if (details.action === "logs" && details.logFiles) {
|
|
247
309
|
fields.push(
|
|
248
310
|
new Text(
|
|
@@ -263,16 +325,22 @@ Note: User always sees process updates in the UI. The notify flags control wheth
|
|
|
263
325
|
});
|
|
264
326
|
}
|
|
265
327
|
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
328
|
+
const footerItems: Array<{
|
|
329
|
+
label: string;
|
|
330
|
+
value: string;
|
|
331
|
+
tone: "accent" | "success" | "error" | "warning" | "muted";
|
|
332
|
+
}> = [];
|
|
333
|
+
if (!details.success) {
|
|
334
|
+
footerItems.push({
|
|
335
|
+
label: "status",
|
|
336
|
+
value: "error",
|
|
337
|
+
tone: "error",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
const footer =
|
|
341
|
+
footerItems.length > 0
|
|
342
|
+
? new ToolFooter(theme, { items: footerItems })
|
|
343
|
+
: undefined;
|
|
276
344
|
|
|
277
345
|
return new ToolBody({ fields, footer }, options, theme);
|
|
278
346
|
},
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type * as nodeFs from "node:fs";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import { describe, expect, it, vi } from "vitest";
|
|
3
4
|
import { resolveShellExecutable } from "./command-executor";
|
|
4
5
|
|
|
5
6
|
vi.mock("node:fs", async (importOriginal) => {
|
|
6
|
-
const actual = await importOriginal<typeof
|
|
7
|
+
const actual = await importOriginal<typeof nodeFs>();
|
|
7
8
|
return { ...actual, existsSync: vi.fn() };
|
|
8
9
|
});
|
|
9
10
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard shortcuts configuration for the Process Dock.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ProcessesKeybindings {
|
|
6
|
+
/** Toggle dock visibility (global) */
|
|
7
|
+
toggleDock: string;
|
|
8
|
+
/** Scroll logs up */
|
|
9
|
+
scrollUp: string;
|
|
10
|
+
/** Scroll logs down */
|
|
11
|
+
scrollDown: string;
|
|
12
|
+
/** Focus previous process */
|
|
13
|
+
prevProcess: string;
|
|
14
|
+
/** Focus next process */
|
|
15
|
+
nextProcess: string;
|
|
16
|
+
/** Toggle focus mode */
|
|
17
|
+
toggleFocus: string;
|
|
18
|
+
/** Toggle follow mode */
|
|
19
|
+
toggleFollow: string;
|
|
20
|
+
/** Kill focused process */
|
|
21
|
+
killProcess: string;
|
|
22
|
+
/** Clear finished processes */
|
|
23
|
+
clearFinished: string;
|
|
24
|
+
/** Collapse/close dock */
|
|
25
|
+
closeDock: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_KEYBINDINGS: ProcessesKeybindings = {
|
|
29
|
+
toggleDock: "", // Disabled - conflicts with editor shortcuts
|
|
30
|
+
scrollUp: "k",
|
|
31
|
+
scrollDown: "j",
|
|
32
|
+
prevProcess: "h",
|
|
33
|
+
nextProcess: "l",
|
|
34
|
+
toggleFocus: "f",
|
|
35
|
+
toggleFollow: "Shift+F",
|
|
36
|
+
killProcess: "x",
|
|
37
|
+
clearFinished: "c",
|
|
38
|
+
closeDock: "q",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Interface for config that may contain keybindings overrides
|
|
43
|
+
*/
|
|
44
|
+
export interface ProcessesConfigKeybindings {
|
|
45
|
+
toggleDock?: string;
|
|
46
|
+
scrollUp?: string;
|
|
47
|
+
scrollDown?: string;
|
|
48
|
+
prevProcess?: string;
|
|
49
|
+
nextProcess?: string;
|
|
50
|
+
toggleFocus?: string;
|
|
51
|
+
toggleFollow?: string;
|
|
52
|
+
killProcess?: string;
|
|
53
|
+
clearFinished?: string;
|
|
54
|
+
closeDock?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load keybindings from config, falling back to defaults.
|
|
59
|
+
*/
|
|
60
|
+
export function loadKeybindings(config: {
|
|
61
|
+
keybindings?: ProcessesConfigKeybindings;
|
|
62
|
+
}): ProcessesKeybindings {
|
|
63
|
+
const user = config.keybindings ?? {};
|
|
64
|
+
return {
|
|
65
|
+
toggleDock: user.toggleDock ?? DEFAULT_KEYBINDINGS.toggleDock,
|
|
66
|
+
scrollUp: user.scrollUp ?? DEFAULT_KEYBINDINGS.scrollUp,
|
|
67
|
+
scrollDown: user.scrollDown ?? DEFAULT_KEYBINDINGS.scrollDown,
|
|
68
|
+
prevProcess: user.prevProcess ?? DEFAULT_KEYBINDINGS.prevProcess,
|
|
69
|
+
nextProcess: user.nextProcess ?? DEFAULT_KEYBINDINGS.nextProcess,
|
|
70
|
+
toggleFocus: user.toggleFocus ?? DEFAULT_KEYBINDINGS.toggleFocus,
|
|
71
|
+
toggleFollow: user.toggleFollow ?? DEFAULT_KEYBINDINGS.toggleFollow,
|
|
72
|
+
killProcess: user.killProcess ?? DEFAULT_KEYBINDINGS.killProcess,
|
|
73
|
+
clearFinished: user.clearFinished ?? DEFAULT_KEYBINDINGS.clearFinished,
|
|
74
|
+
closeDock: user.closeDock ?? DEFAULT_KEYBINDINGS.closeDock,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Shell AST helpers. Duplicated from pi-toolchain since cross-extension imports are not allowed.
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
Command,
|
|
5
|
+
Program,
|
|
6
|
+
SimpleCommand,
|
|
7
|
+
Statement,
|
|
8
|
+
Word,
|
|
9
|
+
WordPart,
|
|
10
|
+
} from "@aliou/sh";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a Word node to its literal string value.
|
|
14
|
+
* Concatenates Literal, SglQuoted, and simple DblQuoted parts.
|
|
15
|
+
* For parts containing parameter expansions, command substitutions, etc.,
|
|
16
|
+
* includes the raw text representation (e.g. `$VAR`).
|
|
17
|
+
*/
|
|
18
|
+
export function wordToString(word: Word): string {
|
|
19
|
+
return word.parts.map(partToString).join("");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function partToString(part: WordPart): string {
|
|
23
|
+
switch (part.type) {
|
|
24
|
+
case "Literal":
|
|
25
|
+
return part.value;
|
|
26
|
+
case "SglQuoted":
|
|
27
|
+
return part.value;
|
|
28
|
+
case "DblQuoted":
|
|
29
|
+
return part.parts.map(partToString).join("");
|
|
30
|
+
case "ParamExp":
|
|
31
|
+
return part.short
|
|
32
|
+
? `$${part.param.value}`
|
|
33
|
+
: `\${${part.param.value}${part.op ?? ""}${part.value ? wordToString(part.value) : ""}}`;
|
|
34
|
+
case "CmdSubst":
|
|
35
|
+
return "$(...)";
|
|
36
|
+
case "ArithExp":
|
|
37
|
+
return `$((${part.expr}))`;
|
|
38
|
+
case "ProcSubst":
|
|
39
|
+
return `${part.op}(...)`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Walk the AST and call `callback` for every SimpleCommand found at any
|
|
45
|
+
* nesting depth. Returns early if callback returns `true`.
|
|
46
|
+
*/
|
|
47
|
+
export function walkCommands(
|
|
48
|
+
node: Program,
|
|
49
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
50
|
+
): void {
|
|
51
|
+
for (const stmt of node.body) {
|
|
52
|
+
if (walkStatement(stmt, callback)) return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function walkStatement(
|
|
57
|
+
stmt: Statement,
|
|
58
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
59
|
+
): boolean {
|
|
60
|
+
return walkCommand(stmt.command, callback);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function walkStatements(
|
|
64
|
+
stmts: Statement[],
|
|
65
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
66
|
+
): boolean {
|
|
67
|
+
for (const stmt of stmts) {
|
|
68
|
+
if (walkStatement(stmt, callback)) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function walkCommand(
|
|
74
|
+
cmd: Command,
|
|
75
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
76
|
+
): boolean {
|
|
77
|
+
switch (cmd.type) {
|
|
78
|
+
case "SimpleCommand":
|
|
79
|
+
return callback(cmd) === true;
|
|
80
|
+
|
|
81
|
+
case "Pipeline":
|
|
82
|
+
return walkStatements(cmd.commands, callback);
|
|
83
|
+
|
|
84
|
+
case "Logical":
|
|
85
|
+
return (
|
|
86
|
+
walkStatement(cmd.left, callback) || walkStatement(cmd.right, callback)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
case "Subshell":
|
|
90
|
+
case "Block":
|
|
91
|
+
return walkStatements(cmd.body, callback);
|
|
92
|
+
|
|
93
|
+
case "IfClause":
|
|
94
|
+
return (
|
|
95
|
+
walkStatements(cmd.cond, callback) ||
|
|
96
|
+
walkStatements(cmd.then, callback) ||
|
|
97
|
+
(cmd.else ? walkStatements(cmd.else, callback) : false)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
case "ForClause":
|
|
101
|
+
case "SelectClause":
|
|
102
|
+
case "WhileClause":
|
|
103
|
+
return (
|
|
104
|
+
("cond" in cmd && cmd.cond
|
|
105
|
+
? walkStatements(cmd.cond, callback)
|
|
106
|
+
: false) || walkStatements(cmd.body, callback)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
case "CaseClause":
|
|
110
|
+
for (const item of cmd.items) {
|
|
111
|
+
if (walkStatements(item.body, callback)) return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
|
|
115
|
+
case "FunctionDecl":
|
|
116
|
+
return walkStatements(cmd.body, callback);
|
|
117
|
+
|
|
118
|
+
case "TimeClause":
|
|
119
|
+
return walkStatement(cmd.command, callback);
|
|
120
|
+
|
|
121
|
+
case "CoprocClause":
|
|
122
|
+
return walkStatement(cmd.body, callback);
|
|
123
|
+
|
|
124
|
+
case "CStyleLoop":
|
|
125
|
+
return walkStatements(cmd.body, callback);
|
|
126
|
+
|
|
127
|
+
case "TestClause":
|
|
128
|
+
case "ArithCmd":
|
|
129
|
+
case "DeclClause":
|
|
130
|
+
case "LetClause":
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|