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