Add a shortcut to easily browse the output of the last command run in the shell

This commit is contained in:
Kovid Goyal 2021-07-13 21:14:06 +05:30
parent 51fa25e03d
commit 07b971ad5f
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
10 changed files with 103 additions and 6 deletions

View File

@ -11,9 +11,9 @@ windows are:
Scrolling Scrolling
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
======================== ======================= ========================= =======================
Action Shortcut Action Shortcut
======================== ======================= ========================= =======================
Line up :sc:`scroll_line_up` (also :kbd:`⌥+⌘+⇞` and :kbd:`⌘+↑` on macOS) Line up :sc:`scroll_line_up` (also :kbd:`⌥+⌘+⇞` and :kbd:`⌘+↑` on macOS)
Line down :sc:`scroll_line_down` (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) 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) Bottom :sc:`scroll_end` (also :kbd:`⌘+↘` on macOS)
Previous shell prompt :sc:`scroll_to_previous_prompt` (see :ref:`shell_integration`) Previous shell prompt :sc:`scroll_to_previous_prompt` (see :ref:`shell_integration`)
Next shell prompt :sc:`scroll_to_next_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 Tabs
~~~~~~~~~~~ ~~~~~~~~~~~

View File

@ -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`` instead. If you want line wrap markers as well, use ``screen-ansi``
or just ``screen``. For the scrollback buffer as well, use or just ``screen``. For the scrollback buffer as well, use
``history``, ``ansi-history`` or ``screen-history``. To get ``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 Using kittens to script kitty, without any terminal UI

View File

@ -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) return as_text(as_ansi=True, alternate_screen=True)
if arg == '@ansi_alternate_scrollback': if arg == '@ansi_alternate_scrollback':
return as_text(as_ansi=True, alternate_screen=True, add_history=True) 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 return None
@ -1072,6 +1076,9 @@ class Boss:
data = sel.encode('utf-8') if sel else None data = sel.encode('utf-8') if sel else None
elif type_of_input is None: elif type_of_input is None:
data = 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: else:
raise ValueError('Unknown type_of_input: {}'.format(type_of_input)) raise ValueError('Unknown type_of_input: {}'.format(type_of_input))
else: else:

View File

@ -1042,6 +1042,7 @@ class Screen:
pass pass
as_text_non_visual = as_text as_text_non_visual = as_text
as_text_alternate = as_text as_text_alternate = as_text
last_cmd_output = as_text
def scroll_until_cursor(self) -> None: def scroll_until_cursor(self) -> None:
pass pass

View File

@ -122,14 +122,15 @@ computers (for example, over ssh) or as other users.
--stdin-source --stdin-source
type=choices type=choices
default=none 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 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 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 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 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 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 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 --stdin-add-formatting

View File

@ -2919,6 +2919,16 @@ For more details on piping screen and buffer contents to external programs,
see :doc:`launch`. 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() # }}} egr() # }}}

View File

@ -718,6 +718,8 @@ defaults.map = [
KeyDefinition(False, KeyAction('scroll_to_prompt', (1,)), 1024, False, 100, ()), KeyDefinition(False, KeyAction('scroll_to_prompt', (1,)), 1024, False, 100, ()),
# show_scrollback # show_scrollback
KeyDefinition(False, KeyAction('show_scrollback'), 1024, False, 104, ()), KeyDefinition(False, KeyAction('show_scrollback'), 1024, False, 104, ()),
# show_last_command_output
KeyDefinition(False, KeyAction('show_last_command_output'), 1024, False, 103, ()),
# new_window # new_window
KeyDefinition(False, KeyAction('new_window'), 1024, False, 57345, ()), KeyDefinition(False, KeyAction('new_window'), 1024, False, 57345, ()),
# new_os_window # new_os_window

View File

@ -2342,6 +2342,41 @@ as_text_alternate(Screen *self, PyObject *args) {
return ans; 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* static PyObject*
@ -3246,6 +3281,7 @@ static PyMethodDef methods[] = {
MND(as_text, METH_VARARGS) MND(as_text, METH_VARARGS)
MND(as_text_non_visual, METH_VARARGS) MND(as_text_non_visual, METH_VARARGS)
MND(as_text_alternate, METH_VARARGS) MND(as_text_alternate, METH_VARARGS)
MND(last_cmd_output, METH_VARARGS)
MND(tab, METH_NOARGS) MND(tab, METH_NOARGS)
MND(backspace, METH_NOARGS) MND(backspace, METH_NOARGS)
MND(linefeed, METH_NOARGS) MND(linefeed, METH_NOARGS)

View File

@ -914,6 +914,11 @@ class Window:
) -> str: ) -> str:
return as_text(self.screen, as_ansi, add_history, add_wrap_markers, alternate_screen, add_cursor) 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 @property
def cwd_of_child(self) -> Optional[str]: def cwd_of_child(self) -> Optional[str]:
return self.child.foreground_cwd or self.child.current_cwd 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) data = self.pipe_data(text, has_wrap_markers=True)
get_boss().display_scrollback(self, data['text'], data['input_line_number']) 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: def paste_bytes(self, text: Union[str, bytes]) -> None:
# paste raw bytes without any processing # paste raw bytes without any processing
if isinstance(text, str): if isinstance(text, str):

View File

@ -919,6 +919,9 @@ class TestScreen(BaseTest):
def mark_prompt(): def mark_prompt():
parse_bytes(s, '\033]133;A\007'.encode('ascii')) 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): for i in range(4):
mark_prompt() mark_prompt()
s.draw(f'$ {i}') s.draw(f'$ {i}')
@ -945,3 +948,21 @@ class TestScreen(BaseTest):
s.draw(str(i)) s.draw(str(i))
self.assertTrue(s.scroll_to_prompt()) self.assertTrue(s.scroll_to_prompt())
self.ae(str(s.visual_line(0)), '$ 0') 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')