From 1023084eb98c53b873080bc503f77a6a8e90eedb Mon Sep 17 00:00:00 2001 From: pagedown Date: Tue, 20 Dec 2022 21:08:26 +0800 Subject: [PATCH] macOS: Allow to customize Hide, Minimize, and Quit global menu shortcuts All previously hard-coded shortcuts under macOS can be configured now. --- docs/changelog.rst | 3 +++ kitty/boss.py | 39 +++++++++++++++++++-------- kitty/child-monitor.c | 4 +++ kitty/cocoa_window.m | 53 ++++++++++++++++++++++++++++--------- kitty/fast_data_types.pyi | 12 +++++++++ kitty/glfw.c | 40 ++++++++++++++++++++++++++++ kitty/main.py | 3 ++- kitty/options/definition.py | 20 ++++++++++++++ kitty/options/types.py | 4 +++ kitty/state.h | 4 +++ 10 files changed, 157 insertions(+), 25 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7bb1bab59..be26f6dc9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -71,6 +71,9 @@ Detailed list of changes - ZSH Integration: Bind :kbd:`alt+left` and :kbd:`alt+right` to move by word if not already bound. This mimics the default bindings in Terminal.app (:iss:`5793`) +- macOS: Allow to customize :sc:`Hide `, :sc:`Hide Others `, :sc:`Minimize `, and :sc:`Quit ` global menu shortcuts. Note that :opt:`clear_all_shortcuts` will remove these shortcuts now (:iss:`948`) + + 0.26.5 [2022-11-07] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/boss.py b/kitty/boss.py index aa4915e77..9c19f8a22 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -33,18 +33,21 @@ from .constants import ( ) from .fast_data_types import ( CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT, - GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, IMPERATIVE_CLOSE_REQUESTED, - NO_CLOSE_REQUESTED, ChildMonitor, Color, EllipticCurveKey, KeyEvent, SingleKey, - add_timer, apply_options_update, background_opacity_of, change_background_opacity, - change_os_window_state, cocoa_set_menubar_title, create_os_window, - current_application_quit_request, current_focused_os_window_id, current_os_window, - destroy_global_data, focus_os_window, get_boss, get_options, get_os_window_size, - global_font_size, last_focused_os_window_id, mark_os_window_for_close, - os_window_font_size, patch_global_colors, redirect_mouse_handling, ring_bell, + GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, + IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED, ChildMonitor, Color, + EllipticCurveKey, KeyEvent, SingleKey, add_timer, apply_options_update, + background_opacity_of, change_background_opacity, change_os_window_state, + cocoa_hide_app, cocoa_hide_other_apps, cocoa_minimize_os_window, + cocoa_set_menubar_title, create_os_window, + current_application_quit_request, current_focused_os_window_id, + current_os_window, destroy_global_data, focus_os_window, get_boss, + get_options, get_os_window_size, global_font_size, + last_focused_os_window_id, mark_os_window_for_close, os_window_font_size, + patch_global_colors, redirect_mouse_handling, ring_bell, run_with_activation_token, safe_pipe, send_data_to_peer, - set_application_quit_request, set_background_image, set_boss, set_in_sequence_mode, - set_options, set_os_window_size, set_os_window_title, thread_write, - toggle_fullscreen, toggle_maximized, toggle_secure_input, + set_application_quit_request, set_background_image, set_boss, + set_in_sequence_mode, set_options, set_os_window_size, set_os_window_title, + thread_write, toggle_fullscreen, toggle_maximized, toggle_secure_input, ) from .key_encoding import get_name_to_functional_number_map from .keys import get_shortcut, shortcut_matches @@ -926,6 +929,20 @@ class Boss: def toggle_macos_secure_keyboard_entry(self) -> None: toggle_secure_input() + @ac('misc', 'Hide macOS kitty application') + def hide_macos_app(self) -> None: + cocoa_hide_app() + + @ac('misc', 'Hide macOS other applications') + def hide_macos_other_apps(self) -> None: + cocoa_hide_other_apps() + + @ac('misc', 'Minimize macOS window') + def minimize_macos_window(self) -> None: + osw_id = current_os_window() + if osw_id is not None: + cocoa_minimize_os_window(osw_id) + def start(self, first_os_window_id: int, startup_sessions: Iterable[Session]) -> None: if not getattr(self, 'io_thread_started', False): self.child_monitor.start() diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 89441b470..7f7b223c0 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -1108,6 +1108,10 @@ process_cocoa_pending_actions(void) { if (cocoa_pending_actions[TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY]) { call_boss(toggle_macos_secure_keyboard_entry, NULL); } if (cocoa_pending_actions[TOGGLE_FULLSCREEN]) { call_boss(toggle_fullscreen, NULL); } if (cocoa_pending_actions[OPEN_KITTY_WEBSITE]) { call_boss(open_kitty_website, NULL); } + if (cocoa_pending_actions[HIDE]) { call_boss(hide_macos_app, NULL); } + if (cocoa_pending_actions[HIDE_OTHERS]) { call_boss(hide_macos_other_apps, NULL); } + if (cocoa_pending_actions[MINIMIZE]) { call_boss(minimize_macos_window, NULL); } + if (cocoa_pending_actions[QUIT]) { call_boss(quit, NULL); } if (cocoa_pending_actions_data.wd) { if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "sO", cocoa_pending_actions_data.wd, Py_True); } if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "sO", cocoa_pending_actions_data.wd, Py_True); } diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index 6b0dcb854..8608780dd 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -236,12 +236,30 @@ PENDING(reload_config, RELOAD_CONFIG) PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY) PENDING(toggle_fullscreen, TOGGLE_FULLSCREEN) PENDING(open_kitty_website, OPEN_KITTY_WEBSITE) +PENDING(hide_macos_app, HIDE) +PENDING(hide_macos_other_apps, HIDE_OTHERS) +PENDING(minimize_macos_window, MINIMIZE) +PENDING(quit, QUIT) - (BOOL)validateMenuItem:(NSMenuItem *)item { if (item.action == @selector(toggle_macos_secure_keyboard_entry:)) { item.state = [SecureKeyboardEntryController sharedInstance].isDesired ? NSControlStateValueOn : NSControlStateValueOff; } else if (item.action == @selector(toggle_fullscreen:)) { item.title = ([NSApp currentSystemPresentationOptions] & NSApplicationPresentationFullScreen) ? @"Exit Full Screen" : @"Enter Full Screen"; + if (![NSApp keyWindow]) return NO; + } else if (item.action == @selector(minimize_macos_window:)) { + NSWindow *window = [NSApp keyWindow]; + if (!window || window.miniaturized || [NSApp currentSystemPresentationOptions] & NSApplicationPresentationFullScreen) return NO; + } else if (item.action == @selector(close_os_window:) || + item.action == @selector(close_tab:) || + item.action == @selector(close_window:) || + item.action == @selector(reset_terminal:) || + item.action == @selector(clear_terminal_and_scrollback:) || + item.action == @selector(previous_tab:) || + item.action == @selector(next_tab:) || + item.action == @selector(detach_tab:)) + { + if (![NSApp keyWindow]) return NO; } return YES; } @@ -272,6 +290,7 @@ typedef struct { GlobalShortcut new_os_window, close_os_window, close_tab, edit_config_file, reload_config; GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal, clear_terminal_and_scrollback; GlobalShortcut toggle_macos_secure_keyboard_entry, toggle_fullscreen, open_kitty_website; + GlobalShortcut hide_macos_app, hide_macos_other_apps, minimize_macos_window, quit; } GlobalShortcuts; static GlobalShortcuts global_shortcuts; @@ -287,6 +306,7 @@ cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) { else Q(new_tab); else Q(next_tab); else Q(previous_tab); else Q(new_window); else Q(close_window); else Q(reset_terminal); else Q(clear_terminal_and_scrollback); else Q(reload_config); else Q(toggle_macos_secure_keyboard_entry); else Q(toggle_fullscreen); else Q(open_kitty_website); + else Q(hide_macos_app); else Q(hide_macos_other_apps); else Q(minimize_macos_window); else Q(quit); #undef Q if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; } int cocoa_mods; @@ -574,13 +594,8 @@ cocoa_create_global_menu(void) { [servicesMenu release]; [appMenu addItem:[NSMenuItem separatorItem]]; - [appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", app_name] - action:@selector(hide:) - keyEquivalent:@"h"]; - [[appMenu addItemWithTitle:@"Hide Others" - action:@selector(hideOtherApplications:) - keyEquivalent:@"h"] - setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand]; + MENU_ITEM(appMenu, ([NSString stringWithFormat:@"Hide %@", app_name]), hide_macos_app); + MENU_ITEM(appMenu, @"Hide Others", hide_macos_other_apps); [appMenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""]; @@ -589,9 +604,7 @@ cocoa_create_global_menu(void) { MENU_ITEM(appMenu, @"Secure Keyboard Entry", toggle_macos_secure_keyboard_entry); [appMenu addItem:[NSMenuItem separatorItem]]; - [appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", app_name] - action:@selector(terminate:) - keyEquivalent:@"q"]; + MENU_ITEM(appMenu, ([NSString stringWithFormat:@"Quit %@", app_name]), quit); [appMenu release]; NSMenuItem* shellMenuItem = @@ -619,9 +632,7 @@ cocoa_create_global_menu(void) { NSMenu* windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; [windowMenuItem setSubmenu:windowMenu]; - [windowMenu addItemWithTitle:@"Minimize" - action:@selector(performMiniaturize:) - keyEquivalent:@"m"]; + MENU_ITEM(windowMenu, @"Minimize", minimize_macos_window); [windowMenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""]; @@ -723,6 +734,22 @@ cocoa_toggle_secure_keyboard_entry(void) { [[NSUserDefaults standardUserDefaults] setBool:k.isDesired forKey:@"SecureKeyboardEntry"]; } +void +cocoa_hide(void) { + [[NSApplication sharedApplication] performSelectorOnMainThread:@selector(hide:) withObject:nil waitUntilDone:NO]; +} + +void +cocoa_hide_others(void) { + [[NSApplication sharedApplication] performSelectorOnMainThread:@selector(hideOtherApplications:) withObject:nil waitUntilDone:NO]; +} + +void +cocoa_minimize(void *w) { + NSWindow *window = (NSWindow*)w; + if (window && !window.miniaturized) [window performSelectorOnMainThread:@selector(performMiniaturize:) withObject:nil waitUntilDone:NO]; +} + void cocoa_focus_window(void *w) { NSWindow *window = (NSWindow*)w; diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 4d23f4d53..b9df5db54 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -741,6 +741,18 @@ def cocoa_set_dock_icon(icon_path: str) -> None: pass +def cocoa_hide_app() -> None: + pass + + +def cocoa_hide_other_apps() -> None: + pass + + +def cocoa_minimize_os_window(os_window_id: Optional[int] = None) -> None: + pass + + def locale_is_valid(name: str) -> bool: pass diff --git a/kitty/glfw.c b/kitty/glfw.c index 0096a54b0..320340672 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -25,6 +25,9 @@ extern void cocoa_set_titlebar_appearance(void *w, unsigned int theme); extern void cocoa_set_titlebar_color(void *w, color_type color); extern bool cocoa_alt_option_key_pressed(unsigned long); extern void cocoa_toggle_secure_keyboard_entry(void); +extern void cocoa_hide(void); +extern void cocoa_hide_others(void); +extern void cocoa_minimize(void *w); extern void cocoa_set_uncaught_exception_handler(void); extern void cocoa_update_menu_bar_title(PyObject*); extern size_t cocoa_get_workspace_ids(void *w, size_t *workspace_ids, size_t array_sz); @@ -1272,6 +1275,22 @@ toggle_secure_input(PYNOARG) { Py_RETURN_NONE; } +static PyObject* +cocoa_hide_app(PYNOARG) { +#ifdef __APPLE__ + cocoa_hide(); +#endif + Py_RETURN_NONE; +} + +static PyObject* +cocoa_hide_other_apps(PYNOARG) { +#ifdef __APPLE__ + cocoa_hide_others(); +#endif + Py_RETURN_NONE; +} + static void ring_audio_bell(void) { static monotonic_t last_bell_at = -1; @@ -1349,6 +1368,24 @@ toggle_maximized(PyObject UNUSED *self, PyObject *args) { Py_RETURN_FALSE; } +static PyObject* +cocoa_minimize_os_window(PyObject UNUSED *self, PyObject *args) { + id_type os_window_id = 0; + if (!PyArg_ParseTuple(args, "|K", &os_window_id)) return NULL; +#ifdef __APPLE__ + OSWindow *w = os_window_id ? find_os_window(os_window_id) : current_os_window(); + if (!w || !w->handle) Py_RETURN_NONE; + if (!glfwGetCocoaWindow) { PyErr_SetString(PyExc_RuntimeError, "Failed to load glfwGetCocoaWindow"); return NULL; } + void *window = glfwGetCocoaWindow(w->handle); + if (!window) Py_RETURN_NONE; + cocoa_minimize(window); +#else + PyErr_SetString(PyExc_RuntimeError, "cocoa_minimize_os_window() is only supported on macOS"); + return NULL; +#endif + Py_RETURN_NONE; +} + static PyObject* change_os_window_state(PyObject *self UNUSED, PyObject *args) { char *state; @@ -1718,6 +1755,9 @@ static PyMethodDef module_methods[] = { METHODB(dbus_send_notification, METH_VARARGS), #endif METHODB(cocoa_window_id, METH_O), + METHODB(cocoa_hide_app, METH_NOARGS), + METHODB(cocoa_hide_other_apps, METH_NOARGS), + METHODB(cocoa_minimize_os_window, METH_VARARGS), {"glfw_init", (PyCFunction)glfw_init, METH_VARARGS, ""}, {"glfw_terminate", (PyCFunction)glfw_terminate, METH_NOARGS, ""}, {"glfw_get_physical_dpi", (PyCFunction)glfw_get_physical_dpi, METH_NOARGS, ""}, diff --git a/kitty/main.py b/kitty/main.py index 1bc3cdbe8..1a4b589d2 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -181,7 +181,8 @@ def _run_app(opts: Options, args: CLIOptions, prewarm: PrewarmProcess, bad_lines func_map[parts].append(k) for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab', - 'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry', 'toggle_fullscreen'): + 'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry', 'toggle_fullscreen', + 'hide_macos_app', 'hide_macos_other_apps', 'minimize_macos_window', 'quit'): val = get_macos_shortcut_for(func_map, ac) if val is not None: global_shortcuts[ac] = val diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 1671650bf..fae5198d2 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -4002,5 +4002,25 @@ map('Open kitty Website', f'open_kitty_website shift+cmd+/ open_url {website_url()}', only='macos', ) + +map('Hide macOS kitty application', + 'hide_macos_app cmd+h hide_macos_app', + only='macos', + ) + +map('Hide macOS other applications', + 'hide_macos_other_apps opt+cmd+h hide_macos_other_apps', + only='macos', + ) + +map('Minimize macOS window', + 'minimize_macos_window cmd+m minimize_macos_window', + only='macos', + ) + +map('Quit kitty', + 'quit cmd+q quit', + only='macos', + ) egr() # }}} egr() # }}} diff --git a/kitty/options/types.py b/kitty/options/types.py index e06d29f90..42a5aa34c 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -937,6 +937,10 @@ if is_macos: defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=44), definition='load_config_file')) # noqa defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=44), definition='debug_config')) # noqa defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=47), definition='open_url https://sw.kovidgoyal.net/kitty/')) # noqa + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=104), definition='hide_macos_app')) # noqa + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=104), definition='hide_macos_other_apps')) # noqa + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=109), definition='minimize_macos_window')) # noqa + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=113), definition='quit')) # noqa defaults.mouse_map = [ # click_url_or_select MouseMapping(repeat_count=-2, definition='mouse_handle_click selection link prompt'), # noqa diff --git a/kitty/state.h b/kitty/state.h index 8f675d64e..165888eba 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -320,6 +320,10 @@ typedef enum { TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY, TOGGLE_FULLSCREEN, OPEN_KITTY_WEBSITE, + HIDE, + HIDE_OTHERS, + MINIMIZE, + QUIT, NUM_COCOA_PENDING_ACTIONS } CocoaPendingAction;