1/* $OpenBSD: menu.c,v 1.52 2023/08/15 07:01: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
26struct 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
48void
49menu_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
58void
59menu_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
142struct menu *
143menu_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
154void
155menu_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
169struct screen *
170menu_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. */
184void
185menu_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
195void
196menu_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
224void
225menu_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
240int
241menu_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 '\002': /* C-b */
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 '\006': /* C-f */
398		break;
399	case '\r':
400		goto chosen;
401	case '\033': /* Escape */
402	case '\003': /* C-c */
403	case '\007': /* C-g */
404	case 'q':
405		return (1);
406	}
407	return (0);
408
409chosen:
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
440static void
441menu_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	gc->attr = 0;
457}
458
459struct menu_data *
460menu_prepare(struct menu *menu, int flags, int starting_choice,
461    struct cmdq_item *item, u_int px, u_int py, struct client *c,
462    enum box_lines lines, const char *style, const char *selected_style,
463    const char *border_style, struct cmd_find_state *fs, menu_choice_cb cb,
464    void *data)
465{
466	struct menu_data	*md;
467	int			 choice;
468	const char		*name;
469	struct options		*o = c->session->curw->window->options;
470
471	if (c->tty.sx < menu->width + 4 || c->tty.sy < menu->count + 2)
472		return (NULL);
473	if (px + menu->width + 4 > c->tty.sx)
474		px = c->tty.sx - menu->width - 4;
475	if (py + menu->count + 2 > c->tty.sy)
476		py = c->tty.sy - menu->count - 2;
477
478	if (lines == BOX_LINES_DEFAULT)
479		lines = options_get_number(o, "menu-border-lines");
480
481	md = xcalloc(1, sizeof *md);
482	md->item = item;
483	md->flags = flags;
484	md->border_lines = lines;
485
486	menu_set_style(c, &md->style, style, "menu-style");
487	menu_set_style(c, &md->selected_style, selected_style,
488	    "menu-selected-style");
489	menu_set_style(c, &md->border_style, border_style, "menu-border-style");
490
491	if (fs != NULL)
492		cmd_find_copy_state(&md->fs, fs);
493	screen_init(&md->s, menu->width + 4, menu->count + 2, 0);
494	if (~md->flags & MENU_NOMOUSE)
495		md->s.mode |= (MODE_MOUSE_ALL|MODE_MOUSE_BUTTON);
496	md->s.mode &= ~MODE_CURSOR;
497
498	md->px = px;
499	md->py = py;
500
501	md->menu = menu;
502	md->choice = -1;
503
504	if (md->flags & MENU_NOMOUSE) {
505		if (starting_choice >= (int)menu->count) {
506			starting_choice = menu->count - 1;
507			choice = starting_choice + 1;
508			for (;;) {
509				name = menu->items[choice - 1].name;
510				if (name != NULL && *name != '-') {
511					md->choice = choice - 1;
512					break;
513				}
514				if (--choice == 0)
515					choice = menu->count;
516				if (choice == starting_choice + 1)
517					break;
518			}
519		} else if (starting_choice >= 0) {
520			choice = starting_choice;
521			for (;;) {
522				name = menu->items[choice].name;
523				if (name != NULL && *name != '-') {
524					md->choice = choice;
525					break;
526				}
527				if (++choice == (int)menu->count)
528					choice = 0;
529				if (choice == starting_choice)
530					break;
531			}
532		}
533	}
534
535	md->cb = cb;
536	md->data = data;
537	return (md);
538}
539
540int
541menu_display(struct menu *menu, int flags, int starting_choice,
542    struct cmdq_item *item, u_int px, u_int py, struct client *c,
543    enum box_lines lines, const char *style, const char *selected_style,
544    const char *border_style, struct cmd_find_state *fs, menu_choice_cb cb,
545    void *data)
546{
547	struct menu_data	*md;
548
549	md = menu_prepare(menu, flags, starting_choice, item, px, py, c, lines,
550	    style, selected_style, border_style, fs, cb, data);
551	if (md == NULL)
552		return (-1);
553	server_client_set_overlay(c, 0, NULL, menu_mode_cb, menu_draw_cb,
554	    menu_key_cb, menu_free_cb, NULL, md);
555	return (0);
556}
557