Implement DECCARA

Ability to set text attributes/colors in arbitrary screen regions,
instead of working via the cursor. Note that kitty extends the original
DECCARA spec from the VT-510 to allow setting all supported SGR
attributes.
This commit is contained in:
Kovid Goyal 2017-12-17 18:03:39 +05:30
parent 2d62e10a7e
commit d48b76508b
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 239 additions and 43 deletions

View File

@ -45,33 +45,38 @@ cursor_reset_display_attrs(Cursor *self) {
self->decoration = 0; self->bold = false; self->italic = false; self->reverse = false; self->strikethrough = false;
}
static inline void
parse_color(unsigned int *params, unsigned int *i, unsigned int count, uint32_t *result) {
unsigned int attr;
uint8_t r, g, b;
if (*i < count) {
attr = params[(*i)++];
switch(attr) {
case 5:
if (*i < count) *result = (params[(*i)++] & 0xFF) << 8 | 1;
break;
case 2: \
if (*i < count - 2) {
/* Ignore the first parameter in a four parameter RGB */
/* sequence (unused color space id), see https://github.com/kovidgoyal/kitty/issues/227 */
if (*i < count - 3) (*i)++;
r = params[(*i)++] & 0xFF;
g = params[(*i)++] & 0xFF;
b = params[(*i)++] & 0xFF;
*result = r << 24 | g << 16 | b << 8 | 2;
}
break;
}
}
}
void
cursor_from_sgr(Cursor *self, unsigned int *params, unsigned int count) {
#define SET_COLOR(which) \
if (i < count) { \
attr = params[i++];\
switch(attr) { \
case 5: \
if (i < count) \
self->which = (params[i++] & 0xFF) << 8 | 1; \
break; \
case 2: \
if (i < count - 2) { \
/* Ignore the first parameter in a four parameter RGB */ \
/* sequence (unused color space id), see https://github.com/kovidgoyal/kitty/issues/227 */ \
if (i < count - 3) i++; \
r = params[i++] & 0xFF; \
g = params[i++] & 0xFF; \
b = params[i++] & 0xFF; \
self->which = r << 24 | g << 16 | b << 8 | 2; \
}\
break; \
} \
} \
break;
#define SET_COLOR(which) { parse_color(params, &i, count, &self->which); } break;
START_ALLOW_CASE_RANGE
unsigned int i = 0, attr;
uint8_t r, g, b;
if (!count) { params[0] = 0; count = 1; }
while (i < count) {
attr = params[i++];
@ -102,7 +107,6 @@ cursor_from_sgr(Cursor *self, unsigned int *params, unsigned int count) {
self->reverse = false; break;
case 29:
self->strikethrough = false; break;
START_ALLOW_CASE_RANGE
case 30 ... 37:
self->fg = ((attr - 30) << 8) | 1; break;
case 38:
@ -119,13 +123,86 @@ START_ALLOW_CASE_RANGE
self->fg = ((attr - 90 + 8) << 8) | 1; break;
case 100 ... 107:
self->bg = ((attr - 100 + 8) << 8) | 1; break;
END_ALLOW_CASE_RANGE
case DECORATION_FG_CODE:
SET_COLOR(decoration_fg);
SET_COLOR(decoration_fg);
case DECORATION_FG_CODE + 1:
self->decoration_fg = 0; break;
}
}
#undef SET_COLOR
END_ALLOW_CASE_RANGE
}
void
apply_sgr_to_cells(Cell *first_cell, unsigned int cell_count, unsigned int *params, unsigned int count) {
#define RANGE for(unsigned c = 0; c < cell_count; c++, cell++)
#define SET(shift) RANGE { cell->attrs |= (1 << shift); } break;
#define RESET(shift) RANGE { cell->attrs &= ~(1 << shift); } break;
#define SETM(val, mask, shift) { RANGE { cell->attrs &= ~(mask << shift); cell->attrs |= ((val) << shift); } break; }
#define SET_COLOR(which) { color_type color = 0; parse_color(params, &i, count, &color); if (color) { RANGE { cell->which = color; }} } break;
#define SIMPLE(which, val) RANGE { cell->which = (val); } break;
unsigned int i = 0, attr;
if (!count) { params[0] = 0; count = 1; }
while (i < count) {
Cell *cell = first_cell;
attr = params[i++];
switch(attr) {
case 0:
RANGE { cell->attrs &= WIDTH_MASK; cell->fg = 0; cell->bg = 0; cell->decoration_fg = 0; }
break;
case 1:
SET(BOLD_SHIFT);
case 3:
SET(ITALIC_SHIFT);
case 4:
if (i < count) { uint8_t val = MIN(3, params[i]); i++; SETM(val, DECORATION_MASK, DECORATION_SHIFT); }
else { SETM(1, DECORATION_MASK, DECORATION_SHIFT); }
case 7:
SET(REVERSE_SHIFT);
case 9:
SET(STRIKE_SHIFT);
case 21:
SETM(2, DECORATION_MASK, DECORATION_SHIFT);
case 22:
RESET(BOLD_SHIFT);
case 23:
RESET(ITALIC_SHIFT);
case 24:
SETM(0, DECORATION_MASK, DECORATION_SHIFT);
case 27:
RESET(REVERSE_SHIFT);
case 29:
RESET(STRIKE_SHIFT);
START_ALLOW_CASE_RANGE
case 30 ... 37:
SIMPLE(fg, ((attr - 30) << 8) | 1);
case 38:
SET_COLOR(fg);
case 39:
SIMPLE(fg, 0);
case 40 ... 47:
SIMPLE(bg, ((attr - 40) << 8) | 1);
case 48:
SET_COLOR(bg);
case 49:
SIMPLE(bg, 0);
case 90 ... 97:
SIMPLE(fg, ((attr - 90 + 8) << 8) | 1);
case 100 ... 107:
SIMPLE(bg, ((attr - 100 + 8) << 8) | 1);
END_ALLOW_CASE_RANGE
case DECORATION_FG_CODE:
SET_COLOR(decoration_fg);
case DECORATION_FG_CODE + 1:
SIMPLE(decoration_fg, 0);
}
}
#undef RESET
#undef SET_COLOR
#undef SET
#undef SETM
#undef RANGE
}
static inline int
@ -211,9 +288,9 @@ static PyMemberDef members[] = {
{"y", T_UINT, offsetof(Cursor, y), 0, "y"},
{"shape", T_INT, offsetof(Cursor, shape), 0, "shape"},
{"decoration", T_UBYTE, offsetof(Cursor, decoration), 0, "decoration"},
{"fg", T_ULONG, offsetof(Cursor, fg), 0, "fg"},
{"bg", T_ULONG, offsetof(Cursor, bg), 0, "bg"},
{"decoration_fg", T_ULONG, offsetof(Cursor, decoration_fg), 0, "decoration_fg"},
{"fg", T_UINT, offsetof(Cursor, fg), 0, "fg"},
{"bg", T_UINT, offsetof(Cursor, bg), 0, "bg"},
{"decoration_fg", T_UINT, offsetof(Cursor, decoration_fg), 0, "decoration_fg"},
{NULL} /* Sentinel */
};

View File

@ -176,7 +176,7 @@ typedef struct {
unsigned int x, y;
uint8_t decoration;
CursorShape shape;
unsigned long fg, bg, decoration_fg;
color_type fg, bg, decoration_fg;
} Cursor;
@ -260,6 +260,7 @@ Cursor* cursor_copy(Cursor*);
void cursor_copy_to(Cursor *src, Cursor *dest);
void cursor_reset_display_attrs(Cursor*);
void cursor_from_sgr(Cursor *self, unsigned int *params, unsigned int count);
void apply_sgr_to_cells(Cell *first_cell, unsigned int cell_count, unsigned int *params, unsigned int count);
const char* cursor_as_sgr(Cursor*, Cursor*);
double monotonic();

View File

@ -75,10 +75,11 @@ _report_error(PyObject *dump_callback, const char *fmt, ...) {
}
static void
_report_params(PyObject *dump_callback, const char *name, unsigned int *params, unsigned int count) {
_report_params(PyObject *dump_callback, const char *name, unsigned int *params, unsigned int count, Region *r) {
static char buf[MAX_PARAMS*3] = {0};
unsigned int i, p;
for(i = 0, p=0; i < count && p < MAX_PARAMS*3-20; i++) {
unsigned int i, p=0;
if (r) p += snprintf(buf + p, sizeof(buf) - 2, "%u %u %u %u ", r->top, r->left, r->bottom, r->right);
for(i = 0; i < count && p < MAX_PARAMS*3-20; i++) {
int n = snprintf(buf + p, MAX_PARAMS*3 - p, "%u ", params[i]);
if (n < 0) break;
p += n;
@ -107,7 +108,7 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
#define REPORT_DRAW(ch) \
Py_XDECREF(PyObject_CallFunction(dump_callback, "sC", "draw", ch)); PyErr_Clear();
#define REPORT_PARAMS(name, params, num) _report_params(dump_callback, #name, params, num_params)
#define REPORT_PARAMS(name, params, num, region) _report_params(dump_callback, name, params, num_params, region)
#define FLUSH_DRAW \
Py_XDECREF(PyObject_CallFunction(dump_callback, "sO", "draw", Py_None)); PyErr_Clear();
@ -395,13 +396,13 @@ repr_csi_params(unsigned int *params, unsigned int num_params) {
}
static inline void
parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, unsigned int *params, PyObject DUMP_UNUSED *dump_callback) {
parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, unsigned int *params, PyObject DUMP_UNUSED *dump_callback, const char *report_name DUMP_UNUSED, Region *region) {
enum State { START, NORMAL, MULTIPLE, COLOR, COLOR1, COLOR3 };
enum State state = START;
unsigned int num_params, num_start, i;
#define READ_PARAM { params[num_params++] = utoi(buf + num_start, i - num_start); }
#define SEND_SGR { REPORT_PARAMS(select_graphic_rendition, params, num_params); select_graphic_rendition(screen, params, num_params); state = START; num_params = 0; }
#define SEND_SGR { REPORT_PARAMS(report_name, params, num_params, region); select_graphic_rendition(screen, params, num_params, region); state = START; num_params = 0; }
for (i=0, num_start=0, num_params=0; i < num && num_params < MAX_PARAMS; i++) {
switch(buf[i]) {
@ -521,6 +522,40 @@ parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, unsigned int *params,
#undef SEND_SGR
}
static inline unsigned int
parse_region(Region *r, uint32_t *buf, unsigned int num) {
unsigned int i, start, params[8] = {0}, num_params=0;
for (i=0, start=0; i < num && num_params < 4; i++) {
switch(buf[i]) {
IS_DIGIT
break;
default:
if (i > start) params[num_params++] = utoi(buf + start, i - start);
else if (i == start && buf[i] == ';') params[num_params++] = 0;
start = i + 1;
break;
}
}
switch(num_params) {
case 0:
break;
case 1:
r->top = params[0];
break;
case 2:
r->top = params[0]; r->left = params[1];
break;
case 3:
r->top = params[0]; r->left = params[1]; r->bottom = params[2];
break;
default:
r->top = params[0]; r->left = params[1]; r->bottom = params[2]; r->right = params[3];
break;
}
return i;
}
static inline void
dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
#define CALL_CSI_HANDLER1(name, defval) \
@ -573,9 +608,18 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
buf++; num--;
}
if (code == SGR && !start_modifier) {
parse_sgr(screen, buf, num, params, dump_callback);
parse_sgr(screen, buf, num, params, dump_callback, "select_graphic_rendition", NULL);
return;
}
if (code == 'r' && !start_modifier && num > 0 && buf[num - 1] == '$') {
// DECCARA
Region r = {0};
unsigned int consumed = parse_region(&r, buf, --num);
num -= consumed; buf += consumed;
parse_sgr(screen, buf, num, params, dump_callback, "deccara", &r);
return;
}
if (num > 0) {
switch(buf[num-1]) {
CSI_SECONDARY
@ -656,6 +700,12 @@ dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
}
REPORT_ERROR("Unknown CSI r sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
break;
case 'x':
if (!start_modifier && end_modifier == '*') {
CALL_CSI_HANDLER1(screen_decsace, 0);
}
REPORT_ERROR("Unknown CSI x sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
break;
case DECSCUSR:
CALL_CSI_HANDLER1M(screen_set_cursor, 1);
case SU:

View File

@ -347,8 +347,36 @@ screen_alignment_display(Screen *self) {
}
void
select_graphic_rendition(Screen *self, unsigned int *params, unsigned int count) {
cursor_from_sgr(self->cursor, params, count);
select_graphic_rendition(Screen *self, unsigned int *params, unsigned int count, Region *region_) {
if (region_) {
Region region = *region_;
if (!region.top) region.top = 1;
if (!region.left) region.left = 1;
if (!region.bottom) region.bottom = self->lines;
if (!region.right) region.right = self->columns;
if (self->modes.mDECOM) {
region.top += self->margin_top; region.bottom += self->margin_top;
}
region.left -= 1; region.top -= 1; region.right -= 1; region.bottom -= 1; // switch to zero based indexing
if (self->modes.mDECSACE) {
index_type x = MIN(region.left, self->columns - 1);
index_type num = region.right >= x ? region.right - x + 1 : 0;
num = MIN(num, self->columns - x);
for (index_type y = region.top; y < MIN(region.bottom + 1, self->lines); y++) {
linebuf_init_line(self->linebuf, y);
apply_sgr_to_cells(self->linebuf->line->cells + x, num, params, count);
}
} else {
index_type x, num;
for (index_type y = region.top; y < MIN(region.bottom + 1, self->lines); y++) {
if (y == region.top) { x = MIN(region.left, self->columns - 1); num = self->columns - x; }
else if (y == region.bottom) { x = 0; num = MIN(region.right + 1, self->columns); }
else { x = 0; num = self->columns; }
linebuf_init_line(self->linebuf, y);
apply_sgr_to_cells(self->linebuf->line->cells + x, num, params, count);
}
}
} else cursor_from_sgr(self->cursor, params, count);
}
static inline void
@ -501,6 +529,11 @@ screen_set_mode(Screen *self, unsigned int mode) {
set_mode_from_const(self, mode, true);
}
void
screen_decsace(Screen *self, unsigned int val) {
self->modes.mDECSACE = val == 2 ? true : false;
}
void
screen_reset_mode(Screen *self, unsigned int mode) {
set_mode_from_const(self, mode, false);
@ -1390,7 +1423,7 @@ static PyObject*
_select_graphic_rendition(Screen *self, PyObject *args) {
unsigned int params[256] = {0};
for (int i = 0; i < PyTuple_GET_SIZE(args); i++) { params[i] = PyLong_AsUnsignedLong(PyTuple_GET_ITEM(args, i)); }
select_graphic_rendition(self, params, PyList_GET_SIZE(args));
select_graphic_rendition(self, params, PyList_GET_SIZE(args), NULL);
Py_RETURN_NONE;
}

View File

@ -12,7 +12,7 @@ typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } Scr
typedef struct {
bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM, mDECCKM,
mBRACKETED_PASTE, mFOCUS_TRACKING, mEXTENDED_KEYBOARD;
mBRACKETED_PASTE, mFOCUS_TRACKING, mEXTENDED_KEYBOARD, mDECSACE;
MouseTrackingMode mouse_tracking_mode;
MouseTrackingProtocol mouse_tracking_protocol;
bool eight_bit_controls; // S8C1T
@ -90,6 +90,7 @@ void screen_backtab(Screen *self, unsigned int);
void screen_clear_tab_stop(Screen *self, unsigned int how);
void screen_set_mode(Screen *self, unsigned int mode);
void screen_reset_mode(Screen *self, unsigned int mode);
void screen_decsace(Screen *self, unsigned int);
void screen_insert_characters(Screen *self, unsigned int count);
void screen_cursor_up(Screen *self, unsigned int count/*=1*/, bool do_carriage_return/*=false*/, int move_direction/*=-1*/);
void screen_set_cursor(Screen *self, unsigned int mode, uint8_t secondary);
@ -115,7 +116,7 @@ uint32_t* translation_table(uint32_t which);
void screen_request_capabilities(Screen *, char, PyObject *);
void screen_set_8bit_controls(Screen *, bool);
void report_device_attributes(Screen *self, unsigned int UNUSED mode, char start_modifier);
void select_graphic_rendition(Screen *self, unsigned int *params, unsigned int count);
void select_graphic_rendition(Screen *self, unsigned int *params, unsigned int count, Region*);
void report_device_status(Screen *self, unsigned int which, bool UNUSED);
void report_mode_status(Screen *self, unsigned int which, bool);
void screen_apply_selection(Screen *self, void *address, size_t size);

View File

@ -278,3 +278,37 @@ class TestParser(BaseTest):
e('s=', 'Malformed graphics control block, expecting an integer value')
e('s==', 'Malformed graphics control block, expecting an integer value for key: s')
e('s=1=', 'Malformed graphics control block, expecting a comma or semi-colon after a value, found: 0x3d')
def test_deccara(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
pb('\033[$r', ('deccara', '0 0 0 0 0 '))
pb('\033[;;;;4:3;38:5:10;48:2:1:2:3;1$r',
('deccara', '0 0 0 0 4 3 '), ('deccara', '0 0 0 0 38 5 10 '), ('deccara', '0 0 0 0 48 2 1 2 3 '), ('deccara', '0 0 0 0 1 '))
for y in range(s.lines):
line = s.line(y)
for x in range(s.columns):
c = line.cursor_from(x)
self.ae(c.bold, True)
self.ae(c.italic, False)
self.ae(c.decoration, 3)
self.ae(c.fg, (10 << 8) | 1)
self.ae(c.bg, (1 << 24 | 2 << 16 | 3 << 8 | 2))
self.ae(s.line(0).cursor_from(0).bold, True)
pb('\033[1;2;2;3;22;39$r', ('deccara', '1 2 2 3 22 '), ('deccara', '1 2 2 3 39 '))
self.ae(s.line(0).cursor_from(0).bold, True)
line = s.line(0)
for x in range(1, s.columns):
c = line.cursor_from(x)
self.ae(c.bold, False)
self.ae(c.fg, 0)
line = s.line(1)
for x in range(0, 3):
c = line.cursor_from(x)
self.ae(c.bold, False)
self.ae(line.cursor_from(3).bold, True)
pb('\033[2*x\033[3;2;4;3;34$r\033[*x', ('screen_decsace', 2), ('deccara', '3 2 4 3 34 '), ('screen_decsace', 0))
for y in range(2, 4):
line = s.line(y)
for x in range(s.columns):
self.ae(line.cursor_from(x).fg, (10 << 8 | 1) if x < 1 or x > 2 else (4 << 8) | 1)