1 /* $OpenBSD: cmd-display-menu.c,v 1.43 2024/03/21 11:51:32 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 <paths.h> 22 #include <stdlib.h> 23 #include <string.h> 24 25 #include "tmux.h" 26 27 /* 28 * Display a menu on a client. 29 */ 30 31 static enum args_parse_type cmd_display_menu_args_parse(struct args *, 32 u_int, char **); 33 static enum cmd_retval cmd_display_menu_exec(struct cmd *, 34 struct cmdq_item *); 35 static enum cmd_retval cmd_display_popup_exec(struct cmd *, 36 struct cmdq_item *); 37 38 const struct cmd_entry cmd_display_menu_entry = { 39 .name = "display-menu", 40 .alias = "menu", 41 42 .args = { "b:c:C:H:s:S:MOt:T:x:y:", 1, -1, cmd_display_menu_args_parse }, 43 .usage = "[-MO] [-b border-lines] [-c target-client] " 44 "[-C starting-choice] [-H selected-style] [-s style] " 45 "[-S border-style] " CMD_TARGET_PANE_USAGE "[-T title] " 46 "[-x position] [-y position] name key command ...", 47 48 .target = { 't', CMD_FIND_PANE, 0 }, 49 50 .flags = CMD_AFTERHOOK|CMD_CLIENT_CFLAG, 51 .exec = cmd_display_menu_exec 52 }; 53 54 const struct cmd_entry cmd_display_popup_entry = { 55 .name = "display-popup", 56 .alias = "popup", 57 58 .args = { "Bb:Cc:d:e:Eh:s:S:t:T:w:x:y:", 0, -1, NULL }, 59 .usage = "[-BCE] [-b border-lines] [-c target-client] " 60 "[-d start-directory] [-e environment] [-h height] " 61 "[-s style] [-S border-style] " CMD_TARGET_PANE_USAGE 62 "[-T title] [-w width] [-x position] [-y position] " 63 "[shell-command]", 64 65 .target = { 't', CMD_FIND_PANE, 0 }, 66 67 .flags = CMD_AFTERHOOK|CMD_CLIENT_CFLAG, 68 .exec = cmd_display_popup_exec 69 }; 70 71 static enum args_parse_type 72 cmd_display_menu_args_parse(struct args *args, u_int idx, __unused char **cause) 73 { 74 u_int i = 0; 75 enum args_parse_type type = ARGS_PARSE_STRING; 76 77 for (;;) { 78 type = ARGS_PARSE_STRING; 79 if (i == idx) 80 break; 81 if (*args_string(args, i++) == '\0') 82 continue; 83 84 type = ARGS_PARSE_STRING; 85 if (i++ == idx) 86 break; 87 88 type = ARGS_PARSE_COMMANDS_OR_STRING; 89 if (i++ == idx) 90 break; 91 } 92 return (type); 93 } 94 95 static int 96 cmd_display_menu_get_position(struct client *tc, struct cmdq_item *item, 97 struct args *args, u_int *px, u_int *py, u_int w, u_int h) 98 { 99 struct tty *tty = &tc->tty; 100 struct cmd_find_state *target = cmdq_get_target(item); 101 struct key_event *event = cmdq_get_event(item); 102 struct session *s = tc->session; 103 struct winlink *wl = target->wl; 104 struct window_pane *wp = target->wp; 105 struct style_ranges *ranges = NULL; 106 struct style_range *sr = NULL; 107 const char *xp, *yp; 108 char *p; 109 int top; 110 u_int line, ox, oy, sx, sy, lines, position; 111 long n; 112 struct format_tree *ft; 113 114 /* 115 * Work out the position from the -x and -y arguments. This is the 116 * bottom-left position. 117 */ 118 119 /* If the popup is too big, stop now. */ 120 if (w > tty->sx || h > tty->sy) 121 return (0); 122 123 /* Create format with mouse position if any. */ 124 ft = format_create_from_target(item); 125 if (event->m.valid) { 126 format_add(ft, "popup_mouse_x", "%u", event->m.x); 127 format_add(ft, "popup_mouse_y", "%u", event->m.y); 128 } 129 130 /* 131 * If there are any status lines, add this window position and the 132 * status line position. 133 */ 134 top = status_at_line(tc); 135 if (top != -1) { 136 lines = status_line_size(tc); 137 if (top == 0) 138 top = lines; 139 else 140 top = 0; 141 position = options_get_number(s->options, "status-position"); 142 143 for (line = 0; line < lines; line++) { 144 ranges = &tc->status.entries[line].ranges; 145 TAILQ_FOREACH(sr, ranges, entry) { 146 if (sr->type != STYLE_RANGE_WINDOW) 147 continue; 148 if (sr->argument == (u_int)wl->idx) 149 break; 150 } 151 if (sr != NULL) 152 break; 153 } 154 155 if (sr != NULL) { 156 format_add(ft, "popup_window_status_line_x", "%u", 157 sr->start); 158 if (position == 0) { 159 format_add(ft, "popup_window_status_line_y", 160 "%u", line + 1 + h); 161 } else { 162 format_add(ft, "popup_window_status_line_y", 163 "%u", tty->sy - lines + line); 164 } 165 } 166 167 if (position == 0) 168 format_add(ft, "popup_status_line_y", "%u", lines + h); 169 else { 170 format_add(ft, "popup_status_line_y", "%u", 171 tty->sy - lines); 172 } 173 } else 174 top = 0; 175 176 /* Popup width and height. */ 177 format_add(ft, "popup_width", "%u", w); 178 format_add(ft, "popup_height", "%u", h); 179 180 /* Position so popup is in the centre. */ 181 n = (long)(tty->sx - 1) / 2 - w / 2; 182 if (n < 0) 183 format_add(ft, "popup_centre_x", "%u", 0); 184 else 185 format_add(ft, "popup_centre_x", "%ld", n); 186 n = (tty->sy - 1) / 2 + h / 2; 187 if (n >= tty->sy) 188 format_add(ft, "popup_centre_y", "%u", tty->sy - h); 189 else 190 format_add(ft, "popup_centre_y", "%ld", n); 191 192 /* Position of popup relative to mouse. */ 193 if (event->m.valid) { 194 n = (long)event->m.x - w / 2; 195 if (n < 0) 196 format_add(ft, "popup_mouse_centre_x", "%u", 0); 197 else 198 format_add(ft, "popup_mouse_centre_x", "%ld", n); 199 n = event->m.y - h / 2; 200 if (n + h >= tty->sy) { 201 format_add(ft, "popup_mouse_centre_y", "%u", 202 tty->sy - h); 203 } else 204 format_add(ft, "popup_mouse_centre_y", "%ld", n); 205 n = (long)event->m.y + h; 206 if (n >= tty->sy) 207 format_add(ft, "popup_mouse_top", "%u", tty->sy - 1); 208 else 209 format_add(ft, "popup_mouse_top", "%ld", n); 210 n = event->m.y - h; 211 if (n < 0) 212 format_add(ft, "popup_mouse_bottom", "%u", 0); 213 else 214 format_add(ft, "popup_mouse_bottom", "%ld", n); 215 } 216 217 /* Position in pane. */ 218 tty_window_offset(&tc->tty, &ox, &oy, &sx, &sy); 219 n = top + wp->yoff - oy + h; 220 if (n >= tty->sy) 221 format_add(ft, "popup_pane_top", "%u", tty->sy - h); 222 else 223 format_add(ft, "popup_pane_top", "%ld", n); 224 format_add(ft, "popup_pane_bottom", "%u", top + wp->yoff + wp->sy - oy); 225 format_add(ft, "popup_pane_left", "%u", wp->xoff - ox); 226 n = (long)wp->xoff + wp->sx - ox - w; 227 if (n < 0) 228 format_add(ft, "popup_pane_right", "%u", 0); 229 else 230 format_add(ft, "popup_pane_right", "%ld", n); 231 232 /* Expand horizontal position. */ 233 xp = args_get(args, 'x'); 234 if (xp == NULL || strcmp(xp, "C") == 0) 235 xp = "#{popup_centre_x}"; 236 else if (strcmp(xp, "R") == 0) 237 xp = "#{popup_pane_right}"; 238 else if (strcmp(xp, "P") == 0) 239 xp = "#{popup_pane_left}"; 240 else if (strcmp(xp, "M") == 0) 241 xp = "#{popup_mouse_centre_x}"; 242 else if (strcmp(xp, "W") == 0) 243 xp = "#{popup_window_status_line_x}"; 244 p = format_expand(ft, xp); 245 n = strtol(p, NULL, 10); 246 if (n + w >= tty->sx) 247 n = tty->sx - w; 248 else if (n < 0) 249 n = 0; 250 *px = n; 251 log_debug("%s: -x: %s = %s = %u (-w %u)", __func__, xp, p, *px, w); 252 free(p); 253 254 /* Expand vertical position */ 255 yp = args_get(args, 'y'); 256 if (yp == NULL || strcmp(yp, "C") == 0) 257 yp = "#{popup_centre_y}"; 258 else if (strcmp(yp, "P") == 0) 259 yp = "#{popup_pane_bottom}"; 260 else if (strcmp(yp, "M") == 0) 261 yp = "#{popup_mouse_top}"; 262 else if (strcmp(yp, "S") == 0) 263 yp = "#{popup_status_line_y}"; 264 else if (strcmp(yp, "W") == 0) 265 yp = "#{popup_window_status_line_y}"; 266 p = format_expand(ft, yp); 267 n = strtol(p, NULL, 10); 268 if (n < h) 269 n = 0; 270 else 271 n -= h; 272 if (n + h >= tty->sy) 273 n = tty->sy - h; 274 else if (n < 0) 275 n = 0; 276 *py = n; 277 log_debug("%s: -y: %s = %s = %u (-h %u)", __func__, yp, p, *py, h); 278 free(p); 279 280 format_free(ft); 281 return (1); 282 } 283 284 static enum cmd_retval 285 cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item) 286 { 287 struct args *args = cmd_get_args(self); 288 struct cmd_find_state *target = cmdq_get_target(item); 289 struct key_event *event = cmdq_get_event(item); 290 struct client *tc = cmdq_get_target_client(item); 291 struct menu *menu = NULL; 292 struct menu_item menu_item; 293 const char *key, *name, *value; 294 const char *style = args_get(args, 's'); 295 const char *border_style = args_get(args, 'S'); 296 const char *selected_style = args_get(args, 'H'); 297 enum box_lines lines = BOX_LINES_DEFAULT; 298 char *title, *cause; 299 int flags = 0, starting_choice = 0; 300 u_int px, py, i, count = args_count(args); 301 struct options *o = target->s->curw->window->options; 302 struct options_entry *oe; 303 304 305 if (tc->overlay_draw != NULL) 306 return (CMD_RETURN_NORMAL); 307 308 if (args_has(args, 'C')) { 309 if (strcmp(args_get(args, 'C'), "-") == 0) 310 starting_choice = -1; 311 else { 312 starting_choice = args_strtonum(args, 'C', 0, UINT_MAX, 313 &cause); 314 if (cause != NULL) { 315 cmdq_error(item, "starting choice %s", cause); 316 free(cause); 317 return (CMD_RETURN_ERROR); 318 } 319 } 320 } 321 322 if (args_has(args, 'T')) 323 title = format_single_from_target(item, args_get(args, 'T')); 324 else 325 title = xstrdup(""); 326 menu = menu_create(title); 327 free(title); 328 329 for (i = 0; i != count; /* nothing */) { 330 name = args_string(args, i++); 331 if (*name == '\0') { 332 menu_add_item(menu, NULL, item, tc, target); 333 continue; 334 } 335 336 if (count - i < 2) { 337 cmdq_error(item, "not enough arguments"); 338 menu_free(menu); 339 return (CMD_RETURN_ERROR); 340 } 341 key = args_string(args, i++); 342 343 menu_item.name = name; 344 menu_item.key = key_string_lookup_string(key); 345 menu_item.command = args_string(args, i++); 346 347 menu_add_item(menu, &menu_item, item, tc, target); 348 } 349 if (menu == NULL) { 350 cmdq_error(item, "invalid menu arguments"); 351 return (CMD_RETURN_ERROR); 352 } 353 if (menu->count == 0) { 354 menu_free(menu); 355 return (CMD_RETURN_NORMAL); 356 } 357 if (!cmd_display_menu_get_position(tc, item, args, &px, &py, 358 menu->width + 4, menu->count + 2)) { 359 menu_free(menu); 360 return (CMD_RETURN_NORMAL); 361 } 362 363 value = args_get(args, 'b'); 364 if (value != NULL) { 365 oe = options_get(o, "menu-border-lines"); 366 lines = options_find_choice(options_table_entry(oe), value, 367 &cause); 368 if (lines == -1) { 369 cmdq_error(item, "menu-border-lines %s", cause); 370 free(cause); 371 return (CMD_RETURN_ERROR); 372 } 373 } 374 375 if (args_has(args, 'O')) 376 flags |= MENU_STAYOPEN; 377 if (!event->m.valid && !args_has(args, 'M')) 378 flags |= MENU_NOMOUSE; 379 if (menu_display(menu, flags, starting_choice, item, px, py, tc, lines, 380 style, selected_style, border_style, target, NULL, NULL) != 0) 381 return (CMD_RETURN_NORMAL); 382 return (CMD_RETURN_WAIT); 383 } 384 385 static enum cmd_retval 386 cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) 387 { 388 struct args *args = cmd_get_args(self); 389 struct cmd_find_state *target = cmdq_get_target(item); 390 struct session *s = target->s; 391 struct client *tc = cmdq_get_target_client(item); 392 struct tty *tty = &tc->tty; 393 const char *value, *shell, *shellcmd = NULL; 394 const char *style = args_get(args, 's'); 395 const char *border_style = args_get(args, 'S'); 396 char *cwd, *cause = NULL, **argv = NULL, *title; 397 int flags = 0, argc = 0; 398 enum box_lines lines = BOX_LINES_DEFAULT; 399 u_int px, py, w, h, count = args_count(args); 400 struct args_value *av; 401 struct environ *env = NULL; 402 struct options *o = s->curw->window->options; 403 struct options_entry *oe; 404 405 if (args_has(args, 'C')) { 406 server_client_clear_overlay(tc); 407 return (CMD_RETURN_NORMAL); 408 } 409 if (tc->overlay_draw != NULL) 410 return (CMD_RETURN_NORMAL); 411 412 h = tty->sy / 2; 413 if (args_has(args, 'h')) { 414 h = args_percentage(args, 'h', 1, tty->sy, tty->sy, &cause); 415 if (cause != NULL) { 416 cmdq_error(item, "height %s", cause); 417 free(cause); 418 return (CMD_RETURN_ERROR); 419 } 420 } 421 422 w = tty->sx / 2; 423 if (args_has(args, 'w')) { 424 w = args_percentage(args, 'w', 1, tty->sx, tty->sx, &cause); 425 if (cause != NULL) { 426 cmdq_error(item, "width %s", cause); 427 free(cause); 428 return (CMD_RETURN_ERROR); 429 } 430 } 431 432 if (w > tty->sx) 433 w = tty->sx; 434 if (h > tty->sy) 435 h = tty->sy; 436 if (!cmd_display_menu_get_position(tc, item, args, &px, &py, w, h)) 437 return (CMD_RETURN_NORMAL); 438 439 value = args_get(args, 'b'); 440 if (args_has(args, 'B')) 441 lines = BOX_LINES_NONE; 442 else if (value != NULL) { 443 oe = options_get(o, "popup-border-lines"); 444 lines = options_find_choice(options_table_entry(oe), value, 445 &cause); 446 if (cause != NULL) { 447 cmdq_error(item, "popup-border-lines %s", cause); 448 free(cause); 449 return (CMD_RETURN_ERROR); 450 } 451 } 452 453 value = args_get(args, 'd'); 454 if (value != NULL) 455 cwd = format_single_from_target(item, value); 456 else 457 cwd = xstrdup(server_client_get_cwd(tc, s)); 458 if (count == 0) 459 shellcmd = options_get_string(s->options, "default-command"); 460 else if (count == 1) 461 shellcmd = args_string(args, 0); 462 if (count <= 1 && (shellcmd == NULL || *shellcmd == '\0')) { 463 shellcmd = NULL; 464 shell = options_get_string(s->options, "default-shell"); 465 if (!checkshell(shell)) 466 shell = _PATH_BSHELL; 467 cmd_append_argv(&argc, &argv, shell); 468 } else 469 args_to_vector(args, &argc, &argv); 470 471 if (args_has(args, 'e') >= 1) { 472 env = environ_create(); 473 av = args_first_value(args, 'e'); 474 while (av != NULL) { 475 environ_put(env, av->string, 0); 476 av = args_next_value(av); 477 } 478 } 479 480 if (args_has(args, 'T')) 481 title = format_single_from_target(item, args_get(args, 'T')); 482 else 483 title = xstrdup(""); 484 if (args_has(args, 'E') > 1) 485 flags |= POPUP_CLOSEEXITZERO; 486 else if (args_has(args, 'E')) 487 flags |= POPUP_CLOSEEXIT; 488 if (popup_display(flags, lines, item, px, py, w, h, env, shellcmd, argc, 489 argv, cwd, title, tc, s, style, border_style, NULL, NULL) != 0) { 490 cmd_free_argv(argc, argv); 491 if (env != NULL) 492 environ_free(env); 493 free(cwd); 494 free(title); 495 return (CMD_RETURN_NORMAL); 496 } 497 if (env != NULL) 498 environ_free(env); 499 free(cwd); 500 free(title); 501 cmd_free_argv(argc, argv); 502 return (CMD_RETURN_WAIT); 503 } 504