feat: settings for menu shortcuts on macOS (#3481)

* feat: settings for menu shortcuts on macOS

---------

Co-authored-by: Alexsander Falcucci <alex.falcucci@gmail.com>
This commit is contained in:
Ole 2026-04-22 10:26:25 +01:00 committed by GitHub
parent b5363a36b7
commit c8b801493a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 355 additions and 46 deletions

View file

@ -118,6 +118,69 @@ pub struct CmdLineSettings {
#[arg(long = "no-system-native-tabs", action = ArgAction::SetTrue, value_parser = FalseyValueParser::new())] #[arg(long = "no-system-native-tabs", action = ArgAction::SetTrue, value_parser = FalseyValueParser::new())]
_no_system_native_tabs: bool, _no_system_native_tabs: bool,
/// Set the Window > New Window shortcut
#[cfg(target_os = "macos")]
#[arg(
long = "system-new-window-hotkey",
env = "NEOVIDE_SYSTEM_NEW_WINDOW_HOTKEY",
default_value = "cmd+n"
)]
pub system_new_window_hotkey: String,
/// Set the Neovide > Hide shortcut
#[cfg(target_os = "macos")]
#[arg(
long = "system-hide-hotkey",
env = "NEOVIDE_SYSTEM_HIDE_HOTKEY",
default_value = "cmd+h"
)]
pub system_hide_hotkey: String,
/// Set the Neovide > Hide Others shortcut
#[cfg(target_os = "macos")]
#[arg(
long = "system-hide-others-hotkey",
env = "NEOVIDE_SYSTEM_HIDE_OTHERS_HOTKEY",
default_value = "cmd+alt+h"
)]
pub system_hide_others_hotkey: String,
/// Set the Neovide > Quit shortcut
#[cfg(target_os = "macos")]
#[arg(
long = "system-quit-hotkey",
env = "NEOVIDE_SYSTEM_QUIT_HOTKEY",
default_value = "cmd+q"
)]
pub system_quit_hotkey: String,
/// Set the Window > Minimize shortcut
#[cfg(target_os = "macos")]
#[arg(
long = "system-minimize-hotkey",
env = "NEOVIDE_SYSTEM_MINIMIZE_HOTKEY",
default_value = "cmd+m"
)]
pub system_minimize_hotkey: String,
/// Set the Window > Enter Full Screen shortcut
#[cfg(target_os = "macos")]
#[arg(
long = "system-fullscreen-hotkey",
env = "NEOVIDE_SYSTEM_FULLSCREEN_HOTKEY",
default_value = "cmd+ctrl+f"
)]
pub system_fullscreen_hotkey: String,
/// Set the Window > Editors shortcut when native tabs are visible
#[cfg(target_os = "macos")]
#[arg(
long = "system-show-all-tabs-hotkey",
env = "NEOVIDE_SYSTEM_SHOW_ALL_TABS_HOTKEY",
default_value = "cmd+shift+e"
)]
pub system_show_all_tabs_hotkey: String,
/// Cycle to the previous system tab when pressed inside Neovide /// Cycle to the previous system tab when pressed inside Neovide
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[arg( #[arg(
@ -458,6 +521,86 @@ mod tests {
assert!(handle_command_line_arguments(args, &settings).is_err()); assert!(handle_command_line_arguments(args, &settings).is_err());
} }
#[test]
#[cfg(target_os = "macos")]
fn test_system_new_window_hotkey_flag() {
let settings = Settings::new();
let args: Vec<String> = ["neovide", "--system-new-window-hotkey", "cmd+shift+n"]
.iter()
.map(|s| s.to_string())
.collect();
handle_command_line_arguments(args, &settings).expect("Could not parse arguments");
assert_eq!(settings.get::<CmdLineSettings>().system_new_window_hotkey, "cmd+shift+n");
}
#[test]
#[cfg(target_os = "macos")]
fn test_system_new_window_hotkey_environment_variable() {
let settings = Settings::new();
let args: Vec<String> = ["neovide"].iter().map(|s| s.to_string()).collect();
let _env = ScopedEnv::set("NEOVIDE_SYSTEM_NEW_WINDOW_HOTKEY", "ctrl+shift+n");
handle_command_line_arguments(args, &settings).expect("Could not parse arguments");
assert_eq!(settings.get::<CmdLineSettings>().system_new_window_hotkey, "ctrl+shift+n");
}
#[test]
#[cfg(target_os = "macos")]
fn test_system_menu_hotkey_flags() {
let settings = Settings::new();
let args: Vec<String> = [
"neovide",
"--system-hide-hotkey",
"ctrl+h",
"--system-hide-others-hotkey",
"ctrl+alt+h",
"--system-quit-hotkey",
"cmd+shift+q",
"--system-minimize-hotkey",
"cmd+shift+m",
"--system-fullscreen-hotkey",
"ctrl+alt+f",
"--system-show-all-tabs-hotkey",
"cmd+e",
]
.iter()
.map(|s| s.to_string())
.collect();
handle_command_line_arguments(args, &settings).expect("Could not parse arguments");
let cmdline = settings.get::<CmdLineSettings>();
assert_eq!(cmdline.system_hide_hotkey, "ctrl+h");
assert_eq!(cmdline.system_hide_others_hotkey, "ctrl+alt+h");
assert_eq!(cmdline.system_quit_hotkey, "cmd+shift+q");
assert_eq!(cmdline.system_minimize_hotkey, "cmd+shift+m");
assert_eq!(cmdline.system_fullscreen_hotkey, "ctrl+alt+f");
assert_eq!(cmdline.system_show_all_tabs_hotkey, "cmd+e");
}
#[test]
#[cfg(target_os = "macos")]
fn test_system_menu_hotkey_environment_variables() {
let settings = Settings::new();
let args: Vec<String> = ["neovide"].iter().map(|s| s.to_string()).collect();
let _hide = ScopedEnv::set("NEOVIDE_SYSTEM_HIDE_HOTKEY", "ctrl+h");
let _hide_others = ScopedEnv::set("NEOVIDE_SYSTEM_HIDE_OTHERS_HOTKEY", "ctrl+alt+h");
let _quit = ScopedEnv::set("NEOVIDE_SYSTEM_QUIT_HOTKEY", "ctrl+q");
let _minimize = ScopedEnv::set("NEOVIDE_SYSTEM_MINIMIZE_HOTKEY", "ctrl+m");
let _fullscreen = ScopedEnv::set("NEOVIDE_SYSTEM_FULLSCREEN_HOTKEY", "ctrl+shift+f");
let _show_all_tabs = ScopedEnv::set("NEOVIDE_SYSTEM_SHOW_ALL_TABS_HOTKEY", "ctrl+shift+e");
handle_command_line_arguments(args, &settings).expect("Could not parse arguments");
let cmdline = settings.get::<CmdLineSettings>();
assert_eq!(cmdline.system_hide_hotkey, "ctrl+h");
assert_eq!(cmdline.system_hide_others_hotkey, "ctrl+alt+h");
assert_eq!(cmdline.system_quit_hotkey, "ctrl+q");
assert_eq!(cmdline.system_minimize_hotkey, "ctrl+m");
assert_eq!(cmdline.system_fullscreen_hotkey, "ctrl+shift+f");
assert_eq!(cmdline.system_show_all_tabs_hotkey, "ctrl+shift+e");
}
#[test] #[test]
fn test_grid() { fn test_grid() {
let settings = Settings::new(); let settings = Settings::new();

View file

@ -15,9 +15,9 @@ use objc2::{
}; };
use objc2_app_kit::{ use objc2_app_kit::{
NSApplication, NSAutoresizingMaskOptions, NSColor, NSEvent, NSEventModifierFlags, NSFont, NSApplication, NSAutoresizingMaskOptions, NSColor, NSEvent, NSFont, NSFontAttributeName,
NSFontAttributeName, NSFontDescriptor, NSFontWeight, NSFontWeightLight, NSImage, NSMenu, NSFontDescriptor, NSFontWeight, NSFontWeightLight, NSImage, NSMenu, NSMenuDelegate, NSMenuItem,
NSMenuDelegate, NSMenuItem, NSTextView, NSView, NSWindow, NSWindowDidBecomeKeyNotification, NSTextView, NSView, NSWindow, NSWindowDidBecomeKeyNotification,
NSWindowDidBecomeMainNotification, NSWindowStyleMask, NSWindowTabbingMode, NSWindowDidBecomeMainNotification, NSWindowStyleMask, NSWindowTabbingMode,
NSWindowTitleVisibility, NSWorkspace, NSWindowTitleVisibility, NSWorkspace,
}; };
@ -36,6 +36,7 @@ use crate::bridge::{
use crate::renderer::fonts::font_options::FontOptions; use crate::renderer::fonts::font_options::FontOptions;
use crate::settings::Settings; use crate::settings::Settings;
use crate::utils::expand_tilde; use crate::utils::expand_tilde;
use crate::window::macos::tab_navigation::KeyCombo;
use crate::{cmd_line::CmdLineSettings, frame::Frame}; use crate::{cmd_line::CmdLineSettings, frame::Frame};
use winit::{event_loop::EventLoopProxy, window::Window}; use winit::{event_loop::EventLoopProxy, window::Window};
@ -316,6 +317,22 @@ fn open_external_url(url: &str) {
} }
} }
fn apply_menu_item_hotkey(item: &NSMenuItem, raw: &str, setting_name: &str) {
let Some(shortcut) = KeyCombo::parse(raw) else {
return;
};
let Some(key) = shortcut.to_key() else {
log::warn!(
"macOS menu shortcut '{raw}' for {setting_name} uses an unsupported named key; ignoring"
);
return;
};
item.setKeyEquivalent(key.as_ref());
item.setKeyEquivalentModifierMask(shortcut.to_modifiers());
}
#[derive(Debug)] #[derive(Debug)]
pub struct MacosWindowFeature { pub struct MacosWindowFeature {
ns_window: Retained<NSWindow>, ns_window: Retained<NSWindow>,
@ -1111,7 +1128,7 @@ impl MacosWindowFeature {
return; return;
} }
*menu_cell.borrow_mut() = Some(Menu::new(mtm)); *menu_cell.borrow_mut() = Some(Menu::new(mtm, self.settings.as_ref()));
let app = NSApplication::sharedApplication(mtm); let app = NSApplication::sharedApplication(mtm);
#[allow(deprecated)] #[allow(deprecated)]
app.activateIgnoringOtherApps(true); app.activateIgnoringOtherApps(true);
@ -1516,7 +1533,7 @@ struct Menu {
} }
impl Menu { impl Menu {
fn new(mtm: MainThreadMarker) -> Self { fn new(mtm: MainThreadMarker, settings: &Settings) -> Self {
let menu = Menu { let menu = Menu {
quit_handler: QuitHandler::new(mtm), quit_handler: QuitHandler::new(mtm),
help_menu_handler: HelpMenuHandler::new(mtm), help_menu_handler: HelpMenuHandler::new(mtm),
@ -1526,11 +1543,11 @@ impl Menu {
_window_menu_observer: WindowMenuNotificationHandler::register(mtm), _window_menu_observer: WindowMenuNotificationHandler::register(mtm),
window_menu_delegate: WindowMenuDelegate::new(mtm), window_menu_delegate: WindowMenuDelegate::new(mtm),
}; };
menu.add_menus(mtm); menu.add_menus(mtm, &settings.get::<CmdLineSettings>());
menu menu
} }
fn add_app_menu(&self, mtm: MainThreadMarker) -> Retained<NSMenu> { fn add_app_menu(&self, mtm: MainThreadMarker, settings: &CmdLineSettings) -> Retained<NSMenu> {
unsafe { unsafe {
let app_menu = NSMenu::new(mtm); let app_menu = NSMenu::new(mtm);
let process_name = NSProcessInfo::processInfo().processName(); let process_name = NSProcessInfo::processInfo().processName();
@ -1551,15 +1568,16 @@ impl Menu {
// application window operations // application window operations
let hide_item = NSMenuItem::new(mtm); let hide_item = NSMenuItem::new(mtm);
hide_item.setTitle(&ns_string!("Hide ").stringByAppendingString(&process_name)); hide_item.setTitle(&ns_string!("Hide ").stringByAppendingString(&process_name));
hide_item.setKeyEquivalent(ns_string!("h")); apply_menu_item_hotkey(&hide_item, &settings.system_hide_hotkey, "system_hide_hotkey");
hide_item.setAction(Some(sel!(hide:))); hide_item.setAction(Some(sel!(hide:)));
app_menu.addItem(&hide_item); app_menu.addItem(&hide_item);
let hide_others_item = NSMenuItem::new(mtm); let hide_others_item = NSMenuItem::new(mtm);
hide_others_item.setTitle(ns_string!("Hide Others")); hide_others_item.setTitle(ns_string!("Hide Others"));
hide_others_item.setKeyEquivalent(ns_string!("h")); apply_menu_item_hotkey(
hide_others_item.setKeyEquivalentModifierMask( &hide_others_item,
NSEventModifierFlags::Option | NSEventModifierFlags::Command, &settings.system_hide_others_hotkey,
"system_hide_others_hotkey",
); );
hide_others_item.setAction(Some(sel!(hideOtherApplications:))); hide_others_item.setAction(Some(sel!(hideOtherApplications:)));
app_menu.addItem(&hide_others_item); app_menu.addItem(&hide_others_item);
@ -1574,7 +1592,7 @@ impl Menu {
let quit_item = NSMenuItem::new(mtm); let quit_item = NSMenuItem::new(mtm);
quit_item.setTitle(&ns_string!("Quit ").stringByAppendingString(&process_name)); quit_item.setTitle(&ns_string!("Quit ").stringByAppendingString(&process_name));
quit_item.setKeyEquivalent(ns_string!("q")); apply_menu_item_hotkey(&quit_item, &settings.system_quit_hotkey, "system_quit_hotkey");
quit_item.setAction(Some(sel!(quit:))); quit_item.setAction(Some(sel!(quit:)));
quit_item.setTarget(Some(&self.quit_handler)); quit_item.setTarget(Some(&self.quit_handler));
app_menu.addItem(&quit_item); app_menu.addItem(&quit_item);
@ -1583,12 +1601,12 @@ impl Menu {
} }
} }
fn add_menus(&self, mtm: MainThreadMarker) { fn add_menus(&self, mtm: MainThreadMarker, settings: &CmdLineSettings) {
let app = NSApplication::sharedApplication(mtm); let app = NSApplication::sharedApplication(mtm);
let main_menu = NSMenu::new(mtm); let main_menu = NSMenu::new(mtm);
let app_menu = self.add_app_menu(mtm); let app_menu = self.add_app_menu(mtm, settings);
let app_menu_item = NSMenuItem::new(mtm); let app_menu_item = NSMenuItem::new(mtm);
app_menu_item.setSubmenu(Some(&app_menu)); app_menu_item.setSubmenu(Some(&app_menu));
if let Some(services_menu) = app_menu.itemWithTitle(ns_string!("Services")) { if let Some(services_menu) = app_menu.itemWithTitle(ns_string!("Services")) {
@ -1596,7 +1614,7 @@ impl Menu {
} }
main_menu.addItem(&app_menu_item); main_menu.addItem(&app_menu_item);
let win_menu = self.add_window_menu(mtm); let win_menu = self.add_window_menu(mtm, settings);
let win_menu_item = NSMenuItem::new(mtm); let win_menu_item = NSMenuItem::new(mtm);
win_menu_item.setSubmenu(Some(&win_menu)); win_menu_item.setSubmenu(Some(&win_menu));
main_menu.addItem(&win_menu_item); main_menu.addItem(&win_menu_item);
@ -1612,7 +1630,11 @@ impl Menu {
app.setMainMenu(Some(&main_menu)); app.setMainMenu(Some(&main_menu));
} }
fn add_window_menu(&self, mtm: MainThreadMarker) -> Retained<NSMenu> { fn add_window_menu(
&self,
mtm: MainThreadMarker,
settings: &CmdLineSettings,
) -> Retained<NSMenu> {
unsafe { unsafe {
let menu = NSMenu::new(mtm); let menu = NSMenu::new(mtm);
menu.setTitle(ns_string!("Window")); menu.setTitle(ns_string!("Window"));
@ -1622,16 +1644,21 @@ impl Menu {
let full_screen_item = NSMenuItem::new(mtm); let full_screen_item = NSMenuItem::new(mtm);
full_screen_item.setTitle(ns_string!("Enter Full Screen")); full_screen_item.setTitle(ns_string!("Enter Full Screen"));
full_screen_item.setKeyEquivalent(ns_string!("f")); apply_menu_item_hotkey(
full_screen_item.setAction(Some(sel!(toggleFullScreen:))); &full_screen_item,
full_screen_item.setKeyEquivalentModifierMask( &settings.system_fullscreen_hotkey,
NSEventModifierFlags::Control | NSEventModifierFlags::Command, "system_fullscreen_hotkey",
); );
full_screen_item.setAction(Some(sel!(toggleFullScreen:)));
menu.addItem(&full_screen_item); menu.addItem(&full_screen_item);
let create_new_window = NSMenuItem::new(mtm); let create_new_window = NSMenuItem::new(mtm);
create_new_window.setTitle(ns_string!("New Window")); create_new_window.setTitle(ns_string!("New Window"));
create_new_window.setKeyEquivalent(ns_string!("n")); apply_menu_item_hotkey(
&create_new_window,
&settings.system_new_window_hotkey,
"system_new_window_hotkey",
);
create_new_window.setAction(Some(sel!(neovideCreateWindow:))); create_new_window.setAction(Some(sel!(neovideCreateWindow:)));
create_new_window.setTarget(Some(&self.new_window_handler)); create_new_window.setTarget(Some(&self.new_window_handler));
menu.addItem(&create_new_window); menu.addItem(&create_new_window);
@ -1639,9 +1666,10 @@ impl Menu {
if should_show_native_tab_bar() { if should_show_native_tab_bar() {
let show_all_tabs_item = NSMenuItem::new(mtm); let show_all_tabs_item = NSMenuItem::new(mtm);
show_all_tabs_item.setTitle(ns_string!("Editors")); show_all_tabs_item.setTitle(ns_string!("Editors"));
show_all_tabs_item.setKeyEquivalent(ns_string!("e")); apply_menu_item_hotkey(
show_all_tabs_item.setKeyEquivalentModifierMask( &show_all_tabs_item,
NSEventModifierFlags::Command | NSEventModifierFlags::Shift, &settings.system_show_all_tabs_hotkey,
"system_show_all_tabs_hotkey",
); );
show_all_tabs_item.setAction(Some(sel!(neovideShowAllTabs:))); show_all_tabs_item.setAction(Some(sel!(neovideShowAllTabs:)));
show_all_tabs_item.setTarget(Some(&self.tab_overview_handler)); show_all_tabs_item.setTarget(Some(&self.tab_overview_handler));
@ -1650,7 +1678,11 @@ impl Menu {
let min_item = NSMenuItem::new(mtm); let min_item = NSMenuItem::new(mtm);
min_item.setTitle(ns_string!("Minimize")); min_item.setTitle(ns_string!("Minimize"));
min_item.setKeyEquivalent(ns_string!("m")); apply_menu_item_hotkey(
&min_item,
&settings.system_minimize_hotkey,
"system_minimize_hotkey",
);
min_item.setAction(Some(sel!(performMiniaturize:))); min_item.setAction(Some(sel!(performMiniaturize:)));
menu.addItem(&min_item); menu.addItem(&min_item);
menu menu

View file

@ -3,7 +3,10 @@
use std::{env, fs, sync::mpsc, time::Duration}; use std::{env, fs, sync::mpsc, time::Duration};
use notify_debouncer_full::{new_debouncer, notify::RecursiveMode}; use notify_debouncer_full::{new_debouncer, notify::RecursiveMode};
use serde::Deserialize; use serde::{
Deserialize, Deserializer,
de::{Error as DeError, Unexpected},
};
use winit::event_loop::EventLoopProxy; use winit::event_loop::EventLoopProxy;
use crate::{ use crate::{
@ -45,6 +48,27 @@ pub fn config_path() -> PathBuf {
}) })
} }
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum HotkeyConfigValue {
String(String),
Bool(bool),
}
fn deserialize_optional_hotkey<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
match Option::<HotkeyConfigValue>::deserialize(deserializer)? {
None => Ok(None),
Some(HotkeyConfigValue::String(value)) => Ok(Some(value)),
Some(HotkeyConfigValue::Bool(false)) => Ok(Some("false".to_string())),
Some(HotkeyConfigValue::Bool(true)) => {
Err(D::Error::invalid_value(Unexpected::Bool(true), &"a shortcut string or false"))
}
}
}
#[derive(Debug, Deserialize, Default, Clone)] #[derive(Debug, Deserialize, Default, Clone)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Config { pub struct Config {
@ -67,9 +91,27 @@ pub struct Config {
pub vsync: Option<bool>, pub vsync: Option<bool>,
pub wsl: Option<bool>, pub wsl: Option<bool>,
pub backtraces_path: Option<PathBuf>, pub backtraces_path: Option<PathBuf>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_pinned_hotkey: Option<String>, pub system_pinned_hotkey: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_switcher_hotkey: Option<String>, pub system_switcher_hotkey: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_new_window_hotkey: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_hide_hotkey: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_hide_others_hotkey: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_quit_hotkey: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_minimize_hotkey: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_fullscreen_hotkey: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_show_all_tabs_hotkey: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_tab_prev_hotkey: Option<String>, pub system_tab_prev_hotkey: Option<String>,
#[serde(default, deserialize_with = "deserialize_optional_hotkey")]
pub system_tab_next_hotkey: Option<String>, pub system_tab_next_hotkey: Option<String>,
pub icon: Option<String>, pub icon: Option<String>,
pub chdir: Option<PathBuf>, pub chdir: Option<PathBuf>,
@ -179,6 +221,27 @@ impl Config {
if let Some(switcher_hotkey) = &self.system_switcher_hotkey { if let Some(switcher_hotkey) = &self.system_switcher_hotkey {
unsafe { env::set_var("NEOVIDE_SYSTEM_SWITCHER_HOTKEY", switcher_hotkey) }; unsafe { env::set_var("NEOVIDE_SYSTEM_SWITCHER_HOTKEY", switcher_hotkey) };
} }
if let Some(new_window_hotkey) = &self.system_new_window_hotkey {
unsafe { env::set_var("NEOVIDE_SYSTEM_NEW_WINDOW_HOTKEY", new_window_hotkey) };
}
if let Some(hide_hotkey) = &self.system_hide_hotkey {
unsafe { env::set_var("NEOVIDE_SYSTEM_HIDE_HOTKEY", hide_hotkey) };
}
if let Some(hide_others_hotkey) = &self.system_hide_others_hotkey {
unsafe { env::set_var("NEOVIDE_SYSTEM_HIDE_OTHERS_HOTKEY", hide_others_hotkey) };
}
if let Some(quit_hotkey) = &self.system_quit_hotkey {
unsafe { env::set_var("NEOVIDE_SYSTEM_QUIT_HOTKEY", quit_hotkey) };
}
if let Some(minimize_hotkey) = &self.system_minimize_hotkey {
unsafe { env::set_var("NEOVIDE_SYSTEM_MINIMIZE_HOTKEY", minimize_hotkey) };
}
if let Some(fullscreen_hotkey) = &self.system_fullscreen_hotkey {
unsafe { env::set_var("NEOVIDE_SYSTEM_FULLSCREEN_HOTKEY", fullscreen_hotkey) };
}
if let Some(show_all_tabs_hotkey) = &self.system_show_all_tabs_hotkey {
unsafe { env::set_var("NEOVIDE_SYSTEM_SHOW_ALL_TABS_HOTKEY", show_all_tabs_hotkey) };
}
if let Some(tab_prev_hotkey) = &self.system_tab_prev_hotkey { if let Some(tab_prev_hotkey) = &self.system_tab_prev_hotkey {
unsafe { env::set_var("NEOVIDE_SYSTEM_TAB_PREV_HOTKEY", tab_prev_hotkey) }; unsafe { env::set_var("NEOVIDE_SYSTEM_TAB_PREV_HOTKEY", tab_prev_hotkey) };
} }

View file

@ -1,4 +1,7 @@
use log::warn; use log::warn;
use objc2::rc::Retained;
use objc2_app_kit::NSEventModifierFlags;
use objc2_foundation::NSString;
use winit::{ use winit::{
event::{ElementState, KeyEvent, Modifiers}, event::{ElementState, KeyEvent, Modifiers},
keyboard::{Key, NamedKey}, keyboard::{Key, NamedKey},
@ -7,19 +10,19 @@ use winit::{
use crate::{CmdLineSettings, settings::Settings}; use crate::{CmdLineSettings, settings::Settings};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(crate) enum TabNavigationAction { pub enum TabNavigationAction {
Next, Next,
Previous, Previous,
} }
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct TabNavigationHotkeys { pub struct TabNavigationHotkeys {
next: Option<KeyCombo>, next: Option<KeyCombo>,
prev: Option<KeyCombo>, prev: Option<KeyCombo>,
} }
impl TabNavigationHotkeys { impl TabNavigationHotkeys {
pub(crate) fn new(settings: &Settings) -> Self { pub fn new(settings: &Settings) -> Self {
let cmdline = settings.get::<CmdLineSettings>(); let cmdline = settings.get::<CmdLineSettings>();
Self { Self {
next: KeyCombo::parse(&cmdline.system_tab_next_hotkey), next: KeyCombo::parse(&cmdline.system_tab_next_hotkey),
@ -27,7 +30,7 @@ impl TabNavigationHotkeys {
} }
} }
pub(crate) fn action_for( pub fn action_for(
&self, &self,
event: &KeyEvent, event: &KeyEvent,
modifiers: &Modifiers, modifiers: &Modifiers,
@ -45,8 +48,8 @@ impl TabNavigationHotkeys {
} }
} }
#[derive(Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct KeyCombo { pub struct KeyCombo {
command: bool, command: bool,
control: bool, control: bool,
option: bool, option: bool,
@ -55,7 +58,7 @@ struct KeyCombo {
} }
impl KeyCombo { impl KeyCombo {
fn parse(raw: &str) -> Option<Self> { pub fn parse(raw: &str) -> Option<Self> {
let trimmed = raw.trim(); let trimmed = raw.trim();
if trimmed.is_empty() || is_disabled_keyword(trimmed) { if trimmed.is_empty() || is_disabled_keyword(trimmed) {
return None; return None;
@ -72,6 +75,41 @@ impl KeyCombo {
.build(raw) .build(raw)
} }
pub fn to_modifiers(self) -> NSEventModifierFlags {
let mut flags = NSEventModifierFlags::empty();
if self.command {
flags |= NSEventModifierFlags::Command;
}
if self.control {
flags |= NSEventModifierFlags::Control;
}
if self.option {
flags |= NSEventModifierFlags::Option;
}
if self.shift {
flags |= NSEventModifierFlags::Shift;
}
flags
}
/// Constructs an `NSString` representing the key component of this combo, if it's a character
/// key. Named keys will return `None`.
pub fn to_key(self) -> Option<Retained<NSString>> {
match self.key {
KeyMatch::Char(character) => Some(NSString::from_str(&character.to_string())),
KeyMatch::Named(_named) => {
// TODO: Figure out how to represent named keys in a way that can be used with
// NSEvent. For now, we don't support this.
None
}
}
}
fn matches(&self, event: &KeyEvent, modifiers: &Modifiers) -> bool { fn matches(&self, event: &KeyEvent, modifiers: &Modifiers) -> bool {
if !self.modifiers_match(modifiers) { if !self.modifiers_match(modifiers) {
return false; return false;
@ -121,15 +159,12 @@ fn parse_token(value: &str, raw: &str) -> Option<ParsedToken> {
fn parse_character_key(value: &str, raw: &str) -> Option<char> { fn parse_character_key(value: &str, raw: &str) -> Option<char> {
let mut chars = value.chars(); let mut chars = value.chars();
let Some(ch) = chars.next() else { let Some(ch) = chars.next() else {
warn!("macOS tab navigation shortcut '{}' has no key; ignoring", raw); warn!("macOS shortcut '{}' has no key; ignoring", raw);
return None; return None;
}; };
if chars.next().is_some() { if chars.next().is_some() {
warn!( warn!("macOS shortcut '{}' must end with a single character key; ignoring", raw);
"macOS tab navigation shortcut '{}' must end with a single character key; ignoring",
raw
);
return None; return None;
} }
@ -166,7 +201,7 @@ impl ParseState {
fn set_key(&mut self, value: KeyMatch, raw: &str) -> Option<()> { fn set_key(&mut self, value: KeyMatch, raw: &str) -> Option<()> {
match self.key { match self.key {
Some(_) => { Some(_) => {
warn!("macOS tab navigation shortcut '{}' has multiple keys; ignoring", raw); warn!("macOS shortcut '{}' has multiple keys; ignoring", raw);
None None
} }
None => { None => {
@ -183,10 +218,7 @@ impl ParseState {
option: self.option, option: self.option,
shift: self.shift, shift: self.shift,
key: self.key.or_else(|| { key: self.key.or_else(|| {
warn!( warn!("macOS shortcut '{}' is missing a key component; ignoring", raw);
"macOS tab navigation shortcut '{}' is missing a key component; ignoring",
raw
);
None None
})?, })?,
}) })
@ -197,7 +229,7 @@ fn is_disabled_keyword(value: &str) -> bool {
value.trim().eq_ignore_ascii_case("false") value.trim().eq_ignore_ascii_case("false")
} }
#[derive(Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum KeyMatch { enum KeyMatch {
Char(char), Char(char),
Named(NamedKey), Named(NamedKey),

View file

@ -264,6 +264,23 @@ default to mimic a standalone window. Enable this option to keep the tab bar vis
shows up as a tab immediately. The setting applies to windows opened through both global shortcuts shows up as a tab immediately. The setting applies to windows opened through both global shortcuts
and the Editors menu entry. and the Editors menu entry.
### Menu Shortcuts
```sh
--system-hide-hotkey <combo> or $NEOVIDE_SYSTEM_HIDE_HOTKEY
--system-hide-others-hotkey <combo> or $NEOVIDE_SYSTEM_HIDE_OTHERS_HOTKEY
--system-quit-hotkey <combo> or $NEOVIDE_SYSTEM_QUIT_HOTKEY
--system-new-window-hotkey <combo> or $NEOVIDE_SYSTEM_NEW_WINDOW_HOTKEY
--system-minimize-hotkey <combo> or $NEOVIDE_SYSTEM_MINIMIZE_HOTKEY
--system-fullscreen-hotkey <combo> or $NEOVIDE_SYSTEM_FULLSCREEN_HOTKEY
--system-show-all-tabs-hotkey <combo> or $NEOVIDE_SYSTEM_SHOW_ALL_TABS_HOTKEY
```
Remaps the macOS menu shortcuts used by Neovide. The defaults are `cmd+h`, `cmd+alt+h`, `cmd+q`,
`cmd+n`, `cmd+m`, `cmd+ctrl+f`, and `cmd+shift+e` for Hide, Hide Others, Quit, New Window,
Minimize, Enter Full Screen, and Editors respectively. Set any of them to `false` or leave them
empty to remove the shortcut while keeping the menu item available.
### System Tab Navigation ### System Tab Navigation
```sh ```sh

View file

@ -48,6 +48,13 @@ tabs = true
system-native-tabs = false # macOS only system-native-tabs = false # macOS only
system-pinned-hotkey = "cmd+ctrl+z" # macOS only system-pinned-hotkey = "cmd+ctrl+z" # macOS only
system-switcher-hotkey = "cmd+ctrl+n" # macOS only, requires system-native-tabs = true system-switcher-hotkey = "cmd+ctrl+n" # macOS only, requires system-native-tabs = true
system-new-window-hotkey = "cmd+n" # macOS only
system-hide-hotkey = "cmd+h" # macOS only
system-hide-others-hotkey = "cmd+alt+h" # macOS only
system-quit-hotkey = "cmd+q" # macOS only
system-minimize-hotkey = "cmd+m" # macOS only
system-fullscreen-hotkey = "cmd+ctrl+f" # macOS only
system-show-all-tabs-hotkey = "cmd+shift+e" # macOS only
system-tab-prev-hotkey = "cmd+shift+[" # macOS only system-tab-prev-hotkey = "cmd+shift+[" # macOS only
system-tab-next-hotkey = "cmd+shift+]" # macOS only system-tab-next-hotkey = "cmd+shift+]" # macOS only
title-hidden = false title-hidden = false

View file

@ -918,8 +918,8 @@ single host window.
Set `system-native-tabs = true` to merge windows into a tab group. The native tab bar stays hidden Set `system-native-tabs = true` to merge windows into a tab group. The native tab bar stays hidden
until more than one tab exists to keep a clean single-window look. until more than one tab exists to keep a clean single-window look.
Use Window > New Window (cmd+n) or the Dock menu to open another Neovide window. If native tabs Use Window > New Window (default: `cmd+n`) or the Dock menu to open another Neovide window. If
are enabled, new windows become tabs in the host window. native tabs are enabled, new windows become tabs in the host window.
If you have native tabs enabled, the Window menu shows an Editors entry and the Editors hotkey If you have native tabs enabled, the Window menu shows an Editors entry and the Editors hotkey
becomes available. You can also remap the in-app tab cycling shortcuts. becomes available. You can also remap the in-app tab cycling shortcuts.
@ -957,6 +957,21 @@ system-pinned-hotkey = "ctrl+shift+z"
system-switcher-hotkey = "ctrl+shift+n" system-switcher-hotkey = "ctrl+shift+n"
``` ```
You can also remap the macOS application and Window menu shortcuts:
```toml
system-hide-hotkey = "cmd+h"
system-hide-others-hotkey = "cmd+alt+h"
system-quit-hotkey = "cmd+q"
system-new-window-hotkey = "cmd+n"
system-minimize-hotkey = "cmd+m"
system-fullscreen-hotkey = "cmd+ctrl+f"
system-show-all-tabs-hotkey = "cmd+shift+e"
```
Set any of them to `false` (or an empty value) to remove the menu shortcut while keeping the menu
item.
When `system-native-tabs` is enabled, you can also customize the in-app tab navigation shortcuts: When `system-native-tabs` is enabled, you can also customize the in-app tab navigation shortcuts:
```toml ```toml