Skip to content

Commit

Permalink
Add support for the color settings stack that XTerm copied from us wi…
Browse files Browse the repository at this point in the history
…thout acknowledgement and decided to use incompatible escape codes for.

Completely in keeping with that project's past behavior.
See #879

XTerm announcement:
https://www.mail-archive.com/xorg@lists.x.org/msg06419.html
  • Loading branch information
kovidgoyal committed Dec 21, 2020
1 parent e97f1a4 commit 5f8dee8
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 29 deletions.
7 changes: 7 additions & 0 deletions docs/changelog.rst
Expand Up @@ -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]
-------------------

Expand Down
8 changes: 8 additions & 0 deletions docs/protocol-extensions.rst
Expand Up @@ -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
----------------------
Expand Down
7 changes: 4 additions & 3 deletions kittens/tui/operations.py
Expand Up @@ -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, ''),
Expand Down Expand Up @@ -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:
Expand All @@ -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


Expand Down
15 changes: 12 additions & 3 deletions kitty/client.py
Expand Up @@ -11,7 +11,6 @@
import sys
from contextlib import suppress
from typing import Any
from functools import partial


CSI = '\033['
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down
48 changes: 29 additions & 19 deletions kitty/colors.c
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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));
}

Expand All @@ -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;
}
Expand All @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions kitty/data-types.h
Expand Up @@ -241,7 +241,6 @@ typedef struct {
typedef struct {
DynamicColor dynamic_colors;
uint32_t color_table[256];
bool valid;
} ColorStackEntry;

typedef struct {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
21 changes: 19 additions & 2 deletions kitty/parser.c
Expand Up @@ -439,7 +439,8 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
case '*': \
case '\'': \
case ' ': \
case '$':
case '$': \
case '#':


static inline void
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions kitty/screen.c
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions kitty/screen.h
Expand Up @@ -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);
Expand Down
28 changes: 28 additions & 0 deletions kitty_tests/screen.py
Expand Up @@ -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)

0 comments on commit 5f8dee8

Please sign in to comment.