Coverage Report

Created: 2026-01-10 06:21

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