diff --git a/docs/kittens/custom.rst b/docs/kittens/custom.rst index 0cadd9eae..ae9886578 100644 --- a/docs/kittens/custom.rst +++ b/docs/kittens/custom.rst @@ -93,8 +93,13 @@ 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``. To get the output +of the first command run in the shell on screen, use ``first-output`` +or ``first-output-ansi`` or ``first-output-screen-ansi``. 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 +or ``output-screen-ansi``. To get the first command output below the last +scrolled position via scroll_to_prompt, use ``last-visited-output`` or +``last-visited-output-ansi`` or ``last-visited-output-screen-ansi``. Note that +using ``first-output`` or ``output`` or ``last-visited-output`` requires :ref:`shell_integration`. diff --git a/kitty/boss.py b/kitty/boss.py index 0420be26c..051145c5d 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -110,10 +110,18 @@ 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 == '@first_cmd_output_on_screen': + return w.first_cmd_output_on_screen(add_wrap_markers=add_wrap_markers) + if arg == '@ansi_first_cmd_output_on_screen': + return w.first_cmd_output_on_screen(as_ansi=True, add_wrap_markers=add_wrap_markers) 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) + if arg == '@last_visited_cmd_output': + return w.last_visited_cmd_output(add_wrap_markers=add_wrap_markers) + if arg == '@ansi_last_visited_cmd_output': + return w.last_visited_cmd_output(as_ansi=True, add_wrap_markers=add_wrap_markers) return None @@ -1314,9 +1322,15 @@ class Boss: data = sel.encode('utf-8') if sel else None elif type_of_input is None: data = None + elif type_of_input in ('first-output', 'first-output-screen', 'first-output-screen-ansi', 'first-output-ansi'): + q = type_of_input.split('-') + data = w.first_cmd_output_on_screen(as_ansi='ansi' in q, add_wrap_markers='screen' in q).encode('utf-8') 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') + elif type_of_input in ('last-visited-output', 'last-visited-output-screen', 'last-visited-output-screen-ansi', 'last-visited-output-ansi'): + q = type_of_input.split('-') + data = w.last_visited_cmd_output(as_ansi='ansi' in q, add_wrap_markers='screen' in q).encode('utf-8') else: raise ValueError(f'Unknown type_of_input: {type_of_input}') else: diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 5b63de36a..c7a83acc7 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1120,7 +1120,9 @@ class Screen: pass as_text_non_visual = as_text as_text_alternate = as_text + first_cmd_output_on_screen = as_text last_cmd_output = as_text + last_visited_cmd_output = as_text def scroll_until_cursor(self) -> None: pass diff --git a/kitty/launch.py b/kitty/launch.py index 494331b6f..5f83f2e73 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -123,15 +123,17 @@ computers (for example, over ssh) or as other users. --stdin-source type=choices default=none -choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback,@last_cmd_output +choices=none,@selection,@screen,@screen_scrollback,@alternate,@alternate_scrollback,@first_cmd_output_on_screen,@last_cmd_output,@last_visited_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. :code:`@last_cmd_output` is the output from the last command run in the shell, -this needs :ref:`shell_integration` to work. +secondary screen will be the screen you return to when quitting the application. +:code:`@first_cmd_output_on_screen` is the output from the first command run in the shell on screen, +:code:`@last_cmd_output` is the output from the last command run in the shell, +:code:`@last_visited_cmd_output` is the first output below the last scrolled position via +scroll_to_prompt, this three needs :ref:`shell_integration` to work. --stdin-add-formatting @@ -322,7 +324,8 @@ def launch( if opts.stdin_source != 'none': q = str(opts.stdin_source) if opts.stdin_add_formatting: - if q in ('@screen', '@screen_scrollback', '@alternate', '@alternate_scrollback'): + if q in ('@screen', '@screen_scrollback', '@alternate', '@alternate_scrollback', + '@first_cmd_output_on_screen', '@last_cmd_output', '@last_visited_cmd_output'): q = '@ansi_' + q[1:] if opts.stdin_add_line_wrap_markers: q += '_wrap' diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 73970fc3c..ec83b9153 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -3032,6 +3032,10 @@ 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 first command output on the screen, use :code:`@first_cmd_output_on_screen`. +To get the first command output below the last scrolled position via scroll_to_prompt, use +:code:`@last_visited_cmd_output`. ''') egr() # }}} diff --git a/kitty/rc/get_text.py b/kitty/rc/get_text.py index 06182ad51..f0c85b43b 100644 --- a/kitty/rc/get_text.py +++ b/kitty/rc/get_text.py @@ -17,7 +17,8 @@ class GetText(RemoteCommand): ''' match: The tab to focus - extent: One of :code:`screen`, :code:`last_cmd_output`, :code:`all`, or :code:`selection` + extent: One of :code:`screen`, :code:`first_cmd_output_on_screen`, :code:`last_cmd_output`, \ + :code:`last_visited_cmd_output`, :code:`all`, or :code:`selection` ansi: Boolean, if True send ANSI formatting codes cursor: Boolean, if True send cursor position/style as ANSI codes wrap_markers: Boolean, if True add wrap markers to output @@ -28,11 +29,14 @@ class GetText(RemoteCommand): options_spec = MATCH_WINDOW_OPTION + '''\n --extent default=screen -choices=screen, all, selection, last_cmd_output +choices=screen, all, selection, first_cmd_output_on_screen, last_cmd_output, last_visited_cmd_output What text to get. The default of :code:`screen` means all text currently on the screen. :code:`all` means all the screen+scrollback and :code:`selection` means the -currently selected text. Finally, :code:`last_cmd_output` means the output of the last -command that was run in the window, which requires :ref:`shell_integration` to be enabled. +currently selected text. :code:`first_cmd_output_on_screen` means the output of the first +command that was run in the window on screen. :code:`last_cmd_output` means +the output of the last command that was run in the window. :code:`last_visited_cmd_output` means +the first command output below the last scrolled position via scroll_to_prompt. The last three +requires :ref:`shell_integration` to be enabled. --ansi @@ -73,11 +77,21 @@ If specified get text from the window this command is run in, rather than the ac window = self.windows_for_match_payload(boss, window, payload_get)[0] if payload_get('extent') == 'selection': ans = window.text_for_selection() + elif payload_get('extent') == 'first_cmd_output_on_screen': + ans = window.first_cmd_output_on_screen( + as_ansi=bool(payload_get('ansi')), + add_wrap_markers=bool(payload_get('wrap_markers')), + ) elif payload_get('extent') == 'last_cmd_output': ans = window.last_cmd_output( as_ansi=bool(payload_get('ansi')), add_wrap_markers=bool(payload_get('wrap_markers')), ) + elif payload_get('extent') == 'last_visited_cmd_output': + ans = window.last_visited_cmd_output( + as_ansi=bool(payload_get('ansi')), + add_wrap_markers=bool(payload_get('wrap_markers')), + ) else: ans = window.as_text( as_ansi=bool(payload_get('ansi')), diff --git a/kitty/screen.c b/kitty/screen.c index b13d32802..69817e20c 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2646,6 +2646,7 @@ as_text_alternate(Screen *self, PyObject *args) { typedef struct OutputOffset { Screen *screen; int start; + unsigned num_lines; } OutputOffset; static Line* @@ -2654,37 +2655,122 @@ get_line_from_offset(void *x, int y) { return range_line_(r->screen, r->start + y); } +static bool +find_cmd_output(Screen *self, OutputOffset *oo, index_type start_y, unsigned int scrolled_by, int direction, bool on_screen_only) { + bool found_prompt = false, found_output = false, found_next_prompt = false; + int start = 0, end = 0; + int y1 = start_y - scrolled_by, y2 = y1; + const int upward_limit = -self->historybuf->count; + const int downward_limit = self->lines - 1; + const int screen_limit = -scrolled_by + downward_limit; + + // find around + if (direction == 0) { + Line *line = range_line_(self, y1); + if (line->attrs.prompt_kind == PROMPT_START) { + found_prompt = true; + // change direction to downwards to find command output + direction = 1; + } else if (line->attrs.prompt_kind == OUTPUT_START) { + found_output = true; start = y1; + found_prompt = true; + // keep finding the first output start upwards + } + y1--; y2++; + } + + // find upwards + if (direction <= 0) { + // find around: only needs to find the first output start + // find upwards: find prompt after the output, and the first output + while (y1 >= upward_limit) { + Line *line = range_line_(self, y1); + if (line->attrs.prompt_kind == PROMPT_START) { + if (direction == 0) { + // find around: stop at prompt start + start = y1 + 1; + break; + } + found_next_prompt = true; end = y1; + } else if (line->attrs.prompt_kind == OUTPUT_START) { + start = y1; + break; + } + y1--; + } + if (y1 < upward_limit) { + start = upward_limit; + } else { + // resizing screen can cause multiple consecutive output start lines, + // so find the first one + while (start > upward_limit) { + Line *line = range_line_(self, start - 1); + if (line->attrs.prompt_kind != OUTPUT_START) break; + start--; + } + } + found_output = true; + found_prompt = true; + } + + // find downwards + if (direction >= 0) { + while (y2 <= downward_limit) { + if (on_screen_only && !found_output && y2 > screen_limit) break; + Line *line = range_line_(self, y2); + if (line->attrs.prompt_kind == PROMPT_START) { + if (!found_prompt) found_prompt = true; + else if (found_output && !found_next_prompt) { + found_next_prompt = true; end = y2; + break; + } + } else if (line->attrs.prompt_kind == OUTPUT_START && found_prompt && !found_output) { + found_output = true; start = y2; + } + y2++; + } + } + + if (found_next_prompt) { + oo->num_lines = end - start; + } else if (found_output) { + oo->num_lines = (direction < 0 ? start_y : downward_limit) - start; + } else return false; + oo->start = start; + return true; +} + +static PyObject* +first_cmd_output_on_screen(Screen *self, PyObject *args) { + if (self->linebuf != self->main_linebuf) return PyUnicode_FromString(""); + + OutputOffset oo = {.screen=self}; + if (find_cmd_output(self, &oo, 0, self->scrolled_by, 1, true)) { + return as_text_generic(args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf); + } + return PyUnicode_FromString(""); +} + 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->attrs.prompt_kind == PROMPT_START) prompt_pos = y; - if (line->attrs.prompt_kind == OUTPUT_START) { - oo.start = y; - num_lines = prompt_pos - y; - break; - } - y--; + if (find_cmd_output(self, &oo, self->cursor->y, self->scrolled_by, -1, false)) { + return as_text_generic(args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf); } - if (y < limit) { - oo.start = limit; - num_lines = prompt_pos - limit; - } else { - // resizing screen can cause multiple consecutive output start lines, - // so find the first one - while (oo.start > limit) { - Line *line = range_line_(self, oo.start - 1); - if (line->attrs.prompt_kind != OUTPUT_START) break; - oo.start--; num_lines++; - } + return PyUnicode_FromString(""); +} + +static PyObject* +last_visited_cmd_output(Screen *self, PyObject *args) { + if (self->linebuf != self->main_linebuf || self->last_visited_prompt_scrolled_by > self->historybuf->count) return PyUnicode_FromString(""); + + OutputOffset oo = {.screen=self}; + if (find_cmd_output(self, &oo, 0, self->last_visited_prompt_scrolled_by, 0, false)) { + return as_text_generic(args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf); } - return as_text_generic(args, &oo, get_line_from_offset, num_lines, &self->as_ansi_buf); + return PyUnicode_FromString(""); } @@ -3677,7 +3763,9 @@ static PyMethodDef methods[] = { MND(as_text, METH_VARARGS) MND(as_text_non_visual, METH_VARARGS) MND(as_text_alternate, METH_VARARGS) + MND(first_cmd_output_on_screen, METH_VARARGS) MND(last_cmd_output, METH_VARARGS) + MND(last_visited_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 e690c4d67..611ab4696 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -1041,11 +1041,21 @@ class Window: ) -> str: return as_text(self.screen, as_ansi, add_history, add_wrap_markers, alternate_screen, add_cursor) + def first_cmd_output_on_screen(self, as_ansi: bool = False, add_wrap_markers: bool = False) -> str: + lines: List[str] = [] + self.screen.first_cmd_output_on_screen(lines.append, as_ansi, add_wrap_markers) + return ''.join(lines) + 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) + def last_visited_cmd_output(self, as_ansi: bool = False, add_wrap_markers: bool = False) -> str: + lines: List[str] = [] + self.screen.last_visited_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 @@ -1075,6 +1085,16 @@ 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) + @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.first_cmd_output_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) + @ac('cp', ''' Show output from the last shell command in a pager like less @@ -1085,6 +1105,16 @@ class Window: text = text.replace('\r\n', '\n').replace('\r', '\n') get_boss().display_scrollback(self, text, title='Last command output', report_cursor=False) + @ac('cp', ''' + Show the first output below the last scrolled position via scroll_to_prompt in a pager like less + + Requires :ref:`shell_integration` to work + ''') + def show_last_visited_command_output(self) -> None: + text = self.last_visited_cmd_output(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) + 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 1ab8d54e5..9e0835198 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -957,6 +957,11 @@ class TestScreen(BaseTest): self.assertTrue(s.scroll_to_prompt()) self.ae(str(s.visual_line(0)), '$ 1') + def fco(): + a = [] + s.first_cmd_output_on_screen(a.append) + return ''.join(a) + def lco(): a = [] s.last_cmd_output(a.append) @@ -965,6 +970,7 @@ class TestScreen(BaseTest): s = self.create_screen() s.draw('abcd'), s.index(), s.carriage_return() s.draw('12'), s.index(), s.carriage_return() + self.ae(fco(), '') self.ae(lco(), 'abcd\n12') s = self.create_screen() mark_prompt(), s.draw('$ 0') @@ -973,4 +979,5 @@ class TestScreen(BaseTest): s.draw('abcd'), s.index(), s.carriage_return() s.draw('12'), s.index(), s.carriage_return() mark_prompt(), s.draw('$ 1') + self.ae(fco(), 'abcd\n12') self.ae(lco(), 'abcd\n12')