Coverage Report

Created: 2026-10-28 06:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tmux/tty-draw.c
Line
Count
Source
1
/* $OpenBSD: tty-draw.c,v 1.19 2026/10/13 22:44:53 nicm Exp $ */
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 (!wrapped &&
56
0
      nx >= 10 &&
57
0
      !tty_fake_bce(tty, defaults, bg)) {
58
    /* Off the end of the line, use EL if available. */
59
0
    if (px + nx >= tty->sx && tty_term_has(tty->term, TTYC_EL)) {
60
0
      tty_cursor(tty, px, py);
61
0
      tty_putcode(tty, TTYC_EL);
62
0
      return;
63
0
    }
64
65
    /* At the start of the line. Use EL1. */
66
0
    if (px == 0 && tty_term_has(tty->term, TTYC_EL1)) {
67
0
      tty_cursor(tty, px + nx - 1, py);
68
0
      tty_putcode(tty, TTYC_EL1);
69
0
      return;
70
0
    }
71
72
    /* Section of line. Use ECH if possible. */
73
0
    if (tty_term_has(tty->term, TTYC_ECH)) {
74
0
      tty_cursor(tty, px, py);
75
0
      tty_putcode_i(tty, TTYC_ECH, nx);
76
0
      return;
77
0
    }
78
0
  }
79
80
  /* Couldn't use an escape sequence, use spaces. */
81
0
  if (px != 0 || !wrapped)
82
0
    tty_cursor(tty, px, py);
83
0
  if (nx == 1)
84
0
    tty_putc(tty, ' ');
85
0
  else if (nx == 2)
86
0
    tty_putn(tty, "  ", 2, 2);
87
0
  else
88
0
    tty_repeat_space(tty, nx);
89
0
}
90
91
/* Is this cell empty? */
92
static u_int
93
tty_draw_line_get_empty(const struct grid_cell *gc,
94
    const struct grid_cell *last, u_int nx)
95
0
{
96
0
  u_int empty = 0;
97
98
0
  if (gc->data.width > nx)
99
0
    empty = nx;
100
0
  else if (gc->flags & GRID_FLAG_PADDING)
101
0
    empty = 1;
102
0
  else if (gc->data.width == 0)
103
0
    empty = 1;
104
0
  else if (gc->flags & GRID_FLAG_SELECTED)
105
0
    empty = 0;
106
0
  else if (gc->bg == last->bg && gc->attr == 0 && gc->link == 0) {
107
0
    if (gc->flags & GRID_FLAG_CLEARED)
108
0
      empty = 1;
109
0
    else if (gc->flags & GRID_FLAG_TAB)
110
0
      empty = gc->data.width;
111
0
    else if (gc->data.size == 1 && *gc->data.data == ' ')
112
0
      empty = 1;
113
0
  }
114
0
  return (empty);
115
0
}
116
117
/* Draw a line from screen to tty. */
118
void
119
tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
120
    u_int atx, u_int aty, const struct tty_style_ctx *style_ctx)
121
0
{
122
0
  struct grid   *gd = s->grid;
123
0
  const struct grid_cell  *gcp;
124
0
  struct grid_cell   gc, ngc, last;
125
0
  struct grid_line  *gl;
126
0
  u_int      i, j, last_i, cx, ex, width;
127
0
  u_int      cellsize, bg;
128
0
  int      flags, empty, wrapped = 0;
129
0
  char       buf[1000];
130
0
  size_t       len;
131
0
  enum tty_draw_line_state current_state, next_state;
132
0
  struct tty_style_ctx   default_style_ctx = { 0 };
133
0
  const struct grid_cell  *defaults;
134
#ifdef ENABLE_IMAGES
135
  struct image    *im;
136
  u_int      x, y;
137
#endif
138
139
0
  if (style_ctx == NULL) {
140
0
    default_style_ctx.defaults = &grid_default_cell;
141
0
    default_style_ctx.hyperlinks = s->hyperlinks;
142
0
    style_ctx = &default_style_ctx;
143
0
  }
144
0
  defaults = style_ctx->defaults;
145
146
  /*
147
   * py is the line in the screen to draw. px is the start x and nx is
148
   * the width to draw. atx,aty is the line on the terminal to draw it.
149
   */
150
0
  log_debug("%s: px=%u py=%u nx=%u atx=%u aty=%u", __func__, px, py, nx,
151
0
      atx, aty);
152
153
  /* There is no point in drawing more than the end of the terminal. */
154
0
  if (atx >= tty->sx)
155
0
    return;
156
0
  if (atx + nx >= tty->sx)
157
0
    nx = tty->sx - atx;
158
0
  if (nx == 0)
159
0
    return;
160
161
  /*
162
   * Clamp the width to cellsize - note this is not cellused, because
163
   * there may be empty background cells after it (from BCE).
164
   */
165
0
  cellsize = grid_get_line(gd, gd->hsize + py)->cellsize;
166
0
  if (screen_size_x(s) > cellsize)
167
0
    ex = cellsize;
168
0
  else
169
0
    ex = screen_size_x(s);
170
0
  log_debug("%s: drawing %u-%u,%u (end %u) at %u,%u; defaults: fg=%d, "
171
0
      "bg=%d", __func__, px, px + nx, py, ex, atx, aty, defaults->fg,
172
0
      defaults->bg);
173
174
  /* Turn off cursor while redrawing and reset region and margins. */
175
0
  flags = (tty->flags & TTY_NOCURSOR);
176
0
  tty->flags |= TTY_NOCURSOR;
177
0
  tty_update_mode(tty, tty->mode, s);
178
0
  tty_region_off(tty);
179
0
  tty_margin_off(tty);
180
181
  /* Start with the default cell as the last cell. */
182
0
  memcpy(&last, &grid_default_cell, sizeof last);
183
0
  last.bg = defaults->bg;
184
0
  tty_default_attributes(tty, 8, style_ctx);
185
186
  /*
187
   * If there is padding at the start, we must have truncated a wide
188
   * character. Clear it.
189
   */
190
0
  cx = 0;
191
0
  for (i = px; i < px + nx; i++) {
192
0
    grid_view_get_cell(gd, i, py, &gc);
193
0
    if (~gc.flags & GRID_FLAG_PADDING)
194
0
      break;
195
#ifdef ENABLE_IMAGES
196
    if (image_grid_get_at(gd, i, gd->hsize + py, &im, &x, &y) == 1)
197
      break;
198
#endif
199
0
    cx++;
200
0
  }
201
0
  if (cx != 0) {
202
    /* Find the previous cell for the background colour. */
203
0
    for (i = px + 1; i > 0; i--) {
204
0
      grid_view_get_cell(gd, i - 1, py, &gc);
205
0
      if (~gc.flags & GRID_FLAG_PADDING)
206
0
        break;
207
0
    }
208
0
    if (i == 0)
209
0
      bg = defaults->bg;
210
0
    else {
211
0
      bg = gc.bg;
212
0
      if (gc.flags & GRID_FLAG_SELECTED) {
213
0
        memcpy(&ngc, &gc, sizeof ngc);
214
0
        if (screen_select_cell(s, &ngc, &gc))
215
0
          bg = ngc.bg;
216
0
      }
217
0
    }
218
0
    tty_attributes(tty, &last, style_ctx);
219
0
    log_debug("%s: clearing %u padding cells", __func__, cx);
220
0
    tty_draw_line_clear(tty, atx, aty, cx, defaults, bg, 0);
221
0
    if (cx == ex)
222
0
      goto out;
223
0
    atx += cx;
224
0
    px += cx;
225
0
    nx -= cx;
226
0
  }
227
228
  /* Did the previous line wrap on to this one? */
229
0
  if (py != 0 &&
230
0
      atx == 0 &&
231
0
      tty->cx >= tty->sx &&
232
0
      tty->cy != UINT_MAX &&
233
0
      tty->cy + 1 == aty &&
234
0
      nx == tty->sx) {
235
0
    gl = grid_get_line(gd, gd->hsize + py - 1);
236
0
    if (gl->flags & GRID_LINE_WRAPPED)
237
0
      wrapped = 1;
238
0
  }
239
240
  /* Loop over each character in the range. */
241
0
  last_i = i = 0;
242
0
  len = 0;
243
0
  width = 0;
244
0
  current_state = TTY_DRAW_LINE_FIRST;
245
0
  for (;;) {
246
    /* Work out the next state. */
247
0
    if (i == nx) {
248
      /*
249
       * If this is the last cell, we are done. But we need to
250
       * go through the loop again to flush anything in
251
       * the buffer.
252
       */
253
0
      empty = 0;
254
0
      next_state = TTY_DRAW_LINE_DONE;
255
0
      gcp = &grid_default_cell;
256
0
    } else {
257
0
      if (i > nx)
258
0
        fatalx("position %u > width %u", i, nx);
259
260
0
      if (px >= ex || i >= ex - px) {
261
        /* Outside the area being drawn. */
262
0
        empty = nx - i;
263
0
        gcp = &grid_default_cell;
264
265
#ifdef ENABLE_IMAGES
266
        /* Map an image cell if needed. */
267
        gcp = image_map_cell(tty, gd, px + i,
268
            gd->hsize + py, gcp, &gc, &empty);
269
#endif
270
271
        /* Flush the background before clearing. */
272
0
        if (empty != 0 && gcp->bg != last.bg)
273
0
          empty = 0;
274
0
      } else {
275
        /* Get the current cell. */
276
0
        grid_view_get_cell(gd, px + i, py, &gc);
277
0
        gcp = &gc;
278
279
#ifdef ENABLE_IMAGES
280
        /* Map an image cell if needed. */
281
        gcp = image_map_cell(tty, gd, px + i,
282
            gd->hsize + py, gcp, &gc, NULL);
283
#endif
284
285
        /* Work out empty cells. */
286
0
        empty = tty_draw_line_get_empty(gcp, &last,
287
0
            nx - i);
288
0
        if (empty == 0) {
289
          /* Update for codeset if needed. */
290
0
          gcp = tty_check_codeset(tty, gcp);
291
292
          /* And for selection. */
293
0
          if (gcp->flags & GRID_FLAG_SELECTED) {
294
0
            memcpy(&ngc, gcp, sizeof ngc);
295
0
            if (screen_select_cell(s, &ngc,
296
0
                gcp))
297
0
              gcp = &ngc;
298
0
          }
299
0
        }
300
0
      }
301
302
      /* Work out the next state. */
303
0
      if (empty != 0)
304
0
        next_state = TTY_DRAW_LINE_EMPTY;
305
0
      else if (current_state == TTY_DRAW_LINE_FIRST)
306
0
        next_state = TTY_DRAW_LINE_SAME;
307
0
      else if (grid_cells_look_equal(gcp, &last)) {
308
0
        if (gcp->data.size > (sizeof buf) - len)
309
0
          next_state = TTY_DRAW_LINE_FLUSH;
310
0
        else
311
0
          next_state = TTY_DRAW_LINE_SAME;
312
0
      } else if (current_state == TTY_DRAW_LINE_NEW1)
313
0
        next_state = TTY_DRAW_LINE_NEW2;
314
0
      else
315
0
        next_state = TTY_DRAW_LINE_NEW1;
316
0
    }
317
0
    if (log_get_level() != 0) {
318
0
      log_debug("%s: cell %u empty %u, bg %u; state: "
319
0
          "current %s, next %s", __func__, px + i, empty,
320
0
          gcp->bg, tty_draw_line_states[current_state],
321
0
          tty_draw_line_states[next_state]);
322
0
    }
323
324
    /* If the state has changed, flush any collected data. */
325
0
    if (next_state != current_state) {
326
0
      if (current_state == TTY_DRAW_LINE_EMPTY) {
327
0
        tty_attributes(tty, &last, style_ctx);
328
0
        tty_draw_line_clear(tty, atx + last_i, aty,
329
0
            i - last_i, defaults, last.bg, wrapped);
330
0
        wrapped = 0;
331
0
      } else if (next_state != TTY_DRAW_LINE_SAME &&
332
0
          len != 0) {
333
0
        tty_attributes(tty, &last, style_ctx);
334
0
        if (atx + i - width != 0 || !wrapped)
335
0
          tty_cursor(tty, atx + i - width, aty);
336
0
        if (~last.attr & GRID_ATTR_CHARSET)
337
0
          tty_putn(tty, buf, len, width);
338
0
        else {
339
0
          for (j = 0; j < len; j++)
340
0
            tty_putc(tty, buf[j]);
341
0
        }
342
0
        len = 0;
343
0
        width = 0;
344
0
        wrapped = 0;
345
0
      }
346
0
      last_i = i;
347
0
    }
348
349
    /* Append the cell if it is not empty and not padding. */
350
0
    if (next_state != TTY_DRAW_LINE_EMPTY) {
351
0
      memcpy(buf + len, gcp->data.data, gcp->data.size);
352
0
      len += gcp->data.size;
353
0
      width += gcp->data.width;
354
0
    }
355
356
    /* If this is the last cell, we are done. */
357
0
    if (next_state == TTY_DRAW_LINE_DONE)
358
0
      break;
359
360
    /* Otherwise move to the next. */
361
0
    current_state = next_state;
362
0
    memcpy(&last, gcp, sizeof last);
363
0
    if (empty != 0)
364
0
      i += empty;
365
0
    else
366
0
      i += gcp->data.width;
367
0
  }
368
369
0
out:
370
0
  tty->flags = (tty->flags & ~TTY_NOCURSOR)|flags;
371
0
  tty_update_mode(tty, tty->mode, s);
372
0
}