1 /* $OpenBSD$ */ 2 3 /* 4 * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott (at) 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 { 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, 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 142 struct menu * 143 menu_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 154 void 155 menu_free(struct menu *menu) 156 { 157 u_int i; 158 159 for (i = 0; i < menu->count; i++) { 160 free(__UNCONST(menu->items[i].name)); 161 free(__UNCONST(menu->items[i].command)); 162 } 163 free(menu->items); 164 165 free(__UNCONST(menu->title)); 166 free(menu); 167 } 168 169 struct screen * 170 menu_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. */ 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 { 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 195 void 196 menu_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 224 void 225 menu_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 240 int 241 menu_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_BTAB: 302 case KEYC_UP: 303 case 'k': 304 if (old == -1) 305 old = 0; 306 do { 307 if (md->choice == -1 || md->choice == 0) 308 md->choice = count - 1; 309 else 310 md->choice--; 311 name = menu->items[md->choice].name; 312 } while ((name == NULL || *name == '-') && md->choice != old); 313 c->flags |= CLIENT_REDRAWOVERLAY; 314 return (0); 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 */ 325 case KEYC_DOWN: 326 case 'j': 327 if (old == -1) 328 old = 0; 329 do { 330 if (md->choice == -1 || md->choice == count - 1) 331 md->choice = 0; 332 else 333 md->choice++; 334 name = menu->items[md->choice].name; 335 } while ((name == NULL || *name == '-') && md->choice != old); 336 c->flags |= CLIENT_REDRAWOVERLAY; 337 return (0); 338 case KEYC_PPAGE: 339 case 'b'|KEYC_CTRL: 340 if (md->choice < 6) 341 md->choice = 0; 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 == '-') { 383 md->choice++; 384 name = menu->items[md->choice].name; 385 } 386 c->flags |= CLIENT_REDRAWOVERLAY; 387 break; 388 case 'G': 389 case KEYC_END: 390 md->choice = count - 1; 391 name = menu->items[md->choice].name; 392 while (name == NULL || *name == '-') { 393 md->choice--; 394 name = menu->items[md->choice].name; 395 } 396 c->flags |= CLIENT_REDRAWOVERLAY; 397 break; 398 case 'f'|KEYC_CTRL: 399 break; 400 case '\r': 401 goto chosen; 402 case '\033': /* Escape */ 403 case 'c'|KEYC_CTRL: 404 case 'g'|KEYC_CTRL: 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]; 414 if (item->name == NULL || *item->name == '-') { 415 if (md->flags & MENU_STAYOPEN) 416 return (0); 417 return (1); 418 } 419 if (md->cb != NULL) { 420 md->cb(md->menu, md->choice, item->key, md->data); 421 md->cb = NULL; 422 return (1); 423 } 424 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); 435 } 436 cmdq_free_state(state); 437 438 return (1); 439 } 440 441 static void 442 menu_resize_cb(struct client *c, void *data) 443 { 444 struct menu_data *md = data; 445 u_int nx, ny, w, h; 446 447 if (md == NULL) 448 return; 449 450 nx = md->px; 451 ny = md->py; 452 453 w = md->menu->width + 4; 454 h = md->menu->count + 2; 455 456 if (nx + w > c->tty.sx) { 457 if (c->tty.sx <= w) 458 nx = 0; 459 else 460 nx = c->tty.sx - w; 461 } 462 463 if (ny + h > c->tty.sy) { 464 if (c->tty.sy <= h) 465 ny = 0; 466 else 467 ny = c->tty.sy - h; 468 } 469 md->px = nx; 470 md->py = ny; 471 } 472 473 static void 474 menu_set_style(struct client *c, struct grid_cell *gc, const char *style, 475 const char *option) 476 { 477 struct style sytmp; 478 struct options *o = c->session->curw->window->options; 479 480 memcpy(gc, &grid_default_cell, sizeof *gc); 481 style_apply(gc, o, option, NULL); 482 if (style != NULL) { 483 style_set(&sytmp, &grid_default_cell); 484 if (style_parse(&sytmp, gc, style) == 0) { 485 gc->fg = sytmp.gc.fg; 486 gc->bg = sytmp.gc.bg; 487 } 488 } 489 } 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 { 498 struct menu_data *md; 499 int choice; 500 const char *name; 501 struct options *o = c->session->curw->window->options; 502 503 if (c->tty.sx < menu->width + 4 || c->tty.sy < menu->count + 2) 504 return (NULL); 505 if (px + menu->width + 4 > c->tty.sx) 506 px = c->tty.sx - menu->width - 4; 507 if (py + menu->count + 2 > c->tty.sy) 508 py = c->tty.sy - menu->count - 2; 509 510 if (lines == BOX_LINES_DEFAULT) 511 lines = options_get_number(o, "menu-border-lines"); 512 513 md = xcalloc(1, sizeof *md); 514 md->item = item; 515 md->flags = flags; 516 md->border_lines = lines; 517 518 menu_set_style(c, &md->style, style, "menu-style"); 519 menu_set_style(c, &md->selected_style, selected_style, 520 "menu-selected-style"); 521 menu_set_style(c, &md->border_style, border_style, "menu-border-style"); 522 523 if (fs != NULL) 524 cmd_find_copy_state(&md->fs, fs); 525 screen_init(&md->s, menu->width + 4, menu->count + 2, 0); 526 if (~md->flags & MENU_NOMOUSE) 527 md->s.mode |= (MODE_MOUSE_ALL|MODE_MOUSE_BUTTON); 528 md->s.mode &= ~MODE_CURSOR; 529 530 md->px = px; 531 md->py = py; 532 533 md->menu = menu; 534 md->choice = -1; 535 536 if (md->flags & MENU_NOMOUSE) { 537 if (starting_choice >= (int)menu->count) { 538 starting_choice = menu->count - 1; 539 choice = starting_choice + 1; 540 for (;;) { 541 name = menu->items[choice - 1].name; 542 if (name != NULL && *name != '-') { 543 md->choice = choice - 1; 544 break; 545 } 546 if (--choice == 0) 547 choice = menu->count; 548 if (choice == starting_choice + 1) 549 break; 550 } 551 } else if (starting_choice >= 0) { 552 choice = starting_choice; 553 for (;;) { 554 name = menu->items[choice].name; 555 if (name != NULL && *name != '-') { 556 md->choice = choice; 557 break; 558 } 559 if (++choice == (int)menu->count) 560 choice = 0; 561 if (choice == starting_choice) 562 break; 563 } 564 } 565 } 566 567 md->cb = cb; 568 md->data = data; 569 return (md); 570 } 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 { 579 struct menu_data *md; 580 581 md = menu_prepare(menu, flags, starting_choice, item, px, py, c, lines, 582 style, selected_style, border_style, fs, cb, data); 583 if (md == NULL) 584 return (-1); 585 server_client_set_overlay(c, 0, NULL, menu_mode_cb, menu_draw_cb, 586 menu_key_cb, menu_free_cb, menu_resize_cb, md); 587 return (0); 588 } 589