1/* $OpenBSD: menu.c,v 1.52 2023/08/15 07:01:47 nicm Exp $ */ 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 26struct 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 48void 49menu_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 58void 59menu_add_item(struct menu *menu, const struct menu_item *item, 60 struct cmdq_item *qitem, struct client *c, struct cmd_find_state *fs) 61{ 62 struct menu_item *new_item; 63 const char *key = NULL, *cmd, *suffix = ""; 64 char *s, *trimmed, *name; 65 u_int width, max_width; 66 int line; 67 size_t keylen, slen; 68 69 line = (item == NULL || item->name == NULL || *item->name == '\0'); 70 if (line && menu->count == 0) 71 return; 72 if (line && menu->items[menu->count - 1].name == NULL) 73 return; 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 80 if (line) 81 return; 82 83 if (fs != NULL) 84 s = format_single_from_state(qitem, item->name, c, fs); 85 else 86 s = format_single(qitem, item->name, c, NULL, NULL, NULL); 87 if (*s == '\0') { /* no item if empty after format expanded */ 88 menu->count--; 89 return; 90 } 91 max_width = c->tty.sx - 4; 92 93 slen = strlen(s); 94 if (*s != '-' && item->key != KEYC_UNKNOWN && item->key != KEYC_NONE) { 95 key = key_string_lookup_key(item->key, 0); 96 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 if (keylen <= max_width / 4) 104 max_width -= keylen; 105 else if (keylen >= max_width || slen >= max_width - keylen) 106 key = NULL; 107 } 108 109 if (slen > max_width) { 110 max_width--; 111 suffix = ">"; 112 } 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); 120 121 new_item->name = name; 122 free(s); 123 124 cmd = item->command; 125 if (cmd != NULL) { 126 if (fs != NULL) 127 s = format_single_from_state(qitem, cmd, c, fs); 128 else 129 s = format_single(qitem, cmd, c, NULL, NULL, NULL); 130 } else 131 s = NULL; 132 new_item->command = s; 133 new_item->key = item->key; 134 135 width = format_width(new_item->name); 136 if (*new_item->name == '-') 137 width--; 138 if (width > menu->width) 139 menu->width = width; 140} 141 142struct menu * 143menu_create(const char *title) 144{ 145 struct menu *menu; 146 147 menu = xcalloc(1, sizeof *menu); 148 menu->title = xstrdup(title); 149 menu->width = format_width(title); 150 151 return (menu); 152} 153 154void 155menu_free(struct menu *menu) 156{ 157 u_int i; 158 159 for (i = 0; i < menu->count; i++) { 160 free((void *)menu->items[i].name); 161 free((void *)menu->items[i].command); 162 } 163 free(menu->items); 164 165 free((void *)menu->title); 166 free(menu); 167} 168 169struct screen * 170menu_mode_cb(__unused struct client *c, void *data, u_int *cx, u_int *cy) 171{ 172 struct menu_data *md = data; 173 174 *cx = md->px + 2; 175 if (md->choice == -1) 176 *cy = md->py; 177 else 178 *cy = md->py + 1 + md->choice; 179 180 return (&md->s); 181} 182 183/* Return parts of the input range which are not obstructed by the menu. */ 184void 185menu_check_cb(__unused struct client *c, void *data, u_int px, u_int py, 186 u_int nx, struct overlay_ranges *r) 187{ 188 struct menu_data *md = data; 189 struct menu *menu = md->menu; 190 191 server_client_overlay_range(md->px, md->py, menu->width + 4, 192 menu->count + 2, px, py, nx, r); 193} 194 195void 196menu_draw_cb(struct client *c, void *data, 197 __unused struct screen_redraw_ctx *rctx) 198{ 199 struct menu_data *md = data; 200 struct tty *tty = &c->tty; 201 struct screen *s = &md->s; 202 struct menu *menu = md->menu; 203 struct screen_write_ctx ctx; 204 u_int i, px = md->px, py = md->py; 205 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 } 213 214 screen_write_menu(&ctx, menu, md->choice, md->border_lines, 215 &md->style, &md->border_style, &md->selected_style); 216 screen_write_stop(&ctx); 217 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 } 222} 223 224void 225menu_free_cb(__unused struct client *c, void *data) 226{ 227 struct menu_data *md = data; 228 229 if (md->item != NULL) 230 cmdq_continue(md->item); 231 232 if (md->cb != NULL) 233 md->cb(md->menu, UINT_MAX, KEYC_NONE, md->data); 234 235 screen_free(&md->s); 236 menu_free(md->menu); 237 free(md); 238} 239 240int 241menu_key_cb(struct client *c, void *data, struct key_event *event) 242{ 243 struct menu_data *md = data; 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; 248 const char *name = NULL; 249 const struct menu_item *item; 250 struct cmdq_state *state; 251 enum cmd_parse_status status; 252 char *error; 253 254 if (KEYC_IS_MOUSE(event->key)) { 255 if (md->flags & MENU_NOMOUSE) { 256 if (MOUSE_BUTTONS(m->b) != MOUSE_BUTTON_1) 257 return (1); 258 return (0); 259 } 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) { 264 if (~md->flags & MENU_STAYOPEN) { 265 if (MOUSE_RELEASE(m->b)) 266 return (1); 267 } else { 268 if (!MOUSE_RELEASE(m->b) && 269 !MOUSE_WHEEL(m->b) && 270 !MOUSE_DRAG(m->b)) 271 return (1); 272 } 273 if (md->choice != -1) { 274 md->choice = -1; 275 c->flags |= CLIENT_REDRAWOVERLAY; 276 } 277 return (0); 278 } 279 if (~md->flags & MENU_STAYOPEN) { 280 if (MOUSE_RELEASE(m->b)) 281 goto chosen; 282 } else { 283 if (!MOUSE_WHEEL(m->b) && !MOUSE_DRAG(m->b)) 284 goto chosen; 285 } 286 md->choice = m->y - (md->py + 1); 287 if (md->choice != old) 288 c->flags |= CLIENT_REDRAWOVERLAY; 289 return (0); 290 } 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 } 300 switch (event->key & ~KEYC_MASK_FLAGS) { 301 case KEYC_UP: 302 case 'k': 303 if (old == -1) 304 old = 0; 305 do { 306 if (md->choice == -1 || md->choice == 0) 307 md->choice = count - 1; 308 else 309 md->choice--; 310 name = menu->items[md->choice].name; 311 } while ((name == NULL || *name == '-') && md->choice != old); 312 c->flags |= CLIENT_REDRAWOVERLAY; 313 return (0); 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 */ 324 case KEYC_DOWN: 325 case 'j': 326 if (old == -1) 327 old = 0; 328 do { 329 if (md->choice == -1 || md->choice == count - 1) 330 md->choice = 0; 331 else 332 md->choice++; 333 name = menu->items[md->choice].name; 334 } while ((name == NULL || *name == '-') && md->choice != old); 335 c->flags |= CLIENT_REDRAWOVERLAY; 336 return (0); 337 case KEYC_PPAGE: 338 case '\002': /* C-b */ 339 if (md->choice < 6) 340 md->choice = 0; 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 == '-') { 382 md->choice++; 383 name = menu->items[md->choice].name; 384 } 385 c->flags |= CLIENT_REDRAWOVERLAY; 386 break; 387 case 'G': 388 case KEYC_END: 389 md->choice = count - 1; 390 name = menu->items[md->choice].name; 391 while (name == NULL || *name == '-') { 392 md->choice--; 393 name = menu->items[md->choice].name; 394 } 395 c->flags |= CLIENT_REDRAWOVERLAY; 396 break; 397 case '\006': /* C-f */ 398 break; 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 409chosen: 410 if (md->choice == -1) 411 return (1); 412 item = &menu->items[md->choice]; 413 if (item->name == NULL || *item->name == '-') { 414 if (md->flags & MENU_STAYOPEN) 415 return (0); 416 return (1); 417 } 418 if (md->cb != NULL) { 419 md->cb(md->menu, md->choice, item->key, md->data); 420 md->cb = NULL; 421 return (1); 422 } 423 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); 434 } 435 cmdq_free_state(state); 436 437 return (1); 438} 439 440static void 441menu_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 459struct menu_data * 460menu_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{ 466 struct menu_data *md; 467 int choice; 468 const char *name; 469 struct options *o = c->session->curw->window->options; 470 471 if (c->tty.sx < menu->width + 4 || c->tty.sy < menu->count + 2) 472 return (NULL); 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; 477 478 if (lines == BOX_LINES_DEFAULT) 479 lines = options_get_number(o, "menu-border-lines"); 480 481 md = xcalloc(1, sizeof *md); 482 md->item = item; 483 md->flags = flags; 484 md->border_lines = lines; 485 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"); 490 491 if (fs != NULL) 492 cmd_find_copy_state(&md->fs, fs); 493 screen_init(&md->s, menu->width + 4, menu->count + 2, 0); 494 if (~md->flags & MENU_NOMOUSE) 495 md->s.mode |= (MODE_MOUSE_ALL|MODE_MOUSE_BUTTON); 496 md->s.mode &= ~MODE_CURSOR; 497 498 md->px = px; 499 md->py = py; 500 501 md->menu = menu; 502 md->choice = -1; 503 504 if (md->flags & MENU_NOMOUSE) { 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 } 532 } 533 } 534 535 md->cb = cb; 536 md->data = data; 537 return (md); 538} 539 540int 541menu_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{ 547 struct menu_data *md; 548 549 md = menu_prepare(menu, flags, starting_choice, item, px, py, c, lines, 550 style, selected_style, border_style, fs, cb, data); 551 if (md == NULL) 552 return (-1); 553 server_client_set_overlay(c, 0, NULL, menu_mode_cb, menu_draw_cb, 554 menu_key_cb, menu_free_cb, NULL, md); 555 return (0); 556} 557