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