Coverage Report

Created: 2026-06-12 06:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tmux/tty-draw.c
Line
Count
Source
1
/* $OpenBSD$ */
2
3
/*
4
 * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
5
 *
6
 * Permission to use, copy, modify, and distribute this software for any
7
 * purpose with or without fee is hereby granted, provided that the above
8
 * copyright notice and this permission notice appear in all copies.
9
 *
10
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
15
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
16
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
 */
18
19
#include <sys/types.h>
20
21
#include <string.h>
22
23
#include "tmux.h"
24
25
/* Current state when drawing line. */
26
enum tty_draw_line_state {
27
  TTY_DRAW_LINE_FIRST,
28
  TTY_DRAW_LINE_FLUSH,
29
  TTY_DRAW_LINE_NEW1,
30
  TTY_DRAW_LINE_NEW2,
31
  TTY_DRAW_LINE_EMPTY,
32
  TTY_DRAW_LINE_SAME,
33
  TTY_DRAW_LINE_DONE
34
};
35
static const char* tty_draw_line_states[] = {
36
  "FIRST",
37
  "FLUSH",
38
  "NEW1",
39
  "NEW2",
40
  "EMPTY",
41
  "SAME",
42
  "DONE"
43
};
44
45
/* Clear part of the line. */
46
static void
47
tty_draw_line_clear(struct tty *tty, u_int px, u_int py, u_int nx,
48
    const struct grid_cell *defaults, u_int bg, int wrapped)
49
0
{
50
  /* Nothing to clear. */
51
0
  if (nx == 0)
52
0
    return;
53
54
  /* If genuine BCE is available, can try escape sequences. */
55
0
  if (tty->client->overlay_check == NULL &&
56
0
      !wrapped &&
57
0
      nx >= 10 &&
58
0
      !tty_fake_bce(tty, defaults, bg)) {
59
    /* Off the end of the line, use EL if available. */
60
0
    if (px + nx >= tty->sx && tty_term_has(tty->term, TTYC_EL)) {
61
0
      tty_cursor(tty, px, py);
62
0
      tty_putcode(tty, TTYC_EL);
63
0
      return;
64
0
    }
65
66
    /* At the start of the line. Use EL1. */
67
0
    if (px == 0 && tty_term_has(tty->term, TTYC_EL1)) {
68
0
      tty_cursor(tty, px + nx - 1, py);
69
0
      tty_putcode(tty, TTYC_EL1);
70
0
      return;
71
0
    }
72
73
    /* Section of line. Use ECH if possible. */
74
0
    if (tty_term_has(tty->term, TTYC_ECH)) {
75
0
      tty_cursor(tty, px, py);
76
0
      tty_putcode_i(tty, TTYC_ECH, nx);
77
0
      return;
78
0
    }
79
0
  }
80
81
        /* Couldn't use an escape sequence, use spaces. */
82
0
  if (px != 0 || !wrapped)
83
0
    tty_cursor(tty, px, py);
84
0
  if (nx == 1)
85
0
    tty_putc(tty, ' ');
86
0
  else if (nx == 2)
87
0
    tty_putn(tty, "  ", 2, 2);
88
0
  else
89
0
    tty_repeat_space(tty, nx);
90
0
}
91
92
/* Draw a line from screen to tty. */
93
void
94
tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
95
    u_int atx, u_int aty, const struct tty_style_ctx *style_ctx)
96
0
{
97
0
  struct grid   *gd = s->grid;
98
0
  const struct grid_cell  *gcp;
99
0
  struct grid_cell   gc, ngc, last;
100
0
  struct grid_line  *gl;
101
0
  u_int      i, j, last_i, cx, ex, width;
102
0
  u_int      cellsize, bg;
103
0
  int      flags, empty, wrapped = 0;
104
0
  char       buf[1000];
105
0
  size_t       len;
106
0
  enum tty_draw_line_state current_state, next_state;
107
0
  struct tty_style_ctx   default_style_ctx = { 0 };
108
109
110
0
  if (style_ctx == NULL) {
111
0
    default_style_ctx.defaults = &grid_default_cell;
112
0
    default_style_ctx.hyperlinks = s->hyperlinks;
113
0
    style_ctx = &default_style_ctx;
114
0
  }
115
116
  /*
117
   * py is the line in the screen to draw. px is the start x and nx is
118
   * the width to draw. atx,aty is the line on the terminal to draw it.
119
   */
120
0
  log_debug("%s: px=%u py=%u nx=%u atx=%u aty=%u", __func__, px, py, nx,
121
0
      atx, aty);
122
123
  /* There is no point in drawing more than the end of the terminal. */
124
0
  if (atx >= tty->sx)
125
0
    return;
126
0
  if (atx + nx >= tty->sx)
127
0
    nx = tty->sx - atx;
128
0
  if (nx == 0)
129
0
    return;
130
131
  /*
132
   * Clamp the width to cellsize - note this is not cellused, because
133
   * there may be empty background cells after it (from BCE).
134
   */
135
0
  cellsize = grid_get_line(gd, gd->hsize + py)->cellsize;
136
0
  if (screen_size_x(s) > cellsize)
137
0
    ex = cellsize;
138
0
  else
139
0
    ex = screen_size_x(s);
140
0
  log_debug("%s: drawing %u-%u,%u (end %u) at %u,%u; defaults: fg=%d, "
141
0
      "bg=%d", __func__, px, px + nx, py, ex, atx, aty,
142
0
      style_ctx->defaults->fg, style_ctx->defaults->bg);
143
144
  /* Turn off cursor while redrawing and reset region and margins. */
145
0
  flags = (tty->flags & TTY_NOCURSOR);
146
0
  tty->flags |= TTY_NOCURSOR;
147
0
  tty_update_mode(tty, tty->mode, s);
148
0
  tty_region_off(tty);
149
0
  tty_margin_off(tty);
150
151
  /* Start with the default cell as the last cell. */
152
0
  memcpy(&last, &grid_default_cell, sizeof last);
153
0
  last.bg = style_ctx->defaults->bg;
154
0
  tty_default_attributes(tty, 8, style_ctx);
155
156
  /*
157
   * If there is padding at the start, we must have truncated a wide
158
   * character. Clear it.
159
   */
160
0
  cx = 0;
161
0
  for (i = px; i < px + nx; i++) {
162
0
    grid_view_get_cell(gd, i, py, &gc);
163
0
    if (~gc.flags & GRID_FLAG_PADDING)
164
0
      break;
165
0
    cx++;
166
0
  }
167
0
  if (cx != 0) {
168
    /* Find the previous cell for the background colour. */
169
0
    for (i = px + 1; i > 0; i--) {
170
0
      grid_view_get_cell(gd, i - 1, py, &gc);
171
0
      if (~gc.flags & GRID_FLAG_PADDING)
172
0
        break;
173
0
    }
174
0
    if (i == 0)
175
0
      bg = style_ctx->defaults->bg;
176
0
    else {
177
0
      bg = gc.bg;
178
0
      if (gc.flags & GRID_FLAG_SELECTED) {
179
0
        memcpy(&ngc, &gc, sizeof ngc);
180
0
        if (screen_select_cell(s, &ngc, &gc))
181
0
          bg = ngc.bg;
182
0
      }
183
0
    }
184
0
      tty_attributes(tty, &last, style_ctx);
185
0
    log_debug("%s: clearing %u padding cells", __func__, cx);
186
0
    tty_draw_line_clear(tty, atx, aty, cx, style_ctx->defaults, bg, 0);
187
0
    if (cx == ex)
188
0
      goto out;
189
0
    atx += cx;
190
0
    px += cx;
191
0
    nx -= cx;
192
0
  }
193
194
  /* Did the previous line wrap on to this one? */
195
0
  if (py != 0 && atx == 0 && tty->cx >= tty->sx && nx == tty->sx) {
196
0
    gl = grid_get_line(gd, gd->hsize + py - 1);
197
0
    if (gl->flags & GRID_LINE_WRAPPED)
198
0
      wrapped = 1;
199
0
  }
200
201
  /* Loop over each character in the range. */
202
0
  last_i = i = 0;
203
0
  len = 0;
204
0
  width = 0;
205
0
  current_state = TTY_DRAW_LINE_FIRST;
206
0
  for (;;) {
207
    /* Work out the next state. */
208
0
    if (i == nx) {
209
      /*
210
       * If this is the last cell, we are done. But we need to
211
       * go through the loop again to flush anything in
212
       * the buffer.
213
       */
214
0
      empty = 0;
215
0
      next_state = TTY_DRAW_LINE_DONE;
216
0
      gcp = &grid_default_cell;
217
0
    } else {
218
0
      if (i > nx)
219
0
        fatalx("position %u > width %u", i, nx);
220
221
      /* Get the current cell. */
222
0
      grid_view_get_cell(gd, px + i, py, &gc);
223
224
      /* Update for codeset if needed. */
225
0
      gcp = tty_check_codeset(tty, &gc);
226
227
      /* And for selection. */
228
0
      if (gcp->flags & GRID_FLAG_SELECTED) {
229
0
        memcpy(&ngc, gcp, sizeof ngc);
230
0
        if (screen_select_cell(s, &ngc, gcp))
231
0
          gcp = &ngc;
232
0
      }
233
234
      /* Work out the the empty width. */
235
0
      empty = 0;
236
0
      if (px >= ex || i >= ex - px) {
237
        /* Outside the area being drawn. */
238
0
        empty = 1;
239
0
      } else if (gcp->data.width > nx - i) {
240
        /* Wide character that has been truncated. */
241
0
        empty = nx - i;
242
0
      } else if (gcp->flags & GRID_FLAG_PADDING) {
243
        /* Orphan padding cell. */
244
0
        empty = 1;
245
0
      } else if (gcp->bg == last.bg && gcp->attr == 0 &&
246
0
          gcp->link == 0) {
247
        /*
248
         * No attributes - empty if cleared, tab or
249
         * space.
250
         */
251
0
        if (gcp->flags & GRID_FLAG_CLEARED)
252
0
          empty = 1;
253
0
        else if (gcp->flags & GRID_FLAG_TAB)
254
0
          empty = gcp->data.width;
255
0
        else if (gcp->data.size == 1 &&
256
0
            *gcp->data.data == ' ')
257
0
          empty = 1;
258
0
      }
259
260
      /* Work out the next state. */
261
0
      if (empty != 0)
262
0
        next_state = TTY_DRAW_LINE_EMPTY;
263
0
      else if (current_state == TTY_DRAW_LINE_FIRST)
264
0
        next_state = TTY_DRAW_LINE_SAME;
265
0
      else if (grid_cells_look_equal(gcp, &last)) {
266
0
        if (gcp->data.size > (sizeof buf) - len)
267
0
          next_state = TTY_DRAW_LINE_FLUSH;
268
0
        else
269
0
          next_state = TTY_DRAW_LINE_SAME;
270
0
      } else if (current_state == TTY_DRAW_LINE_NEW1)
271
0
        next_state = TTY_DRAW_LINE_NEW2;
272
0
      else
273
0
        next_state = TTY_DRAW_LINE_NEW1;
274
0
    }
275
0
    if (log_get_level() != 0) {
276
0
      log_debug("%s: cell %u empty %u, bg %u; state: "
277
0
          "current %s, next %s", __func__, px + i, empty,
278
0
          gcp->bg, tty_draw_line_states[current_state],
279
0
          tty_draw_line_states[next_state]);
280
0
    }
281
282
    /* If the state has changed, flush any collected data. */
283
0
    if (next_state != current_state) {
284
0
      if (current_state == TTY_DRAW_LINE_EMPTY) {
285
0
        tty_attributes(tty, &last, style_ctx);
286
0
        tty_draw_line_clear(tty, atx + last_i, aty,
287
0
            i - last_i, style_ctx->defaults, last.bg,
288
0
            wrapped);
289
0
        wrapped = 0;
290
0
      } else if (next_state != TTY_DRAW_LINE_SAME &&
291
0
          len != 0) {
292
0
        tty_attributes(tty, &last, style_ctx);
293
0
        if (atx + i - width != 0 || !wrapped)
294
0
          tty_cursor(tty, atx + i - width, aty);
295
0
        if (~last.attr & GRID_ATTR_CHARSET)
296
0
          tty_putn(tty, buf, len, width);
297
0
        else {
298
0
          for (j = 0; j < len; j++)
299
0
            tty_putc(tty, buf[j]);
300
0
        }
301
0
        len = 0;
302
0
        width = 0;
303
0
        wrapped = 0;
304
0
      }
305
0
      last_i = i;
306
0
    }
307
308
    /* Append the cell if it is not empty and not padding. */
309
0
    if (next_state != TTY_DRAW_LINE_EMPTY) {
310
0
      memcpy(buf + len, gcp->data.data, gcp->data.size);
311
0
      len += gcp->data.size;
312
0
      width += gcp->data.width;
313
0
    }
314
315
    /* If this is the last cell, we are done. */
316
0
    if (next_state == TTY_DRAW_LINE_DONE)
317
0
      break;
318
319
    /* Otherwise move to the next. */
320
0
    current_state = next_state;
321
0
    memcpy(&last, gcp, sizeof last);
322
0
    if (empty != 0)
323
0
      i += empty;
324
0
    else
325
0
      i += gcp->data.width;
326
0
  }
327
328
0
out:
329
0
  tty->flags = (tty->flags & ~TTY_NOCURSOR)|flags;
330
0
  tty_update_mode(tty, tty->mode, s);
331
0
}