Skip to content

Commit 8522e71

Browse files
committed
Refactor: Extract macOS / Linux-specific menu code
1 parent e16bd91 commit 8522e71

4 files changed

Lines changed: 861 additions & 820 deletions

File tree

apps/desktop/src-tauri/src/menu/CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ Native menu bar for macOS and Linux. Builds platform-specific menus from scratch
44
events, syncs accelerator labels with user-customized shortcuts, and enables/disables items based on
55
window focus context.
66

7+
## File layout
8+
9+
- `mod.rs` — shared types (`MenuState`, `MenuItems`, `MenuItemEntry`, `CommandScope`, `ViewMode`),
10+
constants (all menu item IDs), ID mapping functions (`menu_id_to_command`,
11+
`command_id_to_menu_id`), platform-aware accelerator/label helpers, the `build_menu` dispatcher,
12+
context menu builders, viewer menu builder, and accelerator update functions.
13+
- `macos.rs``build_menu_macos` (full macOS menu bar), `cleanup_macos_menus` (removes
14+
system-injected Edit items, registers Help menu), `set_macos_menu_icons` (SF Symbol icons via
15+
objc2 FFI), and their helpers.
16+
- `linux.rs``build_menu_linux` (full Linux/GTK menu bar with mnemonics, no F-key accelerators).
17+
718
## Key concepts
819

920
### Unified dispatch
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
use std::collections::HashMap;
2+
3+
use tauri::{
4+
AppHandle, Runtime,
5+
menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu},
6+
};
7+
8+
use super::{
9+
ABOUT_ID, CLOSE_OTHER_TABS_ID, CLOSE_TAB_ID, COMMAND_PALETTE_ID, COPY_FILENAME_ID, COPY_PATH_ID, DESELECT_ALL_ID,
10+
EDIT_COPY_ID, EDIT_CUT_ID, EDIT_ID, EDIT_PASTE_ID, EDIT_PASTE_MOVE_ID, ENTER_LICENSE_KEY_ID, FILE_COPY_ID,
11+
FILE_DELETE_ID, FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID, FILE_NEW_FOLDER_ID, FILE_VIEW_ID, GET_INFO_ID,
12+
GO_BACK_ID, GO_FORWARD_ID, GO_PARENT_ID, MenuItems, NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID, PIN_TAB_MENU_ID, PREV_TAB_ID,
13+
QUICK_LOOK_ID, RENAME_ID, SELECT_ALL_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SWAP_PANES_ID,
14+
SWITCH_PANE_ID, VIEW_MODE_BRIEF_ID, VIEW_MODE_FULL_ID, ViewMode, build_sort_submenu, copy_path_accelerator,
15+
register_item, show_in_file_manager_accelerator, show_in_file_manager_label,
16+
};
17+
18+
/// Linux menu: builds all menus from scratch, matching the macOS menu structure.
19+
/// Differences from macOS:
20+
/// - No cmdr app menu (Settings and license go under Edit, About under Help)
21+
/// - "Show in file manager" instead of "Show in Finder"
22+
/// - Function-key accelerators (F2-F8, Shift+F8) omitted — GTK intercepts them
23+
/// before the webview, and is_focused() fails on Linux, so JS dispatch handles these
24+
/// - Tab and Space accelerators omitted (GTK accessibility conflicts)
25+
/// - Placeholder `&` mnemonics (first letter) — final mnemonic pass is Milestone 7
26+
pub(crate) fn build_menu_linux<R: Runtime>(
27+
app: &AppHandle<R>,
28+
show_hidden_files: bool,
29+
view_mode: ViewMode,
30+
has_existing_license: bool,
31+
) -> tauri::Result<MenuItems<R>> {
32+
let menu = Menu::new(app)?;
33+
34+
// --- File menu ---
35+
let open_item = MenuItem::with_id(app, OPEN_ID, "&Open", true, None::<&str>)?;
36+
let file_view_item = MenuItem::with_id(app, FILE_VIEW_ID, "&View", true, None::<&str>)?;
37+
let edit_item = MenuItem::with_id(app, EDIT_ID, "Edit in &editor", true, None::<&str>)?;
38+
let file_copy_item = MenuItem::with_id(app, FILE_COPY_ID, "&Copy...", true, None::<&str>)?;
39+
let file_move_item = MenuItem::with_id(app, FILE_MOVE_ID, "&Move...", true, None::<&str>)?;
40+
let file_new_folder_item = MenuItem::with_id(app, FILE_NEW_FOLDER_ID, "&New folder", true, None::<&str>)?;
41+
let file_delete_item = MenuItem::with_id(app, FILE_DELETE_ID, "&Delete", true, None::<&str>)?;
42+
let file_delete_permanently_item = MenuItem::with_id(
43+
app,
44+
FILE_DELETE_PERMANENTLY_ID,
45+
"Delete &permanently",
46+
true,
47+
None::<&str>,
48+
)?;
49+
let rename_item = MenuItem::with_id(app, RENAME_ID, "Re&name", true, None::<&str>)?;
50+
let show_in_fm_item = MenuItem::with_id(
51+
app,
52+
SHOW_IN_FINDER_ID,
53+
show_in_file_manager_label(),
54+
true,
55+
Some(show_in_file_manager_accelerator()),
56+
)?;
57+
let get_info_item = MenuItem::with_id(app, GET_INFO_ID, "Get &info", true, Some("Cmd+I"))?;
58+
let quick_look_item = MenuItem::with_id(app, QUICK_LOOK_ID, "&Quick look", true, None::<&str>)?;
59+
60+
let file_menu = Submenu::with_items(
61+
app,
62+
"&File",
63+
true,
64+
&[
65+
&open_item,
66+
&file_view_item,
67+
&edit_item,
68+
&PredefinedMenuItem::separator(app)?,
69+
&file_copy_item,
70+
&file_move_item,
71+
&file_new_folder_item,
72+
&file_delete_item,
73+
&file_delete_permanently_item,
74+
&PredefinedMenuItem::separator(app)?,
75+
&rename_item,
76+
&PredefinedMenuItem::separator(app)?,
77+
&show_in_fm_item,
78+
&get_info_item,
79+
&quick_look_item,
80+
],
81+
)?;
82+
menu.append(&file_menu)?;
83+
84+
// --- Edit menu ---
85+
let edit_cut_item = MenuItem::with_id(app, EDIT_CUT_ID, "Cu&t", true, Some("Ctrl+X"))?;
86+
let edit_copy_item = MenuItem::with_id(app, EDIT_COPY_ID, "&Copy", true, Some("Ctrl+C"))?;
87+
let edit_paste_item = MenuItem::with_id(app, EDIT_PASTE_ID, "&Paste", true, Some("Ctrl+V"))?;
88+
let edit_paste_move_item = MenuItem::with_id(app, EDIT_PASTE_MOVE_ID, "&Move here", true, Some("Ctrl+Alt+V"))?;
89+
let select_all_item = MenuItem::with_id(app, SELECT_ALL_ID, "Select &all", true, Some("Cmd+A"))?;
90+
let deselect_all_item = MenuItem::with_id(app, DESELECT_ALL_ID, "D&eselect all", true, Some("Cmd+Shift+A"))?;
91+
let copy_path_item = MenuItem::with_id(app, COPY_PATH_ID, "Cop&y path", true, Some(copy_path_accelerator()))?;
92+
let copy_filename_item = MenuItem::with_id(app, COPY_FILENAME_ID, "Copy file&name", true, None::<&str>)?;
93+
let settings_item = MenuItem::with_id(app, SETTINGS_ID, "&Settings...", true, Some("Cmd+,"))?;
94+
let license_label = if has_existing_license {
95+
"See &license details..."
96+
} else {
97+
"Enter &license key..."
98+
};
99+
let license_item = MenuItem::with_id(app, ENTER_LICENSE_KEY_ID, license_label, true, None::<&str>)?;
100+
101+
let edit_menu = Submenu::with_items(
102+
app,
103+
"&Edit",
104+
true,
105+
&[
106+
&edit_cut_item,
107+
&edit_copy_item,
108+
&edit_paste_item,
109+
&edit_paste_move_item,
110+
&PredefinedMenuItem::separator(app)?,
111+
&select_all_item,
112+
&deselect_all_item,
113+
&PredefinedMenuItem::separator(app)?,
114+
&copy_path_item,
115+
&copy_filename_item,
116+
&PredefinedMenuItem::separator(app)?,
117+
&settings_item,
118+
&license_item,
119+
],
120+
)?;
121+
menu.append(&edit_menu)?;
122+
123+
// --- View menu ---
124+
let view_mode_full_item = CheckMenuItem::with_id(
125+
app,
126+
VIEW_MODE_FULL_ID,
127+
"&Full view",
128+
true,
129+
view_mode == ViewMode::Full,
130+
Some("Cmd+1"),
131+
)?;
132+
let view_mode_brief_item = CheckMenuItem::with_id(
133+
app,
134+
VIEW_MODE_BRIEF_ID,
135+
"&Brief view",
136+
true,
137+
view_mode == ViewMode::Brief,
138+
Some("Cmd+2"),
139+
)?;
140+
let show_hidden_item = CheckMenuItem::with_id(
141+
app,
142+
SHOW_HIDDEN_FILES_ID,
143+
"Show &hidden files",
144+
true,
145+
show_hidden_files,
146+
Some("Cmd+Shift+."),
147+
)?;
148+
let sort_submenu = build_sort_submenu(app, "&Sort by")?;
149+
let switch_pane_item = MenuItem::with_id(app, SWITCH_PANE_ID, "S&witch pane", true, None::<&str>)?;
150+
let swap_panes_item = MenuItem::with_id(app, SWAP_PANES_ID, "Swa&p panes", true, Some("Cmd+U"))?;
151+
let command_palette_item = MenuItem::with_id(
152+
app,
153+
COMMAND_PALETTE_ID,
154+
"&Command palette...",
155+
true,
156+
Some("Cmd+Shift+P"),
157+
)?;
158+
159+
let view_submenu = Submenu::with_items(
160+
app,
161+
"&View",
162+
true,
163+
&[
164+
&view_mode_full_item,
165+
&view_mode_brief_item,
166+
&PredefinedMenuItem::separator(app)?,
167+
&show_hidden_item,
168+
&sort_submenu,
169+
&PredefinedMenuItem::separator(app)?,
170+
&switch_pane_item,
171+
&swap_panes_item,
172+
&PredefinedMenuItem::separator(app)?,
173+
&command_palette_item,
174+
],
175+
)?;
176+
menu.append(&view_submenu)?;
177+
178+
// View mode items are at positions 0 and 1 in our freshly built View submenu
179+
let view_full_pos: usize = 0;
180+
let view_brief_pos: usize = 1;
181+
182+
// --- Go menu ---
183+
let go_back_item = MenuItem::with_id(app, GO_BACK_ID, "&Back", true, Some("Cmd+["))?;
184+
let go_forward_item = MenuItem::with_id(app, GO_FORWARD_ID, "&Forward", true, Some("Cmd+]"))?;
185+
let go_parent_item = MenuItem::with_id(app, GO_PARENT_ID, "&Parent folder", true, Some("Cmd+Up"))?;
186+
187+
let go_menu = Submenu::with_items(
188+
app,
189+
"&Go",
190+
true,
191+
&[
192+
&go_back_item,
193+
&go_forward_item,
194+
&PredefinedMenuItem::separator(app)?,
195+
&go_parent_item,
196+
],
197+
)?;
198+
menu.append(&go_menu)?;
199+
200+
// --- Tab menu ---
201+
let new_tab_item = MenuItem::with_id(app, NEW_TAB_ID, "&New tab", true, Some("Cmd+T"))?;
202+
let close_tab_item = MenuItem::with_id(app, CLOSE_TAB_ID, "&Close tab", true, Some("Cmd+W"))?;
203+
let next_tab_item = MenuItem::with_id(app, NEXT_TAB_ID, "Ne&xt tab", true, Some("Ctrl+Tab"))?;
204+
let prev_tab_item = MenuItem::with_id(app, PREV_TAB_ID, "&Previous tab", true, Some("Ctrl+Shift+Tab"))?;
205+
let pin_tab_item = MenuItem::with_id(app, PIN_TAB_MENU_ID, "P&in tab", true, None::<&str>)?;
206+
let close_other_tabs_item = MenuItem::with_id(app, CLOSE_OTHER_TABS_ID, "Close &other tabs", true, None::<&str>)?;
207+
208+
let tab_menu = Submenu::with_items(
209+
app,
210+
"&Tab",
211+
true,
212+
&[
213+
&new_tab_item,
214+
&close_tab_item,
215+
&PredefinedMenuItem::separator(app)?,
216+
&next_tab_item,
217+
&prev_tab_item,
218+
&PredefinedMenuItem::separator(app)?,
219+
&pin_tab_item,
220+
&close_other_tabs_item,
221+
],
222+
)?;
223+
menu.append(&tab_menu)?;
224+
225+
// --- Help menu ---
226+
let about_item = MenuItem::with_id(app, ABOUT_ID, "&About cmdr", true, None::<&str>)?;
227+
let help_menu = Submenu::with_items(app, "&Help", true, &[&about_item])?;
228+
menu.append(&help_menu)?;
229+
230+
// --- Populate items HashMap for accelerator updates ---
231+
let mut items = HashMap::new();
232+
233+
// File menu positions: open(0), view(1), edit(2), sep(3), copy(4), move(5),
234+
// new_folder(6), delete(7), delete_perm(8), sep(9), rename(10), sep(11),
235+
// show_in_fm(12), get_info(13), quick_look(14)
236+
register_item(&mut items, OPEN_ID, &open_item, &file_menu, 0);
237+
register_item(&mut items, FILE_VIEW_ID, &file_view_item, &file_menu, 1);
238+
register_item(&mut items, EDIT_ID, &edit_item, &file_menu, 2);
239+
register_item(&mut items, FILE_COPY_ID, &file_copy_item, &file_menu, 4);
240+
register_item(&mut items, FILE_MOVE_ID, &file_move_item, &file_menu, 5);
241+
register_item(&mut items, FILE_NEW_FOLDER_ID, &file_new_folder_item, &file_menu, 6);
242+
register_item(&mut items, FILE_DELETE_ID, &file_delete_item, &file_menu, 7);
243+
register_item(
244+
&mut items,
245+
FILE_DELETE_PERMANENTLY_ID,
246+
&file_delete_permanently_item,
247+
&file_menu,
248+
8,
249+
);
250+
register_item(&mut items, RENAME_ID, &rename_item, &file_menu, 10);
251+
register_item(&mut items, SHOW_IN_FINDER_ID, &show_in_fm_item, &file_menu, 12);
252+
register_item(&mut items, GET_INFO_ID, &get_info_item, &file_menu, 13);
253+
register_item(&mut items, QUICK_LOOK_ID, &quick_look_item, &file_menu, 14);
254+
255+
// Edit menu positions: cut(0), copy(1), paste(2), move_here(3), sep(4),
256+
// select_all(5), deselect_all(6), sep(7), copy_path(8), copy_filename(9),
257+
// sep(10), settings(11), license(12)
258+
register_item(&mut items, EDIT_CUT_ID, &edit_cut_item, &edit_menu, 0);
259+
register_item(&mut items, EDIT_COPY_ID, &edit_copy_item, &edit_menu, 1);
260+
register_item(&mut items, EDIT_PASTE_ID, &edit_paste_item, &edit_menu, 2);
261+
register_item(&mut items, EDIT_PASTE_MOVE_ID, &edit_paste_move_item, &edit_menu, 3);
262+
register_item(&mut items, SELECT_ALL_ID, &select_all_item, &edit_menu, 5);
263+
register_item(&mut items, DESELECT_ALL_ID, &deselect_all_item, &edit_menu, 6);
264+
register_item(&mut items, COPY_PATH_ID, &copy_path_item, &edit_menu, 8);
265+
register_item(&mut items, COPY_FILENAME_ID, &copy_filename_item, &edit_menu, 9);
266+
register_item(&mut items, SETTINGS_ID, &settings_item, &edit_menu, 11);
267+
268+
// View menu positions: full(0), brief(1), sep(2), hidden(3), sort(4), sep(5),
269+
// switch(6), swap(7), sep(8), palette(9)
270+
register_item(&mut items, SWITCH_PANE_ID, &switch_pane_item, &view_submenu, 6);
271+
register_item(&mut items, SWAP_PANES_ID, &swap_panes_item, &view_submenu, 7);
272+
register_item(&mut items, COMMAND_PALETTE_ID, &command_palette_item, &view_submenu, 9);
273+
274+
// Go menu positions: back(0), forward(1), sep(2), parent(3)
275+
register_item(&mut items, GO_BACK_ID, &go_back_item, &go_menu, 0);
276+
register_item(&mut items, GO_FORWARD_ID, &go_forward_item, &go_menu, 1);
277+
register_item(&mut items, GO_PARENT_ID, &go_parent_item, &go_menu, 3);
278+
279+
// Tab menu positions: new(0), close(1), sep(2), next(3), prev(4), sep(5), pin(6), close_others(7)
280+
register_item(&mut items, NEW_TAB_ID, &new_tab_item, &tab_menu, 0);
281+
register_item(&mut items, CLOSE_TAB_ID, &close_tab_item, &tab_menu, 1);
282+
register_item(&mut items, NEXT_TAB_ID, &next_tab_item, &tab_menu, 3);
283+
register_item(&mut items, PREV_TAB_ID, &prev_tab_item, &tab_menu, 4);
284+
register_item(&mut items, CLOSE_OTHER_TABS_ID, &close_other_tabs_item, &tab_menu, 7);
285+
286+
// Help menu: about(0)
287+
register_item(&mut items, ABOUT_ID, &about_item, &help_menu, 0);
288+
289+
Ok(MenuItems {
290+
menu,
291+
show_hidden_files: show_hidden_item,
292+
view_mode_full: view_mode_full_item,
293+
view_mode_brief: view_mode_brief_item,
294+
view_submenu,
295+
view_mode_full_position: view_full_pos,
296+
view_mode_brief_position: view_brief_pos,
297+
pin_tab: pin_tab_item,
298+
items,
299+
sort_submenu,
300+
})
301+
}

0 commit comments

Comments
 (0)