Home | History | Annotate | Line # | Download | only in dist
      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