refactor(cli): replace external editor with inline terminal editing
This commit is contained in:
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user