mirror of
https://github.com/neovide/neovide.git
synced 2026-09-10 07:16:25 -04:00
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:
parent
b5363a36b7
commit
c8b801493a
143
src/cmd_line.rs
143
src/cmd_line.rs
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue