diff --git a/docs/changelog.rst b/docs/changelog.rst index f46a5904e..2ff322d65 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,10 @@ To update |kitty|, :doc:`follow the instructions `. 0.20.2 [future] ---------------------- +- A new protocol extension to :ref:`unscroll ` text from the + scrollback buffer onto the screen. Useful, for example, to restore + the screen after showing completions below the shell prompt. + - Linux: Fix binary kitty builds not able to load fonts in WOFF2 format (:iss:`3506`) diff --git a/docs/protocol-extensions.rst b/docs/protocol-extensions.rst index 2949833a9..5f41ee3ed 100644 --- a/docs/protocol-extensions.rst +++ b/docs/protocol-extensions.rst @@ -157,6 +157,37 @@ protocol extension, it can be disabled by specifying ``no-append`` to the :opt:`clipboard_control` setting. +.. _unscroll: + +Unscrolling the screen +----------------------- + +This is a small extension to the `SD (Pan up) escape code +`_ from the VT-420 terminal. The +``SD`` escape code normally causes the text on screen to scroll down by the +specified number of lines, with empty lines appearing at the top of the screen. +This extension allows the new lines to be filled in from the scrollback buffer +instead of being blank. + +The motivation for this is that many modern shells will show completions in a +block of lines under the cursor, this causes some of the on-screen text to be +lost even after the completion is completed, because it has scrolled off +screen. This escape code allows that text to be restored. + +The syntax of the escape code is identical to that of ``SD`` except that it has +a trailing ``+`` modifier. This is legal under the `ECMA 48 standard +`_ +and unused for any other purpose as far as I can tell. So for example, to +unscroll three lines, the escape code would be:: + + CSI 3 + T + +See `discussion here +`_. + +.. versionadded:: 0.20.2 + + .. _desktop_notifications: diff --git a/kitty/parser.c b/kitty/parser.c index b51cfdb70..93ccd2885 100644 --- a/kitty/parser.c +++ b/kitty/parser.c @@ -932,7 +932,13 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) { NO_MODIFIERS(end_modifier, ' ', "Select presentation directions escape code not implemented"); CALL_CSI_HANDLER1(screen_scroll, 1); case SD: - CALL_CSI_HANDLER1(screen_reverse_scroll, 1); + if (!start_modifier && end_modifier == '+') { + CALL_CSI_HANDLER1(screen_reverse_scroll_and_fill_from_scrollback, 1); + } else { + NO_MODIFIERS(start_modifier, 0, ""); + CALL_CSI_HANDLER1(screen_reverse_scroll, 1); + } + break; case DECSTR: if (end_modifier == '$') { // DECRQM diff --git a/kitty/screen.c b/kitty/screen.c index 283d400cf..024100adb 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1136,17 +1136,29 @@ screen_reverse_index(Screen *self) { } else screen_cursor_up(self, 1, false, -1); } -void -screen_reverse_scroll(Screen *self, unsigned int count) { +static void +_reverse_scroll(Screen *self, unsigned int count, bool fill_from_scrollback) { // Scroll the screen down by count lines, not moving the cursor count = MIN(self->lines, count); unsigned int top = self->margin_top, bottom = self->margin_bottom; - while (count > 0) { - count--; + fill_from_scrollback = fill_from_scrollback && self->linebuf == self->main_linebuf; + while (count-- > 0) { + if (fill_from_scrollback) historybuf_pop_line(self->historybuf, self->alt_linebuf->line); INDEX_DOWN; + if (fill_from_scrollback) linebuf_copy_line_to(self->main_linebuf, self->alt_linebuf->line, 0); } } +void +screen_reverse_scroll(Screen *self, unsigned int count) { + _reverse_scroll(self, count, false); +} + +void +screen_reverse_scroll_and_fill_from_scrollback(Screen *self, unsigned int count) { + _reverse_scroll(self, count, true); +} + void screen_carriage_return(Screen *self) { @@ -2888,6 +2900,15 @@ hyperlink_at(Screen *self, PyObject *args) { return Py_BuildValue("s", url); } +static PyObject* +reverse_scroll(Screen *self, PyObject *args) { + int fill_from_scrollback = 0; + unsigned int amt; + if (!PyArg_ParseTuple(args, "I|p", &amt, &fill_from_scrollback)) return NULL; + _reverse_scroll(self, amt, fill_from_scrollback); + Py_RETURN_NONE; +} + #define MND(name, args) {#name, (PyCFunction)name, args, #name}, #define MODEFUNC(name) MND(name, METH_NOARGS) MND(set_##name, METH_O) @@ -2911,6 +2932,7 @@ static PyMethodDef methods[] = { MND(hyperlinks_as_list, METH_NOARGS) MND(garbage_collect_hyperlink_pool, METH_NOARGS) MND(hyperlink_for_id, METH_O) + MND(reverse_scroll, METH_VARARGS) METHOD(current_char_width, METH_NOARGS) MND(insert_lines, METH_VARARGS) MND(delete_lines, METH_VARARGS) diff --git a/kitty/screen.h b/kitty/screen.h index 8b42855f6..a865cab81 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -157,6 +157,7 @@ void screen_reverse_index(Screen *self); void screen_index(Screen *self); void screen_scroll(Screen *self, unsigned int count); void screen_reverse_scroll(Screen *self, unsigned int count); +void screen_reverse_scroll_and_fill_from_scrollback(Screen *self, unsigned int count); void screen_reset(Screen *self); void screen_set_tab_stop(Screen *self); void screen_tab(Screen *self); diff --git a/kitty_tests/parser.py b/kitty_tests/parser.py index 70474844d..485dbfcc2 100644 --- a/kitty_tests/parser.py +++ b/kitty_tests/parser.py @@ -184,6 +184,9 @@ class TestParser(BaseTest): pb('\033[3 @', ('Shift left escape code not implemented',)) pb('\033[3 A', ('Shift right escape code not implemented',)) pb('\033[3;4 S', ('Select presentation directions escape code not implemented',)) + pb('\033[1T', ('screen_reverse_scroll', 1)) + pb('\033[T', ('screen_reverse_scroll', 1)) + pb('\033[+T', ('screen_reverse_scroll_and_fill_from_scrollback', 1)) def test_csi_code_rep(self): s = self.create_screen(8) diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 41a7ac79c..867a66e98 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -315,6 +315,12 @@ class TestScreen(BaseTest): def assert_lines(*lines): return self.ae(lines, tuple(str(s.line(i)) for i in range(s.lines))) + # test the reverse scroll function + s = prepare_screen(map(str, range(6))) + assert_lines('2', '3', '4', '5', '') + s.reverse_scroll(2, True) + assert_lines('0', '1', '2', '3', '4') + # Height increased, width unchanged → pull down lines to fill new space at the top s = prepare_screen(map(str, range(6))) assert_lines('2', '3', '4', '5', '')