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
This commit is contained in:
Kovid Goyal 2020-12-21 21:39:05 +05:30
parent e97f1a4310
commit 5f8dee8384
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
10 changed files with 120 additions and 29 deletions

View file

@ -4,6 +4,13 @@ Changelog
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
To update |kitty|, :doc:`follow the instructions <binary>`.
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]
-------------------

View file

@ -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
----------------------

View file

@ -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

View file

@ -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:

View file

@ -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"

View file

@ -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);

View file

@ -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:

View file

@ -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);

View file

@ -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);

View file

@ -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)