diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 327fd08c3..e57e58361 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -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; ''' diff --git a/kitty/history.c b/kitty/history.c index e0aba9591..34a462f0b 100644 --- a/kitty/history.c +++ b/kitty/history.c @@ -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'; diff --git a/kitty/line-buf.c b/kitty/line-buf.c index a6a2a25a0..b0c02b8ce 100644 --- a/kitty/line-buf.c +++ b/kitty/line-buf.c @@ -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 diff --git a/kitty/line.c b/kitty/line.c index 3cab80e18..b0226c585 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -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 { diff --git a/kitty/line.h b/kitty/line.h index a25bd7137..86d0caf90 100644 --- a/kitty/line.h +++ b/kitty/line.h @@ -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 *); diff --git a/kitty/lineops.h b/kitty/lineops.h index 78923329f..f354868e0 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -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); diff --git a/kitty/parse-multicell-command.h b/kitty/parse-multicell-command.h index 56eb9cc46..35bd474e8 100644 --- a/kitty/parse-multicell-command.h +++ b/kitty/parse-multicell-command.h @@ -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; diff --git a/kitty/screen.c b/kitty/screen.c index 322165bf7..5885c603e 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -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* diff --git a/kitty/vt-parser.c b/kitty/vt-parser.c index 7c06d85b4..a4a9ae232 100644 --- a/kitty/vt-parser.c +++ b/kitty/vt-parser.c @@ -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 diff --git a/kitty_tests/multicell.py b/kitty_tests/multicell.py index 7028117f6..9cc884cb4 100644 --- a/kitty_tests/multicell.py +++ b/kitty_tests/multicell.py @@ -2,9 +2,9 @@ # License: GPLv3 Copyright: 2024, Kovid Goyal -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')