use muda::accelerator::{Accelerator, CMD_OR_CTRL, Code, Modifiers}; use muda::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}; use crate::native_edit::EditCommand; const PREFERENCES: &str = "preferences"; const MODEL_MANAGER: &str = "model-manager"; const NEW_CHAT: &str = "new-chat"; const EXPORT_CHAT: &str = "export-chat"; const TOGGLE_SIDEBAR: &str = "toggle-sidebar"; const HELP: &str = "help"; const UNDO: &str = "undo"; const REDO: &str = "redo"; const CUT: &str = "cut"; const COPY: &str = "copy"; const PASTE: &str = "paste"; const SELECT_ALL: &str = "select-all"; pub(crate) struct NativeMenu { _menu: Menu, new_chat: MenuItem, export_chat: MenuItem, sidebar: CheckMenuItem, undo: MenuItem, redo: MenuItem, cut: MenuItem, copy: MenuItem, paste: MenuItem, select_all: MenuItem, } #[derive(Clone, Copy, Debug)] pub(crate) enum NativeMenuEvent { Preferences, ModelManager, NewChat, ExportChat, ToggleSidebar, Help, Edit(EditCommand), } pub(crate) fn install() -> Result { let menu = Menu::new(); let application = Submenu::new("DS4Server", true); let file = Submenu::new("File", true); let edit = Submenu::new("Edit", true); let view = Submenu::new("View", true); let window = Submenu::new("Window", true); let help = Submenu::new("Help", true); let preferences = MenuItem::with_id( PREFERENCES, "Preferences…", true, Some(Accelerator::new(Some(Modifiers::SUPER), Code::Comma)), ); let model_manager = MenuItem::with_id( MODEL_MANAGER, "Model Manager…", true, Some(Accelerator::new( Some(Modifiers::SUPER | Modifiers::SHIFT), Code::KeyM, )), ); let new_chat = MenuItem::with_id( NEW_CHAT, "New Chat", false, Some(Accelerator::new(Some(Modifiers::SUPER), Code::KeyN)), ); let export_chat = MenuItem::with_id( EXPORT_CHAT, "Export Chat as Markdown…", false, Some(Accelerator::new( Some(Modifiers::SUPER | Modifiers::SHIFT), Code::KeyS, )), ); let sidebar = CheckMenuItem::with_id( TOGGLE_SIDEBAR, "Show Sidebar", true, true, Some(Accelerator::new(Some(Modifiers::SUPER), Code::KeyB)), ); let open_help = MenuItem::with_id( HELP, "DS4Server Help", true, Some(Accelerator::new( Some(Modifiers::SUPER | Modifiers::SHIFT), Code::Slash, )), ); let undo = edit_item(UNDO, "Undo", Code::KeyZ, None); let redo = edit_item(REDO, "Redo", Code::KeyZ, Some(Modifiers::SHIFT)); let cut = edit_item(CUT, "Cut", Code::KeyX, None); let copy = edit_item(COPY, "Copy", Code::KeyC, None); let paste = edit_item(PASTE, "Paste", Code::KeyV, None); let select_all = edit_item(SELECT_ALL, "Select All", Code::KeyA, None); application .append_items(&[ &PredefinedMenuItem::about(None, None), &PredefinedMenuItem::separator(), &preferences, &PredefinedMenuItem::separator(), &PredefinedMenuItem::services(None), &PredefinedMenuItem::separator(), &PredefinedMenuItem::hide(None), &PredefinedMenuItem::hide_others(None), &PredefinedMenuItem::show_all(None), &PredefinedMenuItem::separator(), &PredefinedMenuItem::quit(None), ]) .map_err(|error| error.to_string())?; file.append_items(&[ &new_chat, &export_chat, &PredefinedMenuItem::separator(), &PredefinedMenuItem::close_window(None), ]) .map_err(|error| error.to_string())?; edit.append_items(&[ &undo, &redo, &PredefinedMenuItem::separator(), &cut, ©, &paste, &select_all, ]) .map_err(|error| error.to_string())?; view.append_items(&[&sidebar, &PredefinedMenuItem::separator(), &model_manager]) .map_err(|error| error.to_string())?; window .append_items(&[ &PredefinedMenuItem::minimize(None), &PredefinedMenuItem::maximize(None), &PredefinedMenuItem::close_window(None), &PredefinedMenuItem::bring_all_to_front(None), ]) .map_err(|error| error.to_string())?; help.append(&open_help).map_err(|error| error.to_string())?; menu.append_items(&[&application, &file, &edit, &view, &window, &help]) .map_err(|error| error.to_string())?; menu.init_for_nsapp(); window.set_as_windows_menu_for_nsapp(); Ok(NativeMenu { _menu: menu, new_chat, export_chat, sidebar, undo, redo, cut, copy, paste, select_all, }) } impl NativeMenu { pub(crate) fn update( &self, project_active: bool, chat_active: bool, sidebar_visible: bool, edit: crate::native_edit::EditAvailability, ) { self.new_chat.set_enabled(project_active); self.export_chat.set_enabled(chat_active); self.sidebar.set_checked(sidebar_visible); self.undo.set_enabled(edit.undo); self.redo.set_enabled(edit.redo); self.cut.set_enabled(edit.cut); self.copy.set_enabled(edit.copy); self.paste.set_enabled(edit.paste); self.select_all.set_enabled(edit.select_all); } } pub(crate) fn next_event() -> Option { while let Ok(event) = MenuEvent::receiver().try_recv() { let event = match event.id.0.as_str() { PREFERENCES => NativeMenuEvent::Preferences, MODEL_MANAGER => NativeMenuEvent::ModelManager, NEW_CHAT => NativeMenuEvent::NewChat, EXPORT_CHAT => NativeMenuEvent::ExportChat, TOGGLE_SIDEBAR => NativeMenuEvent::ToggleSidebar, HELP => NativeMenuEvent::Help, UNDO => NativeMenuEvent::Edit(EditCommand::Undo), REDO => NativeMenuEvent::Edit(EditCommand::Redo), CUT => NativeMenuEvent::Edit(EditCommand::Cut), COPY => NativeMenuEvent::Edit(EditCommand::Copy), PASTE => NativeMenuEvent::Edit(EditCommand::Paste), SELECT_ALL => NativeMenuEvent::Edit(EditCommand::SelectAll), _ => continue, }; return Some(event); } None } fn edit_item( id: &'static str, label: &'static str, code: Code, extra_modifier: Option, ) -> MenuItem { MenuItem::with_id( id, label, true, Some(Accelerator::new( Some(CMD_OR_CTRL | extra_modifier.unwrap_or_else(Modifiers::empty)), code, )), ) } #[cfg(test)] mod tests { use super::*; #[test] fn edit_items_are_custom_menu_events() { let paste = edit_item(PASTE, "Paste", Code::KeyV, None); assert_eq!(paste.id().0, PASTE); assert!(paste.is_enabled()); } }