Coverage Report

Created: 2026-08-13 06:04

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.15 2026/07/26 09:02:08 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 (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
/* Is this cell empty? */
93
static u_int
94
tty_draw_line_get_empty(const struct grid_cell *gc,
95
    const struct grid_cell *last, u_int nx)
96
0
{
97
0
  u_int empty = 0;
98
99
0
  if (gc->data.width > nx)
100
0
    empty = nx;
101
0
  else if (gc->flags & GRID_FLAG_PADDING)
102
0
    empty = 1;
103
0
  else if (gc->data.width == 0)
104
0
    empty = 1;
105
0
  else if (gc->flags & GRID_FLAG_SELECTED)
106
0
    empty = 0;
107
0
  else if (gc->bg == last->bg && gc->attr == 0 && gc->link == 0) {
108
0
    if (gc->flags & GRID_FLAG_CLEARED)
109
0
      empty = 1;
110
0
    else if (gc->flags & GRID_FLAG_TAB)
111
0
      empty = gc->data.width;
112
0
    else if (gc->data.size == 1 && *gc->data.data == ' ')
113
0
      empty = 1;
114
0
  }
115
0
  return (empty);
116
0
}
117
118
/* Draw a line from screen to tty. */
119
void
120
tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
121
    u_int atx, u_int aty, const struct tty_style_ctx *style_ctx)
122
0
{
123
0
  struct grid   *gd = s->grid;
124
0
  const struct grid_cell  *gcp;
125
0
  struct grid_cell   gc, ngc, last;
126
0
  struct grid_line  *gl;
127
0
  u_int      i, j, last_i, cx, ex, width;
128
0
  u_int      cellsize, bg;
129
0
  int      flags, empty, wrapped = 0;
130
0
  char       buf[1000];
131
0
  size_t       len;
132
0
  enum tty_draw_line_state current_state, next_state;
133
0
  struct tty_style_ctx   default_style_ctx = { 0 };
134
0
  const struct grid_cell  *defaults;
135
136
0
  if (style_ctx == NULL) {
137
0
    default_style_ctx.defaults = &grid_default_cell;
138
0
    default_style_ctx.hyperlinks = s->hyperlinks;
139
0
    style_ctx = &default_style_ctx;
140
0
  }
141
0
  defaults = style_ctx->defaults;
142
143
  /*
144
   * py is the line in the screen to draw. px is the start x and nx is
145
   * the width to draw. atx,aty is the line on the terminal to draw it.
146
   */
147
0
  log_debug("%s: px=%u py=%u nx=%u atx=%u aty=%u", __func__, px, py, nx,
148
0
      atx, aty);
149
150
  /* There is no point in drawing more than the end of the terminal. */
151
0
  if (atx >= tty->sx)
152
0
    return;
153
0
  if (atx + nx >= tty->sx)
154
0
    nx = tty->sx - atx;
155
0
  if (nx == 0)
156
0
    return;
157
158
  /*
159
   * Clamp the width to cellsize - note this is not cellused, because
160
   * there may be empty background cells after it (from BCE).
161
   */
162
0
  cellsize = grid_get_line(gd, gd->hsize + py)->cellsize;
163
0
  if (screen_size_x(s) > cellsize)
164
0
    ex = cellsize;
165
0
  else
166
0
    ex = screen_size_x(s);
167
0
  log_debug("%s: drawing %u-%u,%u (end %u) at %u,%u; defaults: fg=%d, "
168
0
      "bg=%d", __func__, px, px + nx, py, ex, atx, aty, defaults->fg,
169
0
      defaults->bg);
170
171
  /* Turn off cursor while redrawing and reset region and margins. */
172
0
  flags = (tty->flags & TTY_NOCURSOR);
173
0
  tty->flags |= TTY_NOCURSOR;
174
0
  tty_update_mode(tty, tty->mode, s);
175
0
  tty_region_off(tty);
176
0
  tty_margin_off(tty);
177
178
  /* Start with the default cell as the last cell. */
179
0
  memcpy(&last, &grid_default_cell, sizeof last);
180
0
  last.bg = defaults->bg;
181
0
  tty_default_attributes(tty, 8, style_ctx);
182
183
  /*
184
   * If there is padding at the start, we must have truncated a wide
185
   * character. Clear it.
186
   */
187
0
  cx = 0;
188
0
  for (i = px; i < px + nx; i++) {
189
0
    grid_view_get_cell(gd, i, py, &gc);
190
0
    if (~gc.flags & GRID_FLAG_PADDING)
191
0
      break;
192
0
    cx++;
193
0
  }
194
0
  if (cx != 0) {
195
    /* Find the previous cell for the background colour. */
196
0
    for (i = px + 1; i > 0; i--) {
197
0
      grid_view_get_cell(gd, i - 1, py, &gc);
198
0
      if (~gc.flags & GRID_FLAG_PADDING)
199
0
        break;
200
0
    }
201
0
    if (i == 0)
202
0
      bg = defaults->bg;
203
0
    else {
204
0
      bg = gc.bg;
205
0
      if (gc.flags & GRID_FLAG_SELECTED) {
206
0
        memcpy(&ngc, &gc, sizeof ngc);
207
0
        if (screen_select_cell(s, &ngc, &gc))
208
0
          bg = ngc.bg;
209
0
      }
210
0
    }
211
0
    tty_attributes(tty, &last, style_ctx);
212
0
    log_debug("%s: clearing %u padding cells", __func__, cx);
213
0
    tty_draw_line_clear(tty, atx, aty, cx, defaults, bg, 0);
214
0
    if (cx == ex)
215
0
      goto out;
216
0
    atx += cx;
217
0
    px += cx;
218
0
    nx -= cx;
219
0
  }
220
221
  /* Did the previous line wrap on to this one? */
222
0
  if (py != 0 && atx == 0 && tty->cx >= tty->sx && nx == tty->sx) {
223
0
    gl = grid_get_line(gd, gd->hsize + py - 1);
224
0
    if (gl->flags & GRID_LINE_WRAPPED)
225
0
      wrapped = 1;
226
0
  }
227
228
  /* Loop over each character in the range. */
229
0
  last_i = i = 0;
230
0
  len = 0;
231
0
  width = 0;
232
0
  current_state = TTY_DRAW_LINE_FIRST;
233
0
  for (;;) {
234
    /* Work out the next state. */
235
0
    if (i == nx) {
236
      /*
237
       * If this is the last cell, we are done. But we need to
238
       * go through the loop again to flush anything in
239
       * the buffer.
240
       */
241
0
      empty = 0;
242
0
      next_state = TTY_DRAW_LINE_DONE;
243
0
      gcp = &grid_default_cell;
244
0
    } else {
245
0
      if (i > nx)
246
0
        fatalx("position %u > width %u", i, nx);
247
248
0
      if (px >= ex || i >= ex - px) {
249
        /* Outside the area being drawn. */
250
0
        empty = nx - i;
251
0
        gcp = &grid_default_cell;
252
0
      } else {
253
        /* Get the current cell. */
254
0
        grid_view_get_cell(gd, px + i, py, &gc);
255
256
        /* Work out empty cells. */
257
0
        empty = tty_draw_line_get_empty(&gc, &last,
258
0
            nx - i);
259
0
        if (empty != 0)
260
0
          gcp = &gc;
261
0
        else {
262
          /* Update for codeset if needed. */
263
0
          gcp = tty_check_codeset(tty, &gc);
264
265
          /* And for selection. */
266
0
          if (gcp->flags & GRID_FLAG_SELECTED) {
267
0
            memcpy(&ngc, gcp, sizeof ngc);
268
0
            if (screen_select_cell(s, &ngc,
269
0
                gcp))
270
0
              gcp = &ngc;
271
0
          }
272
0
        }
273
0
      }
274
275
      /* Work out the next state. */
276
0
      if (empty != 0)
277
0
        next_state = TTY_DRAW_LINE_EMPTY;
278
0
      else if (current_state == TTY_DRAW_LINE_FIRST)
279
0
        next_state = TTY_DRAW_LINE_SAME;
280
0
      else if (grid_cells_look_equal(gcp, &last)) {
281
0
        if (gcp->data.size > (sizeof buf) - len)
282
0
          next_state = TTY_DRAW_LINE_FLUSH;
283
0
        else
284
0
          next_state = TTY_DRAW_LINE_SAME;
285
0
      } else if (current_state == TTY_DRAW_LINE_NEW1)
286
0
        next_state = TTY_DRAW_LINE_NEW2;
287
0
      else
288
0
        next_state = TTY_DRAW_LINE_NEW1;
289
0
    }
290
0
    if (log_get_level() != 0) {
291
0
      log_debug("%s: cell %u empty %u, bg %u; state: "
292
0
          "current %s, next %s", __func__, px + i, empty,
293
0
          gcp->bg, tty_draw_line_states[current_state],
294
0
          tty_draw_line_states[next_state]);
295
0
    }
296
297
    /* If the state has changed, flush any collected data. */
298
0
    if (next_state != current_state) {
299
0
      if (current_state == TTY_DRAW_LINE_EMPTY) {
300
0
        tty_attributes(tty, &last, style_ctx);
301
0
        tty_draw_line_clear(tty, atx + last_i, aty,
302
0
            i - last_i, defaults, last.bg, wrapped);
303
0
        wrapped = 0;
304
0
      } else if (next_state != TTY_DRAW_LINE_SAME &&
305
0
          len != 0) {
306
0
        tty_attributes(tty, &last, style_ctx);
307
0
        if (atx + i - width != 0 || !wrapped)
308
0
          tty_cursor(tty, atx + i - width, aty);
309
0
        if (~last.attr & GRID_ATTR_CHARSET)
310
0
          tty_putn(tty, buf, len, width);
311
0
        else {
312
0
          for (j = 0; j < len; j++)
313
0
            tty_putc(tty, buf[j]);
314
0
        }
315
0
        len = 0;
316
0
        width = 0;
317
0
        wrapped = 0;
318
0
      }
319
0
      last_i = i;
320
0
    }
321
322
    /* Append the cell if it is not empty and not padding. */
323
0
    if (next_state != TTY_DRAW_LINE_EMPTY) {
324
0
      memcpy(buf + len, gcp->data.data, gcp->data.size);
325
0
      len += gcp->data.size;
326
0
      width += gcp->data.width;
327
0
    }
328
329
    /* If this is the last cell, we are done. */
330
0
    if (next_state == TTY_DRAW_LINE_DONE)
331
0
      break;
332
333
    /* Otherwise move to the next. */
334
0
    current_state = next_state;
335
0
    memcpy(&last, gcp, sizeof last);
336
0
    if (empty != 0)
337
0
      i += empty;
338
0
    else
339
0
      i += gcp->data.width;
340
0
  }
341
342
0
out:
343
0
  tty->flags = (tty->flags & ~TTY_NOCURSOR)|flags;
344
0
  tty_update_mode(tty, tty->mode, s);
345
0
}