repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/the-system/open-editor.ts
Rawimport { spawn } from "node:child_process";
export type EditorOpenResult =
| { ok: true; file: string; command: string }
| { ok: false; file: string; error: string };
export interface EditorCandidate {
command: string;
args: string[];
wait: boolean;
}
export async function openEditableFile(
file: string,
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform,
): Promise<EditorOpenResult> {
for (const candidate of editorCandidates(file, env, platform)) {
const ok = await trySpawn(candidate);
if (ok) return { ok: true, file, command: candidate.command };
}
return { ok: false, file, error: "No editor opener found" };
}
export function editorCandidates(
file: string,
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform,
): EditorCandidate[] {
const configured = [env.VISUAL, env.EDITOR]
.map((item) => parseEditorCommand(item ?? ""))
.filter((item): item is { command: string; args: string[] } =>
Boolean(item),
)
.map((item) => ({ ...item, args: [...item.args, file], wait: true }));
return [...configured, ...platformCandidates(file, env, platform)];
}
export function parseEditorCommand(
input: string,
): { command: string; args: string[] } | undefined {
const parts = splitCommandLine(input.trim());
const command = parts[0];
if (!command) return undefined;
return { command, args: parts.slice(1) };
}
function platformCandidates(
file: string,
env: NodeJS.ProcessEnv,
platform: NodeJS.Platform,
): EditorCandidate[] {
if (platform === "android" && env.TERMUX_VERSION)
return [{ command: "termux-open", args: [file], wait: false }];
if (platform === "darwin")
return [{ command: "open", args: ["-t", file], wait: false }];
if (platform === "win32")
return [{ command: "notepad.exe", args: [file], wait: false }];
return [
{ command: "xdg-open", args: [file], wait: false },
{ command: "gio", args: ["open", file], wait: false },
{ command: "wslview", args: [file], wait: false },
{ command: "vi", args: [file], wait: true },
];
}
function trySpawn(candidate: EditorCandidate): Promise<boolean> {
return new Promise((resolve) => {
const child = spawn(candidate.command, candidate.args, {
detached: !candidate.wait,
stdio: candidate.wait ? "inherit" : "ignore",
windowsHide: !candidate.wait,
});
let settled = false;
const done = (ok: boolean) => {
if (settled) return;
settled = true;
resolve(ok);
};
child.once("error", () => done(false));
if (candidate.wait) child.once("close", (code) => done(code === 0));
else
child.once("spawn", () => {
child.unref();
done(true);
});
});
}
function splitCommandLine(input: string): string[] {
const parts: string[] = [];
let current = "";
let quote: "'" | '"' | undefined;
for (let index = 0; index < input.length; index++) {
const char = input[index];
if (char === "\\" && quote !== "'") {
const next = input[index + 1];
if (next && (/\s/.test(next) || next === '"' || next === "'")) {
current += next;
index++;
continue;
}
}
if ((char === "'" || char === '"') && !quote) {
quote = char;
continue;
}
if (char === quote) {
quote = undefined;
continue;
}
if (/\s/.test(char) && !quote) {
if (current) parts.push(current);
current = "";
continue;
}
current += char;
}
if (current) parts.push(current);
return parts;
}