refactor(cli): replace external editor with inline terminal editing

This commit is contained in:
2026-06-09 17:31:52 +08:00
parent 37916f6c49
commit 68e98be653
+138 -27
View File
@@ -1,9 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import * as readline from "node:readline"; import * as readline from "node:readline";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { unlink } from "node:fs/promises";
import { loadConfig, saveConfig } from "./src/config"; import { loadConfig, saveConfig } from "./src/config";
import { import {
isGitRepo, isGitRepo,
@@ -108,38 +105,152 @@ async function confirmCommit(message: string): Promise<"y" | "n" | "e"> {
} }
async function editMessage(current: string): Promise<string | null> { async function editMessage(current: string): Promise<string | null> {
const tmpPath = join(tmpdir(), `gai-msg-${Date.now()}`); if (!process.stdin.isTTY) return null;
const header = `# Edit commit message below. Save and close to confirm.\n# Delete all lines to abort.\n`; process.stdout.write(` ${DIM}Edit message (Enter to confirm, Esc to abort):${RESET}\n`);
await Bun.write(tmpPath, header + current);
const editor = const savedRaw = process.stdin.isRaw;
process.env.VISUAL || process.stdin.setRawMode(true);
process.env.EDITOR || process.stdin.resume();
(process.platform === "darwin" ? "vi" : "nano");
const proc = Bun.spawn([editor, tmpPath], { let buffer = current;
stdout: "inherit", let cursor = current.length;
stderr: "inherit", const ESC = "\x1b";
stdin: "inherit", const ENTER = "\r";
}); const CTRL_C = "\x03";
const BACKSPACE = "\x7f";
const DELETE = "\x1b[3~";
const LEFT = "\x1b[D";
const RIGHT = "\x1b[C";
const HOME = "\x1b[H" /* or \x01 */;
const END = "\x1b[F" /* or \x05 */;
const exitCode = await proc.exited; function render() {
if (exitCode !== 0) { process.stdout.write("\x1b[2K\r > " + buffer);
await unlink(tmpPath).catch(() => {}); if (cursor < buffer.length) {
return null; process.stdout.write(`\x1b[${buffer.length - cursor}D`);
}
} }
const content = await Bun.file(tmpPath).text(); process.stdout.write(" > ");
await unlink(tmpPath).catch(() => {}); process.stdout.write(buffer);
const lines = content return new Promise((resolve) => {
.split("\n") let escapeBuf = "";
.filter((line) => !line.startsWith("#"))
.join("\n")
.trim();
return lines || null; function handleSeq(seq: string) {
if (seq === "\x1b[D" || seq === "\x1bOD") {
if (cursor > 0) {
cursor--;
process.stdout.write("\x1b[D");
}
} else if (seq === "\x1b[C" || seq === "\x1bOC") {
if (cursor < buffer.length) {
cursor++;
process.stdout.write("\x1b[C");
}
} else if (seq === "\x1b[H" || seq === "\x1b[1~" || seq === "\x1bOH") {
if (cursor > 0) {
process.stdout.write(`\x1b[${cursor}D`);
cursor = 0;
}
} else if (seq === "\x1b[F" || seq === "\x1b[4~" || seq === "\x1bOF") {
if (cursor < buffer.length) {
process.stdout.write(`\x1b[${buffer.length - cursor}C`);
cursor = buffer.length;
}
} else if (seq === "\x1b[3~") {
if (cursor < buffer.length) {
buffer = buffer.slice(0, cursor) + buffer.slice(cursor + 1);
render();
}
}
}
process.stdin.on("data", (data: Buffer) => {
const key = data.toString();
if (key === CTRL_C) {
process.stdin.setRawMode(savedRaw === true);
process.stdin.pause();
process.stdin.removeAllListeners("data");
process.stdout.write("\n");
resolve(null);
return;
}
if (key === ESC || key.startsWith("\x1b[")) {
escapeBuf = key;
if (key.length >= 3) {
handleSeq(key);
escapeBuf = "";
}
return;
}
if (escapeBuf) {
escapeBuf += key;
if (/^[A-Za-z~]$/.test(key)) {
handleSeq(escapeBuf);
escapeBuf = "";
} else if (escapeBuf.length > 8) {
escapeBuf = "";
}
return;
}
if (key === ENTER) {
process.stdin.setRawMode(savedRaw === true);
process.stdin.pause();
process.stdin.removeAllListeners("data");
process.stdout.write("\n");
const result = buffer.trim();
resolve(result || null);
return;
}
if (key === BACKSPACE) {
if (cursor > 0) {
buffer = buffer.slice(0, cursor - 1) + buffer.slice(cursor);
cursor--;
render();
}
return;
}
if (key === "\x01") {
if (cursor > 0) {
process.stdout.write(`\x1b[${cursor}D`);
cursor = 0;
}
return;
}
if (key === "\x05") {
if (cursor < buffer.length) {
process.stdout.write(`\x1b[${buffer.length - cursor}C`);
cursor = buffer.length;
}
return;
}
if (key === "\x0b") {
buffer = buffer.slice(0, cursor);
render();
return;
}
if (key === "\x15") {
buffer = buffer.slice(cursor);
cursor = 0;
render();
return;
}
if (key.charCodeAt(0) >= 32 && key.charCodeAt(0) < 127) {
buffer = buffer.slice(0, cursor) + key + buffer.slice(cursor);
cursor++;
render();
}
});
});
} }
async function main() { async function main() {