Coverage Report

Created: 2024-09-11 06:44

/src/tmux/menu.c
Line
Count
Source (jump to first uncovered line)
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_UP:
302
0
  case 'k':
303
0
    if (old == -1)
304
0
      old = 0;
305
0
    do {
306
0
      if (md->choice == -1 || md->choice == 0)
307
0
        md->choice = count - 1;
308
0
      else
309
0
        md->choice--;
310
0
      name = menu->items[md->choice].name;
311
0
    } while ((name == NULL || *name == '-') && md->choice != old);
312
0
    c->flags |= CLIENT_REDRAWOVERLAY;
313
0
    return (0);
314
0
  case KEYC_BSPACE:
315
0
    if (~md->flags & MENU_TAB)
316
0
      break;
317
0
    return (1);
318
0
  case '\011': /* Tab */
319
0
    if (~md->flags & MENU_TAB)
320
0
      break;
321
0
    if (md->choice == count - 1)
322
0
      return (1);
323
    /* FALLTHROUGH */
324
0
  case KEYC_DOWN:
325
0
  case 'j':
326
0
    if (old == -1)
327
0
      old = 0;
328
0
    do {
329
0
      if (md->choice == -1 || md->choice == count - 1)
330
0
        md->choice = 0;
331
0
      else
332
0
        md->choice++;
333
0
      name = menu->items[md->choice].name;
334
0
    } while ((name == NULL || *name == '-') && md->choice != old);
335
0
    c->flags |= CLIENT_REDRAWOVERLAY;
336
0
    return (0);
337
0
  case KEYC_PPAGE:
338
0
  case 'b'|KEYC_CTRL:
339
0
    if (md->choice < 6)
340
0
      md->choice = 0;
341
0
    else {
342
0
      i = 5;
343
0
      while (i > 0) {
344
0
        md->choice--;
345
0
        name = menu->items[md->choice].name;
346
0
        if (md->choice != 0 &&
347
0
            (name != NULL && *name != '-'))
348
0
          i--;
349
0
        else if (md->choice == 0)
350
0
          break;
351
0
      }
352
0
    }
353
0
    c->flags |= CLIENT_REDRAWOVERLAY;
354
0
    break;
355
0
  case KEYC_NPAGE:
356
0
    if (md->choice > count - 6) {
357
0
      md->choice = count - 1;
358
0
      name = menu->items[md->choice].name;
359
0
    } else {
360
0
      i = 5;
361
0
      while (i > 0) {
362
0
        md->choice++;
363
0
        name = menu->items[md->choice].name;
364
0
        if (md->choice != count - 1 &&
365
0
            (name != NULL && *name != '-'))
366
0
          i++;
367
0
        else if (md->choice == count - 1)
368
0
          break;
369
0
      }
370
0
    }
371
0
    while (name == NULL || *name == '-') {
372
0
      md->choice--;
373
0
      name = menu->items[md->choice].name;
374
0
    }
375
0
    c->flags |= CLIENT_REDRAWOVERLAY;
376
0
    break;
377
0
  case 'g':
378
0
  case KEYC_HOME:
379
0
    md->choice = 0;
380
0
    name = menu->items[md->choice].name;
381
0
    while (name == NULL || *name == '-') {
382
0
      md->choice++;
383
0
      name = menu->items[md->choice].name;
384
0
    }
385
0
    c->flags |= CLIENT_REDRAWOVERLAY;
386
0
    break;
387
0
  case 'G':
388
0
  case KEYC_END:
389
0
    md->choice = count - 1;
390
0
    name = menu->items[md->choice].name;
391
0
    while (name == NULL || *name == '-') {
392
0
      md->choice--;
393
0
      name = menu->items[md->choice].name;
394
0
    }
395
0
    c->flags |= CLIENT_REDRAWOVERLAY;
396
0
    break;
397
0
  case 'f'|KEYC_CTRL:
398
0
    break;
399
0
  case '\r':
400
0
    goto chosen;
401
0
  case '\033': /* Escape */
402
0
  case 'c'|KEYC_CTRL:
403
0
  case 'g'|KEYC_CTRL:
404
0
  case 'q':
405
0
    return (1);
406
0
  }
407
0
  return (0);
408
409
0
chosen:
410
0
  if (md->choice == -1)
411
0
    return (1);
412
0
  item = &menu->items[md->choice];
413
0
  if (item->name == NULL || *item->name == '-') {
414
0
    if (md->flags & MENU_STAYOPEN)
415
0
      return (0);
416
0
    return (1);
417
0
  }
418
0
  if (md->cb != NULL) {
419
0
      md->cb(md->menu, md->choice, item->key, md->data);
420
0
      md->cb = NULL;
421
0
      return (1);
422
0
  }
423
424
0
  if (md->item != NULL)
425
0
    event = cmdq_get_event(md->item);
426
0
  else
427
0
    event = NULL;
428
0
  state = cmdq_new_state(&md->fs, event, 0);
429
430
0
  status = cmd_parse_and_append(item->command, NULL, c, state, &error);
431
0
  if (status == CMD_PARSE_ERROR) {
432
0
    cmdq_append(c, cmdq_get_error(error));
433
0
    free(error);
434
0
  }
435
0
  cmdq_free_state(state);
436
437
0
  return (1);
438
0
}
439
440
static void
441
menu_set_style(struct client *c, struct grid_cell *gc, const char *style,
442
    const char *option)
443
0
{
444
0
  struct style   sytmp;
445
0
  struct options  *o = c->session->curw->window->options;
446
447
0
  memcpy(gc, &grid_default_cell, sizeof *gc);
448
0
  style_apply(gc, o, option, NULL);
449
0
  if (style != NULL) {
450
0
    style_set(&sytmp, &grid_default_cell);
451
0
    if (style_parse(&sytmp, gc, style) == 0) {
452
0
      gc->fg = sytmp.gc.fg;
453
0
      gc->bg = sytmp.gc.bg;
454
0
    }
455
0
  }
456
0
  gc->attr = 0;
457
0
}
458
459
struct menu_data *
460
menu_prepare(struct menu *menu, int flags, int starting_choice,
461
    struct cmdq_item *item, u_int px, u_int py, struct client *c,
462
    enum box_lines lines, const char *style, const char *selected_style,
463
    const char *border_style, struct cmd_find_state *fs, menu_choice_cb cb,
464
    void *data)
465
0
{
466
0
  struct menu_data  *md;
467
0
  int      choice;
468
0
  const char    *name;
469
0
  struct options    *o = c->session->curw->window->options;
470
471
0
  if (c->tty.sx < menu->width + 4 || c->tty.sy < menu->count + 2)
472
0
    return (NULL);
473
0
  if (px + menu->width + 4 > c->tty.sx)
474
0
    px = c->tty.sx - menu->width - 4;
475
0
  if (py + menu->count + 2 > c->tty.sy)
476
0
    py = c->tty.sy - menu->count - 2;
477
478
0
  if (lines == BOX_LINES_DEFAULT)
479
0
    lines = options_get_number(o, "menu-border-lines");
480
481
0
  md = xcalloc(1, sizeof *md);
482
0
  md->item = item;
483
0
  md->flags = flags;
484
0
  md->border_lines = lines;
485
486
0
  menu_set_style(c, &md->style, style, "menu-style");
487
0
  menu_set_style(c, &md->selected_style, selected_style,
488
0
      "menu-selected-style");
489
0
  menu_set_style(c, &md->border_style, border_style, "menu-border-style");
490
491
0
  if (fs != NULL)
492
0
    cmd_find_copy_state(&md->fs, fs);
493
0
  screen_init(&md->s, menu->width + 4, menu->count + 2, 0);
494
0
  if (~md->flags & MENU_NOMOUSE)
495
0
    md->s.mode |= (MODE_MOUSE_ALL|MODE_MOUSE_BUTTON);
496
0
  md->s.mode &= ~MODE_CURSOR;
497
498
0
  md->px = px;
499
0
  md->py = py;
500
501
0
  md->menu = menu;
502
0
  md->choice = -1;
503
504
0
  if (md->flags & MENU_NOMOUSE) {
505
0
    if (starting_choice >= (int)menu->count) {
506
0
      starting_choice = menu->count - 1;
507
0
      choice = starting_choice + 1;
508
0
      for (;;) {
509
0
        name = menu->items[choice - 1].name;
510
0
        if (name != NULL && *name != '-') {
511
0
          md->choice = choice - 1;
512
0
          break;
513
0
        }
514
0
        if (--choice == 0)
515
0
          choice = menu->count;
516
0
        if (choice == starting_choice + 1)
517
0
          break;
518
0
      }
519
0
    } else if (starting_choice >= 0) {
520
0
      choice = starting_choice;
521
0
      for (;;) {
522
0
        name = menu->items[choice].name;
523
0
        if (name != NULL && *name != '-') {
524
0
          md->choice = choice;
525
0
          break;
526
0
        }
527
0
        if (++choice == (int)menu->count)
528
0
          choice = 0;
529
0
        if (choice == starting_choice)
530
0
          break;
531
0
      }
532
0
    }
533
0
  }
534
535
0
  md->cb = cb;
536
0
  md->data = data;
537
0
  return (md);
538
0
}
539
540
int
541
menu_display(struct menu *menu, int flags, int starting_choice,
542
    struct cmdq_item *item, u_int px, u_int py, struct client *c,
543
    enum box_lines lines, const char *style, const char *selected_style,
544
    const char *border_style, struct cmd_find_state *fs, menu_choice_cb cb,
545
    void *data)
546
0
{
547
0
  struct menu_data  *md;
548
549
0
  md = menu_prepare(menu, flags, starting_choice, item, px, py, c, lines,
550
0
      style, selected_style, border_style, fs, cb, data);
551
0
  if (md == NULL)
552
0
    return (-1);
553
0
  server_client_set_overlay(c, 0, NULL, menu_mode_cb, menu_draw_cb,
554
0
      menu_key_cb, menu_free_cb, NULL, md);
555
0
  return (0);
556
0
}