diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index 44ef1692e..fb5b2cc8f 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -607,135 +607,6 @@ translateKeyToModifierFlag(uint32_t key) static const NSRange kEmptyRange = { NSNotFound, 0 }; -// SecureKeyboardEntryController {{{ -@interface SecureKeyboardEntryController : NSObject - -@property (nonatomic, readonly) BOOL isDesired; -@property (nonatomic, readonly, getter=isEnabled) BOOL enabled; - -+ (instancetype)sharedInstance; - -- (void)toggle; -- (void)update; - -@end - -@implementation SecureKeyboardEntryController { - int _count; - BOOL _desired; -} - -+ (instancetype)sharedInstance { - static id instance; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[self alloc] init]; - }); - return instance; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _desired = false; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationDidResignActive:) - name:NSApplicationDidResignActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationDidBecomeActive:) - name:NSApplicationDidBecomeActiveNotification - object:nil]; - if ([NSApp isActive]) { - [self update]; - } - } - return self; -} - -#pragma mark - API - -- (void)toggle { - // Set _desired to the opposite of the current state. - _desired = !_desired; - debug_key("toggle called. Setting desired to %d", _desired); - - // Try to set the system's state of secure input to the desired state. - [self update]; -} - -- (BOOL)isEnabled { - return !!IsSecureEventInputEnabled(); -} - -- (BOOL)isDesired { - return _desired; -} - -#pragma mark - Notifications - -- (void)applicationDidResignActive:(NSNotification *)notification { - (void)notification; - if (_count > 0) { - debug_key("Application resigning active."); - [self update]; - } -} - -- (void)applicationDidBecomeActive:(NSNotification *)notification { - (void)notification; - if (self.isDesired) { - debug_key("Application became active."); - [self update]; - } -} - -#pragma mark - Private - -- (BOOL)allowed { - return [NSApp isActive]; -} - -- (void)update { - debug_key("Update secure keyboard entry. desired=%d active=%d\n", - (int)self.isDesired, (int)[NSApp isActive]); - const BOOL secure = self.isDesired && [self allowed]; - - if (secure && _count > 0) { - debug_key("Want to turn on secure input but it's already on\n"); - return; - } - - if (!secure && _count == 0) { - debug_key("Want to turn off secure input but it's already off\n"); - return; - } - - debug_key("Before: IsSecureEventInputEnabled returns %d ", (int)self.isEnabled); - if (secure) { - OSErr err = EnableSecureEventInput(); - debug_key("EnableSecureEventInput err=%d ", (int)err); - if (err) { - debug_key("EnableSecureEventInput failed with error %d ", (int)err); - } else { - _count += 1; - } - } else { - OSErr err = DisableSecureEventInput(); - debug_key("DisableSecureEventInput err=%d ", (int)err); - if (err) { - debug_key("DisableSecureEventInput failed with error %d ", (int)err); - } else { - _count -= 1; - } - } - debug_key("After: IsSecureEventInputEnabled returns %d\n", (int)self.isEnabled); -} - -@end -// }}} - // Delegate for window related notifications {{{ @interface GLFWWindowDelegate : NSObject @@ -1586,8 +1457,6 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) { if (self != nil) { glfw_window = initWindow; self.tabbingMode = NSWindowTabbingModeDisallowed; - SecureKeyboardEntryController *k = [SecureKeyboardEntryController sharedInstance]; - if (!k.isDesired && [[NSUserDefaults standardUserDefaults] boolForKey:@"SecureKeyboardEntry"]) [k toggle]; } return self; } @@ -1599,10 +1468,6 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) { - (BOOL)validateMenuItem:(NSMenuItem *)item { if (item.action == @selector(performMiniaturize:)) return YES; - if (item.action == @selector(toggleSecureInput:)) { - item.state = [SecureKeyboardEntryController sharedInstance].isDesired ? NSControlStateValueOn : NSControlStateValueOff; - return YES; - } return [super validateMenuItem:item]; } @@ -1612,13 +1477,6 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) { else [super performMiniaturize:sender]; } -- (void)toggleSecureInput:(id)sender { - (void)sender; - SecureKeyboardEntryController *k = [SecureKeyboardEntryController sharedInstance]; - [k toggle]; - [[NSUserDefaults standardUserDefaults] setBool:k.isDesired forKey:@"SecureKeyboardEntry"]; -} - - (BOOL)canBecomeKeyWindow { // Required for NSWindowStyleMaskBorderless windows diff --git a/kitty/boss.py b/kitty/boss.py index 5354a0817..551f9710d 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -39,7 +39,7 @@ from .fast_data_types import ( set_application_quit_request, set_background_image, set_boss, set_clipboard_string, set_in_sequence_mode, set_options, set_os_window_size, set_os_window_title, thread_write, toggle_fullscreen, - toggle_maximized + toggle_maximized, toggle_secure_input ) from .key_encoding import get_name_to_functional_number_map from .keys import get_shortcut, shortcut_matches @@ -721,6 +721,10 @@ class Boss: def toggle_maximized(self, os_window_id: int = 0) -> None: toggle_maximized(os_window_id) + @ac('misc', 'Toggle macOS secure keyboard entry') + def toggle_macos_secure_keyboard_entry(self) -> None: + toggle_secure_input() + def start(self, first_os_window_id: int) -> None: if not getattr(self, 'io_thread_started', False): self.child_monitor.start() @@ -1890,7 +1894,7 @@ class Boss: from .fonts.box_drawing import set_scale # Update options storage - set_options(opts, is_wayland(), self.args.debug_rendering, self.args.debug_font_fallback) + set_options(opts, is_wayland(), self.args.debug_keyboard, self.args.debug_rendering, self.args.debug_font_fallback) apply_options_update() set_layout_options(opts) set_default_env(opts.env.copy()) diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 9a86453fc..55aee8f73 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -1039,6 +1039,7 @@ process_cocoa_pending_actions(void) { if (cocoa_pending_actions[CLOSE_WINDOW]) { call_boss(close_window, NULL); } if (cocoa_pending_actions[RESET_TERMINAL]) { call_boss(clear_terminal, "sO", "reset", Py_True ); } if (cocoa_pending_actions[RELOAD_CONFIG]) { call_boss(load_config_file, NULL); } + if (cocoa_pending_actions[TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY]) { call_boss(toggle_macos_secure_keyboard_entry, NULL); } if (cocoa_pending_actions_data.wd) { if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "s", cocoa_pending_actions_data.wd); } if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "s", cocoa_pending_actions_data.wd); } diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index 1247cfa14..f1290c570 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -9,6 +9,7 @@ #include "state.h" #include "cleanup.h" #include "monotonic.h" +#include #include #ifndef KITTY_USE_DEPRECATED_MACOS_NOTIFICATION_API #include @@ -75,6 +76,137 @@ find_app_name(void) { return @"kitty"; } +#define debug_key(...) if (global_state.debug_keyboard) { fprintf(stderr, __VA_ARGS__); fflush(stderr); } + +// SecureKeyboardEntryController {{{ +@interface SecureKeyboardEntryController : NSObject + +@property (nonatomic, readonly) BOOL isDesired; +@property (nonatomic, readonly, getter=isEnabled) BOOL enabled; + ++ (instancetype)sharedInstance; + +- (void)toggle; +- (void)update; + +@end + +@implementation SecureKeyboardEntryController { + int _count; + BOOL _desired; +} + ++ (instancetype)sharedInstance { + static id instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _desired = false; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidResignActive:) + name:NSApplicationDidResignActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive:) + name:NSApplicationDidBecomeActiveNotification + object:nil]; + if ([NSApp isActive]) { + [self update]; + } + } + return self; +} + +#pragma mark - API + +- (void)toggle { + // Set _desired to the opposite of the current state. + _desired = !_desired; + debug_key("SecureKeyboardEntry: toggle called. Setting desired to %d ", _desired); + + // Try to set the system's state of secure input to the desired state. + [self update]; +} + +- (BOOL)isEnabled { + return !!IsSecureEventInputEnabled(); +} + +- (BOOL)isDesired { + return _desired; +} + +#pragma mark - Notifications + +- (void)applicationDidResignActive:(NSNotification *)notification { + (void)notification; + if (_count > 0) { + debug_key("SecureKeyboardEntry: Application resigning active."); + [self update]; + } +} + +- (void)applicationDidBecomeActive:(NSNotification *)notification { + (void)notification; + if (self.isDesired) { + debug_key("SecureKeyboardEntry: Application became active."); + [self update]; + } +} + +#pragma mark - Private + +- (BOOL)allowed { + return [NSApp isActive]; +} + +- (void)update { + debug_key("Update secure keyboard entry. desired=%d active=%d\n", + (int)self.isDesired, (int)[NSApp isActive]); + const BOOL secure = self.isDesired && [self allowed]; + + if (secure && _count > 0) { + debug_key("Want to turn on secure input but it's already on\n"); + return; + } + + if (!secure && _count == 0) { + debug_key("Want to turn off secure input but it's already off\n"); + return; + } + + debug_key("Before: IsSecureEventInputEnabled returns %d ", (int)self.isEnabled); + if (secure) { + OSErr err = EnableSecureEventInput(); + debug_key("EnableSecureEventInput err=%d ", (int)err); + if (err) { + debug_key("EnableSecureEventInput failed with error %d ", (int)err); + } else { + _count += 1; + } + } else { + OSErr err = DisableSecureEventInput(); + debug_key("DisableSecureEventInput err=%d ", (int)err); + if (err) { + debug_key("DisableSecureEventInput failed with error %d ", (int)err); + } else { + _count -= 1; + } + } + debug_key("After: IsSecureEventInputEnabled returns %d\n", (int)self.isEnabled); +} + +@end +// }}} + @interface GlobalMenuTarget : NSObject + (GlobalMenuTarget *) shared_instance; @end @@ -95,12 +227,20 @@ PENDING(new_window, NEW_WINDOW) PENDING(close_window, CLOSE_WINDOW) PENDING(reset_terminal, RESET_TERMINAL) PENDING(reload_config, RELOAD_CONFIG) +PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY) - (void)open_kitty_website_url:(id)sender { (void)sender; [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://sw.kovidgoyal.net/kitty/"]]; } +- (BOOL)validateMenuItem:(NSMenuItem *)item { + if (item.action == @selector(toggle_macos_secure_keyboard_entry:)) { + item.state = [SecureKeyboardEntryController sharedInstance].isDesired ? NSControlStateValueOn : NSControlStateValueOff; + } + return YES; +} + #undef PENDING + (GlobalMenuTarget *) shared_instance @@ -108,8 +248,11 @@ PENDING(reload_config, RELOAD_CONFIG) static GlobalMenuTarget *sharedGlobalMenuTarget = nil; @synchronized(self) { - if (!sharedGlobalMenuTarget) + if (!sharedGlobalMenuTarget) { sharedGlobalMenuTarget = [[GlobalMenuTarget alloc] init]; + SecureKeyboardEntryController *k = [SecureKeyboardEntryController sharedInstance]; + if (!k.isDesired && [[NSUserDefaults standardUserDefaults] boolForKey:@"SecureKeyboardEntry"]) [k toggle]; + } return sharedGlobalMenuTarget; } } @@ -123,6 +266,7 @@ typedef struct { 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; + GlobalShortcut toggle_macos_secure_keyboard_entry; } GlobalShortcuts; static GlobalShortcuts global_shortcuts; @@ -137,6 +281,7 @@ cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) { Q(new_os_window); else Q(close_os_window); else Q(close_tab); else Q(edit_config_file); 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(reload_config); + else Q(toggle_macos_secure_keyboard_entry); #undef Q if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; } int cocoa_mods; @@ -419,10 +564,7 @@ cocoa_create_global_menu(void) { keyEquivalent:@""]; [appMenu addItem:[NSMenuItem separatorItem]]; - [[appMenu addItemWithTitle:@"Secure Keyboard Entry" - action:@selector(toggleSecureInput:) - keyEquivalent:@"s"] - setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand]; + MENU_ITEM(appMenu, @"Secure Keyboard Entry", toggle_macos_secure_keyboard_entry); [appMenu addItem:[NSMenuItem separatorItem]]; [appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", app_name] @@ -547,6 +689,13 @@ cocoa_alt_option_key_pressed(NSUInteger flags) { return (q & flags) == q; } +void +cocoa_toggle_secure_keyboard_entry(void) { + SecureKeyboardEntryController *k = [SecureKeyboardEntryController sharedInstance]; + [k toggle]; + [[NSUserDefaults standardUserDefaults] setBool:k.isDesired forKey:@"SecureKeyboardEntry"]; +} + 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 a2d590eac..d04b50ca1 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -526,6 +526,7 @@ def sync_os_window_title(os_window_id: int) -> None: def set_options( opts: Optional[Options], is_wayland: bool = False, + debug_keyboard: bool = False, debug_rendering: bool = False, debug_font_fallback: bool = False ) -> None: @@ -763,6 +764,8 @@ def get_clipboard_string() -> str: def focus_os_window(os_window_id: int, also_raise: bool = True) -> bool: pass +def toggle_secure_input() -> None: + pass def start_profiler(path: str) -> None: pass diff --git a/kitty/glfw.c b/kitty/glfw.c index c3b8e9746..379dfe12a 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -24,6 +24,7 @@ extern void cocoa_set_activation_policy(bool); 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 size_t cocoa_get_workspace_ids(void *w, size_t *workspace_ids, size_t array_sz); extern monotonic_t cocoa_cursor_blink_interval(void); @@ -965,6 +966,13 @@ focus_os_window(OSWindow *w, bool also_raise) { } } +void +toggle_secure_input(void) { +#ifdef __APPLE__ + cocoa_toggle_secure_keyboard_entry(); +#endif +} + // Global functions {{{ static void error_callback(int error, const char* description) { diff --git a/kitty/main.py b/kitty/main.py index ea4e79520..a3e8dc0c0 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -151,7 +151,7 @@ def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) 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'): + 'next_tab', 'new_tab', 'new_window', 'close_window', 'toggle_macos_secure_keyboard_entry'): val = get_macos_shortcut_for(func_map, ac) if val is not None: global_shortcuts[ac] = val @@ -191,7 +191,7 @@ class AppRunner: def __call__(self, opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None: set_scale(opts.box_drawing_scale) - set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback) + set_options(opts, is_wayland(), args.debug_keyboard, args.debug_rendering, args.debug_font_fallback) try: set_font_family(opts, debug_font_matching=args.debug_font_fallback) _run_app(opts, args, bad_lines) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 31f4a236a..18da9cc85 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -3534,6 +3534,11 @@ map('Toggle maximized', 'toggle_maximized kitty_mod+f10 toggle_maximized', ) +map('Toggle macOS secure keyboard entry', + 'toggle_macos_secure_keyboard_entry opt+cmd+s toggle_macos_secure_keyboard_entry', + only="macos", + ) + map('Unicode input', 'input_unicode_character kitty_mod+u kitten unicode_input', ) diff --git a/kitty/options/types.py b/kitty/options/types.py index 724a25c5b..1c184d9d3 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -898,6 +898,7 @@ if is_macos: defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=45), definition='change_font_size all -2.0')) # noqa defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=45), definition='change_font_size all -2.0')) # noqa defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=48), definition='change_font_size all 0')) # noqa + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=115), definition='toggle_macos_secure_keyboard_entry')) # noqa defaults.map.append(KeyDefinition(trigger=SingleKey(mods=12, key=32), definition='kitten unicode_input')) # noqa defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=44), definition='edit_config_file')) # noqa defaults.map.append(KeyDefinition(trigger=SingleKey(mods=10, key=114), definition='clear_terminal reset active')) # noqa diff --git a/kitty/state.c b/kitty/state.c index 1de2752eb..d0930735c 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -684,8 +684,8 @@ PYWRAP0(get_options) { PYWRAP1(set_options) { PyObject *opts; - int is_wayland = 0, debug_rendering = 0, debug_font_fallback = 0; - PA("O|ppp", &opts, &is_wayland, &debug_rendering, &debug_font_fallback); + int is_wayland = 0, debug_keyboard = 0, debug_rendering = 0, debug_font_fallback = 0; + PA("O|pppp", &opts, &is_wayland, &debug_keyboard, &debug_rendering, &debug_font_fallback); if (opts == Py_None) { Py_CLEAR(options_object); Py_RETURN_NONE; @@ -695,6 +695,7 @@ PYWRAP1(set_options) { global_state.has_render_frames = true; #endif if (global_state.is_wayland) global_state.has_render_frames = true; + global_state.debug_keyboard = debug_keyboard ? true : false; global_state.debug_rendering = debug_rendering ? true : false; global_state.debug_font_fallback = debug_font_fallback ? true : false; if (!convert_opts_from_python_opts(opts, &global_state.opts)) return NULL; @@ -821,6 +822,11 @@ PYWRAP1(focus_os_window) { Py_RETURN_FALSE; } +PYWRAP0(toggle_secure_input) { + toggle_secure_input(); + Py_RETURN_NONE; +} + PYWRAP1(set_titlebar_color) { id_type os_window_id; unsigned int color; @@ -1266,6 +1272,7 @@ static PyMethodDef module_methods[] = { MW(current_application_quit_request, METH_NOARGS), MW(set_titlebar_color, METH_VARARGS), MW(focus_os_window, METH_VARARGS), + MW(toggle_secure_input, METH_NOARGS), MW(mark_tab_bar_dirty, METH_O), MW(change_background_opacity, METH_VARARGS), MW(background_opacity_of, METH_O), diff --git a/kitty/state.h b/kitty/state.h index 677cafc8d..57a0c5f72 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -224,7 +224,7 @@ typedef struct { OSWindow *callback_os_window; bool is_wayland; bool has_render_frames; - bool debug_rendering, debug_font_fallback; + bool debug_keyboard, debug_rendering, debug_font_fallback; bool has_pending_resizes, has_pending_closes; bool in_sequence_mode; bool check_for_active_animated_images; @@ -262,6 +262,7 @@ void hide_mouse(OSWindow *w); bool is_mouse_hidden(OSWindow *w); void destroy_os_window(OSWindow *w); void focus_os_window(OSWindow *w, bool also_raise); +void toggle_secure_input(void); void set_os_window_title(OSWindow *w, const char *title); OSWindow* os_window_for_kitty_window(id_type); OSWindow* add_os_window(void); @@ -303,6 +304,7 @@ typedef enum { CLOSE_WINDOW, RESET_TERMINAL, RELOAD_CONFIG, + TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY, NUM_COCOA_PENDING_ACTIONS } CocoaPendingAction; diff --git a/setup.py b/setup.py index 0e42f9f0f..6ea74149d 100755 --- a/setup.py +++ b/setup.py @@ -416,7 +416,7 @@ def kitty_env() -> Env: cflags.extend(pkg_config('lcms2', '--cflags-only-I')) if is_macos: platform_libs = [ - '-framework', 'CoreText', '-framework', 'CoreGraphics', + '-framework', 'Carbon', '-framework', 'CoreText', '-framework', 'CoreGraphics', ] test_program_src = '''#include int main(void) { return 0; }\n'''