diff --git a/docs/changelog.rst b/docs/changelog.rst index 27ec9bd3d..b14448797 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -39,6 +39,9 @@ Detailed list of changes - A new command :command:`edit-in-kitty` to :ref:`edit_file` +- Allow getting the last non-empty command output easily via an action or + remote control (:pull:`4973`) + - Fix a bug that caused :opt:`macos_colorspace` to always be ``default`` regardless of its actual value (:iss:`5129`) - ssh kitten: Fix bash not being executed as a login shell since kitty 0.25.0 (:iss:`5130`) diff --git a/kitty/lineops.h b/kitty/lineops.h index 9c654c4fe..5f2a31a9f 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -73,6 +73,13 @@ left_shift_line(Line *line, index_type at, index_type num) { } } +static inline bool +line_is_empty(const Line *line) { + for (index_type i = 0; i < line->xnum; i++) { + if (line->cpu_cells[i].ch != BLANK_CHAR) return false; + } + return true; +} typedef Line*(get_line_func)(void *, int); void line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch); diff --git a/kitty/rc/get_text.py b/kitty/rc/get_text.py index 401867c48..c49d947c4 100644 --- a/kitty/rc/get_text.py +++ b/kitty/rc/get_text.py @@ -30,14 +30,15 @@ class GetText(RemoteCommand): options_spec = MATCH_WINDOW_OPTION + '''\n --extent default=screen -choices=screen, all, selection, first_cmd_output_on_screen, last_cmd_output, last_visited_cmd_output +choices=screen, all, selection, first_cmd_output_on_screen, last_cmd_output, last_visited_cmd_output, last_non_empty_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. :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. +the first command output below the last scrolled position via scroll_to_prompt. +:code:`last_non_empty_output` is the output from the last command run in the window that had +some non empty output. The last four require :ref:`shell_integration` to be enabled. --ansi @@ -99,6 +100,12 @@ Get text from the window this command is run in, rather than the active window. as_ansi=bool(payload_get('ansi')), add_wrap_markers=bool(payload_get('wrap_markers')), ) + elif payload_get('extent') == 'last_non_empty_output': + ans = window.cmd_output( + CommandOutput.last_non_empty, + as_ansi=bool(payload_get('ansi')), + add_wrap_markers=bool(payload_get('wrap_markers')), + ) elif payload_get('extent') == 'last_visited_cmd_output': ans = window.cmd_output( CommandOutput.last_visited, diff --git a/kitty/screen.c b/kitty/screen.c index ad13e38f0..0bf587f3f 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2859,6 +2859,30 @@ cmd_output(Screen *self, PyObject *args) { if (self->last_visited_prompt.scrolled_by <= self->historybuf->count && self->last_visited_prompt.is_set) { found = find_cmd_output(self, &oo, self->last_visited_prompt.y, self->last_visited_prompt.scrolled_by, 0, false); } break; + case 3: { // last non-empty output + int y = self->cursor->y; + Line *line; + bool reached_upper_limit = false; + while (!found && !reached_upper_limit) { + line = checked_range_line(self, y); + if (!line || (line->attrs.prompt_kind == OUTPUT_START && !line->attrs.continued)) { + int start = line ? y : y + 1; reached_upper_limit = !line; + int y2 = start; unsigned int num_lines = 0; + bool found_content = false; + while ((line = checked_range_line(self, y2)) && line->attrs.prompt_kind != PROMPT_START) { + if (!found_content) found_content = !line_is_empty(line); + num_lines++; y2++; + } + if (found_content) { + found = true; + oo.reached_upper_limit = reached_upper_limit; + oo.start = start; oo.num_lines = num_lines; + break; + } + } + y--; + } + } break; default: PyErr_Format(PyExc_KeyError, "%u is not a valid type of command", which); return NULL; diff --git a/kitty/window.py b/kitty/window.py index de178d624..dc21181dc 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -164,7 +164,7 @@ class DynamicColor(IntEnum): class CommandOutput(IntEnum): - last_run, first_on_screen, last_visited = 0, 1, 2 + last_run, first_on_screen, last_visited, last_non_empty = 0, 1, 2, 3 DYNAMIC_COLOR_CODES = { @@ -1460,6 +1460,14 @@ class Window: def show_last_visited_command_output(self) -> None: self.show_cmd_output(CommandOutput.last_visited, 'Last visited command output') + @ac('cp', ''' + Show the last non-empty output from a shell command in a pager like less + + Requires :ref:`shell_integration` to work + ''') + def show_last_non_empty_command_output(self) -> None: + self.show_cmd_output(CommandOutput.last_non_empty, 'Last non-empty command output') + @ac('cp', 'Paste the specified text into the current window') def paste(self, text: str) -> None: self.paste_with_actions(text) diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index eb928f99d..09adbe5dd 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -1009,9 +1009,9 @@ class TestScreen(BaseTest): self.assertTrue(s.scroll_to_prompt()) self.ae(str(s.visual_line(0)), '$ 1') - def lco(as_ansi=False): + def lco(as_ansi=False, which=0): a = [] - if s.cmd_output(0, a.append, as_ansi): + if s.cmd_output(which, a.append, as_ansi): pht = pagerhist(s, as_ansi=as_ansi, upto_output_start=True) if pht: a.insert(0, pht) @@ -1098,3 +1098,11 @@ class TestScreen(BaseTest): draw_prompt('p1') draw_output(30) self.ae(tuple(map(int, lco().split())), tuple(range(0, 30))) + + # last non empty command output + draw_prompt('a'), draw_output(2, 'a') + draw_prompt('b'), mark_output() + self.ae(lco(), '') + self.ae(lco(which=3), '0a\n1a') + s.draw('running'), s.index(), s.carriage_return() + self.ae(lco(which=3), 'running\n')