diff --git a/docs/changelog.rst b/docs/changelog.rst index b547e7dc9..63f53a96d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -86,6 +86,9 @@ Detailed list of changes - Fix a regression in the previous release that broke :opt:`active_tab_foreground` (:iss:`4620`) +- Fix :ac:`show_last_command_output` not working when the output is stored + partially in the scrollback pager history buffer (:iss:`4435`) + - Improve CWD detection when there are multiple foreground processes in the TTY process group - A new option :opt:`narrow_symbols` to turn off opportunistic wide rendering of private use codepoints diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index c305d1569..9c9a97c84 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -987,7 +987,7 @@ class HistoryBuf: def as_text(self, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None: pass - def pagerhist_as_text(self) -> str: + def pagerhist_as_text(self, upto_output_start: bool = False) -> str: pass def pagerhist_as_bytes(self) -> bytes: @@ -1137,7 +1137,7 @@ class Screen: as_text_non_visual = as_text as_text_alternate = as_text - def cmd_output(self, which: int, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> None: + def cmd_output(self, which: int, callback: Callable[[str], None], as_ansi: bool, insert_wrap_markers: bool) -> bool: pass def scroll_until_cursor_prompt(self) -> None: diff --git a/kitty/history.c b/kitty/history.c index f431245ac..472c1180e 100644 --- a/kitty/history.c +++ b/kitty/history.c @@ -418,8 +418,21 @@ pagerhist_write(HistoryBuf *self, PyObject *what) { Py_RETURN_NONE; } +static const uint8_t* +reverse_find(const uint8_t *haystack, size_t haystack_sz, const uint8_t *needle) { + const size_t needle_sz = strlen((const char*)needle); + if (!needle_sz || needle_sz > haystack_sz) return NULL; + const uint8_t *p = haystack + haystack_sz - (needle_sz - 1); + while (--p >= haystack) { + if (*p == needle[0] && memcmp(p, needle, MIN(needle_sz, haystack_sz - (p - haystack))) == 0) return p; + } + return NULL; +} + static PyObject* -pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) { +pagerhist_as_bytes(HistoryBuf *self, PyObject *args) { + int upto_output_start = 0; + if (!PyArg_ParseTuple(args, "|p", &upto_output_start)) return NULL; #define ph self->pagerhist if (!ph || !ringbuf_bytes_used(ph->ringbuf)) return PyBytes_FromStringAndSize("", 0); pagerhist_ensure_start_is_valid_utf8(ph); @@ -433,6 +446,13 @@ pagerhist_as_bytes(HistoryBuf *self, PyObject *args UNUSED) { uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans); ringbuf_memcpy_from(buf, ph->ringbuf, sz); if (!l.attrs.continued) buf[sz-1] = '\n'; + if (upto_output_start) { + const uint8_t *p = reverse_find(buf, sz, (const uint8_t*)"\x1b]133;C\x1b\\"); + if (p) { + PyObject *t = PyBytes_FromStringAndSize((const char*)p, sz - (p - buf)); + Py_DECREF(ans); ans = t; + } + } return ans; #undef ph } @@ -501,8 +521,8 @@ static PyMethodDef methods[] = { METHOD(as_ansi, METH_O) METHODB(pagerhist_write, METH_O), METHODB(pagerhist_rewrap, METH_O), - METHODB(pagerhist_as_text, METH_NOARGS), - METHODB(pagerhist_as_bytes, METH_NOARGS), + METHODB(pagerhist_as_text, METH_VARARGS), + METHODB(pagerhist_as_bytes, METH_VARARGS), METHODB(as_text, METH_VARARGS), METHOD(dirty_lines, METH_NOARGS) METHOD(push, METH_VARARGS) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 9ea78d252..55eafcef8 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -326,7 +326,7 @@ Separate scrollback history size, used only for browsing the scrollback buffer (in MB). This separate buffer is not available for interactive scrolling but will be piped to the pager program when viewing scrollback buffer in a separate window. The current implementation stores the data in UTF-8, so approximatively -10000 lines per megabyte at 100 chars per line, for pure ASCII text, unformatted +10000 lines per megabyte at 100 chars per line, for pure ASCII, unformatted text. A value of zero or less disables this feature. The maximum allowed size is 4GB. Note that on config reload if this is changed it will only affect newly created windows, not existing ones. diff --git a/kitty/screen.c b/kitty/screen.c index a90ae3426..93841a74e 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2736,6 +2736,7 @@ typedef struct OutputOffset { Screen *screen; int start; unsigned num_lines; + bool reached_upper_limit; } OutputOffset; static Line* @@ -2788,7 +2789,10 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig } y1--; } - if (y1 < upward_limit) start = upward_limit; + if (y1 < upward_limit) { + oo->reached_upper_limit = true; + start = upward_limit; + } found_output = true; found_prompt = true; } @@ -2848,8 +2852,12 @@ cmd_output(Screen *self, PyObject *args) { PyErr_Format(PyExc_KeyError, "%u is not a valid type of command", which); return NULL; } - if (found) return as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf); - Py_RETURN_NONE; + if (found) { + DECREF_AFTER_FUNCTION PyObject *ret = as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf); + if (!ret) return NULL; + } + if (oo.reached_upper_limit && self->linebuf == self->main_linebuf && OPT(scrollback_pager_history_size) > 0) Py_RETURN_TRUE; + Py_RETURN_FALSE; } bool diff --git a/kitty/window.py b/kitty/window.py index a0650ef3f..31d65559d 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -157,6 +157,14 @@ def call_watchers(windowref: Callable[[], Optional['Window']], which: str, data: add_timer(callback, 0, False) +def pagerhist(screen: Screen, as_ansi: bool = False, add_wrap_markers: bool = True, upto_output_start: bool = False) -> str: + pht = screen.historybuf.pagerhist_as_text(upto_output_start) + if pht and (not as_ansi or not add_wrap_markers): + sanitizer = text_sanitizer(as_ansi, add_wrap_markers) + pht = sanitizer(pht) + return pht + + def as_text( screen: Screen, as_ansi: bool = False, @@ -186,13 +194,8 @@ def as_text( ctext += f'\x1b[{code} q' if add_history: - h: List[str] = [] - pht = screen.historybuf.pagerhist_as_text() - if pht: - h.append(pht) - if h and (not as_ansi or not add_wrap_markers): - sanitizer = text_sanitizer(as_ansi, add_wrap_markers) - h = list(map(sanitizer, h)) + pht = pagerhist(screen, as_ansi, add_wrap_markers) + h: List[str] = [pht] if pht else [] screen.historybuf.as_text(h.append, as_ansi, add_wrap_markers) if h: if not screen.linebuf.is_continued(0): @@ -1064,7 +1067,11 @@ class Window: def cmd_output(self, which: CommandOutput = CommandOutput.last_run, as_ansi: bool = False, add_wrap_markers: bool = False) -> str: lines: List[str] = [] - self.screen.cmd_output(which, lines.append, as_ansi, add_wrap_markers) + search_in_pager_hist = self.screen.cmd_output(which, lines.append, as_ansi, add_wrap_markers) + if search_in_pager_hist: + pht = pagerhist(self.screen, as_ansi, add_wrap_markers, True) + if pht: + lines.insert(0, pht) return ''.join(lines) @property diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index e42cd35ea..3fcbbf86f 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -5,6 +5,7 @@ from kitty.fast_data_types import ( DECAWM, DECCOLM, DECOM, IRM, Cursor, parse_bytes ) from kitty.marks import marker_from_function, marker_from_regex +from kitty.window import pagerhist from . import BaseTest @@ -998,7 +999,10 @@ class TestScreen(BaseTest): def lco(as_ansi=False): a = [] - s.cmd_output(0, a.append, as_ansi) + if s.cmd_output(0, a.append, as_ansi): + pht = pagerhist(s, as_ansi=as_ansi, upto_output_start=True) + if pht: + a.insert(0, pht) return ''.join(a) def fco(): @@ -1076,3 +1080,9 @@ class TestScreen(BaseTest): self.ae(lvco(), '0\n1\n2') s.scroll_to_prompt(1) self.ae(lvco(), '0x\n1x') + + # last command output from pager history + s = self.create_screen() + draw_prompt('p1') + draw_output(30) + self.ae(tuple(map(int, lco()[len('\x1b]133;C\x1b\\'):].split())), tuple(range(0, 30)))