Skip to content

Commit 7b0ea13

Browse files
committed
Add Command palette
- Command collection! - Fuzzy search - Modal - Last query state saving - Add menu items - Docs - Tests - Also added a few commands+shortcuts to the menu - Also fixed bug: file explorer keys still worked while modals were open! :o
1 parent bb67ae8 commit 7b0ea13

19 files changed

Lines changed: 1813 additions & 16 deletions

apps/desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"license": "SEE LICENSE IN LICENSE",
2525
"dependencies": {
2626
"@crabnebula/tauri-plugin-drag": "^2.1.0",
27+
"@leeoniya/ufuzzy": "^1.0.19",
2728
"@lottiefiles/dotlottie-svelte": "^0.8.12",
2829
"@tauri-apps/api": "^2",
2930
"@tauri-apps/plugin-fs": "^2.4.4",

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

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,127 @@ pub fn show_main_window<R: Runtime>(window: Window<R>) -> Result<(), String> {
3737
window.show().map_err(|e| e.to_string())
3838
}
3939

40+
/// Toggle hidden files visibility - updates menu checkbox and emits event.
41+
/// This is used by the command palette to sync with menu state.
42+
#[tauri::command]
43+
pub fn toggle_hidden_files<R: Runtime>(app: AppHandle<R>) -> Result<bool, String> {
44+
let menu_state = app.state::<MenuState<R>>();
45+
let guard = menu_state.show_hidden_files.lock().unwrap();
46+
let Some(check_item) = guard.as_ref() else {
47+
return Err("Menu not initialized".to_string());
48+
};
49+
50+
// Get current state and toggle it
51+
let current = check_item.is_checked().unwrap_or(false);
52+
let new_state = !current;
53+
check_item.set_checked(new_state).map_err(|e| e.to_string())?;
54+
55+
// Emit event to frontend with the new state
56+
app.emit("settings-changed", serde_json::json!({ "showHiddenFiles": new_state }))
57+
.map_err(|e| e.to_string())?;
58+
59+
Ok(new_state)
60+
}
61+
62+
/// Set view mode - updates menu radio buttons and emits event.
63+
/// This is used by the command palette to sync with menu state.
64+
#[tauri::command]
65+
pub fn set_view_mode<R: Runtime>(app: AppHandle<R>, mode: String) -> Result<(), String> {
66+
let menu_state = app.state::<MenuState<R>>();
67+
let full_guard = menu_state.view_mode_full.lock().unwrap();
68+
let brief_guard = menu_state.view_mode_brief.lock().unwrap();
69+
70+
let (Some(full_item), Some(brief_item)) = (full_guard.as_ref(), brief_guard.as_ref()) else {
71+
return Err("Menu not initialized".to_string());
72+
};
73+
74+
// Set the correct check state (radio behavior)
75+
let is_full = mode == "full";
76+
full_item.set_checked(is_full).map_err(|e| e.to_string())?;
77+
brief_item.set_checked(!is_full).map_err(|e| e.to_string())?;
78+
79+
// Emit event to frontend
80+
app.emit("view-mode-changed", serde_json::json!({ "mode": mode }))
81+
.map_err(|e| e.to_string())?;
82+
83+
Ok(())
84+
}
85+
86+
// ============================================================================
87+
// Direct file action commands (for command palette and other invocations)
88+
// ============================================================================
89+
90+
/// Show a file in Finder (reveal in parent folder)
91+
#[tauri::command]
92+
#[cfg(target_os = "macos")]
93+
pub fn show_in_finder(path: String) -> Result<(), String> {
94+
Command::new("open")
95+
.arg("-R")
96+
.arg(&path)
97+
.spawn()
98+
.map_err(|e| e.to_string())?;
99+
Ok(())
100+
}
101+
102+
#[tauri::command]
103+
#[cfg(not(target_os = "macos"))]
104+
pub fn show_in_finder(_path: String) -> Result<(), String> {
105+
Err("Show in Finder is only available on macOS".to_string())
106+
}
107+
108+
/// Copy text to clipboard
109+
#[tauri::command]
110+
pub fn copy_to_clipboard<R: Runtime>(app: AppHandle<R>, text: String) -> Result<(), String> {
111+
app.clipboard().write_text(text).map_err(|e| e.to_string())
112+
}
113+
114+
/// Quick Look preview (macOS only)
115+
#[tauri::command]
116+
#[cfg(target_os = "macos")]
117+
pub fn quick_look(path: String) -> Result<(), String> {
118+
Command::new("qlmanage")
119+
.arg("-p")
120+
.arg(&path)
121+
.spawn()
122+
.map_err(|e| e.to_string())?;
123+
Ok(())
124+
}
125+
126+
#[tauri::command]
127+
#[cfg(not(target_os = "macos"))]
128+
pub fn quick_look(_path: String) -> Result<(), String> {
129+
Err("Quick Look is only available on macOS".to_string())
130+
}
131+
132+
/// Open Get Info window in Finder (macOS only)
133+
#[tauri::command]
134+
#[cfg(target_os = "macos")]
135+
pub fn get_info(path: String) -> Result<(), String> {
136+
// Use AppleScript to open the Get Info window
137+
// The path needs to be escaped for AppleScript
138+
let escaped_path = path.replace("\\", "\\\\").replace("\"", "\\\"");
139+
let script = format!(
140+
r#"tell application "Finder"
141+
activate
142+
open information window of (POSIX file "{}" as alias)
143+
end tell"#,
144+
escaped_path
145+
);
146+
147+
Command::new("osascript")
148+
.arg("-e")
149+
.arg(&script)
150+
.spawn()
151+
.map_err(|e| e.to_string())?;
152+
Ok(())
153+
}
154+
155+
#[tauri::command]
156+
#[cfg(not(target_os = "macos"))]
157+
pub fn get_info(_path: String) -> Result<(), String> {
158+
Err("Get Info is only available on macOS".to_string())
159+
}
160+
40161
/// Executes a menu action for the current context.
41162
pub fn execute_menu_action<R: Runtime>(app: &AppHandle<R>, id: &str) {
42163
let state = app.state::<MenuState<R>>();
@@ -53,7 +174,7 @@ pub fn execute_menu_action<R: Runtime>(app: &AppHandle<R>, id: &str) {
53174
crate::menu::SHOW_IN_FINDER_ID => {
54175
#[cfg(target_os = "macos")]
55176
{
56-
let _ = Command::new("open").arg("-R").arg(&context.path).spawn();
177+
let _ = show_in_finder(context.path);
57178
}
58179
}
59180
crate::menu::COPY_PATH_ID => {
@@ -65,14 +186,14 @@ pub fn execute_menu_action<R: Runtime>(app: &AppHandle<R>, id: &str) {
65186
crate::menu::QUICK_LOOK_ID => {
66187
#[cfg(target_os = "macos")]
67188
{
68-
let _ = Command::new("qlmanage").arg("-p").arg(&context.path).spawn();
189+
let _ = quick_look(context.path);
69190
}
70191
}
71192
crate::menu::GET_INFO_ID => {
72-
let _ = app.emit(
73-
"menu-action",
74-
serde_json::json!({ "action": "get-info", "path": context.path }),
75-
);
193+
#[cfg(target_os = "macos")]
194+
{
195+
let _ = get_info(context.path);
196+
}
76197
}
77198
_ => {}
78199
}

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ mod settings;
5050
mod volumes;
5151

5252
use menu::{
53-
ABOUT_ID, GO_BACK_ID, GO_FORWARD_ID, GO_PARENT_ID, MenuState, SHOW_HIDDEN_FILES_ID, VIEW_MODE_BRIEF_ID,
54-
VIEW_MODE_FULL_ID, ViewMode,
53+
ABOUT_ID, COMMAND_PALETTE_ID, GO_BACK_ID, GO_FORWARD_ID, GO_PARENT_ID, MenuState, SHOW_HIDDEN_FILES_ID,
54+
SWITCH_PANE_ID, VIEW_MODE_BRIEF_ID, VIEW_MODE_FULL_ID, ViewMode,
5555
};
5656
use tauri::{Emitter, Manager};
5757

@@ -185,6 +185,12 @@ pub fn run() {
185185
} else if id == ABOUT_ID {
186186
// Emit event to show our custom About window
187187
let _ = app.emit("show-about", ());
188+
} else if id == COMMAND_PALETTE_ID {
189+
// Emit event to show the command palette
190+
let _ = app.emit("show-command-palette", ());
191+
} else if id == SWITCH_PANE_ID {
192+
// Emit event to switch pane
193+
let _ = app.emit("switch-pane", ());
188194
} else {
189195
// Handle file actions
190196
commands::ui::execute_menu_action(app, id);
@@ -209,6 +215,12 @@ pub fn run() {
209215
commands::ui::show_file_context_menu,
210216
commands::ui::show_main_window,
211217
commands::ui::update_menu_context,
218+
commands::ui::toggle_hidden_files,
219+
commands::ui::set_view_mode,
220+
commands::ui::show_in_finder,
221+
commands::ui::copy_to_clipboard,
222+
commands::ui::quick_look,
223+
commands::ui::get_info,
212224
#[cfg(target_os = "macos")]
213225
commands::sync_status::get_sync_status,
214226
#[cfg(target_os = "macos")]

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ pub const COPY_FILENAME_ID: &str = "copy_filename";
1717
pub const GET_INFO_ID: &str = "get_info";
1818
pub const QUICK_LOOK_ID: &str = "quick_look";
1919

20+
/// Menu item ID for command palette.
21+
pub const COMMAND_PALETTE_ID: &str = "command_palette";
22+
23+
/// Menu item ID for Switch Pane.
24+
pub const SWITCH_PANE_ID: &str = "switch_pane";
25+
2026
/// Menu item IDs for navigation (Go menu).
2127
pub const GO_BACK_ID: &str = "go_back";
2228
pub const GO_FORWARD_ID: &str = "go_forward";
@@ -111,7 +117,7 @@ pub fn build_menu<R: Runtime>(
111117
let copy_filename_item =
112118
tauri::menu::MenuItem::with_id(app, COPY_FILENAME_ID, "Copy filename", true, None::<&str>)?;
113119
let get_info_item = tauri::menu::MenuItem::with_id(app, GET_INFO_ID, "Get info", true, Some("Cmd+I"))?;
114-
let quick_look_item = tauri::menu::MenuItem::with_id(app, QUICK_LOOK_ID, "Quick look", true, None::<&str>)?;
120+
let quick_look_item = tauri::menu::MenuItem::with_id(app, QUICK_LOOK_ID, "Quick look", true, Some("Space"))?;
115121

116122
// Find the existing File submenu and add our items to it
117123
for item in menu.items()? {
@@ -171,6 +177,19 @@ pub fn build_menu<R: Runtime>(
171177
submenu.append(&view_mode_brief_item)?;
172178
submenu.append(&tauri::menu::PredefinedMenuItem::separator(app)?)?;
173179
submenu.append(&show_hidden_item)?;
180+
// Add command palette and switch pane after separator
181+
submenu.append(&tauri::menu::PredefinedMenuItem::separator(app)?)?;
182+
let command_palette_item = tauri::menu::MenuItem::with_id(
183+
app,
184+
COMMAND_PALETTE_ID,
185+
"Command palette...",
186+
true,
187+
Some("Cmd+Shift+P"),
188+
)?;
189+
submenu.append(&command_palette_item)?;
190+
let switch_pane_item =
191+
tauri::menu::MenuItem::with_id(app, SWITCH_PANE_ID, "Switch pane", true, Some("Tab"))?;
192+
submenu.append(&switch_pane_item)?;
174193
found_view = true;
175194
break;
176195
}

apps/desktop/src/lib/app-status-store.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,34 @@ export async function saveLastUsedPathForVolume(volumeId: string, path: string):
248248
// Silently fail - persistence is nice-to-have
249249
}
250250
}
251+
252+
// ============================================================================
253+
// Command palette query persistence
254+
// ============================================================================
255+
256+
/**
257+
* Loads the last used command palette query.
258+
* Returns empty string if not previously saved.
259+
*/
260+
export async function loadPaletteQuery(): Promise<string> {
261+
try {
262+
const store = await getStore()
263+
const query = await store.get('paletteQuery')
264+
return typeof query === 'string' ? query : ''
265+
} catch {
266+
return ''
267+
}
268+
}
269+
270+
/**
271+
* Saves the current command palette query for next time.
272+
*/
273+
export async function savePaletteQuery(query: string): Promise<void> {
274+
try {
275+
const store = await getStore()
276+
await store.set('paletteQuery', query)
277+
await store.save()
278+
} catch {
279+
// Silently fail
280+
}
281+
}

0 commit comments

Comments
 (0)