1 /* $OpenBSD$ */
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(__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 *
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           gc->attr = 0;
457 }
458 
459 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)460 menu_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 
540 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)541 menu_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