diff --git a/Makefile.am b/Makefile.am index 81c9c2fc..12bfddf5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -195,6 +195,7 @@ dist_tmux_SOURCES = \ tmux.h \ tmux-protocol.h \ tty-acs.c \ + tty-draw.c \ tty-features.c \ tty-keys.c \ tty-term.c \ diff --git a/grid.c b/grid.c index 7ad6770b..edfbe494 100644 --- a/grid.c +++ b/grid.c @@ -235,7 +235,7 @@ grid_check_y(struct grid *gd, const char *from, u_int py) int grid_cells_look_equal(const struct grid_cell *gc1, const struct grid_cell *gc2) { - int flags1 = gc1->flags, flags2 = gc2->flags;; + int flags1 = gc1->flags, flags2 = gc2->flags; if (gc1->fg != gc2->fg || gc1->bg != gc2->bg) return (0); diff --git a/tmux.h b/tmux.h index 1917ee19..33da541a 100644 --- a/tmux.h +++ b/tmux.h @@ -2522,6 +2522,8 @@ void tty_reset(struct tty *); void tty_region_off(struct tty *); void tty_margin_off(struct tty *); void tty_cursor(struct tty *, u_int, u_int); +int tty_fake_bce(const struct tty *, const struct grid_cell *, u_int); +void tty_repeat_space(struct tty *, u_int); void tty_clipboard_query(struct tty *); void tty_putcode(struct tty *, enum tty_code_code); void tty_putcode_i(struct tty *, enum tty_code_code, int); @@ -2546,7 +2548,11 @@ void tty_repeat_requests(struct tty *, int); void tty_stop_tty(struct tty *); void tty_set_title(struct tty *, const char *); void tty_set_path(struct tty *, const char *); +void tty_default_attributes(struct tty *, const struct grid_cell *, + struct colour_palette *, u_int, struct hyperlinks *); void tty_update_mode(struct tty *, int, struct screen *); + +/* tty-draw.c */ void tty_draw_line(struct tty *, struct screen *, u_int, u_int, u_int, u_int, u_int, const struct grid_cell *, struct colour_palette *); diff --git a/tty-draw.c b/tty-draw.c new file mode 100644 index 00000000..d79325c3 --- /dev/null +++ b/tty-draw.c @@ -0,0 +1,302 @@ +/* $OpenBSD$ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include + +#include "tmux.h" + +enum tty_draw_line_state { + TTY_DRAW_LINE_FIRST, + TTY_DRAW_LINE_FLUSH, + TTY_DRAW_LINE_NEW1, + TTY_DRAW_LINE_NEW2, + TTY_DRAW_LINE_EMPTY, + TTY_DRAW_LINE_SAME, + TTY_DRAW_LINE_PAD, + TTY_DRAW_LINE_DONE +}; +static const char* tty_draw_line_states[] = { + "FIRST", + "FLUSH", + "NEW1", + "NEW2", + "EMPTY", + "SAME", + "PAD", + "DONE" +}; + +/* Clear part of the line. */ +static void +tty_draw_line_clear(struct tty *tty, u_int px, u_int py, u_int nx, + const struct grid_cell *defaults, u_int bg, int wrapped) +{ + /* Nothing to clear. */ + if (nx == 0) + return; + + /* If genuine BCE is available, can try escape sequences. */ + if (!wrapped && nx >= 10 && !tty_fake_bce(tty, defaults, bg)) { + /* Off the end of the line, use EL if available. */ + if (px + nx >= tty->sx && tty_term_has(tty->term, TTYC_EL)) { + tty_cursor(tty, px, py); + tty_putcode(tty, TTYC_EL); + return; + } + + /* At the start of the line. Use EL1. */ + if (px == 0 && tty_term_has(tty->term, TTYC_EL1)) { + tty_cursor(tty, px + nx - 1, py); + tty_putcode(tty, TTYC_EL1); + return; + } + + /* Section of line. Use ECH if possible. */ + if (tty_term_has(tty->term, TTYC_ECH)) { + tty_cursor(tty, px, py); + tty_putcode_i(tty, TTYC_ECH, nx); + return; + } + } + + /* Couldn't use an escape sequence, use spaces. */ + if (px != 0 || !wrapped) + tty_cursor(tty, px, py); + if (nx == 1) + tty_putc(tty, ' '); + else if (nx == 2) + tty_putn(tty, " ", 2, 2); + else + tty_repeat_space(tty, nx); +} + +/* Is this cell empty? */ +static u_int +tty_draw_line_get_empty(struct grid_cell *gc, u_int nx) +{ + u_int empty = 0; + + if (gc->data.width != 1 && gc->data.width > nx) + empty = nx; + else if (gc->attr == 0 && gc->link == 0) { + if (gc->flags & GRID_FLAG_CLEARED) + empty = 1; + else if (gc->flags & GRID_FLAG_TAB) + empty = gc->data.width; + else if (gc->data.size == 1 && *gc->data.data == ' ') + empty = 1; + } + return (empty); +} + +/* Draw a line from screen to tty. */ +void +tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, + u_int atx, u_int aty, const struct grid_cell *defaults, + struct colour_palette *palette) +{ + struct grid *gd = s->grid; + struct grid_cell gc, last; + struct grid_line *gl; + u_int i, j, last_i, cx, ex, width; + u_int cellsize, bg; + int flags, empty, wrapped = 0; + char buf[1000]; + size_t len; + enum tty_draw_line_state current_state, next_state; + + /* + * py is the line in the screen to draw. px is the start x and nx is + * the width to draw. atx,aty is the line on the terminal to draw it. + */ + log_debug("%s: px=%u py=%u nx=%u atx=%u aty=%u", __func__, px, py, nx, + atx, aty); + + /* + * Clamp the width to cellsize - note this is not cellused, because + * there may be empty background cells after it (from BCE). + */ + cellsize = grid_get_line(gd, gd->hsize + py)->cellsize; + if (screen_size_x(s) > cellsize) + ex = cellsize; + else { + ex = screen_size_x(s); + if (px > ex) + return; + if (px + nx > ex) + nx = ex - px; + } + if (ex < nx) + ex = nx; + log_debug("%s: drawing %u-%u,%u (end %u) at %u,%u; defaults: fg=%d, " + "bg=%d", __func__, px, px + nx, py, ex, atx, aty, defaults->fg, + defaults->bg); + + /* + * If there is padding at the start, we must have truncated a wide + * character. Clear it. + */ + cx = 0; + for (i = px; i < px + nx; i++) { + grid_view_get_cell(gd, i, py, &gc); + if (~gc.flags & GRID_FLAG_PADDING) + break; + cx++; + } + if (cx != 0) { + /* Find the previous cell for the background colour. */ + for (i = px + 1; i > 0; i--) { + grid_view_get_cell(gd, i - 1, py, &gc); + if (~gc.flags & GRID_FLAG_PADDING) + break; + } + if (i == 0) + bg = gc.bg; + else + bg = defaults->bg; + tty_attributes(tty, &last, defaults, palette, s->hyperlinks); + log_debug("%s: clearing %u padding cells", __func__, cx); + tty_draw_line_clear(tty, atx, aty, cx, defaults, bg, 0); + if (cx == ex) + return; + atx += cx; + px += cx; + nx -= cx; + ex -= cx; + } + + /* Did the previous line wrap on to this one? */ + if (py != 0 && atx == 0 && tty->cx >= tty->sx && nx == tty->sx) { + gl = grid_get_line(gd, gd->hsize + py - 1); + if (gl->flags & GRID_LINE_WRAPPED) + wrapped = 1; + } + + /* Turn off cursor while redrawing and reset region and margins. */ + flags = (tty->flags & TTY_NOCURSOR); + tty->flags |= TTY_NOCURSOR; + tty_update_mode(tty, tty->mode, s); + tty_region_off(tty); + tty_margin_off(tty); + + /* Start with the default cell as the last cell. */ + memcpy(&last, &grid_default_cell, sizeof last); + last.bg = defaults->bg; + tty_default_attributes(tty, defaults, palette, 8, s->hyperlinks); + + /* Loop over each character in the range. */ + last_i = i = 0; + len = 0; + width = 0; + current_state = TTY_DRAW_LINE_FIRST; + for (;;) { + /* Work out the next state. */ + if (i == nx) { + /* + * If this is the last cell, we are done. But we need to + * go through the loop again to flush anything in + * the buffer. + */ + empty = 0; + next_state = TTY_DRAW_LINE_DONE; + } else { + /* Get the current cell. */ + grid_view_get_cell(gd, px + i, py, &gc); + + /* Work out the the empty width. */ + if (i >= ex) + empty = 1; + else + empty = tty_draw_line_get_empty(&gc, nx - i); + + /* Work out the next state. */ + if (empty != 0) + next_state = TTY_DRAW_LINE_EMPTY; + else if (current_state == TTY_DRAW_LINE_FIRST) + next_state = TTY_DRAW_LINE_SAME; + else if (gc.flags & GRID_FLAG_PADDING) + next_state = TTY_DRAW_LINE_PAD; + else if (grid_cells_look_equal(&gc, &last)) { + if (gc.data.size > (sizeof buf) - len) + next_state = TTY_DRAW_LINE_FLUSH; + else + next_state = TTY_DRAW_LINE_SAME; + } else if (current_state == TTY_DRAW_LINE_NEW1) + next_state = TTY_DRAW_LINE_NEW2; + else + next_state = TTY_DRAW_LINE_NEW1; + } + log_debug("%s: cell %u empty %u, bg %u; state: current %s, " + "next %s", __func__, px + i, empty, gc.bg, + tty_draw_line_states[current_state], + tty_draw_line_states[next_state]); + + /* If the state has changed, flush any collected data. */ + if (next_state != current_state) { + if (current_state == TTY_DRAW_LINE_EMPTY) { + tty_attributes(tty, &last, defaults, palette, + s->hyperlinks); + tty_draw_line_clear(tty, atx + last_i, aty, + i - last_i, defaults, last.bg, wrapped); + wrapped = 0; + } else if (next_state != TTY_DRAW_LINE_SAME && + len != 0) { + tty_attributes(tty, &last, defaults, palette, + s->hyperlinks); + if (atx + i - width != 0 || !wrapped) + tty_cursor(tty, atx + i - width, aty); + if (~last.attr & GRID_ATTR_CHARSET) + tty_putn(tty, buf, len, width); + else { + for (j = 0; j < len; j++) + tty_putc(tty, buf[j]); + } + len = 0; + width = 0; + wrapped = 0; + } + last_i = i; + } + + /* Append the cell if it is not empty and not padding. */ + if (next_state != TTY_DRAW_LINE_EMPTY && + next_state != TTY_DRAW_LINE_PAD) { + memcpy(buf + len, gc.data.data, gc.data.size); + len += gc.data.size; + width += gc.data.width; + } + + /* If this is the last cell, we are done. */ + if (next_state == TTY_DRAW_LINE_DONE) + break; + + /* Otherwise move to the next. */ + current_state = next_state; + memcpy(&last, &gc, sizeof last); + if (empty != 0) + i += empty; + else + i++; + } + + tty->flags = (tty->flags & ~TTY_NOCURSOR)|flags; + tty_update_mode(tty, tty->mode, s); +} + diff --git a/tty.c b/tty.c index 72debdcc..bc1df836 100644 --- a/tty.c +++ b/tty.c @@ -61,15 +61,10 @@ static void tty_region(struct tty *, u_int, u_int); static void tty_margin_pane(struct tty *, const struct tty_ctx *); static void tty_margin(struct tty *, u_int, u_int); static int tty_large_region(struct tty *, const struct tty_ctx *); -static int tty_fake_bce(const struct tty *, const struct grid_cell *, - u_int); static void tty_redraw_region(struct tty *, const struct tty_ctx *); static void tty_emulate_repeat(struct tty *, enum tty_code_code, enum tty_code_code, u_int); -static void tty_repeat_space(struct tty *, u_int); static void tty_draw_pane(struct tty *, const struct tty_ctx *, u_int); -static void tty_default_attributes(struct tty *, const struct grid_cell *, - struct colour_palette *, u_int, struct hyperlinks *); static int tty_check_overlay(struct tty *, u_int, u_int); static void tty_check_overlay_range(struct tty *, u_int, u_int, u_int, struct overlay_ranges *); @@ -912,7 +907,7 @@ tty_emulate_repeat(struct tty *tty, enum tty_code_code code, } } -static void +void tty_repeat_space(struct tty *tty, u_int n) { static char s[500]; @@ -1071,7 +1066,7 @@ tty_large_region(__unused struct tty *tty, const struct tty_ctx *ctx) * Return if BCE is needed but the terminal doesn't have it - it'll need to be * emulated. */ -static int +int tty_fake_bce(const struct tty *tty, const struct grid_cell *gc, u_int bg) { if (tty_term_flag(tty->term, TTYC_BCE)) @@ -1463,191 +1458,6 @@ tty_check_overlay_range(struct tty *tty, u_int px, u_int py, u_int nx, c->overlay_check(c, c->overlay_data, px, py, nx, r); } -void -tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, - u_int atx, u_int aty, const struct grid_cell *defaults, - struct colour_palette *palette) -{ - struct grid *gd = s->grid; - struct grid_cell gc, last; - const struct grid_cell *gcp; - struct grid_line *gl; - struct client *c = tty->client; - struct overlay_ranges r; - u_int i, j, ux, sx, width, hidden, eux, nxx; - u_int cellsize; - int flags, cleared = 0, wrapped = 0; - char buf[512]; - size_t len; - - log_debug("%s: px=%u py=%u nx=%u atx=%u aty=%u", __func__, - px, py, nx, atx, aty); - log_debug("%s: defaults: fg=%d, bg=%d", __func__, defaults->fg, - defaults->bg); - - /* - * py is the line in the screen to draw. - * px is the start x and nx is the width to draw. - * atx,aty is the line on the terminal to draw it. - */ - - flags = (tty->flags & TTY_NOCURSOR); - tty->flags |= TTY_NOCURSOR; - tty_update_mode(tty, tty->mode, s); - - tty_region_off(tty); - tty_margin_off(tty); - - /* - * Clamp the width to cellsize - note this is not cellused, because - * there may be empty background cells after it (from BCE). - */ - sx = screen_size_x(s); - if (nx > sx) - nx = sx; - cellsize = grid_get_line(gd, gd->hsize + py)->cellsize; - if (sx > cellsize) - sx = cellsize; - if (sx > tty->sx) - sx = tty->sx; - if (sx > nx) - sx = nx; - ux = 0; - - if (py == 0) - gl = NULL; - else - gl = grid_get_line(gd, gd->hsize + py - 1); - if (gl == NULL || - (~gl->flags & GRID_LINE_WRAPPED) || - atx != 0 || - tty->cx < tty->sx || - nx < tty->sx) { - if (nx < tty->sx && - atx == 0 && - px + sx != nx && - tty_term_has(tty->term, TTYC_EL1) && - !tty_fake_bce(tty, defaults, 8) && - c->overlay_check == NULL) { - tty_default_attributes(tty, defaults, palette, 8, - s->hyperlinks); - tty_cursor(tty, nx - 1, aty); - tty_putcode(tty, TTYC_EL1); - cleared = 1; - } - } else { - log_debug("%s: wrapped line %u", __func__, aty); - wrapped = 1; - } - - memcpy(&last, &grid_default_cell, sizeof last); - len = 0; - width = 0; - - for (i = 0; i < sx; i++) { - grid_view_get_cell(gd, px + i, py, &gc); - gcp = tty_check_codeset(tty, &gc); - if (len != 0 && - (!tty_check_overlay(tty, atx + ux + width, aty) || - (gcp->attr & GRID_ATTR_CHARSET) || - gcp->flags != last.flags || - gcp->attr != last.attr || - gcp->fg != last.fg || - gcp->bg != last.bg || - gcp->us != last.us || - gcp->link != last.link || - ux + width + gcp->data.width > nx || - (sizeof buf) - len < gcp->data.size)) { - tty_attributes(tty, &last, defaults, palette, - s->hyperlinks); - if (last.flags & GRID_FLAG_CLEARED) { - log_debug("%s: %zu cleared", __func__, len); - tty_clear_line(tty, defaults, aty, atx + ux, - width, last.bg); - } else { - if (!wrapped || atx != 0 || ux != 0) - tty_cursor(tty, atx + ux, aty); - tty_putn(tty, buf, len, width); - } - ux += width; - - len = 0; - width = 0; - wrapped = 0; - } - - if (gcp->flags & GRID_FLAG_SELECTED) - screen_select_cell(s, &last, gcp); - else - memcpy(&last, gcp, sizeof last); - - tty_check_overlay_range(tty, atx + ux, aty, gcp->data.width, - &r); - hidden = 0; - for (j = 0; j < OVERLAY_MAX_RANGES; j++) - hidden += r.nx[j]; - hidden = gcp->data.width - hidden; - if (hidden != 0 && hidden == gcp->data.width) { - if (~gcp->flags & GRID_FLAG_PADDING) - ux += gcp->data.width; - } else if (hidden != 0 || ux + gcp->data.width > nx) { - if (~gcp->flags & GRID_FLAG_PADDING) { - tty_attributes(tty, &last, defaults, palette, - s->hyperlinks); - for (j = 0; j < OVERLAY_MAX_RANGES; j++) { - if (r.nx[j] == 0) - continue; - /* Effective width drawn so far. */ - eux = r.px[j] - atx; - if (eux < nx) { - tty_cursor(tty, r.px[j], aty); - nxx = nx - eux; - if (r.nx[j] > nxx) - r.nx[j] = nxx; - tty_repeat_space(tty, r.nx[j]); - ux = eux + r.nx[j]; - } - } - } - } else if (gcp->attr & GRID_ATTR_CHARSET) { - tty_attributes(tty, &last, defaults, palette, - s->hyperlinks); - tty_cursor(tty, atx + ux, aty); - for (j = 0; j < gcp->data.size; j++) - tty_putc(tty, gcp->data.data[j]); - ux += gcp->data.width; - } else if (~gcp->flags & GRID_FLAG_PADDING) { - memcpy(buf + len, gcp->data.data, gcp->data.size); - len += gcp->data.size; - width += gcp->data.width; - } - } - if (len != 0 && ((~last.flags & GRID_FLAG_CLEARED) || last.bg != 8)) { - tty_attributes(tty, &last, defaults, palette, s->hyperlinks); - if (last.flags & GRID_FLAG_CLEARED) { - log_debug("%s: %zu cleared (end)", __func__, len); - tty_clear_line(tty, defaults, aty, atx + ux, width, - last.bg); - } else { - if (!wrapped || atx != 0 || ux != 0) - tty_cursor(tty, atx + ux, aty); - tty_putn(tty, buf, len, width); - } - ux += width; - } - - if (!cleared && ux < nx) { - log_debug("%s: %u to end of line (%zu cleared)", __func__, - nx - ux, len); - tty_default_attributes(tty, defaults, palette, 8, - s->hyperlinks); - tty_clear_line(tty, defaults, aty, atx + ux, nx - ux, 8); - } - - tty->flags = (tty->flags & ~TTY_NOCURSOR) | flags; - tty_update_mode(tty, tty->mode, s); -} - #ifdef ENABLE_SIXEL /* Update context for client. */ static int @@ -3203,7 +3013,7 @@ tty_default_colours(struct grid_cell *gc, struct window_pane *wp) } } -static void +void tty_default_attributes(struct tty *tty, const struct grid_cell *defaults, struct colour_palette *palette, u_int bg, struct hyperlinks *hl) {