import type { GitforestConfig, DirectoryConfig } from "../types/index.ts"; import { errorToString } from "../utils/errors.ts"; /** Known terminal-based editors that need raw mode handling */ const TERMINAL_EDITORS = ["vim", "nvim", "nano", "vi", "emacs", "hx", "helix"]; /** * Resolve which editor to use for a given project path. * Priority: directory-specific > global config > $EDITOR env > "code" */ export function resolveEditor( projectPath: string, globalEditor?: string, directories?: DirectoryConfig[] ): string { // Check for directory-specific editor if (directories) { const matchedDir = directories.find(d => projectPath.startsWith(d.path.replace(/^~/, process.env.HOME || "")) ); if (matchedDir?.editor) { return matchedDir.editor; } } return globalEditor || process.env.EDITOR || "code"; } /** * Open a file/directory in the configured editor. * Handles terminal vs GUI editors, macOS optimizations. */ export async function openInEditor( projectPath: string, config: GitforestConfig ): Promise<{ success: boolean; error?: string }> { const editor = resolveEditor(projectPath, config.editor, config.directories); // Split command and args once const parts = editor.split(" "); const cmd = parts[0]!; const args = parts.slice(1); // Check if it's a known terminal editor const isTerminal = TERMINAL_EDITORS.includes(cmd); try { if (isTerminal) { // Suspends Ink's raw mode to allow the editor to take over if (process.stdin.setRawMode) { process.stdin.setRawMode(false); } // Spawn with inherit to take over terminal const proc = Bun.spawn([cmd, ...args, projectPath], { stdin: "inherit", stdout: "inherit", stderr: "inherit", }); await proc.exited; // Resume raw mode if (process.stdin.setRawMode) { process.stdin.setRawMode(true); } } else { // GUI Editor // Use 'open' command for macOS GUI editors (VS Code, Cursor) specific optimization if (process.platform === "darwin" && (cmd === "code" || cmd === "cursor")) { const appName = cmd === "code" ? "Visual Studio Code" : "Cursor"; const openArgs = ["open", "-a", appName, projectPath]; openArgs.push("--args", "-n"); // Force new window const subprocess = Bun.spawn(openArgs, { stdin: "ignore", stdout: "ignore", stderr: "ignore", }); subprocess.unref(); } else { // Fallback for other GUI editors // Auto-inject -n for code/cursor if not using 'open' strategy or on other platforms if ((cmd === "code" || cmd === "cursor") && !args.includes("-n") && !args.includes("--new-window")) { args.push("-n"); } const subprocess = Bun.spawn([cmd, ...args, projectPath], { stdin: "ignore", stdout: "ignore", stderr: "ignore", }); subprocess.unref(); } } return { success: true }; } catch (error) { // Ensure raw mode is back if we failed mid-flight if (process.stdin.setRawMode) process.stdin.setRawMode(true); return { success: false, error: errorToString(error) }; } } /** * Open a URL in the default browser. */ export async function openInBrowser( url: string ): Promise<{ success: boolean; error?: string }> { try { Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore", }); return { success: true }; } catch (error) { return { success: false, error: errorToString(error) }; } }