xref: /openbsd-src/usr.bin/tmux/menu.c (revision f90ef06a3045119dcc88b72d8b98ca60e3c00d5a)
1 /* $OpenBSD: menu.c,v 1.51 2023/08/08 08:08: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 
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 	enum box_lines		 border_lines;
33 
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 
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,
59     struct cmdq_item *qitem, struct client *c, struct cmd_find_state *fs)
60 {
61 	struct menu_item	*new_item;
62 	const char		*key = NULL, *cmd, *suffix = "";
63 	char			*s, *trimmed, *name;
64 	u_int			 width, max_width;
65 	int			 line;
66 	size_t			 keylen, slen;
67 
68 	line = (item == NULL || item->name == NULL || *item->name == '\0');
69 	if (line && menu->count == 0)
70 		return;
71 	if (line && menu->items[menu->count - 1].name == NULL)
72 		return;
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 
79 	if (line)
80 		return;
81 
82 	if (fs != NULL)
83 		s = format_single_from_state(qitem, item->name, c, fs);
84 	else
85 		s = format_single(qitem, item->name, c, NULL, NULL, NULL);
86 	if (*s == '\0') { /* no item if empty after format expanded */
87 		menu->count--;
88 		return;
89 	}
90 	max_width = c->tty.sx - 4;
91 
92 	slen = strlen(s);
93 	if (*s != '-' && item->key != KEYC_UNKNOWN && item->key != KEYC_NONE) {
94 		key = key_string_lookup_key(item->key, 0);
95 		keylen = strlen(key) + 3; /* 3 = space and two brackets */
96 
97 		/*
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.
101 		 */
102 		if (keylen <= max_width / 4)
103 			max_width -= keylen;
104 		else if (keylen >= max_width || slen >= max_width - keylen)
105 			key = NULL;
106 	}
107 
108 	if (slen > max_width) {
109 		max_width--;
110 		suffix = ">";
111 	}
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);
119 
120 	new_item->name = name;
121 	free(s);
122 
123 	cmd = item->command;
124 	if (cmd != NULL) {
125 		if (fs != NULL)
126 			s = format_single_from_state(qitem, cmd, c, fs);
127 		else
128 			s = format_single(qitem, cmd, c, NULL, NULL, NULL);
129 	} else
130 		s = NULL;
131 	new_item->command = s;
132 	new_item->key = item->key;
133 
134 	width = format_width(new_item->name);
135 	if (*new_item->name == '-')
136 		width--;
137 	if (width > menu->width)
138 		menu->width = width;
139 }
140 
141 struct menu *
142 menu_create(const char *title)
143 {
144 	struct menu	*menu;
145 
146 	menu = xcalloc(1, sizeof *menu);
147 	menu->title = xstrdup(title);
148 	menu->width = format_width(title);
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++) {
159 		free((void *)menu->items[i].name);
160 		free((void *)menu->items[i].command);
161 	}
162 	free(menu->items);
163 
164 	free((void *)menu->title);
165 	free(menu);
166 }
167 
168 struct screen *
169 menu_mode_cb(__unused struct client *c, void *data, u_int *cx, u_int *cy)
170 {
171 	struct menu_data	*md = data;
172 
173 	*cx = md->px + 2;
174 	if (md->choice == -1)
175 		*cy = md->py;
176 	else
177 		*cy = md->py + 1 + md->choice;
178 
179 	return (&md->s);
180 }
181 
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)
186 {
187 	struct menu_data	*md = data;
188 	struct menu		*menu = md->menu;
189 
190 	server_client_overlay_range(md->px, md->py, menu->width + 4,
191 	    menu->count + 2, px, py, nx, r);
192 }
193 
194 void
195 menu_draw_cb(struct client *c, void *data,
196     __unused struct screen_redraw_ctx *rctx)
197 {
198 	struct menu_data	*md = data;
199 	struct tty		*tty = &c->tty;
200 	struct screen		*s = &md->s;
201 	struct menu		*menu = md->menu;
202 	struct screen_write_ctx	 ctx;
203 	u_int			 i, px = md->px, py = md->py;
204 	struct grid_cell	 gc;
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 	style_apply(&gc, c->session->curw->window->options, "mode-style", NULL);
214 
215 	screen_write_menu(&ctx, menu, md->choice, md->border_lines,
216 	    &md->style, &md->border_style, &gc);
217 	screen_write_stop(&ctx);
218 
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 	}
223 }
224 
225 void
226 menu_free_cb(__unused struct client *c, void *data)
227 {
228 	struct menu_data	*md = data;
229 
230 	if (md->item != NULL)
231 		cmdq_continue(md->item);
232 
233 	if (md->cb != NULL)
234 		md->cb(md->menu, UINT_MAX, KEYC_NONE, md->data);
235 
236 	screen_free(&md->s);
237 	menu_free(md->menu);
238 	free(md);
239 }
240 
241 int
242 menu_key_cb(struct client *c, void *data, struct key_event *event)
243 {
244 	struct menu_data		*md = data;
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;
249 	const char			*name = NULL;
250 	const struct menu_item		*item;
251 	struct cmdq_state		*state;
252 	enum cmd_parse_status		 status;
253 	char				*error;
254 
255 	if (KEYC_IS_MOUSE(event->key)) {
256 		if (md->flags & MENU_NOMOUSE) {
257 			if (MOUSE_BUTTONS(m->b) != MOUSE_BUTTON_1)
258 				return (1);
259 			return (0);
260 		}
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) {
265 			if (~md->flags & MENU_STAYOPEN) {
266 				if (MOUSE_RELEASE(m->b))
267 					return (1);
268 			} else {
269 				if (!MOUSE_RELEASE(m->b) &&
270 				    !MOUSE_WHEEL(m->b) &&
271 				    !MOUSE_DRAG(m->b))
272 					return (1);
273 			}
274 			if (md->choice != -1) {
275 				md->choice = -1;
276 				c->flags |= CLIENT_REDRAWOVERLAY;
277 			}
278 			return (0);
279 		}
280 		if (~md->flags & MENU_STAYOPEN) {
281 			if (MOUSE_RELEASE(m->b))
282 				goto chosen;
283 		} else {
284 			if (!MOUSE_WHEEL(m->b) && !MOUSE_DRAG(m->b))
285 				goto chosen;
286 		}
287 		md->choice = m->y - (md->py + 1);
288 		if (md->choice != old)
289 			c->flags |= CLIENT_REDRAWOVERLAY;
290 		return (0);
291 	}
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 	}
301 	switch (event->key & ~KEYC_MASK_FLAGS) {
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 '\002': /* C-b */
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 '\006': /* C-f */
399 		break;
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];
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 struct menu_data *
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,
444     enum box_lines lines, const char *style, const char *border_style,
445     struct cmd_find_state *fs, menu_choice_cb cb, void *data)
446 {
447 	struct menu_data	*md;
448 	int			 choice;
449 	const char		*name;
450 	struct style		 sytmp;
451 	struct options		*o = c->session->curw->window->options;
452 
453 	if (c->tty.sx < menu->width + 4 || c->tty.sy < menu->count + 2)
454 		return (NULL);
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;
459 
460 	if (lines == BOX_LINES_DEFAULT)
461 		lines = options_get_number(o, "menu-border-lines");
462 
463 	md = xcalloc(1, sizeof *md);
464 	md->item = item;
465 	md->flags = flags;
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;
489 
490 	if (fs != NULL)
491 		cmd_find_copy_state(&md->fs, fs);
492 	screen_init(&md->s, menu->width + 4, menu->count + 2, 0);
493 	if (~md->flags & MENU_NOMOUSE)
494 		md->s.mode |= (MODE_MOUSE_ALL|MODE_MOUSE_BUTTON);
495 	md->s.mode &= ~MODE_CURSOR;
496 
497 	md->px = px;
498 	md->py = py;
499 
500 	md->menu = menu;
501 	md->choice = -1;
502 
503 	if (md->flags & MENU_NOMOUSE) {
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 			}
531 		}
532 	}
533 
534 	md->cb = cb;
535 	md->data = data;
536 	return (md);
537 }
538 
539 int
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,
542     enum box_lines lines, const char *style, const char *border_style,
543     struct cmd_find_state *fs, menu_choice_cb cb, void *data)
544 {
545 	struct menu_data	*md;
546 
547 	md = menu_prepare(menu, flags, starting_choice, item, px, py, c, lines,
548 	    style, border_style, fs, cb, data);
549 	if (md == NULL)
550 		return (-1);
551 	server_client_set_overlay(c, 0, NULL, menu_mode_cb, menu_draw_cb,
552 	    menu_key_cb, menu_free_cb, NULL, md);
553 	return (0);
554 }
555