1mod actions;
8
9pub use actions::MenuAction;
10
11use anyhow::Result;
12use muda::{
13 Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem, Submenu,
14 accelerator::{Accelerator, Code, Modifiers},
15};
16use std::collections::HashMap;
17use std::sync::Arc;
18use winit::window::Window;
19
20pub struct MenuManager {
22 #[allow(dead_code)]
24 menu: Menu,
25 action_map: HashMap<MenuId, MenuAction>,
27}
28
29impl MenuManager {
30 pub fn new() -> Result<Self> {
32 let menu = Menu::new();
33 let mut action_map = HashMap::new();
34
35 #[cfg(target_os = "macos")]
37 let cmd_or_ctrl = Modifiers::META;
38 #[cfg(not(target_os = "macos"))]
39 let cmd_or_ctrl = Modifiers::CONTROL;
40
41 let file_menu = Submenu::new("File", true);
43
44 let new_window = MenuItem::with_id(
45 "new_window",
46 "New Window",
47 true,
48 Some(Accelerator::new(Some(cmd_or_ctrl), Code::KeyN)),
49 );
50 action_map.insert(new_window.id().clone(), MenuAction::NewWindow);
51 file_menu.append(&new_window)?;
52
53 let close_window = MenuItem::with_id(
54 "close_window",
55 "Close Window",
56 true,
57 Some(Accelerator::new(Some(cmd_or_ctrl), Code::KeyW)),
58 );
59 action_map.insert(close_window.id().clone(), MenuAction::CloseWindow);
60 file_menu.append(&close_window)?;
61
62 file_menu.append(&PredefinedMenuItem::separator())?;
63
64 #[cfg(not(target_os = "macos"))]
67 {
68 let quit = MenuItem::with_id(
69 "quit",
70 "Quit",
71 true,
72 Some(Accelerator::new(Some(cmd_or_ctrl), Code::KeyQ)),
73 );
74 action_map.insert(quit.id().clone(), MenuAction::Quit);
75 file_menu.append(&quit)?;
76 }
77
78 menu.append(&file_menu)?;
79
80 let tab_menu = Submenu::new("Tab", true);
82
83 let new_tab = MenuItem::with_id(
84 "new_tab",
85 "New Tab",
86 true,
87 Some(Accelerator::new(Some(cmd_or_ctrl), Code::KeyT)),
88 );
89 action_map.insert(new_tab.id().clone(), MenuAction::NewTab);
90 tab_menu.append(&new_tab)?;
91
92 let close_tab = MenuItem::with_id(
93 "close_tab",
94 "Close Tab",
95 true,
96 None, );
98 action_map.insert(close_tab.id().clone(), MenuAction::CloseTab);
99 tab_menu.append(&close_tab)?;
100
101 tab_menu.append(&PredefinedMenuItem::separator())?;
102
103 let next_tab = MenuItem::with_id(
104 "next_tab",
105 "Next Tab",
106 true,
107 Some(Accelerator::new(
108 Some(cmd_or_ctrl | Modifiers::SHIFT),
109 Code::BracketRight,
110 )),
111 );
112 action_map.insert(next_tab.id().clone(), MenuAction::NextTab);
113 tab_menu.append(&next_tab)?;
114
115 let prev_tab = MenuItem::with_id(
116 "prev_tab",
117 "Previous Tab",
118 true,
119 Some(Accelerator::new(
120 Some(cmd_or_ctrl | Modifiers::SHIFT),
121 Code::BracketLeft,
122 )),
123 );
124 action_map.insert(prev_tab.id().clone(), MenuAction::PreviousTab);
125 tab_menu.append(&prev_tab)?;
126
127 tab_menu.append(&PredefinedMenuItem::separator())?;
128
129 for i in 1..=9 {
131 let code = match i {
132 1 => Code::Digit1,
133 2 => Code::Digit2,
134 3 => Code::Digit3,
135 4 => Code::Digit4,
136 5 => Code::Digit5,
137 6 => Code::Digit6,
138 7 => Code::Digit7,
139 8 => Code::Digit8,
140 9 => Code::Digit9,
141 _ => unreachable!(),
142 };
143 let tab_item = MenuItem::with_id(
144 format!("tab_{}", i),
145 format!("Tab {}", i),
146 true,
147 Some(Accelerator::new(Some(cmd_or_ctrl), code)),
148 );
149 action_map.insert(tab_item.id().clone(), MenuAction::SwitchToTab(i));
150 tab_menu.append(&tab_item)?;
151 }
152
153 menu.append(&tab_menu)?;
154
155 let edit_menu = Submenu::new("Edit", true);
157
158 let copy = MenuItem::with_id(
159 "copy",
160 "Copy",
161 true,
162 Some(Accelerator::new(Some(cmd_or_ctrl), Code::KeyC)),
163 );
164 action_map.insert(copy.id().clone(), MenuAction::Copy);
165 edit_menu.append(©)?;
166
167 let paste = MenuItem::with_id(
168 "paste",
169 "Paste",
170 true,
171 Some(Accelerator::new(Some(cmd_or_ctrl), Code::KeyV)),
172 );
173 action_map.insert(paste.id().clone(), MenuAction::Paste);
174 edit_menu.append(&paste)?;
175
176 let select_all = MenuItem::with_id(
177 "select_all",
178 "Select All",
179 true,
180 Some(Accelerator::new(Some(cmd_or_ctrl), Code::KeyA)),
181 );
182 action_map.insert(select_all.id().clone(), MenuAction::SelectAll);
183 edit_menu.append(&select_all)?;
184
185 edit_menu.append(&PredefinedMenuItem::separator())?;
186
187 let clear_scrollback = MenuItem::with_id(
188 "clear_scrollback",
189 "Clear Scrollback",
190 true,
191 Some(Accelerator::new(
192 Some(cmd_or_ctrl | Modifiers::SHIFT),
193 Code::KeyK,
194 )),
195 );
196 action_map.insert(clear_scrollback.id().clone(), MenuAction::ClearScrollback);
197 edit_menu.append(&clear_scrollback)?;
198
199 let clipboard_history = MenuItem::with_id(
200 "clipboard_history",
201 "Clipboard History",
202 true,
203 Some(Accelerator::new(
204 Some(cmd_or_ctrl | Modifiers::SHIFT),
205 Code::KeyH,
206 )),
207 );
208 action_map.insert(clipboard_history.id().clone(), MenuAction::ClipboardHistory);
209 edit_menu.append(&clipboard_history)?;
210
211 menu.append(&edit_menu)?;
212
213 let view_menu = Submenu::new("View", true);
215
216 let toggle_fullscreen = MenuItem::with_id(
217 "toggle_fullscreen",
218 "Toggle Fullscreen",
219 true,
220 Some(Accelerator::new(None, Code::F11)),
221 );
222 action_map.insert(toggle_fullscreen.id().clone(), MenuAction::ToggleFullscreen);
223 view_menu.append(&toggle_fullscreen)?;
224
225 view_menu.append(&PredefinedMenuItem::separator())?;
226
227 let increase_font = MenuItem::with_id(
228 "increase_font",
229 "Increase Font Size",
230 true,
231 Some(Accelerator::new(Some(cmd_or_ctrl), Code::Equal)),
232 );
233 action_map.insert(increase_font.id().clone(), MenuAction::IncreaseFontSize);
234 view_menu.append(&increase_font)?;
235
236 let decrease_font = MenuItem::with_id(
237 "decrease_font",
238 "Decrease Font Size",
239 true,
240 Some(Accelerator::new(Some(cmd_or_ctrl), Code::Minus)),
241 );
242 action_map.insert(decrease_font.id().clone(), MenuAction::DecreaseFontSize);
243 view_menu.append(&decrease_font)?;
244
245 let reset_font = MenuItem::with_id(
246 "reset_font",
247 "Reset Font Size",
248 true,
249 Some(Accelerator::new(Some(cmd_or_ctrl), Code::Digit0)),
250 );
251 action_map.insert(reset_font.id().clone(), MenuAction::ResetFontSize);
252 view_menu.append(&reset_font)?;
253
254 view_menu.append(&PredefinedMenuItem::separator())?;
255
256 let fps_overlay = MenuItem::with_id(
257 "fps_overlay",
258 "FPS Overlay",
259 true,
260 Some(Accelerator::new(None, Code::F3)),
261 );
262 action_map.insert(fps_overlay.id().clone(), MenuAction::ToggleFpsOverlay);
263 view_menu.append(&fps_overlay)?;
264
265 let settings = MenuItem::with_id(
266 "settings",
267 "Settings",
268 true,
269 Some(Accelerator::new(None, Code::F12)),
270 );
271 action_map.insert(settings.id().clone(), MenuAction::OpenSettings);
272 view_menu.append(&settings)?;
273
274 menu.append(&view_menu)?;
275
276 #[cfg(target_os = "macos")]
278 {
279 let window_menu = Submenu::new("Window", true);
280
281 let minimize = MenuItem::with_id(
282 "minimize",
283 "Minimize",
284 true,
285 Some(Accelerator::new(Some(Modifiers::META), Code::KeyM)),
286 );
287 action_map.insert(minimize.id().clone(), MenuAction::Minimize);
288 window_menu.append(&minimize)?;
289
290 let zoom = MenuItem::with_id("zoom", "Zoom", true, None);
291 action_map.insert(zoom.id().clone(), MenuAction::Zoom);
292 window_menu.append(&zoom)?;
293
294 menu.append(&window_menu)?;
295 }
296
297 let help_menu = Submenu::new("Help", true);
299
300 let keyboard_shortcuts = MenuItem::with_id(
301 "keyboard_shortcuts",
302 "Keyboard Shortcuts",
303 true,
304 Some(Accelerator::new(None, Code::F1)),
305 );
306 action_map.insert(keyboard_shortcuts.id().clone(), MenuAction::ShowHelp);
307 help_menu.append(&keyboard_shortcuts)?;
308
309 help_menu.append(&PredefinedMenuItem::separator())?;
310
311 let about = MenuItem::with_id("about", "About par-term", true, None);
312 action_map.insert(about.id().clone(), MenuAction::About);
313 help_menu.append(&about)?;
314
315 menu.append(&help_menu)?;
316
317 Ok(Self { menu, action_map })
318 }
319
320 #[allow(unused_variables)] pub fn init_for_window(&self, window: &Arc<Window>) -> Result<()> {
326 #[cfg(target_os = "macos")]
327 {
328 self.menu.init_for_nsapp();
330 log::info!("Initialized macOS global menu bar");
333 Ok(())
334 }
335
336 #[cfg(target_os = "windows")]
337 {
338 use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
339 if let Ok(handle) = window.window_handle() {
340 if let RawWindowHandle::Win32(win32_handle) = handle.as_raw() {
341 unsafe {
343 self.menu.init_for_hwnd(win32_handle.hwnd.get() as _)?;
344 }
345 log::info!("Initialized Windows menu bar for window");
346 }
347 }
348 Ok(())
349 }
350
351 #[cfg(any(
352 target_os = "linux",
353 target_os = "dragonfly",
354 target_os = "freebsd",
355 target_os = "netbsd",
356 target_os = "openbsd"
357 ))]
358 {
359 use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
362 if let Ok(handle) = window.window_handle() {
363 if let RawWindowHandle::Xlib(xlib_handle) = handle.as_raw() {
364 log::info!("Linux X11 menu support (using GTK integration)");
367 } else if let RawWindowHandle::Wayland(_wayland_handle) = handle.as_raw() {
368 log::info!("Linux Wayland menu support (using GTK integration)");
369 }
370 }
371 log::info!("Linux menu bar initialized (GTK-based)");
374 Ok(())
375 }
376
377 #[cfg(not(any(
378 target_os = "macos",
379 target_os = "windows",
380 target_os = "linux",
381 target_os = "dragonfly",
382 target_os = "freebsd",
383 target_os = "netbsd",
384 target_os = "openbsd"
385 )))]
386 {
387 log::warn!("Menu bar not supported on this platform");
388 Ok(())
389 }
390 }
391
392 pub fn poll_events(&self) -> impl Iterator<Item = MenuAction> + '_ {
394 std::iter::from_fn(|| {
395 match MenuEvent::receiver().try_recv() {
397 Ok(event) => self.action_map.get(&event.id).copied(),
398 Err(_) => None,
399 }
400 })
401 }
402}