From 5f8dee8384dc50e59b7394157d5c5d352c4dc836 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 21 Dec 2020 21:39:05 +0530 Subject: [PATCH] Add support for the color settings stack that XTerm copied from us without acknowledgement and decided to use incompatible escape codes for. Completely in keeping with that project's past behavior. See https://github.com/kovidgoyal/kitty/issues/879 XTerm announcement: https://www.mail-archive.com/xorg@lists.x.org/msg06419.html --- docs/changelog.rst | 7 ++++++ docs/protocol-extensions.rst | 8 ++++++ kittens/tui/operations.py | 7 +++--- kitty/client.py | 15 ++++++++--- kitty/colors.c | 48 ++++++++++++++++++++++-------------- kitty/data-types.h | 5 ++-- kitty/parser.c | 21 ++++++++++++++-- kitty/screen.c | 9 +++++++ kitty/screen.h | 1 + kitty_tests/screen.py | 28 +++++++++++++++++++++ 10 files changed, 120 insertions(+), 29 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index cc550b6e4..7b8a11deb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,13 @@ Changelog |kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator. To update |kitty|, :doc:`follow the instructions `. +0.20.0 [future] +---------------------- + +- Add support for the color settings stack that XTerm copied from us without + acknowledgement and decided to use incompatible escape codes for. + + 0.19.3 [2020-12-19] ------------------- diff --git a/docs/protocol-extensions.rst b/docs/protocol-extensions.rst index 0e177eb72..e1ea0646b 100644 --- a/docs/protocol-extensions.rst +++ b/docs/protocol-extensions.rst @@ -199,6 +199,14 @@ These escape codes save/restore the colors, default background, default foreground, selection background, selection foreground and cursor color and the 256 colors of the ANSI color table. +.. note:: In July 2020, after several years, XTerm copied this protocol + extension, without acknowledgement, and using incompatible escape codes + (XTPUSHCOLORS, XTPOPCOLORS, XTREPORTCOLORS). And they decided to save not + just the dynamic colors but the entire ANSI color table. In the interests of + promoting interoperability, kitty added support for XTerm's escape codes as + well, and changed this extension to also save/restore the entire ANSI color + table. + Pasting to clipboard ---------------------- diff --git a/kittens/tui/operations.py b/kittens/tui/operations.py index 07abfd93c..c8c91e624 100644 --- a/kittens/tui/operations.py +++ b/kittens/tui/operations.py @@ -20,7 +20,8 @@ SAVE_CURSOR = '\0337' RESTORE_CURSOR = '\0338' SAVE_PRIVATE_MODE_VALUES = '\033[?s' RESTORE_PRIVATE_MODE_VALUES = '\033[?r' - +SAVE_COLORS = '\033[#P' +RESTORE_COLORS = '\033[#Q' MODES = dict( LNM=(20, ''), IRM=(4, ''), @@ -270,7 +271,7 @@ def init_state(alternate_screen: bool = True) -> str: reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') + reset_mode('MOUSE_SGR_MODE') + reset_mode('MOUSE_UTF8_MODE') + set_mode('BRACKETED_PASTE') + set_mode('EXTENDED_KEYBOARD') + - '\033]30001\033\\' + + SAVE_COLORS + '\033[*x' # reset DECSACE to default region select ) if alternate_screen: @@ -285,7 +286,7 @@ def reset_state(normal_screen: bool = True) -> str: ans += reset_mode('ALTERNATE_SCREEN') ans += RESTORE_PRIVATE_MODE_VALUES ans += RESTORE_CURSOR - ans += '\033]30101\033\\' + ans += RESTORE_COLORS return ans diff --git a/kitty/client.py b/kitty/client.py index 27ce5b9a2..e6fadf791 100644 --- a/kitty/client.py +++ b/kitty/client.py @@ -11,7 +11,6 @@ import sys from contextlib import suppress from typing import Any -from functools import partial CSI = '\033[' @@ -109,6 +108,18 @@ def screen_delete_characters(count: int) -> None: write(CSI + '%dP' % count) +def screen_push_colors(which: int) -> None: + write(CSI + '%d#P' % which) + + +def screen_pop_colors(which: int) -> None: + write(CSI + '%d#Q' % which) + + +def screen_report_colors() -> None: + write(CSI + '#R') + + def screen_insert_characters(count: int) -> None: write(CSI + '%d@' % count) @@ -193,8 +204,6 @@ def write_osc(code: int, string: str = '') -> None: set_dynamic_color = set_color_table_color = write_osc -screen_push_dynamic_colors = partial(write_osc, 30001) -screen_pop_dynamic_colors = partial(write_osc, 30101) def replay(raw: str) -> None: diff --git a/kitty/colors.c b/kitty/colors.c index 394b0b1f0..e04f9fc76 100644 --- a/kitty/colors.c +++ b/kitty/colors.c @@ -79,6 +79,7 @@ new(PyTypeObject *type, PyObject UNUSED *args, PyObject UNUSED *kwds) { static void dealloc(ColorProfile* self) { + if (self->color_stack) free(self->color_stack); Py_TYPE(self)->tp_free((PyObject*)self); } @@ -321,7 +322,6 @@ copy_color_table_to_buffer(ColorProfile *self, color_type *buf, int offset, size static void push_onto_color_stack_at(ColorProfile *self, unsigned int i) { self->color_stack[i].dynamic_colors = self->overridden; - self->color_stack[i].valid = true; memcpy(self->color_stack[i].color_table, self->color_table, sizeof(self->color_stack->color_table)); } @@ -333,18 +333,25 @@ copy_from_color_stack_at(ColorProfile *self, unsigned int i) { bool colorprofile_push_colors(ColorProfile *self, unsigned int idx) { + if (idx > 10) return false; + size_t sz = idx ? idx : self->color_stack_idx + 1; + sz = MIN(10u, sz); + if (self->color_stack_sz < sz) { + self->color_stack = realloc(self->color_stack, sz * sizeof(self->color_stack[0])); + if (self->color_stack == NULL) fatal("Out of memory while ensuring space for %zu elements in color stack", sz); + memset(self->color_stack + self->color_stack_sz, 0, (sz - self->color_stack_sz) * sizeof(self->color_stack[0])); + self->color_stack_sz = sz; + } if (idx == 0) { - for (unsigned i = 0; i < arraysz(self->color_stack); i++) { - if (!self->color_stack[i].valid) { - push_onto_color_stack_at(self, i); - return true; - } - } - memmove(self->color_stack, self->color_stack + 1, sizeof(self->color_stack) - sizeof(self->color_stack[0])); - push_onto_color_stack_at(self, arraysz(self->color_stack) - 1); + if (self->color_stack_idx >= self->color_stack_sz) { + memmove(self->color_stack, self->color_stack + 1, (self->color_stack_sz - 1) * sizeof(self->color_stack[0])); + idx = self->color_stack_sz - 1; + } else idx = self->color_stack_idx++; + push_onto_color_stack_at(self, idx); return true; } - if (idx < arraysz(self->color_stack)) { + idx -= 1; + if (idx < self->color_stack_sz) { push_onto_color_stack_at(self, idx); return true; } @@ -354,22 +361,25 @@ colorprofile_push_colors(ColorProfile *self, unsigned int idx) { bool colorprofile_pop_colors(ColorProfile *self, unsigned int idx) { if (idx == 0) { - for (unsigned i = arraysz(self->color_stack) - 1; i-- > 0; ) { - if (self->color_stack[i].valid) { - copy_from_color_stack_at(self, i); - self->color_stack[i].valid = false; - return true; - } - } - return false; + if (!self->color_stack_idx) return false; + copy_from_color_stack_at(self, --self->color_stack_idx); + memset(self->color_stack + self->color_stack_idx, 0, sizeof(self->color_stack[0])); + return true; } - if (idx < arraysz(self->color_stack)) { + idx -= 1; + if (idx < self->color_stack_sz) { copy_from_color_stack_at(self, idx); return true; } return false; } +void +colorprofile_report_stack(ColorProfile *self, unsigned int *idx, unsigned int *count) { + *count = self->color_stack_idx; + *idx = self->color_stack_idx ? self->color_stack_idx - 1 : 0; +} + static PyObject* color_table_address(ColorProfile *self, PyObject *a UNUSED) { #define color_table_address_doc "Pointer address to start of color table" diff --git a/kitty/data-types.h b/kitty/data-types.h index b324334bc..7de3bb8d4 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -241,7 +241,6 @@ typedef struct { typedef struct { DynamicColor dynamic_colors; uint32_t color_table[256]; - bool valid; } ColorStackEntry; typedef struct { @@ -250,7 +249,8 @@ typedef struct { bool dirty; uint32_t color_table[256]; uint32_t orig_color_table[256]; - ColorStackEntry color_stack[16]; + ColorStackEntry *color_stack; + unsigned int color_stack_idx, color_stack_sz; DynamicColor configured, overridden; color_type mark_foregrounds[MARK_MASK+1], mark_backgrounds[MARK_MASK+1]; } ColorProfile; @@ -312,6 +312,7 @@ float cursor_text_as_bg(ColorProfile *self); void copy_color_table_to_buffer(ColorProfile *self, color_type *address, int offset, size_t stride); bool colorprofile_push_colors(ColorProfile*, unsigned int); bool colorprofile_pop_colors(ColorProfile*, unsigned int); +void colorprofile_report_stack(ColorProfile*, unsigned int*, unsigned int*); void set_mouse_cursor(MouseShape); void enter_event(void); diff --git a/kitty/parser.c b/kitty/parser.c index d05b59b98..d95b2201f 100644 --- a/kitty/parser.c +++ b/kitty/parser.c @@ -439,7 +439,8 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) { case '*': \ case '\'': \ case ' ': \ - case '$': + case '$': \ + case '#': static inline void @@ -759,7 +760,23 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) { case DL: CALL_CSI_HANDLER1(screen_delete_lines, 1); case DCH: - CALL_CSI_HANDLER1(screen_delete_characters, 1); + if (end_modifier == '#' && !start_modifier) { + CALL_CSI_HANDLER1(screen_push_colors, 0); + } else { + CALL_CSI_HANDLER1(screen_delete_characters, 1); + } + case 'Q': + if (end_modifier == '#' && !start_modifier) { CALL_CSI_HANDLER1(screen_pop_colors, 0); } + REPORT_ERROR("Unknown CSI Q sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params); + break; + case 'R': + if (end_modifier == '#' && !start_modifier) { + REPORT_COMMAND(screen_report_color_stack); + screen_report_color_stack(screen); + break; + } + REPORT_ERROR("Unknown CSI R sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params); + break; case ECH: CALL_CSI_HANDLER1(screen_erase_characters, 1); case DA: diff --git a/kitty/screen.c b/kitty/screen.c index 3810116b0..97ef0bc07 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1603,6 +1603,15 @@ screen_pop_colors(Screen *self, unsigned int idx) { colorprofile_pop_colors(self->color_profile, idx); } +void +screen_report_color_stack(Screen *self) { + unsigned int idx, count; + colorprofile_report_stack(self->color_profile, &idx, &count); + char buf[128] = {0}; + snprintf(buf, arraysz(buf), "%u;%u#Q", idx, count); + write_escape_code_to_child(self, CSI, buf); +} + void screen_handle_print(Screen *self, PyObject *msg) { CALLBACK("handle_remote_print", "O", msg); diff --git a/kitty/screen.h b/kitty/screen.h index b99ac8ddb..3d426907a 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -181,6 +181,7 @@ void screen_change_charset(Screen *, uint32_t to); void screen_handle_cmd(Screen *, PyObject *cmd); void screen_push_colors(Screen *, unsigned int); void screen_pop_colors(Screen *, unsigned int); +void screen_report_color_stack(Screen *); void screen_handle_print(Screen *, PyObject *cmd); void screen_designate_charset(Screen *, uint32_t which, uint32_t as); void screen_use_latin1(Screen *, bool); diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 859ac808d..59ce4d0b5 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -716,3 +716,31 @@ class TestScreen(BaseTest): self.ae(str(s.linebuf), '0\n5\n6\n7\n\n') self.ae(str(s.historybuf), '') + + def test_color_stack(self): + s = self.create_screen() + c = s.callbacks + + def w(code): + return parse_bytes(s, ('\033[' + code).encode('ascii')) + + def ac(idx, count): + self.ae(c.wtcbuf, f'\033[{idx};{count}#Q'.encode('ascii')) + c.clear() + + w('#R') + ac(0, 0) + + w('#P') + w('#R') + ac(0, 1) + w('#10P') + w('#R') + ac(0, 1) + w('#Q') + w('#R') + ac(0, 0) + for i in range(20): + w('#P') + w('#R') + ac(9, 10)