diff --git a/docs/basic.rst b/docs/basic.rst index 6b631b3c4..ea048c321 100644 --- a/docs/basic.rst +++ b/docs/basic.rst @@ -25,6 +25,7 @@ Previous shell prompt :sc:`scroll_to_previous_prompt` (see :ref:`shell_int Next shell prompt :sc:`scroll_to_next_prompt` (see :ref:`shell_integration`) Browse scrollback in less :sc:`show_scrollback` Browse last cmd output :sc:`show_last_command_output` (see :ref:`shell_integration`) +Search scrollback in less :sc:`search_scrollback` (also :kbd:`⌘+F` on macOS) ========================= ======================= The scroll actions only take effect when the terminal is in the main screen. diff --git a/docs/changelog.rst b/docs/changelog.rst index 9015b47ed..0277346ef 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -177,6 +177,10 @@ Detailed list of changes - Do not rewrap the text in the alternate screen buffer. Avoids flicker during live resize with no :opt:`resize_debounce_time` (:disc:`9142`) +- Add a default mapping :ac:`search_scrollback` to open the scrollback in a + pager in search mode. If any text is currently selected it is automatically + searched for. + 0.44.0 [2025-11-03] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/boss.py b/kitty/boss.py index 8ad1c404e..f350f7508 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -3402,4 +3402,6 @@ class Boss: def ungrab_keyboard(self) -> None: grab_keyboard(False) - + def search_scrollback_in_active(self) -> None: + if w := self.active_window: + w.search_scrollback() diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 4ed473a19..d1f1fe805 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -1242,6 +1242,7 @@ 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[MACOS_CYCLE_THROUGH_OS_WINDOWS]) { call_boss(macos_cycle_through_os_windows, NULL); } if (cocoa_pending_actions[MACOS_CYCLE_THROUGH_OS_WINDOWS_BACKWARDS]) { call_boss(macos_cycle_through_os_windows_backwards, NULL); } + if (cocoa_pending_actions[SEARCH_SCROLLBACK]) { call_boss(search_scrollback_in_active, 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); } diff --git a/kitty/cocoa_window.h b/kitty/cocoa_window.h index b7587f989..69fad6b0b 100644 --- a/kitty/cocoa_window.h +++ b/kitty/cocoa_window.h @@ -32,6 +32,7 @@ typedef enum { TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY, MACOS_CYCLE_THROUGH_OS_WINDOWS, MACOS_CYCLE_THROUGH_OS_WINDOWS_BACKWARDS, + SEARCH_SCROLLBACK, TOGGLE_FULLSCREEN, OPEN_KITTY_WEBSITE, HIDE, diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index a2d533098..61ec80c7f 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -260,6 +260,7 @@ PENDING(reload_config, RELOAD_CONFIG) PENDING(toggle_macos_secure_keyboard_entry, TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY) PENDING(macos_cycle_through_os_windows, MACOS_CYCLE_THROUGH_OS_WINDOWS) PENDING(macos_cycle_through_os_windows_backwards, MACOS_CYCLE_THROUGH_OS_WINDOWS_BACKWARDS) +PENDING(search_scrollback, SEARCH_SCROLLBACK) PENDING(toggle_fullscreen, TOGGLE_FULLSCREEN) PENDING(open_kitty_website, OPEN_KITTY_WEBSITE) PENDING(hide_macos_app, HIDE) @@ -320,7 +321,7 @@ typedef struct { GlobalShortcut previous_tab, next_tab, new_tab, new_window, close_window, reset_terminal; GlobalShortcut clear_terminal_and_scrollback, clear_screen, clear_scrollback, clear_last_command; GlobalShortcut toggle_macos_secure_keyboard_entry, toggle_fullscreen, open_kitty_website; - GlobalShortcut hide_macos_app, hide_macos_other_apps, minimize_macos_window, quit; + 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; } GlobalShortcuts; static GlobalShortcuts global_shortcuts; @@ -339,7 +340,7 @@ cocoa_set_global_shortcut(PyObject *self UNUSED, PyObject *args) { else Q(clear_terminal_and_scrollback); else Q(clear_scrollback); else Q(clear_screen); else Q(clear_last_command); 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); + 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); #undef Q if (gs == NULL) { PyErr_SetString(PyExc_KeyError, "Unknown shortcut name"); return NULL; } @@ -795,6 +796,7 @@ cocoa_create_global_menu(void) { MENU_ITEM(editMenu, @"Clear Scrollback", clear_scrollback); MENU_ITEM(editMenu, @"Clear Screen", clear_screen); MENU_ITEM(editMenu, @"Clear Last Command", clear_last_command); + MENU_ITEM(editMenu, @"Find", search_scrollback); [editMenu release]; NSMenuItem* windowMenuItem = diff --git a/kitty/main.py b/kitty/main.py index 23ecfb824..3c0be0b48 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -194,7 +194,7 @@ def set_cocoa_global_shortcuts(opts: Options) -> dict[str, SingleKey]: 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', 'macos_cycle_through_os_windows', 'macos_cycle_through_os_windows_backwards', - 'hide_macos_app', 'hide_macos_other_apps', 'minimize_macos_window', 'quit'): + 'hide_macos_app', 'hide_macos_other_apps', 'minimize_macos_window', 'quit', 'search_scrollback'): 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 a5be71490..29e2ef2b7 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -3984,6 +3984,22 @@ To get the output of the last jumped to command, use :code:`@last_visited_cmd_ou Requires :ref:`shell integration ` to work. ''' ) + +map('Search the scrollback within a pager', + 'search_scrollback kitty_mod+/ search_scrollback', + long_text=''' +Search for currently selected text in the scrollback using the configured :opt:`scrollback_pager`. +Assumes that pressing the :kbd:`/` key triggers search mode in the pager. If you want to create +a manual mapping with a special pager for this, you can use something like: + + map f1 combine : launch --stdin-source=@screen_scrollback --stdin-add-formatting --type=overlay mypager : send_key / + +For more sophisticated control, such as using the current selection, use :ac:`remote_control_script`. +''') + +map('Search the scrollback within a pager', 'search_scrollback cmd+f search_scrollback', only='macos') + + egr() # }}} diff --git a/kitty/options/types.py b/kitty/options/types.py index aae01e95a..09d4164cc 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -849,6 +849,8 @@ defaults.map = [ KeyDefinition(trigger=SingleKey(mods=256, key=104), definition='show_scrollback'), # show_last_command_output KeyDefinition(trigger=SingleKey(mods=256, key=103), definition='show_last_command_output'), + # search_scrollback + KeyDefinition(trigger=SingleKey(mods=256, key=47), definition='search_scrollback'), # new_window KeyDefinition(trigger=SingleKey(mods=256, key=57345), definition='new_window'), # new_os_window @@ -982,6 +984,7 @@ if is_macos: defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57355), definition='scroll_page_down')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57356), definition='scroll_home')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57357), definition='scroll_end')) + defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=102), definition='search_scrollback')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=57345), definition='new_window')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=8, key=110), definition='new_os_window')) defaults.map.append(KeyDefinition(trigger=SingleKey(mods=9, key=100), definition='close_window')) diff --git a/kitty/window.py b/kitty/window.py index b77c17284..692a28770 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -2101,11 +2101,29 @@ class Window: # actions {{{ @ac('cp', 'Show scrollback in a pager like less') - def show_scrollback(self) -> None: + def show_scrollback(self) -> Optional['Window']: text = self.as_text(as_ansi=True, add_history=True, add_wrap_markers=True) data = self.pipe_data(text, has_wrap_markers=True) cursor_on_screen = self.screen.scrolled_by < self.screen.lines - self.screen.cursor.y - get_boss().display_scrollback(self, data['text'], data['input_line_number'], report_cursor=cursor_on_screen) + return get_boss().display_scrollback(self, data['text'], data['input_line_number'], report_cursor=cursor_on_screen) + + @ac('cp', ''' + Search scrollback in a pager like less. If there is selected text, it is automatically searched for. + Note that this assumes that pressing the / key triggers search mode in the page configured as the + scrollback pager. + ''') + def search_scrollback(self) -> None: + text = self.text_for_selection() + w = self.show_scrollback() + if w is not None: + w.send_key('/') + if text: + btext = text.encode() + sanitized = replace_c0_codes_except_nl_space_tab(btext) + if not w.screen.in_bracketed_paste_mode: + sanitized = sanitized.replace(b'\n', b'\x1bE') + w.screen.paste_bytes(sanitized) + w.send_key('enter') def show_cmd_output(self, which: CommandOutput, title: str = 'Command output', as_ansi: bool = True, add_wrap_markers: bool = True) -> None: text = self.cmd_output(which, as_ansi=as_ansi, add_wrap_markers=add_wrap_markers)