diff --git a/docs/basic.rst b/docs/basic.rst index 37382909c..61969c897 100644 --- a/docs/basic.rst +++ b/docs/basic.rst @@ -11,9 +11,9 @@ windows are: Scrolling ~~~~~~~~~~~~~~ -======================== ======================= +========================= ======================= Action Shortcut -======================== ======================= +========================= ======================= Line up :sc:`scroll_line_up` (also :kbd:`⌥+⌘+⇞` and :kbd:`⌘+↑` on macOS) Line down :sc:`scroll_line_down` (also :kbd:`⌥+⌘+⇟` and :kbd:`⌘+↓` on macOS) Page up :sc:`scroll_page_up` (also :kbd:`⌘+⇞` on macOS) @@ -22,7 +22,9 @@ Top :sc:`scroll_home` (also :kbd:`⌘+↖` on macOS) Bottom :sc:`scroll_end` (also :kbd:`⌘+↘` on macOS) Previous shell prompt :sc:`scroll_to_previous_prompt` (see :ref:`shell_integration`) 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`) +========================= ======================= Tabs ~~~~~~~~~~~ diff --git a/docs/kittens/custom.rst b/docs/kittens/custom.rst index 2fa2b209a..0cadd9eae 100644 --- a/docs/kittens/custom.rst +++ b/docs/kittens/custom.rst @@ -92,7 +92,10 @@ This will send the plain text of the active window to the kitten's instead. If you want line wrap markers as well, use ``screen-ansi`` or just ``screen``. For the scrollback buffer as well, use ``history``, ``ansi-history`` or ``screen-history``. To get -the currently selected text, use ``selection``. +the currently selected text, use ``selection``. To get the output +of the last command run in the shell, use ``output`` or ``output-ansi`` +or ``output-screen-ansi``. Note that using ``output`` requires +:ref:`shell_integration`. Using kittens to script kitty, without any terminal UI diff --git a/kitty/boss.py b/kitty/boss.py index b66f568db..57a07b6cf 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -107,6 +107,10 @@ def data_for_at(w: Optional[Window], arg: str, add_wrap_markers: bool = False) - return as_text(as_ansi=True, alternate_screen=True) if arg == '@ansi_alternate_scrollback': return as_text(as_ansi=True, alternate_screen=True, add_history=True) + if arg == '@last_cmd_output': + return w.last_cmd_output(add_wrap_markers=add_wrap_markers) + if arg == '@ansi_last_cmd_output': + return w.last_cmd_output(as_ansi=True, add_wrap_markers=add_wrap_markers) return None @@ -1072,6 +1076,9 @@ class Boss: data = sel.encode('utf-8') if sel else None elif type_of_input is None: data = None + elif type_of_input in ('output', 'output-screen', 'output-screen-ansi', 'output-ansi'): + q = type_of_input.split('-') + data = w.last_cmd_output(as_ansi='ansi' in q, add_wrap_markers='screen' in q).encode('utf-8') else: raise ValueError('Unknown type_of_input: {}'.format(type_of_input)) else: diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 9e9a82290..29491567b 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1042,6 +1042,7 @@ class Screen: pass as_text_non_visual = as_text as_text_alternate = as_text + last_cmd_output = as_text def scroll_until_cursor(self) -> None: pass diff --git a/kitty/launch.py b/kitty/launch.py index 8c4ab2091..da1aa8724 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -122,14 +122,15 @@ computers (for example, over ssh) or as other users. --stdin-source type=choices default=none -choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback +choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback,@last_cmd_output Pass the screen contents as :code:`STDIN` to the child process. :code:`@selection` is the currently selected text. :code:`@screen` is the contents of the currently active window. :code:`@screen_scrollback` is the same as :code:`@screen`, but includes the scrollback buffer as well. :code:`@alternate` is the secondary screen of the current active window. For example if you run a full screen terminal application, the secondary screen will be the screen you return to when quitting the -application. +application. :code:`@last_cmd_output` is the output from the last command run in the shell, +this needs :ref:`shell_integration` to work. --stdin-add-formatting diff --git a/kitty/options/definition.py b/kitty/options/definition.py index c0dae4194..b5563fc0c 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -2919,6 +2919,16 @@ For more details on piping screen and buffer contents to external programs, see :doc:`launch`. ''' ) + +map('Browse output of the last shell command in less', + '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. +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 +''') egr() # }}} diff --git a/kitty/options/types.py b/kitty/options/types.py index 82345c1a2..540ef7964 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -718,6 +718,8 @@ defaults.map = [ KeyDefinition(False, KeyAction('scroll_to_prompt', (1,)), 1024, False, 100, ()), # show_scrollback KeyDefinition(False, KeyAction('show_scrollback'), 1024, False, 104, ()), + # show_last_command_output + KeyDefinition(False, KeyAction('show_last_command_output'), 1024, False, 103, ()), # new_window KeyDefinition(False, KeyAction('new_window'), 1024, False, 57345, ()), # new_os_window diff --git a/kitty/screen.c b/kitty/screen.c index 769ec52e4..67987d292 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2342,6 +2342,41 @@ as_text_alternate(Screen *self, PyObject *args) { return ans; } +typedef struct OutputOffset { + Screen *screen; + int start; +} OutputOffset; + +static Line* +get_line_from_offset(void *x, int y) { + OutputOffset *r = x; + return range_line_(r->screen, r->start + y); +} + +static PyObject* +last_cmd_output(Screen *self, PyObject *args) { + if (self->linebuf != self->main_linebuf) return PyUnicode_FromString(""); + + OutputOffset oo = {.screen=self}; + unsigned num_lines = 0; + int prompt_pos = self->cursor->y, y = self->cursor->y; + const int limit = -self->historybuf->count; + while (y >= limit) { + Line *line = range_line_(self, y); + if (line->is_prompt_start) prompt_pos = y; + if (line->is_output_start) { + oo.start = y; + num_lines = prompt_pos - y; + break; + } + y--; + } + if (y < limit) { + oo.start = limit; + num_lines = prompt_pos - limit; + } + return as_text_generic(args, &oo, get_line_from_offset, num_lines, &self->as_ansi_buf); +} static PyObject* @@ -3246,6 +3281,7 @@ static PyMethodDef methods[] = { MND(as_text, METH_VARARGS) MND(as_text_non_visual, METH_VARARGS) MND(as_text_alternate, METH_VARARGS) + MND(last_cmd_output, METH_VARARGS) MND(tab, METH_NOARGS) MND(backspace, METH_NOARGS) MND(linefeed, METH_NOARGS) diff --git a/kitty/window.py b/kitty/window.py index 403fa5fc5..0cd2a0b38 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -914,6 +914,11 @@ class Window: ) -> str: return as_text(self.screen, as_ansi, add_history, add_wrap_markers, alternate_screen, add_cursor) + def last_cmd_output(self, as_ansi: bool = False, add_wrap_markers: bool = False) -> str: + lines: List[str] = [] + self.screen.last_cmd_output(lines.append, as_ansi, add_wrap_markers) + return ''.join(lines) + @property def cwd_of_child(self) -> Optional[str]: return self.child.foreground_cwd or self.child.current_cwd @@ -942,6 +947,15 @@ class Window: data = self.pipe_data(text, has_wrap_markers=True) get_boss().display_scrollback(self, data['text'], data['input_line_number']) + def show_last_command_output(self) -> None: + ''' + @ac:cp: Show output from the last shell command in a pager like less + + Requires :ref:`shell_integration` to work + ''' + text = self.last_cmd_output(as_ansi=True, add_wrap_markers=True) + get_boss().display_scrollback(self, text, title='Last command output') + def paste_bytes(self, text: Union[str, bytes]) -> None: # paste raw bytes without any processing if isinstance(text, str): diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index c84606bb9..2938436d7 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -919,6 +919,9 @@ class TestScreen(BaseTest): def mark_prompt(): parse_bytes(s, '\033]133;A\007'.encode('ascii')) + def mark_output(): + parse_bytes(s, '\033]133;C\007'.encode('ascii')) + for i in range(4): mark_prompt() s.draw(f'$ {i}') @@ -945,3 +948,21 @@ class TestScreen(BaseTest): s.draw(str(i)) self.assertTrue(s.scroll_to_prompt()) self.ae(str(s.visual_line(0)), '$ 0') + + def lco(): + a = [] + s.last_cmd_output(a.append) + return ''.join(a) + + s = self.create_screen() + s.draw('abcd'), s.index(), s.carriage_return() + s.draw('12'), s.index(), s.carriage_return() + self.ae(lco(), 'abcd\n12') + s = self.create_screen() + mark_prompt(), s.draw('$ 0') + s.carriage_return(), s.index() + mark_output() + s.draw('abcd'), s.index(), s.carriage_return() + s.draw('12'), s.index(), s.carriage_return() + mark_prompt(), s.draw('$ 1') + self.ae(lco(), 'abcd\n12')