Skip to content

Commit fdb5896

Browse files
committed
feat: add Ctrl+E shortcut to open files in $EDITOR from selector
Adds keyboard shortcut in the interactive file picker that opens the selected file in the user's configured editor. Shows helpful message if $EDITOR is not set. Returns to selector after editing with refreshed preview.
1 parent cf475d3 commit fdb5896

File tree

1 file changed

+88
-13
lines changed

1 file changed

+88
-13
lines changed

src/file-selector.ts

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ import {
1616
} from "@inquirer/core";
1717
import { readFileSync, existsSync } from "node:fs";
1818
import { homedir } from "os";
19+
import { spawnSync } from "node:child_process";
1920
import type { AgentFile } from "./cli";
2021

22+
/** Result from file selector - either a path to run or edit */
23+
export interface FileSelectorResult {
24+
action: "run" | "edit";
25+
path: string;
26+
}
27+
2128
// Extended key event type (runtime has more properties than type declares)
2229
interface ExtendedKeyEvent extends KeypressEvent {
2330
sequence?: string;
@@ -202,7 +209,7 @@ export interface FileSelectorConfig {
202209
/**
203210
* Interactive file selector with preview pane
204211
*/
205-
export const fileSelector = createPrompt<string, FileSelectorConfig>(
212+
export const fileSelector = createPrompt<FileSelectorResult, FileSelectorConfig>(
206213
(config, done) => {
207214
const { files, pageSize = 15 } = config;
208215
const prefix = usePrefix({ status: "idle", theme: makeTheme({}) });
@@ -226,7 +233,15 @@ export const fileSelector = createPrompt<string, FileSelectorConfig>(
226233
const extKey = key as ExtendedKeyEvent;
227234
if (isEnterKey(key)) {
228235
if (currentFile) {
229-
done(currentFile.path);
236+
done({ action: "run", path: currentFile.path });
237+
}
238+
return;
239+
}
240+
241+
// Ctrl+E to edit the file in $EDITOR
242+
if (key.ctrl && key.name === "e") {
243+
if (currentFile) {
244+
done({ action: "edit", path: currentFile.path });
230245
}
231246
return;
232247
}
@@ -385,13 +400,60 @@ export const fileSelector = createPrompt<string, FileSelectorConfig>(
385400
// Help line
386401
outputLines.push("");
387402
outputLines.push(
388-
`\x1b[90m↑↓ navigate PgUp/PgDn scroll preview Enter select Esc clear filter\x1b[0m`
403+
`\x1b[90m↑↓ navigate PgUp/PgDn scroll preview Enter run Ctrl+E edit Esc clear filter\x1b[0m`
389404
);
390405

391406
return outputLines.join("\n");
392407
}
393408
);
394409

410+
/**
411+
* Open a file in the user's $EDITOR
412+
* Returns true if successful, false if editor not configured or failed
413+
*/
414+
function openInEditor(filePath: string): boolean {
415+
const editor = process.env.EDITOR || process.env.VISUAL;
416+
417+
if (!editor) {
418+
console.error(
419+
"\x1b[33mNo $EDITOR environment variable set.\x1b[0m\n" +
420+
"Set it in your shell config (e.g., ~/.bashrc or ~/.zshrc):\n" +
421+
" export EDITOR=vim\n" +
422+
" export EDITOR=nano\n" +
423+
" export EDITOR=\"code --wait\"\n"
424+
);
425+
return false;
426+
}
427+
428+
try {
429+
// Parse editor command (may include flags like "code --wait")
430+
const parts = editor.split(/\s+/);
431+
const cmd = parts[0]!;
432+
const args = [...parts.slice(1), filePath];
433+
434+
const result = spawnSync(cmd, args, {
435+
stdio: "inherit",
436+
shell: false,
437+
});
438+
439+
if (result.error) {
440+
console.error(
441+
`\x1b[31mFailed to open editor "${editor}":\x1b[0m ${result.error.message}\n` +
442+
"Check that your $EDITOR is installed and in your PATH."
443+
);
444+
return false;
445+
}
446+
447+
return result.status === 0;
448+
} catch (error) {
449+
console.error(
450+
`\x1b[31mFailed to open editor "${editor}":\x1b[0m ${error}\n` +
451+
"Check that your $EDITOR is installed and in your PATH."
452+
);
453+
return false;
454+
}
455+
}
456+
395457
/**
396458
* Show interactive file picker with preview and return selected file path
397459
*/
@@ -402,15 +464,28 @@ export async function showFileSelectorWithPreview(
402464
return undefined;
403465
}
404466

405-
try {
406-
const selected = await fileSelector({
407-
message: "Select an agent to run:",
408-
files,
409-
pageSize: 15,
410-
});
411-
return selected;
412-
} catch {
413-
// User cancelled (Ctrl+C) or other error
414-
return undefined;
467+
// Loop to allow editing and returning to selector
468+
while (true) {
469+
try {
470+
const result = await fileSelector({
471+
message: "Select an agent to run:",
472+
files,
473+
pageSize: 15,
474+
});
475+
476+
if (result.action === "edit") {
477+
// Open in editor, then return to selector
478+
openInEditor(result.path);
479+
// Clear file content cache so preview reflects edits
480+
fileContentCache.clear();
481+
continue;
482+
}
483+
484+
// action === "run"
485+
return result.path;
486+
} catch {
487+
// User cancelled (Ctrl+C) or other error
488+
return undefined;
489+
}
415490
}
416491
}

0 commit comments

Comments
 (0)