xref: /openbsd/usr.bin/tmux/menu.c (revision f3288aa0)
1 /* $OpenBSD: menu.c,v 1.54 2024/10/17 17:10:41 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 	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
menu_add_items(struct menu * menu,const struct menu_item * items,struct cmdq_item * qitem,struct client * c,struct cmd_find_state * fs)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
menu_add_item(struct menu * menu,const struct menu_item * item,struct cmdq_item * qitem,struct client * c,struct cmd_find_state * fs)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 *
menu_create(const char * title)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
menu_free(struct menu * menu)155 menu_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 
169 struct screen *
menu_mode_cb(__unused struct client * c,void * data,u_int * cx,u_int * cy)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
menu_check_cb(__unused struct client * c,void * data,u_int px,u_int py,u_int nx,struct overlay_ranges * r)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
menu_draw_cb(struct client * c,void * data,__unused struct screen_redraw_ctx * rctx)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
menu_free_cb(__unused struct client * c,void * data)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
menu_key_cb(struct client * c,void * data,struct key_event * event)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_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 'b'|KEYC_CTRL:
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 'f'|KEYC_CTRL:
398 		break;
399 	case '\r':
400 		goto chosen;
401 	case '\033': /* Escape */
402 	case 'c'|KEYC_CTRL:
403 	case 'g'|KEYC_CTRL:
404 	case 'q':
405 		return (1);
406 	}
407 	return (0);
408 
409 chosen:
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 
440 static void
menu_set_style(struct client * c,struct grid_cell * gc,const char * style,const char * option)441 menu_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 }
457 
458 struct menu_data *
menu_prepare(struct menu * menu,int flags,int starting_choice,struct cmdq_item * item,u_int px,u_int py,struct client * c,enum box_lines lines,const char * style,const char * selected_style,const char * border_style,struct cmd_find_state * fs,menu_choice_cb cb,void * data)459 menu_prepare(struct menu *menu, int flags, int starting_choice,
460     struct cmdq_item *item, u_int px, u_int py, struct client *c,
461     enum box_lines lines, const char *style, const char *selected_style,
462     const char *border_style, struct cmd_find_state *fs, menu_choice_cb cb,
463     void *data)
464 {
465 	struct menu_data	*md;
466 	int			 choice;
467 	const char		*name;
468 	struct options		*o = c->session->curw->window->options;
469 
470 	if (c->tty.sx < menu->width + 4 || c->tty.sy < menu->count + 2)
471 		return (NULL);
472 	if (px + menu->width + 4 > c->tty.sx)
473 		px = c->tty.sx - menu->width - 4;
474 	if (py + menu->count + 2 > c->tty.sy)
475 		py = c->tty.sy - menu->count - 2;
476 
477 	if (lines == BOX_LINES_DEFAULT)
478 		lines = options_get_number(o, "menu-border-lines");
479 
480 	md = xcalloc(1, sizeof *md);
481 	md->item = item;
482 	md->flags = flags;
483 	md->border_lines = lines;
484 
485 	menu_set_style(c, &md->style, style, "menu-style");
486 	menu_set_style(c, &md->selected_style, selected_style,
487 	    "menu-selected-style");
488 	menu_set_style(c, &md->border_style, border_style, "menu-border-style");
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
menu_display(struct menu * menu,int flags,int starting_choice,struct cmdq_item * item,u_int px,u_int py,struct client * c,enum box_lines lines,const char * style,const char * selected_style,const char * border_style,struct cmd_find_state * fs,menu_choice_cb cb,void * data)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 *selected_style,
543     const char *border_style, struct cmd_find_state *fs, menu_choice_cb cb,
544     void *data)
545 {
546 	struct menu_data	*md;
547 
548 	md = menu_prepare(menu, flags, starting_choice, item, px, py, c, lines,
549 	    style, selected_style, border_style, fs, cb, data);
550 	if (md == NULL)
551 		return (-1);
552 	server_client_set_overlay(c, 0, NULL, menu_mode_cb, menu_draw_cb,
553 	    menu_key_cb, menu_free_cb, NULL, md);
554 	return (0);
555 }
556