diff --git a/docs/shell-integration.rst b/docs/shell-integration.rst index 26c6d37a5..e4f5a0431 100644 --- a/docs/shell-integration.rst +++ b/docs/shell-integration.rst @@ -74,29 +74,40 @@ on the output, define the following in :file:`kitty.conf`: .. code:: conf - mouse_map right press ungrabbed mouse_select_command_output + mouse_map right press ungrabbed mouse_select_command_output Now, when you right click on the output, the entire output is selected, ready to be copied. The feature to jump to previous prompts ( -:sc:`scroll_to_previous_prompt` and :sc:`scroll_to_next_prompt`) can be -integrated with browsing command output as well. For example, define the +:sc:`scroll_to_previous_prompt` and :sc:`scroll_to_next_prompt`) and mouse +actions (``mouse_select_command_output`` and ``mouse_show_command_output``) can +be integrated with browsing command output as well. For example, define the following mapping in :file:`kitty.conf`: .. code:: conf - map f1 show_last_visited_command_output + map f1 show_last_visited_command_output -Now, pressing :kbd:`F1` will cause the output of the last jumped to command to -be opened in a pager for easy browsing. +Now, pressing :kbd:`F1` will cause the output of the last jumped to command or +the last mouse clicked command output to be opened in a pager for easy browsing. + +In addition, You can define shortcut to get the first command output on screen. +For example, define the following in :file:`kitty.conf`: + +.. code:: conf + + map f1 show_first_command_output_on_screen + +Now, pressing :kbd:`F1` will cause the output of the first command output on +screen to be opened in a pager. You can also add shortcut to scroll to the last jumped position. For example, define the following in :file:`kitty.conf`: .. code:: conf - map f1 scroll_to_prompt 0 + map f1 scroll_to_prompt 0 How it works @@ -110,9 +121,9 @@ different shells. .. tab:: bash/zsh - For these shells, kitty adds a couple of lines to - the bottom of the shell's rc files (in an atomic manner) to load the shell - integration code. + For these shells, kitty adds a couple of lines to + the bottom of the shell's rc files (in an atomic manner) to load the shell + integration code. .. tab:: fish diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 2c2be9a9f..3d00f8a66 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1272,7 +1272,7 @@ def set_window_padding(os_window_id: int, tab_id: int, window_id: int, left: int def click_mouse_url(os_window_id: int, tab_id: int, window_id: int) -> bool: pass -def click_mouse_cmd_output(os_window_id: int, tab_id: int, window_id: int) -> bool: +def click_mouse_cmd_output(os_window_id: int, tab_id: int, window_id: int, select_cmd_output: bool) -> bool: pass def move_cursor_to_mouse_if_in_prompt(os_window_id: int, tab_id: int, window_id: int) -> bool: diff --git a/kitty/mouse.c b/kitty/mouse.c index 4867b84a2..65afe7ad6 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -424,6 +424,12 @@ mouse_open_url(Window *w) { return screen_open_url(screen); } +bool +mouse_set_last_visited_cmd_output(Window *w) { + Screen *screen = w->render_data.screen; + return screen_set_last_visited_prompt(screen, w->mouse_pos.cell_y); +} + bool mouse_select_cmd_output(Window *w) { Screen *screen = w->render_data.screen; diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 6e939ec45..4f2a81680 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -616,6 +616,13 @@ mma('Select line from point even when grabbed', mma('Extend the current selection even when grabbed', 'extend_selection_grabbed shift+right press ungrabbed,grabbed mouse_selection extend', ) + +mma('Show clicked command output in pager', + 'show_clicked_cmd_output_ungrabbed ctrl+shift+right press ungrabbed mouse_show_command_output', + long_text=''' +Requires :ref:`shell_integration` to work. +''' + ) egr() # }}} egr() # }}} @@ -3006,11 +3013,14 @@ Requires :ref:`shell_integration` to work. map('Scroll to next shell prompt', 'scroll_to_next_prompt kitty_mod+x scroll_to_prompt 1', long_text=''' +When the parameter is zero, scroll to the last jumped position, or the last clicked position by +command output related mouse actions. + Requires :ref:`shell_integration` to work. ''' ) -map('Browse scrollback buffer in less', +map('Browse scrollback buffer in pager', 'show_scrollback kitty_mod+h show_scrollback', long_text=''' You can pipe the contents of the current screen + history buffer as @@ -3024,17 +3034,27 @@ see :doc:`launch`. ''' ) -map('Browse output of the last shell command in less', +map('Browse output of the last shell command in pager', 'show_last_command_output kitty_mod+g show_last_command_output', long_text=''' -Requires :ref:`shell_integration` to work. You can pipe the output -of the last command run in the shell using the :doc:`launch` function. +You can also define additional shortcuts to get the command output. +For example, to get the first command output on screen:: + + map f1 show_first_command_output_on_screen + +To get the command output that was last accessed by a keyboard action or mouse action:: + + map f1 show_last_visited_command_output + +You can pipe the output of the last command run in the shell using the :doc:`launch` function. For example, the following opens the output in less in an overlay window:: map f1 launch --stdin-source=@last_cmd_output --stdin-add-formatting --type=overlay less +G -R To get the output of the first command on the screen, use :code:`@first_cmd_output_on_screen`. To get the output of the last jumped to command, use :code:`@last_visited_cmd_output`. + +Requires :ref:`shell_integration` to work. ''') egr() # }}} diff --git a/kitty/options/types.py b/kitty/options/types.py index 87689d3d5..63df3109a 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -951,4 +951,6 @@ defaults.mouse_map = [ MouseMapping(1, 1, 1, True, KeyAction('mouse_selection', (1,))), # extend_selection_grabbed MouseMapping(1, 1, 1, False, KeyAction('mouse_selection', (1,))), + # show_clicked_cmd_output_ungrabbed + MouseMapping(1, 5, 1, False, KeyAction('mouse_show_command_output')), ] diff --git a/kitty/screen.c b/kitty/screen.c index 9c048ffcd..02141bb77 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1269,7 +1269,7 @@ screen_cursor_to_column(Screen *self, unsigned int column) { linebuf_init_line(self->linebuf, bottom); \ historybuf_add_line(self->historybuf, self->linebuf->line, &self->as_ansi_buf); \ self->history_line_added_count++; \ - if (self->last_visited_prompt.is_set && self->last_visited_prompt.scrolled_by < self->historybuf->count) self->last_visited_prompt.scrolled_by++; \ + if (self->last_visited_prompt.is_set && self->last_visited_prompt.scrolled_by <= self->historybuf->count) self->last_visited_prompt.scrolled_by++; \ } \ linebuf_clear_line(self->linebuf, bottom, true); \ self->is_dirty = true; \ @@ -2024,7 +2024,7 @@ screen_history_scroll_to_prompt(Screen *self, int num_of_prompts_to_jump) { if (self->linebuf != self->main_linebuf) return false; unsigned int old = self->scrolled_by; if (num_of_prompts_to_jump == 0) { - if (!self->last_visited_prompt.is_set || self->last_visited_prompt.scrolled_by > self->historybuf->count) return false; + if (!self->last_visited_prompt.is_set || self->last_visited_prompt.scrolled_by > self->historybuf->count || self->last_visited_prompt.y >= self->lines) return false; self->scrolled_by = self->last_visited_prompt.scrolled_by; } else { int delta = num_of_prompts_to_jump < 0 ? -1 : 1; @@ -2041,8 +2041,7 @@ screen_history_scroll_to_prompt(Screen *self, int num_of_prompts_to_jump) { } #undef ensure_y_ok self->scrolled_by = y >= 0 ? 0 : -y; - self->last_visited_prompt.scrolled_by = self->scrolled_by; - self->last_visited_prompt.is_set = true; + screen_set_last_visited_prompt(self, 0); } if (old != self->scrolled_by) self->scroll_changed = true; return old != self->scrolled_by; @@ -2756,7 +2755,7 @@ cmd_output(Screen *self, PyObject *args) { break; case 2: // last visited cmd if (self->last_visited_prompt.scrolled_by <= self->historybuf->count && self->last_visited_prompt.is_set) { - found = find_cmd_output(self, &oo, 0, self->last_visited_prompt.scrolled_by, 0, false); + found = find_cmd_output(self, &oo, self->last_visited_prompt.y, self->last_visited_prompt.scrolled_by, 0, false); } break; default: PyErr_Format(PyExc_KeyError, "%u is not a valid type of command", which); @@ -2766,6 +2765,15 @@ cmd_output(Screen *self, PyObject *args) { Py_RETURN_NONE; } +bool +screen_set_last_visited_prompt(Screen *self, index_type y) { + if (y >= self->lines) return false; + self->last_visited_prompt.scrolled_by = self->scrolled_by; + self->last_visited_prompt.y = y; + self->last_visited_prompt.is_set = true; + return true; +} + bool screen_select_cmd_output(Screen *self, index_type y) { if (y >= self->lines) return false; diff --git a/kitty/screen.h b/kitty/screen.h index 25f090726..f557b71e2 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -146,6 +146,7 @@ typedef struct { } last_rendered_window_char; struct { unsigned int scrolled_by; + index_type y; bool is_set; } last_visited_prompt; } Screen; @@ -246,6 +247,7 @@ void set_active_hyperlink(Screen*, char*, char*); hyperlink_id_type screen_mark_hyperlink(Screen*, index_type, index_type); void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload); bool screen_open_url(Screen*); +bool screen_set_last_visited_prompt(Screen*, index_type); bool screen_select_cmd_output(Screen*, index_type); void screen_dirty_sprite_positions(Screen *self); void screen_rescale_images(Screen *self); diff --git a/kitty/state.c b/kitty/state.c index 1c0b5eb79..782b2b897 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -1091,12 +1091,13 @@ click_mouse_url(id_type os_window_id, id_type tab_id, id_type window_id) { } static bool -click_mouse_cmd_output(id_type os_window_id, id_type tab_id, id_type window_id) { - bool selected = false; +click_mouse_cmd_output(id_type os_window_id, id_type tab_id, id_type window_id, int select_cmd_output) { + bool handled = false; WITH_WINDOW(os_window_id, tab_id, window_id); - selected = mouse_select_cmd_output(window); + handled = mouse_set_last_visited_cmd_output(window); + if (select_cmd_output && handled) handled = mouse_select_cmd_output(window); END_WITH_WINDOW; - return selected; + return handled; } static bool @@ -1127,8 +1128,8 @@ PYWRAP1(click_mouse_url) { } PYWRAP1(click_mouse_cmd_output) { - id_type a, b, c; PA("KKK", &a, &b, &c); - if (click_mouse_cmd_output(a, b, c)) { Py_RETURN_TRUE; } + id_type a, b, c; int d; PA("KKKp", &a, &b, &c, &d); + if (click_mouse_cmd_output(a, b, c, d)) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } diff --git a/kitty/state.h b/kitty/state.h index 9eed55746..dbe7ea932 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -311,6 +311,7 @@ void update_os_window_title(OSWindow *os_window); void fake_scroll(Window *w, int amount, bool upwards); Window* window_for_window_id(id_type kitty_window_id); bool mouse_open_url(Window *w); +bool mouse_set_last_visited_cmd_output(Window *w); bool mouse_select_cmd_output(Window *w); bool move_cursor_to_mouse_if_at_shell_prompt(Window *w); void mouse_selection(Window *w, int code, int button); diff --git a/kitty/window.py b/kitty/window.py index 757919c14..a36b32166 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -1014,8 +1014,17 @@ class Window: Requires :ref:`shell_integration` to work ''') - def mouse_select_cmd_output(self) -> None: - click_mouse_cmd_output(self.os_window_id, self.tab_id, self.id) + def mouse_select_command_output(self) -> None: + click_mouse_cmd_output(self.os_window_id, self.tab_id, self.id, True) + + @ac('mouse', ''' + Show clicked command output in a pager like less + + Requires :ref:`shell_integration` to work + ''') + def mouse_show_command_output(self) -> None: + if click_mouse_cmd_output(self.os_window_id, self.tab_id, self.id, False): + self.show_cmd_output(CommandOutput.last_visited, 'Clicked command output') # }}} def text_for_selection(self) -> str: @@ -1087,15 +1096,18 @@ class Window: 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) + def show_cmd_output(self, which: CommandOutput, title: str = 'Command ouptut', 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) + text = text.replace('\r\n', '\n').replace('\r', '\n') + get_boss().display_scrollback(self, text, title=title, report_cursor=False) + @ac('cp', ''' Show output from the first shell command on screen in a pager like less Requires :ref:`shell_integration` to work ''') def show_first_command_output_on_screen(self) -> None: - text = self.cmd_output(CommandOutput.first_on_screen, as_ansi=True, add_wrap_markers=True) - text = text.replace('\r\n', '\n').replace('\r', '\n') - get_boss().display_scrollback(self, text, title='First command output on screen', report_cursor=False) + self.show_cmd_output(CommandOutput.first_on_screen, 'First command output on screen') @ac('cp', ''' Show output from the last shell command in a pager like less @@ -1103,19 +1115,16 @@ class Window: Requires :ref:`shell_integration` to work ''') def show_last_command_output(self) -> None: - text = self.cmd_output(CommandOutput.last_run, as_ansi=True, add_wrap_markers=True) - text = text.replace('\r\n', '\n').replace('\r', '\n') - get_boss().display_scrollback(self, text, title='Last command output', report_cursor=False) + self.show_cmd_output(CommandOutput.last_run, 'Last command output') @ac('cp', ''' - Show the first output below the last scrolled position via scroll_to_prompt in a pager like less + Show the first command output below the last scrolled position via scroll_to_prompt + or the last mouse clicked command output in a pager like less Requires :ref:`shell_integration` to work ''') def show_last_visited_command_output(self) -> None: - text = self.cmd_output(CommandOutput.last_visited, as_ansi=True, add_wrap_markers=True) - text = text.replace('\r\n', '\n').replace('\r', '\n') - get_boss().display_scrollback(self, text, title='Last visited command output', report_cursor=False) + self.show_cmd_output(CommandOutput.last_visited, 'Last visited command output') def paste_bytes(self, text: Union[str, bytes]) -> None: # paste raw bytes without any processing