Skip to content

Commit 7eb66ac

Browse files
committed
App: Add "Open in Editor (F4) feature.
1 parent cd6a3d1 commit 7eb66ac

10 files changed

Lines changed: 89 additions & 9 deletions

File tree

apps/desktop/src-tauri/src/commands/ui.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,24 @@ pub fn get_info(_path: String) -> Result<(), String> {
158158
Err("Get Info is only available on macOS".to_string())
159159
}
160160

161+
/// Open file in the system's default text editor (macOS only)
162+
#[tauri::command]
163+
#[cfg(target_os = "macos")]
164+
pub fn open_in_editor(path: String) -> Result<(), String> {
165+
Command::new("open")
166+
.arg("-t")
167+
.arg(&path)
168+
.spawn()
169+
.map_err(|e| e.to_string())?;
170+
Ok(())
171+
}
172+
173+
#[tauri::command]
174+
#[cfg(not(target_os = "macos"))]
175+
pub fn open_in_editor(_path: String) -> Result<(), String> {
176+
Err("Open in editor is only available on macOS".to_string())
177+
}
178+
161179
/// Executes a menu action for the current context.
162180
pub fn execute_menu_action<R: Runtime>(app: &AppHandle<R>, id: &str) {
163181
let state = app.state::<MenuState<R>>();
@@ -171,6 +189,12 @@ pub fn execute_menu_action<R: Runtime>(app: &AppHandle<R>, id: &str) {
171189
crate::menu::OPEN_ID => {
172190
let _ = app.opener().open_path(&context.path, None::<&str>);
173191
}
192+
crate::menu::EDIT_ID => {
193+
#[cfg(target_os = "macos")]
194+
{
195+
let _ = open_in_editor(context.path);
196+
}
197+
}
174198
crate::menu::SHOW_IN_FINDER_ID => {
175199
#[cfg(target_os = "macos")]
176200
{

apps/desktop/src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ pub fn run() {
269269
commands::ui::copy_to_clipboard,
270270
commands::ui::quick_look,
271271
commands::ui::get_info,
272+
commands::ui::open_in_editor,
272273
mcp::pane_state::update_left_pane_state,
273274
mcp::pane_state::update_right_pane_state,
274275
mcp::pane_state::update_focused_pane,

apps/desktop/src-tauri/src/mcp/executor.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use tauri::{AppHandle, Emitter, Manager, Runtime};
99
use super::pane_state::PaneStateStore;
1010
use super::protocol::{INTERNAL_ERROR, INVALID_PARAMS};
1111
use crate::commands::ui::{
12-
copy_to_clipboard, get_info, quick_look, set_view_mode, show_in_finder, toggle_hidden_files,
12+
copy_to_clipboard, get_info, open_in_editor, quick_look, set_view_mode, show_in_finder, toggle_hidden_files,
1313
};
1414

1515
/// Result of tool execution.
@@ -187,6 +187,10 @@ fn execute_file_command<R: Runtime>(app: &AppHandle<R>, name: &str) -> ToolResul
187187
.ok_or_else(|| ToolError::internal("No file under cursor"))?;
188188

189189
match name {
190+
"file_openInEditor" => {
191+
open_in_editor(file_under_cursor.path.clone()).map_err(ToolError::internal)?;
192+
Ok(json!({"success": true, "path": file_under_cursor.path}))
193+
}
190194
"file_showInFinder" => {
191195
show_in_finder(file_under_cursor.path.clone()).map_err(ToolError::internal)?;
192196
Ok(json!({"success": true, "path": file_under_cursor.path}))

apps/desktop/src-tauri/src/mcp/tests.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,12 @@ fn test_tool_input_schemas_are_valid() {
105105
#[test]
106106
fn test_total_tool_count() {
107107
let tools = get_all_tools();
108-
// 3 app + 3 view + 1 pane + 12 nav + 8 sort + 5 file + 2 volume + 5 selection = 39
108+
// 3 app + 3 view + 1 pane + 12 nav + 8 sort + 6 file + 2 volume + 5 selection = 40
109109
// (context tools and volume_list moved to resources)
110110
assert_eq!(
111111
tools.len(),
112-
39,
113-
"Expected 39 tools, got {}. Did you add/remove tools?",
112+
40,
113+
"Expected 40 tools, got {}. Did you add/remove tools?",
114114
tools.len()
115115
);
116116
}

apps/desktop/src-tauri/src/mcp/tools.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ fn get_sort_tools() -> Vec<Tool> {
115115
/// Get file action tools.
116116
fn get_file_tools() -> Vec<Tool> {
117117
vec![
118+
Tool::no_params(
119+
"file_openInEditor",
120+
"Open file under the cursor in the default text editor",
121+
),
118122
Tool::no_params("file_showInFinder", "Show file under the cursor in Finder"),
119123
Tool::no_params("file_copyPath", "Copy path of the file under the cursor to clipboard"),
120124
Tool::no_params(
@@ -205,9 +209,9 @@ mod tests {
205209
#[test]
206210
fn test_all_tools_count() {
207211
let tools = get_all_tools();
208-
// 3 app + 3 view + 1 pane + 12 nav + 8 sort + 5 file + 2 volume + 5 selection = 39
212+
// 3 app + 3 view + 1 pane + 12 nav + 8 sort + 6 file + 2 volume + 5 selection = 40
209213
// (context tools and volume_list moved to resources)
210-
assert_eq!(tools.len(), 39);
214+
assert_eq!(tools.len(), 40);
211215
}
212216

213217
#[test]

apps/desktop/src-tauri/src/menu.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub const SHOW_HIDDEN_FILES_ID: &str = "show_hidden_files";
1111
pub const VIEW_MODE_FULL_ID: &str = "view_mode_full";
1212
pub const VIEW_MODE_BRIEF_ID: &str = "view_mode_brief";
1313
pub const OPEN_ID: &str = "open";
14+
pub const EDIT_ID: &str = "edit";
1415
pub const SHOW_IN_FINDER_ID: &str = "show_in_finder";
1516
pub const COPY_PATH_ID: &str = "copy_path";
1617
pub const COPY_FILENAME_ID: &str = "copy_filename";
@@ -119,6 +120,7 @@ pub fn build_menu<R: Runtime>(
119120

120121
// Add File menu items
121122
let open_item = tauri::menu::MenuItem::with_id(app, OPEN_ID, "Open", true, None::<&str>)?;
123+
let edit_item = tauri::menu::MenuItem::with_id(app, EDIT_ID, "Edit", true, Some("F4"))?;
122124
let show_in_finder_item =
123125
tauri::menu::MenuItem::with_id(app, SHOW_IN_FINDER_ID, "Show in Finder", true, Some("Opt+Cmd+O"))?;
124126
let copy_path_item =
@@ -139,6 +141,7 @@ pub fn build_menu<R: Runtime>(
139141
submenu.prepend(&copy_filename_item)?;
140142
submenu.prepend(&copy_path_item)?;
141143
submenu.prepend(&show_in_finder_item)?;
144+
submenu.prepend(&edit_item)?;
142145
submenu.prepend(&open_item)?;
143146
break;
144147
}
@@ -302,6 +305,7 @@ pub fn build_context_menu<R: Runtime>(
302305
let menu = Menu::new(app)?;
303306

304307
let open_item = tauri::menu::MenuItem::with_id(app, OPEN_ID, "Open", true, None::<&str>)?;
308+
let edit_item = tauri::menu::MenuItem::with_id(app, EDIT_ID, "Edit", true, Some("F4"))?;
305309
let show_in_finder_item =
306310
tauri::menu::MenuItem::with_id(app, SHOW_IN_FINDER_ID, "Show in Finder", true, Some("Opt+Cmd+O"))?;
307311
let copy_path_item =
@@ -319,6 +323,7 @@ pub fn build_context_menu<R: Runtime>(
319323
// Add items to menu
320324
if !is_directory {
321325
menu.append(&open_item)?;
326+
menu.append(&edit_item)?;
322327
}
323328
menu.append(&show_in_finder_item)?;
324329
menu.append(&tauri::menu::PredefinedMenuItem::separator(app)?)?;

apps/desktop/src/lib/commands/command-registry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ export const commands: Command[] = [
184184
// ============================================================================
185185
// File list - File action commands
186186
// ============================================================================
187+
{
188+
id: 'file.edit',
189+
name: 'Edit in default editor',
190+
scope: 'Main window/File list',
191+
showInPalette: true,
192+
shortcuts: ['F4'],
193+
},
187194
{
188195
id: 'file.showInFinder',
189196
name: 'Show in Finder',

apps/desktop/src/lib/file-explorer/FilePane.svelte

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
listVolumes,
3434
mountNetworkShare,
3535
openFile,
36+
openInEditor,
3637
showFileContextMenu,
3738
type UnlistenFn,
3839
updateMenuContext,
@@ -964,6 +965,13 @@
964965
}
965966
}
966967
968+
/** Gets the file entry under the cursor from the current list view */
969+
function getEntryUnderCursor(): FileEntry | undefined {
970+
const listRef = viewMode === 'brief' ? briefListRef : fullListRef
971+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return
972+
return listRef?.getEntryAt(cursorIndex)
973+
}
974+
967975
// Exported so DualPaneExplorer can forward keyboard events
968976
export function handleKeyDown(e: KeyboardEvent) {
969977
if (isNetworkView) {
@@ -973,16 +981,24 @@
973981
974982
// Handle Enter key - navigate into the entry under the cursor
975983
if (e.key === 'Enter') {
976-
const listRef = viewMode === 'brief' ? briefListRef : fullListRef
977-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
978-
const entry: FileEntry | undefined = listRef?.getEntryAt(cursorIndex)
984+
const entry = getEntryUnderCursor()
979985
if (entry) {
980986
e.preventDefault()
981987
void handleNavigate(entry)
982988
return
983989
}
984990
}
985991
992+
// Handle F4 key - open file in default text editor
993+
if (e.key === 'F4') {
994+
const entry = getEntryUnderCursor()
995+
if (entry && !entry.isDirectory) {
996+
e.preventDefault()
997+
void openInEditor(entry.path)
998+
return
999+
}
1000+
}
1001+
9861002
// Handle Backspace or ⌘↑ - go to parent directory
9871003
if ((e.key === 'Backspace' || (e.key === 'ArrowUp' && e.metaKey)) && hasParent) {
9881004
e.preventDefault()

apps/desktop/src/lib/tauri-commands.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,15 @@ export async function getInfo(path: string): Promise<void> {
403403
await invoke('get_info', { path })
404404
}
405405

406+
/**
407+
* Open file in the system's default text editor (macOS only).
408+
* Uses `open -t` which opens the file in the default text editor.
409+
* @param path - Absolute path to the file.
410+
*/
411+
export async function openInEditor(path: string): Promise<void> {
412+
await invoke('open_in_editor', { path })
413+
}
414+
406415
/**
407416
* Shows the main window.
408417
* Should be called when the frontend is ready to avoid white flash.

apps/desktop/src/routes/+page.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
copyToClipboard,
1717
quickLook,
1818
getInfo,
19+
openInEditor,
1920
toggleHiddenFiles,
2021
setViewMode,
2122
getWindowTitle,
@@ -452,6 +453,15 @@
452453
return
453454
454455
// === File action commands ===
456+
case 'file.edit': {
457+
const entryUnderCursor = explorerRef?.getFileAndPathUnderCursor()
458+
if (entryUnderCursor) {
459+
await openInEditor(entryUnderCursor.path)
460+
}
461+
explorerRef?.refocus()
462+
return
463+
}
464+
455465
case 'file.showInFinder': {
456466
const entryUnderCursor = explorerRef?.getFileAndPathUnderCursor()
457467
if (entryUnderCursor) {

0 commit comments

Comments
 (0)