Coverage Report

Created: 2026-04-12 06:58

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tmux/menu.c
Line
Count
Source
1
/* $OpenBSD$ */
2
3
/*
4
 * Copyright (c) 2019 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 <stdlib.h>
22
#include <string.h>
23
24
#include "tmux.h"
25
26
struct menu_data {
27
  struct cmdq_item  *item;
28
  int      flags;
29
30
  char      *style;
31
  char      *border_style;
32
  char      *selected_style;
33
34
  struct grid_cell   style_gc;
35
  struct grid_cell   border_style_gc;
36
  struct grid_cell   selected_style_gc;
37
  enum box_lines     border_lines;
38
39
  struct cmd_find_state  fs;
40
  struct screen    s;
41
  struct visible_ranges  r;
42
43
  u_int      px;
44
  u_int      py;
45
46
  struct menu   *menu;
47
  int      choice;
48
49
  menu_choice_cb     cb;
50
  void      *data;
51
};
52
53
void
54
menu_add_items(struct menu *menu, const struct menu_item *items,
55
    struct cmdq_item *qitem, struct client *c, struct cmd_find_state *fs)
56
0
{
57
0
  const struct menu_item  *loop;
58
59
0
  for (loop = items; loop->name != NULL; loop++)
60
0
    menu_add_item(menu, loop, qitem, c, fs);
61
0
}
62
63
void
64
menu_add_item(struct menu *menu, const struct menu_item *item,
65
    struct cmdq_item *qitem, struct client *c, struct cmd_find_state *fs)
66
0
{
67
0
  struct menu_item  *new_item;
68
0
  const char    *key = NULL, *cmd, *suffix = "";
69
0
  char      *s, *trimmed, *name;
70
0
  u_int      width, max_width;
71
0
  int      line;
72
0
  size_t       keylen, slen;
73
74
0
  line = (item == NULL || item->name == NULL || *item->name == '\0');
75
0
  if (line && menu->count == 0)
76
0
    return;
77
0
  if (line && menu->items[menu->count - 1].name == NULL)
78
0
    return;
79
80
0
  menu->items = xreallocarray(menu->items, menu->count + 1,
81
0
      sizeof *menu->items);
82
0
  new_item = &menu->items[menu->count++];
83
0
  memset(new_item, 0, sizeof *new_item);
84
85
0
  if (line)
86
0
    return;
87
88
0
  if (fs != NULL)
89
0
    s = format_single_from_state(qitem, item->name, c, fs);
90
0
  else
91
0
    s = format_single(qitem, item->name, c, NULL, NULL, NULL);
92
0
  if (*s == '\0') { /* no item if empty after format expanded */
93
0
    free(s);
94
0
    menu->count--;
95
0
    return;
96
0
  }
97
0
  max_width = c->tty.sx - 4;
98
99
0
  slen = strlen(s);
100
0
  if (*s != '-' && item->key != KEYC_UNKNOWN && item->key != KEYC_NONE) {
101
0
    key = key_string_lookup_key(item->key, 0);
102
0
    keylen = strlen(key) + 3; /* 3 = space and two brackets */
103
104
    /*
105
     * Add the key if it is shorter than a quarter of the available
106
     * space or there is space for the entire item text and the
107
     * key.
108
     */
109
0
    if (keylen <= max_width / 4)
110
0
      max_width -= keylen;
111
0
    else if (keylen >= max_width || slen >= max_width - keylen)
112
0
      key = NULL;
113
0
  }
114
115
0
  if (slen > max_width) {
116
0
    max_width--;
117
0
    suffix = ">";
118
0
  }
119
0
  trimmed = format_trim_right(s, max_width);
120
0
  if (key != NULL) {
121
0
    xasprintf(&name, "%s%s#[default] #[align=right](%s)",
122
0
        trimmed, suffix, key);
123
0
  } else
124
0
    xasprintf(&name, "%s%s", trimmed, suffix);
125
0
  free(trimmed);
126
127
0
  new_item->name = name;
128
0
  free(s);
129
130
0
  cmd = item->command;
131
0
  if (cmd != NULL) {
132
0
    if (fs != NULL)
133
0
      s = format_single_from_state(qitem, cmd, c, fs);
134
0
    else
135
0
      s = format_single(qitem, cmd, c, NULL, NULL, NULL);
136
0
  } else
137
0
    s = NULL;
138
0
  new_item->command = s;
139
0
  new_item->key = item->key;
140
141
0
  width = format_width(new_item->name);
142
0
  if (*new_item->name == '-')
143
0
    width--;
144
0
  if (width > menu->width)
145
0
    menu->width = width;
146
0
}
147
148
struct menu *
149
menu_create(const char *title)
150
0
{
151
0
  struct menu *menu;
152
153
0
  menu = xcalloc(1, sizeof *menu);
154
0
  menu->title = xstrdup(title);
155
0
  menu->width = format_width(title);
156
157
0
  return (menu);
158
0
}
159
160
void
161
menu_free(struct menu *menu)
162
0
{
163
0
  u_int i;
164
165
0
  if (menu == NULL)
166
0
    return;
167
168
0
  for (i = 0; i < menu->count; i++) {
169
0
    free((void *)menu->items[i].name);
170
0
    free((void *)menu->items[i].command);
171
0
  }
172
0
  free(menu->items);
173
174
0
  free((void *)menu->title);
175
0
  free(menu);
176
0
}
177
178
struct screen *
179
menu_mode_cb(__unused struct client *c, void *data, u_int *cx, u_int *cy)
180
0
{
181
0
  struct menu_data  *md = data;
182
183
0
  *cx = md->px + 2;
184
0
  if (md->choice == -1)
185
0
    *cy = md->py;
186
0
  else
187
0
    *cy = md->py + 1 + md->choice;
188
189
0
  return (&md->s);
190
0
}
191
192
/* Return parts of the input range which are not obstructed by the menu. */
193
struct visible_ranges *
194
menu_check_cb(__unused struct client *c, void *data, u_int px, u_int py,
195
    u_int nx)
196
0
{
197
0
  struct menu_data  *md = data;
198
0
  struct menu   *menu = md->menu;
199
200
0
  server_client_overlay_range(md->px, md->py, menu->width + 4,
201
0
      menu->count + 2, px, py, nx, &md->r);
202
0
  return (&md->r);
203
0
}
204
205
static void
206
menu_reapply_styles(struct menu_data *md, struct client *c)
207
0
{
208
0
  struct session    *s = c->session;
209
0
  struct options    *o;
210
0
  struct format_tree  *ft;
211
0
  struct style     sytmp;
212
213
0
  if (s == NULL)
214
0
    return;
215
0
  o = s->curw->window->options;
216
217
0
  ft = format_create_defaults(NULL, c, s, s->curw, NULL);
218
219
  /* Reapply menu style from options. */
220
0
  memcpy(&md->style_gc, &grid_default_cell, sizeof md->style_gc);
221
0
  style_apply(&md->style_gc, o, "menu-style", ft);
222
0
  if (md->style != NULL) {
223
0
    style_set(&sytmp, &grid_default_cell);
224
0
    if (style_parse(&sytmp, &md->style_gc, md->style) == 0) {
225
0
      md->style_gc.fg = sytmp.gc.fg;
226
0
      md->style_gc.bg = sytmp.gc.bg;
227
0
    }
228
0
  }
229
230
  /* Reapply selected style from options. */
231
0
  memcpy(&md->selected_style_gc, &grid_default_cell,
232
0
      sizeof md->selected_style_gc);
233
0
  style_apply(&md->selected_style_gc, o, "menu-selected-style", ft);
234
0
  if (md->selected_style != NULL) {
235
0
    style_set(&sytmp, &grid_default_cell);
236
0
    if (style_parse(&sytmp, &md->selected_style_gc,
237
0
        md->selected_style) == 0) {
238
0
      md->selected_style_gc.fg = sytmp.gc.fg;
239
0
      md->selected_style_gc.bg = sytmp.gc.bg;
240
0
    }
241
0
  }
242
243
  /* Reapply border style from options. */
244
0
  memcpy(&md->border_style_gc, &grid_default_cell,
245
0
      sizeof md->border_style_gc);
246
0
  style_apply(&md->border_style_gc, o, "menu-border-style", ft);
247
0
  if (md->border_style != NULL) {
248
0
    style_set(&sytmp, &grid_default_cell);
249
0
    if (style_parse(&sytmp, &md->border_style_gc,
250
0
        md->border_style) == 0) {
251
0
      md->border_style_gc.fg = sytmp.gc.fg;
252
0
      md->border_style_gc.bg = sytmp.gc.bg;
253
0
    }
254
0
  }
255
256
0
  format_free(ft);
257
0
}
258
259
void
260
menu_draw_cb(struct client *c, void *data,
261
    __unused struct screen_redraw_ctx *rctx)
262
0
{
263
0
  struct menu_data  *md = data;
264
0
  struct tty    *tty = &c->tty;
265
0
  struct screen   *s = &md->s;
266
0
  struct menu   *menu = md->menu;
267
0
  struct screen_write_ctx  ctx;
268
0
  u_int      i, px = md->px, py = md->py;
269
270
0
  menu_reapply_styles(md, c);
271
272
0
  screen_write_start(&ctx, s);
273
0
  screen_write_clearscreen(&ctx, 8);
274
275
0
  if (md->border_lines != BOX_LINES_NONE) {
276
0
    screen_write_box(&ctx, menu->width + 4, menu->count + 2,
277
0
        md->border_lines, &md->border_style_gc, menu->title);
278
0
  }
279
280
0
  screen_write_menu(&ctx, menu, md->choice, md->border_lines,
281
0
      &md->style_gc, &md->border_style_gc, &md->selected_style_gc);
282
0
  screen_write_stop(&ctx);
283
284
0
  for (i = 0; i < screen_size_y(&md->s); i++) {
285
0
    tty_draw_line(tty, s, 0, i, menu->width + 4, px, py + i,
286
0
        &grid_default_cell, NULL);
287
0
  }
288
0
}
289
290
void
291
menu_free_cb(__unused struct client *c, void *data)
292
0
{
293
0
  struct menu_data  *md = data;
294
295
0
  if (md->item != NULL)
296
0
    cmdq_continue(md->item);
297
298
0
  if (md->cb != NULL)
299
0
    md->cb(md->menu, UINT_MAX, KEYC_NONE, md->data);
300
301
0
  free(md->r.ranges);
302
0
  screen_free(&md->s);
303
304
0
  menu_free(md->menu);
305
0
  free(md->style);
306
0
  free(md->selected_style);
307
0
  free(md->border_style);
308
0
  free(md);
309
0
}
310
311
int
312
menu_key_cb(struct client *c, void *data, struct key_event *event)
313
0
{
314
0
  struct menu_data    *md = data;
315
0
  struct menu     *menu = md->menu;
316
0
  struct mouse_event    *m = &event->m;
317
0
  u_int        i;
318
0
  int        count = menu->count, old = md->choice;
319
0
  const char      *name = NULL;
320
0
  const struct menu_item    *item;
321
0
  struct cmdq_state   *state;
322
0
  enum cmd_parse_status    status;
323
0
  char        *error;
324
325
0
  if (KEYC_IS_MOUSE(event->key)) {
326
0
    if (md->flags & MENU_NOMOUSE) {
327
0
      if (MOUSE_BUTTONS(m->b) != MOUSE_BUTTON_1)
328
0
        return (1);
329
0
      return (0);
330
0
    }
331
0
    if (m->x < md->px ||
332
0
        m->x > md->px + 4 + menu->width ||
333
0
        m->y < md->py + 1 ||
334
0
        m->y > md->py + 1 + count - 1) {
335
0
      if (~md->flags & MENU_STAYOPEN) {
336
0
        if (MOUSE_RELEASE(m->b))
337
0
          return (1);
338
0
      } else {
339
0
        if (!MOUSE_RELEASE(m->b) &&
340
0
            !MOUSE_WHEEL(m->b) &&
341
0
            !MOUSE_DRAG(m->b))
342
0
          return (1);
343
0
      }
344
0
      if (md->choice != -1) {
345
0
        md->choice = -1;
346
0
        c->flags |= CLIENT_REDRAWOVERLAY;
347
0
      }
348
0
      return (0);
349
0
    }
350
0
    if (~md->flags & MENU_STAYOPEN) {
351
0
      if (MOUSE_RELEASE(m->b))
352
0
        goto chosen;
353
0
    } else {
354
0
      if (!MOUSE_WHEEL(m->b) && !MOUSE_DRAG(m->b))
355
0
        goto chosen;
356
0
    }
357
0
    md->choice = m->y - (md->py + 1);
358
0
    if (md->choice != old)
359
0
      c->flags |= CLIENT_REDRAWOVERLAY;
360
0
    return (0);
361
0
  }
362
0
  for (i = 0; i < (u_int)count; i++) {
363
0
    name = menu->items[i].name;
364
0
    if (name == NULL || *name == '-')
365
0
      continue;
366
0
    if ((event->key & ~KEYC_MASK_FLAGS) == menu->items[i].key) {
367
0
      md->choice = i;
368
0
      goto chosen;
369
0
    }
370
0
  }
371
0
  switch (event->key & ~KEYC_MASK_FLAGS) {
372
0
  case KEYC_BTAB:
373
0
  case KEYC_UP:
374
0
  case 'k':
375
0
    if (old == -1)
376
0
      old = 0;
377
0
    do {
378
0
      if (md->choice == -1 || md->choice == 0)
379
0
        md->choice = count - 1;
380
0
      else
381
0
        md->choice--;
382
0
      name = menu->items[md->choice].name;
383
0
    } while ((name == NULL || *name == '-') && md->choice != old);
384
0
    c->flags |= CLIENT_REDRAWOVERLAY;
385
0
    return (0);
386
0
  case KEYC_BSPACE:
387
0
    if (~md->flags & MENU_TAB)
388
0
      break;
389
0
    return (1);
390
0
  case '\011': /* Tab */
391
0
    if (~md->flags & MENU_TAB)
392
0
      break;
393
0
    if (md->choice == count - 1)
394
0
      return (1);
395
    /* FALLTHROUGH */
396
0
  case KEYC_DOWN:
397
0
  case 'j':
398
0
    if (old == -1)
399
0
      old = 0;
400
0
    do {
401
0
      if (md->choice == -1 || md->choice == count - 1)
402
0
        md->choice = 0;
403
0
      else
404
0
        md->choice++;
405
0
      name = menu->items[md->choice].name;
406
0
    } while ((name == NULL || *name == '-') && md->choice != old);
407
0
    c->flags |= CLIENT_REDRAWOVERLAY;
408
0
    return (0);
409
0
  case KEYC_PPAGE:
410
0
  case 'b'|KEYC_CTRL:
411
0
    if (md->choice < 6)
412
0
      md->choice = 0;
413
0
    else {
414
0
      i = 5;
415
0
      while (i > 0) {
416
0
        md->choice--;
417
0
        name = menu->items[md->choice].name;
418
0
        if (md->choice != 0 &&
419
0
            (name != NULL && *name != '-'))
420
0
          i--;
421
0
        else if (md->choice == 0)
422
0
          break;
423
0
      }
424
0
    }
425
0
    c->flags |= CLIENT_REDRAWOVERLAY;
426
0
    break;
427
0
  case KEYC_NPAGE:
428
0
    if (md->choice > count - 6) {
429
0
      md->choice = count - 1;
430
0
      name = menu->items[md->choice].name;
431
0
    } else {
432
0
      i = 5;
433
0
      while (i > 0) {
434
0
        md->choice++;
435
0
        name = menu->items[md->choice].name;
436
0
        if (md->choice != count - 1 &&
437
0
            (name != NULL && *name != '-'))
438
0
          i--;
439
0
        else if (md->choice == count - 1)
440
0
          break;
441
0
      }
442
0
    }
443
0
    while (name == NULL || *name == '-') {
444
0
      md->choice--;
445
0
      name = menu->items[md->choice].name;
446
0
    }
447
0
    c->flags |= CLIENT_REDRAWOVERLAY;
448
0
    break;
449
0
  case 'g':
450
0
  case KEYC_HOME:
451
0
    md->choice = 0;
452
0
    name = menu->items[md->choice].name;
453
0
    while (name == NULL || *name == '-') {
454
0
      md->choice++;
455
0
      name = menu->items[md->choice].name;
456
0
    }
457
0
    c->flags |= CLIENT_REDRAWOVERLAY;
458
0
    break;
459
0
  case 'G':
460
0
  case KEYC_END:
461
0
    md->choice = count - 1;
462
0
    name = menu->items[md->choice].name;
463
0
    while (name == NULL || *name == '-') {
464
0
      md->choice--;
465
0
      name = menu->items[md->choice].name;
466
0
    }
467
0
    c->flags |= CLIENT_REDRAWOVERLAY;
468
0
    break;
469
0
  case 'f'|KEYC_CTRL:
470
0
    break;
471
0
  case '\r':
472
0
    goto chosen;
473
0
  case '\033': /* Escape */
474
0
  case 'c'|KEYC_CTRL:
475
0
  case 'g'|KEYC_CTRL:
476
0
  case 'q':
477
0
    return (1);
478
0
  }
479
0
  return (0);
480
481
0
chosen:
482
0
  if (md->choice == -1)
483
0
    return (1);
484
0
  item = &menu->items[md->choice];
485
0
  if (item->name == NULL || *item->name == '-') {
486
0
    if (md->flags & MENU_STAYOPEN)
487
0
      return (0);
488
0
    return (1);
489
0
  }
490
0
  if (md->cb != NULL) {
491
0
      md->cb(md->menu, md->choice, item->key, md->data);
492
0
      md->cb = NULL;
493
0
      return (1);
494
0
  }
495
496
0
  if (md->item != NULL)
497
0
    event = cmdq_get_event(md->item);
498
0
  else
499
0
    event = NULL;
500
0
  state = cmdq_new_state(&md->fs, event, 0);
501
502
0
  status = cmd_parse_and_append(item->command, NULL, c, state, &error);
503
0
  if (status == CMD_PARSE_ERROR) {
504
0
    cmdq_append(c, cmdq_get_error(error));
505
0
    free(error);
506
0
  }
507
0
  cmdq_free_state(state);
508
509
0
  return (1);
510
0
}
511
512
static void
513
menu_resize_cb(struct client *c, void *data)
514
0
{
515
0
  struct menu_data  *md = data;
516
0
  u_int      nx, ny, w, h;
517
518
0
  if (md == NULL)
519
0
    return;
520
521
0
  nx = md->px;
522
0
  ny = md->py;
523
524
0
  w = md->menu->width + 4;
525
0
  h = md->menu->count + 2;
526
527
0
  if (nx + w > c->tty.sx) {
528
0
    if (c->tty.sx <= w)
529
0
      nx = 0;
530
0
    else
531
0
      nx = c->tty.sx - w;
532
0
  }
533
534
0
  if (ny + h > c->tty.sy) {
535
0
    if (c->tty.sy <= h)
536
0
      ny = 0;
537
0
    else
538
0
      ny = c->tty.sy - h;
539
0
  }
540
0
  md->px = nx;
541
0
  md->py = ny;
542
0
}
543
544
struct menu_data *
545
menu_prepare(struct menu *menu, int flags, int starting_choice,
546
    struct cmdq_item *item, u_int px, u_int py, struct client *c,
547
    enum box_lines lines, const char *style, const char *selected_style,
548
    const char *border_style, struct cmd_find_state *fs, menu_choice_cb cb,
549
    void *data)
550
0
{
551
0
  struct menu_data  *md;
552
0
  int      choice;
553
0
  const char    *name;
554
0
  struct options    *o = c->session->curw->window->options;
555
556
0
  if (c->tty.sx < menu->width + 4 || c->tty.sy < menu->count + 2)
557
0
    return (NULL);
558
0
  if (px + menu->width + 4 > c->tty.sx)
559
0
    px = c->tty.sx - menu->width - 4;
560
0
  if (py + menu->count + 2 > c->tty.sy)
561
0
    py = c->tty.sy - menu->count - 2;
562
563
0
  if (lines == BOX_LINES_DEFAULT)
564
0
    lines = options_get_number(o, "menu-border-lines");
565
566
0
  md = xcalloc(1, sizeof *md);
567
0
  md->item = item;
568
0
  md->flags = flags;
569
0
  md->border_lines = lines;
570
571
0
  if (style != NULL)
572
0
    md->style = xstrdup(style);
573
0
  if (selected_style != NULL)
574
0
    md->selected_style = xstrdup(selected_style);
575
0
  if (border_style != NULL)
576
0
    md->border_style = xstrdup(border_style);
577
578
0
  if (fs != NULL)
579
0
    cmd_find_copy_state(&md->fs, fs);
580
0
  screen_init(&md->s, menu->width + 4, menu->count + 2, 0);
581
0
  if (~md->flags & MENU_NOMOUSE)
582
0
    md->s.mode |= (MODE_MOUSE_ALL|MODE_MOUSE_BUTTON);
583
0
  md->s.mode &= ~MODE_CURSOR;
584
585
0
  md->px = px;
586
0
  md->py = py;
587
588
0
  md->menu = menu;
589
0
  md->choice = -1;
590
591
0
  if (md->flags & MENU_NOMOUSE) {
592
0
    if (starting_choice >= (int)menu->count) {
593
0
      starting_choice = menu->count - 1;
594
0
      choice = starting_choice + 1;
595
0
      for (;;) {
596
0
        name = menu->items[choice - 1].name;
597
0
        if (name != NULL && *name != '-') {
598
0
          md->choice = choice - 1;
599
0
          break;
600
0
        }
601
0
        if (--choice == 0)
602
0
          choice = menu->count;
603
0
        if (choice == starting_choice + 1)
604
0
          break;
605
0
      }
606
0
    } else if (starting_choice >= 0) {
607
0
      choice = starting_choice;
608
0
      for (;;) {
609
0
        name = menu->items[choice].name;
610
0
        if (name != NULL && *name != '-') {
611
0
          md->choice = choice;
612
0
          break;
613
0
        }
614
0
        if (++choice == (int)menu->count)
615
0
          choice = 0;
616
0
        if (choice == starting_choice)
617
0
          break;
618
0
      }
619
0
    }
620
0
  }
621
622
0
  md->cb = cb;
623
0
  md->data = data;
624
0
  return (md);
625
0
}
626
627
int
628
menu_display(struct menu *menu, int flags, int starting_choice,
629
    struct cmdq_item *item, u_int px, u_int py, struct client *c,
630
    enum box_lines lines, const char *style, const char *selected_style,
631
    const char *border_style, struct cmd_find_state *fs, menu_choice_cb cb,
632
    void *data)
633
0
{
634
0
  struct menu_data  *md;
635
636
0
  md = menu_prepare(menu, flags, starting_choice, item, px, py, c, lines,
637
0
      style, selected_style, border_style, fs, cb, data);
638
0
  if (md == NULL)
639
0
    return (-1);
640
0
  server_client_set_overlay(c, 0, NULL, menu_mode_cb, menu_draw_cb,
641
0
      menu_key_cb, menu_free_cb, menu_resize_cb, md);
642
0
  return (0);
643
0
}