diff --git a/docs/changelog.rst b/docs/changelog.rst index 3da92a126..37d54238d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -207,6 +207,8 @@ Detailed list of changes - Fix a crash when user tries to select while the client program is using synchronised rendering and generating large amounts of output (:iss:`9778`) +- macOS: Add Copy and Paste menu items to the Edit menu in the global menu bar (:iss:`9780`) + 0.46.2 [2026-03-21] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/boss.py b/kitty/boss.py index a010db4b1..3cb2d550c 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -3706,3 +3706,7 @@ class Boss: def search_scrollback_in_active(self) -> None: if w := self.active_window: w.search_scrollback() + + def copy_or_noop(self) -> None: + if w := self.active_window: + w.copy_or_noop() diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index b8468a690..c26995561 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -1349,6 +1349,8 @@ process_cocoa_pending_actions(void) { 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[PASTE_FROM_CLIPBOARD]) { call_boss(paste_from_clipboard, NULL); } + if (cocoa_pending_actions[COPY_OR_NOOP]) { call_boss(copy_or_noop, 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.h b/kitty/cocoa_window.h index 773d576c7..375f3fa89 100644 --- a/kitty/cocoa_window.h +++ b/kitty/cocoa_window.h @@ -39,6 +39,8 @@ typedef enum { HIDE_OTHERS, MINIMIZE, QUIT, + PASTE_FROM_CLIPBOARD, + COPY_OR_NOOP, USER_MENU_ACTION, COCOA_NOTIFICATION_UNTRACKED, diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index 50249f785..84b80900f 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -267,6 +267,8 @@ PENDING(hide_macos_app, HIDE) PENDING(hide_macos_other_apps, HIDE_OTHERS) PENDING(minimize_macos_window, MINIMIZE) PENDING(quit, QUIT) +PENDING(paste_from_clipboard, PASTE_FROM_CLIPBOARD) +PENDING(copy_or_noop, COPY_OR_NOOP) - (BOOL)validateMenuItem:(NSMenuItem *)item { if (item.action == @selector(toggle_macos_secure_keyboard_entry:)) { @@ -290,6 +292,21 @@ PENDING(quit, QUIT) item.action == @selector(detach_tab:)) { if (![NSApp keyWindow]) return NO; + } else if (item.action == @selector(paste_from_clipboard:)) { + if (![NSApp keyWindow]) return NO; + NSPasteboard *pb = [NSPasteboard generalPasteboard]; + if (![pb stringForType:NSPasteboardTypeString]) return NO; + } else if (item.action == @selector(copy_or_noop:)) { + if (![NSApp keyWindow]) return NO; + OSWindow *osw = current_os_window(); + if (osw && osw->num_tabs > osw->active_tab) { + Tab *tab = osw->tabs + osw->active_tab; + if (tab->num_windows > tab->active_window) { + Screen *screen = tab->windows[tab->active_window].render_data.screen; + if (screen && screen_has_selection(screen)) return YES; + } + } + return NO; } return YES; } @@ -323,6 +340,7 @@ typedef struct { GlobalShortcut toggle_macos_secure_keyboard_entry, toggle_fullscreen, open_kitty_website; GlobalShortcut hide_macos_app, hide_macos_other_apps, minimize_macos_window, quit, search_scrollback; GlobalShortcut macos_cycle_through_os_windows, macos_cycle_through_os_windows_backwards; + GlobalShortcut paste_from_clipboard, copy_or_noop; } GlobalShortcuts; static GlobalShortcuts global_shortcuts; @@ -342,6 +360,7 @@ cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) { else Q(open_kitty_website); else Q(hide_macos_app); else Q(hide_macos_other_apps); else Q(minimize_macos_window); else Q(quit); else Q(search_scrollback); else Q(macos_cycle_through_os_windows); else Q(macos_cycle_through_os_windows_backwards); + else Q(paste_from_clipboard); else Q(copy_or_noop); #undef Q if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; } int cocoa_mods; @@ -797,6 +816,9 @@ cocoa_create_global_menu(void) { MENU_ITEM(editMenu, @"Clear Screen", clear_screen); MENU_ITEM(editMenu, @"Clear Last Command", clear_last_command); MENU_ITEM(editMenu, @"Find", search_scrollback); + [editMenu addItem:[NSMenuItem separatorItem]]; + MENU_ITEM(editMenu, @"Copy", copy_or_noop); + MENU_ITEM(editMenu, @"Paste", paste_from_clipboard); [editMenu release]; NSMenuItem* windowMenuItem = diff --git a/kitty/main.py b/kitty/main.py index 3c0be0b48..f7860b82c 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -219,6 +219,12 @@ def set_cocoa_global_shortcuts(opts: Options) -> dict[str, SingleKey]: val = get_macos_shortcut_for(func_map, f'open_url {website_url()}', lookup_name='open_kitty_website') if val is not None: global_shortcuts['open_kitty_website'] = val + val = get_macos_shortcut_for(func_map, 'paste_from_clipboard') + if val is not None: + global_shortcuts['paste_from_clipboard'] = val + val = get_macos_shortcut_for(func_map, 'copy_or_noop') + if val is not None: + global_shortcuts['copy_or_noop'] = val return global_shortcuts