Allow non-width multicell code to fill multiple cells using native width algo

This commit is contained in:
Kovid Goyal 2024-12-02 20:52:11 +05:30
parent 844d538e40
commit b1c80d212f
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
10 changed files with 302 additions and 163 deletions

View file

@ -140,7 +140,7 @@ def generate(
case PAYLOAD: {
sz = parser_buf_pos - pos;
payload_start = pos;
g.payload_sz = MAX(BUF_EXTRA, sz);
g.payload_sz = sz;
pos = parser_buf_pos;
} break;
'''

View file

@ -264,10 +264,10 @@ static void
pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) {
PagerHistoryBuf *ph = self->pagerhist;
if (!ph) return;
const GPUCell *prev_cell = NULL;
Line l = {.xnum=self->xnum, .text_cache=self->text_cache};
init_line(self, self->start_of_data, &l);
line_as_ansi(&l, as_ansi_buf, &prev_cell, 0, l.xnum, 0);
ANSILineState s = {.output_buf=as_ansi_buf};
line_as_ansi(&l, &s, 0, l.xnum, 0);
pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3);
if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) {
char line_end[2]; size_t num = 0;
@ -353,11 +353,10 @@ static PyObject*
as_ansi(HistoryBuf *self, PyObject *callback) {
#define as_ansi_doc "as_ansi(callback) -> The contents of this buffer as ANSI escaped text. callback is called with each successive line."
Line l = {.xnum=self->xnum, .text_cache=self->text_cache};
const GPUCell *prev_cell = NULL;
ANSIBuf output = {0};
ANSIBuf output = {0}; ANSILineState s = {.output_buf=&output};
for(unsigned int i = 0; i < self->count; i++) {
init_line(self, i, &l);
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
line_as_ansi(&l, &s, 0, l.xnum, 0);
if (!l.cpu_cells[l.xnum - 1].next_char_was_wrapped) {
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
output.buf[output.len++] = '\n';

View file

@ -455,11 +455,10 @@ as_ansi(LineBuf *self, PyObject *callback) {
Line l = {.xnum=self->xnum, .text_cache=self->text_cache};
// remove trailing empty lines
index_type ylimit = self->ynum - 1;
const GPUCell *prev_cell = NULL;
ANSIBuf output = {0};
ANSIBuf output = {0}; ANSILineState s = {.output_buf=&output};
do {
init_line(self, &l, self->line_map[ylimit]);
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
line_as_ansi(&l, &s, 0, l.xnum, 0);
if (output.len) break;
ylimit--;
} while(ylimit > 0);
@ -467,7 +466,7 @@ as_ansi(LineBuf *self, PyObject *callback) {
for(index_type i = 0; i <= ylimit; i++) {
bool output_newline = !linebuf_line_ends_with_continuation(self, i);
init_line(self, &l, self->line_map[i]);
line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0);
line_as_ansi(&l, &s, 0, l.xnum, 0);
if (output_newline) {
ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false);
output.buf[output.len++] = 10; // 10 = \n

View file

@ -53,50 +53,90 @@ nonnegative_integer_as_utf32(unsigned num, ANSIBuf *output) {
return num_digits;
}
static unsigned
write_multicell_ansi_prefix(const CPUCell *mcd, ANSIBuf *output) {
unsigned pos = output->len;
ensure_space_for(output, buf, output->buf[0], output->len + 128, capacity, 2048, false);
#define w(x) output->buf[output->len++] = x
w(0x1b); w(']');
for (unsigned i = 0; i < sizeof(xstr(TEXT_SIZE_CODE)) - 1; i++) w(xstr(TEXT_SIZE_CODE)[i]);
w(';');
if (mcd->width > 1) {
w('w'); w('='); nonnegative_integer_as_utf32(mcd->width, output); w(':');
}
if (mcd->scale > 1) {
w('s'); w('='); nonnegative_integer_as_utf32(mcd->scale, output); w(':');
}
if (mcd->subscale) {
w('S'); w('='); nonnegative_integer_as_utf32(mcd->subscale, output); w(':');
}
if (output->buf[output->len - 1] == ':') output->len--;
w(';');
#undef w
return output->len - pos;
static void
ensure_space_in_ansi_output_buf(ANSILineState *s, size_t extra) {
ensure_space_for(s->output_buf, buf, s->output_buf->buf[0], s->output_buf->len + extra, capacity, 2048, false);
}
static unsigned
text_in_cell_ansi(const CPUCell *c, TextCache *tc, ANSIBuf *output) {
unsigned n = 0;
if (c->is_multicell) {
if (c->x || c->y) return 0;
n = write_multicell_ansi_prefix(c, output);
write_multicell_ansi_prefix(ANSILineState *s, const CPUCell *mcd) {
ensure_space_in_ansi_output_buf(s, 128);
s->current_multicell_state = mcd;
s->escape_code_written = true;
unsigned pos = s->output_buf->len;
#define w(x) s->output_buf->buf[s->output_buf->len++] = x
w(0x1b); w(']');
for (unsigned i = 0; i < sizeof(xstr(TEXT_SIZE_CODE)) - 1; i++) w(xstr(TEXT_SIZE_CODE)[i]);
w(';');
w('w'); w('='); nonnegative_integer_as_utf32(mcd->width, s->output_buf); w(':');
if (mcd->scale > 1) {
w('s'); w('='); nonnegative_integer_as_utf32(mcd->scale, s->output_buf); w(':');
}
if (mcd->subscale) {
w('S'); w('='); nonnegative_integer_as_utf32(mcd->subscale, s->output_buf); w(':');
}
if (s->output_buf->buf[s->output_buf->len - 1] == ':') s->output_buf->len--;
w(';');
#undef w
return s->output_buf->len - pos;
}
static void
close_multicell(ANSILineState *s) {
if (s->current_multicell_state) {
ensure_space_in_ansi_output_buf(s, 1);
s->output_buf->buf[s->output_buf->len++] = '\a';
s->current_multicell_state = NULL;
}
}
static void
start_multicell_if_needed(ANSILineState *s, const CPUCell *c) {
if (!c->natural_width || c->scale > 1 || c->subscale || c->vertical_align) write_multicell_ansi_prefix(s, c);
}
static bool
multicell_is_continuation_of_previous(const CPUCell *prev, const CPUCell *curr) {
if (prev->scale != curr->scale || prev->subscale != curr->subscale || prev->vertical_align != curr->vertical_align) return false;
if (prev->natural_width) return curr->natural_width;
return prev->width == curr->width && !curr->natural_width;
}
static void
text_in_cell_ansi(ANSILineState *s, const CPUCell *c, TextCache *tc) {
if (c->is_multicell) {
if (c->x || c->y) return;
if (s->current_multicell_state) {
if (!multicell_is_continuation_of_previous(s->current_multicell_state, c)) {
close_multicell(s);
start_multicell_if_needed(s, c);
}
} else start_multicell_if_needed(s, c);
} else close_multicell(s);
size_t pos = s->output_buf->len;
if (c->ch_is_idx) {
n += tc_chars_at_index_ansi(tc, c->ch_or_idx, output);
tc_chars_at_index_ansi(tc, c->ch_or_idx, s->output_buf);
} else {
ensure_space_for(output, buf, output->buf[0], output->len + 2, capacity, 2048, false);
if (c->ch_or_idx) {
output->buf[output->len++] = c->ch_or_idx;
n += 1;
ensure_space_in_ansi_output_buf(s, 2);
s->output_buf->buf[s->output_buf->len++] = c->ch_or_idx;
}
if (s->output_buf->len > pos) {
switch (s->output_buf->buf[pos]) {
case 0: s->output_buf->buf[pos] = ' '; break;
case '\t': {
unsigned num_cells_to_skip_for_tab = 0, n = s->output_buf->len - pos;
if (n - pos > 1) {
num_cells_to_skip_for_tab = s->output_buf->buf[s->output_buf->len - n + 1];
s->output_buf->len -= n - 1;
}
const CPUCell *next = c + 1;
while (num_cells_to_skip_for_tab && pos + 1 < s->limit && cell_is_char(next, ' ')) {
num_cells_to_skip_for_tab--; pos++; next++;
}
} break;
}
}
if (c->is_multicell) {
output->buf[output->len++] = '\a';
n++;
}
return n;
}
@ -407,85 +447,87 @@ write_mark(const char *mark, ANSIBuf *output) {
}
static void
write_sgr_to_ansi_buf(ANSILineState *s, const char *val) {
close_multicell(s);
ensure_space_in_ansi_output_buf(s, 128);
s->escape_code_written = true;
write_sgr(val, s->output_buf);
}
static void
write_ch_to_ansi_buf(ANSILineState *s, char_type ch) {
close_multicell(s);
ensure_space_in_ansi_output_buf(s, 1);
s->output_buf->buf[s->output_buf->len++] = ch;
}
static void
write_hyperlink_to_ansi_buf(ANSILineState *s, hyperlink_id_type hid) {
close_multicell(s);
ensure_space_in_ansi_output_buf(s, 2256);
s->escape_code_written = true;
write_hyperlink(hid, s->output_buf);
}
static void
write_mark_to_ansi_buf(ANSILineState *s, const char *m) {
close_multicell(s);
ensure_space_in_ansi_output_buf(s, 64);
s->escape_code_written = true;
write_mark(m, s->output_buf);
}
bool
line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell, index_type start_at, index_type stop_before, char_type prefix_char) {
#define ENSURE_SPACE(extra) ensure_space_for(output, buf, output->buf[0], output->len + extra, capacity, 2048, false);
#define WRITE_SGR(val) { ENSURE_SPACE(128); escape_code_written = true; write_sgr(val, output); }
#define WRITE_CH(val) { ENSURE_SPACE(1); output->buf[output->len++] = val; }
#define WRITE_HYPERLINK(val) { ENSURE_SPACE(2256); escape_code_written = true; write_hyperlink(val, output); }
#define WRITE_MARK(val) { ENSURE_SPACE(64); escape_code_written = true; write_mark(val, output); }
bool escape_code_written = false;
output->len = 0;
index_type limit = MIN(stop_before, xlimit_for_line(self));
if (prefix_char) { WRITE_CH(prefix_char); }
line_as_ansi(Line *self, ANSILineState *s, index_type start_at, index_type stop_before, char_type prefix_char) {
s->output_buf->len = 0;
s->limit = MIN(stop_before, xlimit_for_line(self));
s->current_multicell_state = NULL;
s->escape_code_written = false;
if (prefix_char) write_ch_to_ansi_buf(s, prefix_char);
switch (self->attrs.prompt_kind) {
case UNKNOWN_PROMPT_KIND:
break;
case PROMPT_START:
WRITE_MARK("A");
break;
case SECONDARY_PROMPT:
WRITE_MARK("A;k=s");
break;
case OUTPUT_START:
WRITE_MARK("C");
break;
case PROMPT_START: write_mark_to_ansi_buf(s, "A"); break;
case SECONDARY_PROMPT: write_mark_to_ansi_buf(s, "A;k=s"); break;
case OUTPUT_START: write_mark_to_ansi_buf(s, "C"); break;
}
if (limit <= start_at) return escape_code_written;
if (s->limit <= start_at) return s->escape_code_written;
static const GPUCell blank_cell = { 0 };
GPUCell *cell;
if (*prev_cell == NULL) *prev_cell = &blank_cell;
if (s->prev_gpu_cell == NULL) s->prev_gpu_cell = &blank_cell;
const CellAttrs mask_for_sgr = {.val=SGR_MASK};
#define CMP_ATTRS (cell->attrs.val & mask_for_sgr.val) != ((*prev_cell)->attrs.val & mask_for_sgr.val)
#define CMP(x) cell->x != (*prev_cell)->x
#define CMP_ATTRS (cell->attrs.val & mask_for_sgr.val) != (s->prev_gpu_cell->attrs.val & mask_for_sgr.val)
#define CMP(x) (cell->x != s->prev_gpu_cell->x)
for (index_type pos=start_at; pos < limit; pos++) {
if (output->hyperlink_pool) {
hyperlink_id_type hid = self->cpu_cells[pos].hyperlink_id;
if (hid != output->active_hyperlink_id) {
WRITE_HYPERLINK(hid);
}
for (s->pos=start_at; s->pos < s->limit; s->pos++) {
if (s->output_buf->hyperlink_pool) {
hyperlink_id_type hid = self->cpu_cells[s->pos].hyperlink_id;
if (hid != s->output_buf->active_hyperlink_id) write_hyperlink_to_ansi_buf(s, hid);
}
cell = &self->gpu_cells[pos];
cell = &self->gpu_cells[s->pos];
if (CMP_ATTRS || CMP(fg) || CMP(bg) || CMP(decoration_fg)) {
const char *sgr = cell_as_sgr(cell, *prev_cell);
if (*sgr) WRITE_SGR(sgr);
const char *sgr = cell_as_sgr(cell, s->prev_gpu_cell);
if (*sgr) write_sgr_to_ansi_buf(s, sgr);
}
unsigned n = text_in_cell_ansi(self->cpu_cells + pos, self->text_cache, output);
if (output->buf[output->len - n] == 0) output->buf[output->len - n] = ' ';
if (output->buf[output->len - n] == '\t') {
unsigned num_cells_to_skip_for_tab = 0;
if (n > 1) {
num_cells_to_skip_for_tab = output->buf[output->len - n + 1];
output->len -= n - 1;
}
while (num_cells_to_skip_for_tab && pos + 1 < limit && cell_is_char(self->cpu_cells + pos + 1, ' ')) {
num_cells_to_skip_for_tab--; pos++;
}
}
*prev_cell = cell;
text_in_cell_ansi(s, self->cpu_cells + s->pos, self->text_cache);
s->prev_gpu_cell = cell;
}
return escape_code_written;
close_multicell(s);
return s->escape_code_written;
#undef CMP_ATTRS
#undef CMP
#undef WRITE_SGR
#undef WRITE_CH
#undef ENSURE_SPACE
#undef WRITE_HYPERLINK
#undef WRITE_MARK
}
static PyObject*
as_ansi(Line* self, PyObject *a UNUSED) {
#define as_ansi_doc "Return the line's contents with ANSI (SGR) escape codes for formatting"
const GPUCell *prev_cell = NULL;
ANSIBuf output = {0};
line_as_ansi(self, &output, &prev_cell, 0, self->xnum, 0);
ANSIBuf output = {0}; ANSILineState s = {.output_buf=&output};
line_as_ansi(self, &s, 0, self->xnum, 0);
PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
free(output.buf);
return ans;
@ -910,7 +952,7 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
RAII_PyObject(cr, PyUnicode_FromString("\r"));
RAII_PyObject(sgr_reset, PyUnicode_FromString("\x1b[m"));
if (nl == NULL || cr == NULL || sgr_reset == NULL) return NULL;
const GPUCell *prev_cell = NULL;
ANSILineState s = {.output_buf=ansibuf};
ansibuf->active_hyperlink_id = 0;
bool need_newline = false;
for (index_type y = 0; y < lines; y++) {
@ -920,11 +962,11 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t
if (as_ansi) {
// less has a bug where it resets colors when it sees a \r, so work
// around it by resetting SGR at the start of every line. This is
// pretty sad performance wise, but I guess it will remain till I
// get around to writing a nice pager kitten.
// pretty sad performance wise, but I guess it will remain as it
// makes writing pagers easier.
// see https://github.com/kovidgoyal/kitty/issues/2381
prev_cell = NULL;
line_as_ansi(line, ansibuf, &prev_cell, 0, line->xnum, 0);
s.prev_gpu_cell = NULL;
line_as_ansi(line, &s, 0, line->xnum, 0);
t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ansibuf->buf, ansibuf->len);
if (t && ansibuf->len > 0) APPEND(sgr_reset);
} else {

View file

@ -61,7 +61,7 @@ typedef union CPUCell {
char_type hyperlink_id: sizeof(hyperlink_id_type) * 8;
char_type next_char_was_wrapped : 1;
char_type is_multicell : 1;
char_type explicitly_set: 1;
char_type natural_width: 1;
char_type x : 8;
char_type y : 4;
char_type subscale: 2;
@ -107,6 +107,15 @@ typedef struct MultiCellCommand {
size_t payload_sz;
} MultiCellCommand;
typedef struct ANSILineOutput {
const GPUCell *prev_gpu_cell;
const CPUCell *current_multicell_state;
index_type pos, limit;
ANSIBuf *output_buf;
bool escape_code_written;
} ANSILineState;
Line* alloc_line(TextCache *text_cache);
void apply_sgr_to_cells(GPUCell *first_cell, unsigned int cell_count, int *params, unsigned int count, bool is_group);
const char* cell_as_sgr(const GPUCell *, const GPUCell *);

View file

@ -72,7 +72,7 @@ char_type line_get_char(Line *self, index_type at);
index_type line_url_start_at(Line *self, index_type x);
index_type line_url_end_at(Line *self, index_type x, bool, char_type, bool, bool, index_type);
bool line_startswith_url_chars(Line*, bool);
bool line_as_ansi(Line *self, ANSIBuf *output, const GPUCell**, index_type start_at, index_type stop_before, char_type prefix_char) __attribute__((nonnull));
bool line_as_ansi(Line *self, ANSILineState *s, index_type start_at, index_type stop_before, char_type prefix_char) __attribute__((nonnull));
unsigned int line_length(Line *self);
size_t cell_as_unicode_for_fallback(const ListOfChars *lc, Py_UCS4 *buf);
size_t cell_as_utf8_for_fallback(const ListOfChars *lc, char *buf);

View file

@ -148,7 +148,7 @@ static inline void parse_multicell_code(PS *self, uint8_t *parser_buf,
case PAYLOAD: {
sz = parser_buf_pos - pos;
payload_start = pos;
g.payload_sz = MAX(BUF_EXTRA, sz);
g.payload_sz = sz;
pos = parser_buf_pos;
} break;

View file

@ -902,6 +902,7 @@ draw_combining_char(Screen *self, text_loop_state *s, char_type ch) {
if (self->lc->chars[base_pos + 1] == VS16 && !cpu_cell->is_multicell && is_emoji_presentation_base(self->lc->chars[base_pos])) {
cpu_cell->is_multicell = true;
cpu_cell->width = 2;
cpu_cell->natural_width = true;
if (!cpu_cell->scale) cpu_cell->scale = 1;
if (xpos + 1 < self->columns) {
CPUCell *second = cp + xpos + 1;
@ -1075,7 +1076,7 @@ draw_text_loop(Screen *self, const uint32_t *chars, size_t num_chars, text_loop_
} else nuke_multicell_char_at(self, self->cursor->x + 1, self->cursor->y, true);
}
zero_cells(s, fc, s->gp + self->cursor->x);
*fc = (CPUCell){.ch_or_idx=ch, .is_multicell=true, .width=2, .scale=1};
*fc = (CPUCell){.ch_or_idx=ch, .is_multicell=true, .width=2, .scale=1, .natural_width=true};
*second = *fc; second->x = 1;
s->gp[self->cursor->x + 1] = s->gp[self->cursor->x];
self->cursor->x += 2;
@ -1133,12 +1134,12 @@ decode_utf8_safe_string(const uint8_t *src, size_t sz, uint32_t *dest) {
// dest must be an array of size at least sz
uint32_t codep = 0;
UTF8State state = 0, prev = UTF8_ACCEPT;
size_t i, d;
for (i = 0, d = 0; i < sz; i++) {
size_t i = 0, d = 0;
for (; i < sz; i++) {
switch(decode_utf8(&state, &codep, src[i])) {
case UTF8_ACCEPT:
// Ignore C0 and C1 chars
if (codep >= ' ' && !(codep >= DEL && codep <= 159)) dest[d++] = codep;
if (codep >= ' ' && !(DEL <= codep && codep <= 159)) dest[d++] = codep;
break;
case UTF8_REJECT:
state = UTF8_ACCEPT;
@ -1150,39 +1151,16 @@ decode_utf8_safe_string(const uint8_t *src, size_t sz, uint32_t *dest) {
return d;
}
void
screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const uint8_t *payload) {
screen_on_input(self);
if (!cmd->payload_sz) return;
ensure_space_for_chars(self->lc, cmd->payload_sz + 1);
self->lc->count = decode_utf8_safe_string(payload, cmd->payload_sz, self->lc->chars);
if (!self->lc->count) return;
index_type width = cmd->width;
if (!width) {
self->lc->chars[self->lc->count] = 0;
width = wcswidth_string(self->lc->chars);
}
if (!width) return;
CPUCell mcd = {
.width=MIN(width, 15u), .scale=MAX(1, MIN(cmd->scale, 15u)), .subscale=MIN(cmd->subscale, 3u),
.explicitly_set=1u, .vertical_align=MIN(cmd->vertical_align, 7u), .is_multicell=true
};
width = mcd.width * mcd.scale;
static void
handle_fixed_width_multicell_command(Screen *self, CPUCell mcd, const ListOfChars *lc) {
index_type width = mcd.width * mcd.scale;
index_type height = mcd.scale;
index_type max_height = self->margin_bottom - self->margin_top + 1;
if (width > self->columns || height > max_height) return;
PREPARE_FOR_DRAW_TEXT;
mcd.hyperlink_id = s.cc.hyperlink_id;
cell_set_chars(&mcd, self->text_cache, self->lc);
if (self->columns < self->cursor->x + width) {
if (self->modes.mDECAWM) {
continue_to_next_line(self);
} else {
self->cursor->x = self->columns - width;
CPUCell *cp = linebuf_cpu_cell_at(self->linebuf, self->cursor->x, self->cursor->y);
if (cp->is_multicell) replace_multicell_char_under_cursor_with_spaces(self);
}
}
cell_set_chars(&mcd, self->text_cache, lc);
move_cursor_past_multicell(self, width);
if (height > 1) {
index_type available_height = self->margin_bottom - self->cursor->y + 1;
if (height > available_height) {
@ -1196,12 +1174,12 @@ screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const
if (self->modes.mIRM) insert_characters(self, self->cursor->x, width, y, true);
}
}
nuke_multicell_char_intersecting_with(self, self->cursor->x, self->cursor->x + width, self->cursor->y, self->cursor->y + height, true);
for (index_type y = self->cursor->y; y < self->cursor->y + height; y++) {
linebuf_init_cells(self->linebuf, y, &s.cp, &s.gp);
linebuf_mark_line_dirty(self->linebuf, y);
mcd.x = 0; mcd.y = y - self->cursor->y;
for (index_type x = self->cursor->x; x < self->cursor->x + width; x++, mcd.x++) {
if (s.cp[x].is_multicell) nuke_multicell_char_at(self, x, y, s.cp[x].x + s.cp[x].y > 0);
s.cp[x] = mcd; s.gp[x] = s.g;
}
}
@ -1209,6 +1187,57 @@ screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const
self->is_dirty = true;
}
static void
handle_variable_width_multicell_command(Screen *self, CPUCell mcd, ListOfChars *lc) {
ensure_space_for_chars(lc, lc->count + 1); lc->chars[lc->count] = 0;
mcd.width = wcswidth_string(lc->chars);
if (!mcd.width) { lc->count = 0; return; }
handle_fixed_width_multicell_command(self, mcd, lc);
lc->count = 0;
}
void
screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const uint8_t *payload) {
screen_on_input(self);
if (!cmd->payload_sz) return;
ensure_space_for_chars(self->lc, cmd->payload_sz + 1);
self->lc->count = decode_utf8_safe_string(payload, cmd->payload_sz, self->lc->chars);
if (!self->lc->count) return;
CPUCell mcd = {
.width=MIN(cmd->width, 15u), .scale=MAX(1, MIN(cmd->scale, 15u)), .subscale=MIN(cmd->subscale, 3u),
.vertical_align=MIN(cmd->vertical_align, 7u), .is_multicell=true
};
if (mcd.width) handle_fixed_width_multicell_command(self, mcd, self->lc);
else {
RAII_ListOfChars(lc);
mcd.natural_width = true;
for (unsigned i = 0; i < self->lc->count; i++) {
char_type ch = self->lc->chars[i];
if (is_ignored_char(ch)) continue;
if (is_combining_char(ch)) {
if (is_flag_codepoint(ch)) {
if (lc.count == 1) {
if (is_flag_pair(lc.chars[0], ch)) {
lc.chars[lc.count++] = ch; handle_variable_width_multicell_command(self, mcd, &lc);
} else {
handle_variable_width_multicell_command(self, mcd, &lc); lc.chars[lc.count++] = ch;
}
} else {
handle_variable_width_multicell_command(self, mcd, &lc); lc.chars[lc.count++] = ch;
}
} else {
if (!lc.count) continue;
lc.chars[lc.count++] = ch;
}
} else {
if (lc.count) handle_variable_width_multicell_command(self, mcd, &lc);
lc.chars[lc.count++] = ch;
}
}
if (lc.count) handle_variable_width_multicell_command(self, mcd, &lc);
}
}
// }}}
// Graphics {{{
@ -3437,9 +3466,9 @@ ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st
RAII_PyObject(nl, PyUnicode_FromString("\n"));
if (!ans || !nl) return NULL;
ANSIBuf output = {0};
const GPUCell *prev_cell = NULL;
bool has_escape_codes = false;
bool need_newline = false;
ANSILineState s = {.output_buf=&output};
for (int i = 0, y = idata.y; y < limit; y++, i++) {
Line *line = range_line_(self, y);
XRange xr = xrange_for_iteration(&idata, y, line);
@ -3456,7 +3485,7 @@ ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st
}
}
}
if (line_as_ansi(line, &output, &prev_cell, xr.x, x_limit, prefix_char)) has_escape_codes = true;
if (line_as_ansi(line, &s, xr.x, x_limit, prefix_char)) has_escape_codes = true;
need_newline = insert_newlines && !line->cpu_cells[line->xnum-1].next_char_was_wrapped;
PyObject *t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len);
if (!t) return NULL;
@ -5177,7 +5206,7 @@ test_parse_written_data(Screen *screen, PyObject *args) {
static PyObject*
multicell_data_as_dict(CPUCell mcd) {
return Py_BuildValue("{sI sI sI sO sI}", "scale", (unsigned int)mcd.scale, "width", (unsigned int)mcd.width, "subscale", (unsigned int)mcd.subscale, "explicitly_set", mcd.explicitly_set ? Py_True : Py_False, "vertical_align", mcd.vertical_align);
return Py_BuildValue("{sI sI sI sO sI}", "scale", (unsigned int)mcd.scale, "width", (unsigned int)mcd.width, "subscale", (unsigned int)mcd.subscale, "natural_width", mcd.natural_width ? Py_True : Py_False, "vertical_align", mcd.vertical_align);
}
static PyObject*

View file

@ -540,7 +540,7 @@ dispatch_osc(PS *self, uint8_t *buf, size_t limit, bool is_extended_osc) {
DISPATCH_OSC_WITH_CODE(clipboard_control);
END_DISPATCH
case 66:
parse_multicell_code(self, buf + i, limit - 1);
parse_multicell_code(self, buf + i, limit - i);
break;
case 133:
#ifdef DUMP_COMMANDS

View file

@ -2,9 +2,9 @@
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
from kitty.fast_data_types import TEXT_SIZE_CODE
from kitty.fast_data_types import TEXT_SIZE_CODE, wcswidth
from . import BaseTest
from . import BaseTest, parse_bytes
from . import draw_multicell as multicell
@ -15,6 +15,11 @@ class TestMulticell(BaseTest):
def test_multicell(self: TestMulticell) -> None:
from kitty.tab_bar import as_rgb
from kitty.window import as_text
def as_ansi():
return as_text(s, as_ansi=True)
def ac(x_, y_, **assertions): # assert cell
cell = s.cpu_cells(y_, x_)
@ -39,7 +44,7 @@ def test_multicell(self: TestMulticell) -> None:
ae('subscale')
ae('vertical_align')
ae('text')
ae('explicitly_set')
ae('natural_width')
if 'cursor' in assertions:
self.ae(assertions['cursor'], (s.cursor.x, s.cursor.y), msg)
@ -85,21 +90,21 @@ def test_multicell(self: TestMulticell) -> None:
s.reset()
ac(0, 0, is_multicell=False)
multicell(s, 'a')
ac(0, 0, is_multicell=True, width=1, scale=1, subscale=0, x=0, y=0, text='a', explicitly_set=True, cursor=(1, 0))
ac(0, 0, is_multicell=True, width=1, scale=1, subscale=0, x=0, y=0, text='a', natural_width=True, cursor=(1, 0))
ac(0, 1, is_multicell=False), ac(1, 0, is_multicell=False), ac(1, 1, is_multicell=False)
s.draw('莊')
ac(0, 0, is_multicell=True, width=1, scale=1, subscale=0, x=0, y=0, text='a', explicitly_set=True)
ac(1, 0, is_multicell=True, width=2, scale=1, subscale=0, x=0, y=0, text='莊', explicitly_set=False, cursor=(3, 0))
ac(2, 0, is_multicell=True, width=2, scale=1, subscale=0, x=1, y=0, text='', explicitly_set=False)
ac(0, 0, is_multicell=True, width=1, scale=1, subscale=0, x=0, y=0, text='a', natural_width=True)
ac(1, 0, is_multicell=True, width=2, scale=1, subscale=0, x=0, y=0, text='莊', natural_width=True, cursor=(3, 0))
ac(2, 0, is_multicell=True, width=2, scale=1, subscale=0, x=1, y=0, text='', natural_width=True)
for x in range(s.columns):
ac(x, 1, is_multicell=False)
s.cursor.x = 0
multicell(s, 'a', width=2, scale=2, subscale=3)
ac(0, 0, is_multicell=True, width=2, scale=2, subscale=3, x=0, y=0, text='a', explicitly_set=True, cursor=(4, 0))
ac(0, 0, is_multicell=True, width=2, scale=2, subscale=3, x=0, y=0, text='a', natural_width=False, cursor=(4, 0))
for x in range(1, 4):
ac(x, 0, is_multicell=True, width=2, scale=2, subscale=3, x=x, y=0, text='', explicitly_set=True)
ac(x, 0, is_multicell=True, width=2, scale=2, subscale=3, x=x, y=0, text='', natural_width=False)
for x in range(0, 4):
ac(x, 1, is_multicell=True, width=2, scale=2, subscale=3, x=x, y=1, text='', explicitly_set=True)
ac(x, 1, is_multicell=True, width=2, scale=2, subscale=3, x=x, y=1, text='', natural_width=False)
# Test draw with cursor in a multicell
s.reset()
@ -148,16 +153,42 @@ def test_multicell(self: TestMulticell) -> None:
self.ae(str(s.line(0)), ' b')
# Test multicell with cursor in a multicell
def big_a(x, y):
def big_a(x, y=0, spaces=False, skip=False):
s.reset()
s.cursor.x, s.cursor.y = 1, 1
multicell(s, 'a', scale=4)
ac(1, 1, x=0, y=0, text='a', scale=4, width=1)
s.cursor.x, s.cursor.y = x, y
multicell(s, 'b', scale=2)
self.ae(4, count_multicells())
for x in range(1, 5):
ac(x, 4, text=' ')
big_a(0, 0), big_a(1, 1), big_a(2, 2), big_a(5, 1)
if skip:
self.ae(20, count_multicells())
assert_cursor_at(2, 4)
self.assertIn('a', str(s.linebuf))
else:
ac(x, y, text='b')
self.ae(4, count_multicells())
for x_ in range(1, 5):
ac(x_, 4, text=' ' if spaces else '')
for y in (0, 1):
big_a(0, y), big_a(1, y), big_a(2, y, spaces=True)
big_a(2, 2, skip=True), big_a(5, 1, skip=True)
# Test multicell with combining and flag codepoints and default width
def seq(text, *expected):
s.reset()
multicell(s, text)
i = iter(expected)
for x in range(s.cursor.x):
cell = s.cpu_cells(0, x)
if cell['x'] == 0:
q = next(i)
ac(x, 0, text=q, width=wcswidth(q))
seq('ab', 'a', 'b')
flag = '\U0001f1ee\U0001f1f3'
seq(flag + 'CD', flag, 'C', 'D')
seq('àn̂X', 'à', 'n̂', 'X')
seq('\U0001f1eea', '\U0001f1ee', 'a')
del flag, seq
# Test insert chars with multicell (aka right shift)
s.reset()
@ -329,8 +360,8 @@ def test_multicell(self: TestMulticell) -> None:
s.erase_in_display(22)
assert_line('ab_c\0\0', -2)
assert_line('\0__\0\0\0', -1)
self.ae(s.historybuf.line(1).as_ansi(), f'a\x1b]{TEXT_SIZE_CODE};s=2;b\x07c')
self.ae(s.historybuf.line(0).as_ansi(), '')
self.ae(s.historybuf.line(1).as_ansi(), f'a\x1b]{TEXT_SIZE_CODE};w=1:s=2;b\x07c')
self.ae(s.historybuf.line(0).as_ansi(), ' ')
# Insert lines
s.reset()
@ -355,3 +386,33 @@ def test_multicell(self: TestMulticell) -> None:
s.delete_lines(1)
for y in range(s.lines):
assert_line('\0' * s.columns, y)
# ansi output
def ta(expected):
actual = as_ansi().rstrip()[3:]
self.ae(expected, actual)
s.reset()
parse_bytes(s, actual.encode())
actual2 = as_ansi().rstrip()[3:]
self.ae(expected, actual2)
s.reset()
s.reset()
s.draw('a')
multicell(s, 'b', width=2)
s.draw('c')
ta('a\x1b]66;w=2;b\x07c')
multicell(s, 'a')
s.cursor.fg = as_rgb(0xffffff)
multicell(s, 'b')
ta('a\x1b[38:2:255:255:255mb')
multicell(s, 'a', scale=2)
multicell(s, 'b', scale=2)
ta('\x1b]66;w=1:s=2;ab\x07')
multicell(s, 'a', scale=2)
s.cursor.fg = as_rgb(0xffffff)
multicell(s, 'b', scale=2)
ta('\x1b]66;w=1:s=2;a\x07\x1b[38:2:255:255:255m\x1b]66;w=1:s=2;b\x07\n\x1b[m\x1b[38:2:255:255:255m')
multicell(s, 'a', scale=3)
multicell(s, 'b', scale=2)
ta('\x1b]66;w=1:s=3;a\x07\x1b]66;w=1:s=2;b\x07')