diff --git a/docs/changelog.rst b/docs/changelog.rst index cc550b6e487..7b8a11deb52 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 0e177eb7228..e1ea0646be6 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 07abfd93c7d..c8c91e6241e 100644 --- a/kittens/tui/operations.py +++ b/kittens/tui/operations.py @@ -20,7 +20,8 @@ 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 27ce5b9a224..e6fadf791f8 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 394b0b1f05b..e04f9fc7612 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 b324334bc21..7de3bb8d4fb 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 d05b59b9806..d95b2201fbe 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 3810116b029..97ef0bc07fb 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 b99ac8ddba6..3d426907abc 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 859ac808d31..59ce4d0b526 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -716,3 +716,31 @@ def test_top_and_bottom_margin(self): 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)