summaryrefslogtreecommitdiffstats
path: root/menu.c
diff options
context:
space:
mode:
Diffstat (limited to 'menu.c')
-rw-r--r--menu.c449
1 files changed, 449 insertions, 0 deletions
diff --git a/menu.c b/menu.c
new file mode 100644
index 0000000..16120be
--- /dev/null
+++ b/menu.c
@@ -0,0 +1,449 @@
+/* $OpenBSD$ */
+
+/*
+ * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
+ * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "tmux.h"
+
+struct menu_data {
+ struct cmdq_item *item;
+ int flags;
+
+ struct cmd_find_state fs;
+ struct screen s;
+
+ u_int px;
+ u_int py;
+
+ struct menu *menu;
+ int choice;
+
+ menu_choice_cb cb;
+ void *data;
+};
+
+void
+menu_add_items(struct menu *menu, const struct menu_item *items,
+ struct cmdq_item *qitem, struct client *c, struct cmd_find_state *fs)
+{
+ const struct menu_item *loop;
+
+ for (loop = items; loop->name != NULL; loop++)
+ menu_add_item(menu, loop, qitem, c, fs);
+}
+
+void
+menu_add_item(struct menu *menu, const struct menu_item *item,
+ struct cmdq_item *qitem, struct client *c, struct cmd_find_state *fs)
+{
+ struct menu_item *new_item;
+ const char *key = NULL, *cmd, *suffix = "";
+ char *s, *trimmed, *name;
+ u_int width, max_width;
+ int line;
+ size_t keylen, slen;
+
+ line = (item == NULL || item->name == NULL || *item->name == '\0');
+ if (line && menu->count == 0)
+ return;
+
+ menu->items = xreallocarray(menu->items, menu->count + 1,
+ sizeof *menu->items);
+ new_item = &menu->items[menu->count++];
+ memset(new_item, 0, sizeof *new_item);
+
+ if (line)
+ return;
+
+ if (fs != NULL)
+ s = format_single_from_state(qitem, item->name, c, fs);
+ else
+ s = format_single(qitem, item->name, c, NULL, NULL, NULL);
+ if (*s == '\0') { /* no item if empty after format expanded */
+ menu->count--;
+ return;
+ }
+ max_width = c->tty.sx - 4;
+
+ slen = strlen(s);
+ if (*s != '-' && item->key != KEYC_UNKNOWN && item->key != KEYC_NONE) {
+ key = key_string_lookup_key(item->key, 0);
+ keylen = strlen(key) + 3; /* 3 = space and two brackets */
+
+ /*
+ * Add the key if it is shorter than a quarter of the available
+ * space or there is space for the entire item text and the
+ * key.
+ */
+ if (keylen <= max_width / 4)
+ max_width -= keylen;
+ else if (keylen >= max_width || slen >= max_width - keylen)
+ key = NULL;
+ }
+
+ if (slen > max_width) {
+ max_width--;
+ suffix = ">";
+ }
+ trimmed = format_trim_right(s, max_width);
+ if (key != NULL) {
+ xasprintf(&name, "%s%s#[default] #[align=right](%s)",
+ trimmed, suffix, key);
+ } else
+ xasprintf(&name, "%s%s", trimmed, suffix);
+ free(trimmed);
+
+ new_item->name = name;
+ free(s);
+
+ cmd = item->command;
+ if (cmd != NULL) {
+ if (fs != NULL)
+ s = format_single_from_state(qitem, cmd, c, fs);
+ else
+ s = format_single(qitem, cmd, c, NULL, NULL, NULL);
+ } else
+ s = NULL;
+ new_item->command = s;
+ new_item->key = item->key;
+
+ width = format_width(new_item->name);
+ if (*new_item->name == '-')
+ width--;
+ if (width > menu->width)
+ menu->width = width;
+}
+
+struct menu *
+menu_create(const char *title)
+{
+ struct menu *menu;
+
+ menu = xcalloc(1, sizeof *menu);
+ menu->title = xstrdup(title);
+ menu->width = format_width(title);
+
+ return (menu);
+}
+
+void
+menu_free(struct menu *menu)
+{
+ u_int i;
+
+ for (i = 0; i < menu->count; i++) {
+ free((void *)menu->items[i].name);
+ free((void *)menu->items[i].command);
+ }
+ free(menu->items);
+
+ free((void *)menu->title);
+ free(menu);
+}
+
+struct screen *
+menu_mode_cb(__unused struct client *c, void *data, __unused u_int *cx,
+ __unused u_int *cy)
+{
+ struct menu_data *md = data;
+
+ return (&md->s);
+}
+
+/* Return parts of the input range which are not obstructed by the menu. */
+void
+menu_check_cb(__unused struct client *c, void *data, u_int px, u_int py,
+ u_int nx, struct overlay_ranges *r)
+{
+ struct menu_data *md = data;
+ struct menu *menu = md->menu;
+
+ server_client_overlay_range(md->px, md->py, menu->width + 4,
+ menu->count + 2, px, py, nx, r);
+}
+
+void
+menu_draw_cb(struct client *c, void *data,
+ __unused struct screen_redraw_ctx *rctx)
+{
+ struct menu_data *md = data;
+ struct tty *tty = &c->tty;
+ struct screen *s = &md->s;
+ struct menu *menu = md->menu;
+ struct screen_write_ctx ctx;
+ u_int i, px = md->px, py = md->py;
+ struct grid_cell gc;
+
+ style_apply(&gc, c->session->curw->window->options, "mode-style", NULL);
+
+ screen_write_start(&ctx, s);
+ screen_write_clearscreen(&ctx, 8);
+ screen_write_menu(&ctx, menu, md->choice, &gc);
+ screen_write_stop(&ctx);
+
+ for (i = 0; i < screen_size_y(&md->s); i++) {
+ tty_draw_line(tty, s, 0, i, menu->width + 4, px, py + i,
+ &grid_default_cell, NULL);
+ }
+}
+
+void
+menu_free_cb(__unused struct client *c, void *data)
+{
+ struct menu_data *md = data;
+
+ if (md->item != NULL)
+ cmdq_continue(md->item);
+
+ if (md->cb != NULL)
+ md->cb(md->menu, UINT_MAX, KEYC_NONE, md->data);
+
+ screen_free(&md->s);
+ menu_free(md->menu);
+ free(md);
+}
+
+int
+menu_key_cb(struct client *c, void *data, struct key_event *event)
+{
+ struct menu_data *md = data;
+ struct menu *menu = md->menu;
+ struct mouse_event *m = &event->m;
+ u_int i;
+ int count = menu->count, old = md->choice;
+ const char *name = NULL;
+ const struct menu_item *item;
+ struct cmdq_state *state;
+ enum cmd_parse_status status;
+ char *error;
+
+ if (KEYC_IS_MOUSE(event->key)) {
+ if (md->flags & MENU_NOMOUSE) {
+ if (MOUSE_BUTTONS(m->b) != MOUSE_BUTTON_1)
+ return (1);
+ return (0);
+ }
+ if (m->x < md->px ||
+ m->x > md->px + 4 + menu->width ||
+ m->y < md->py + 1 ||
+ m->y > md->py + 1 + count - 1) {
+ if (~md->flags & MENU_STAYOPEN) {
+ if (MOUSE_RELEASE(m->b))
+ return (1);
+ } else {
+ if (!MOUSE_RELEASE(m->b) &&
+ !MOUSE_WHEEL(m->b) &&
+ !MOUSE_DRAG(m->b))
+ return (1);
+ }
+ if (md->choice != -1) {
+ md->choice = -1;
+ c->flags |= CLIENT_REDRAWOVERLAY;
+ }
+ return (0);
+ }
+ if (~md->flags & MENU_STAYOPEN) {
+ if (MOUSE_RELEASE(m->b))
+ goto chosen;
+ } else {
+ if (!MOUSE_WHEEL(m->b) && !MOUSE_DRAG(m->b))
+ goto chosen;
+ }
+ md->choice = m->y - (md->py + 1);
+ if (md->choice != old)
+ c->flags |= CLIENT_REDRAWOVERLAY;
+ return (0);
+ }
+ for (i = 0; i < (u_int)count; i++) {
+ name = menu->items[i].name;
+ if (name == NULL || *name == '-')
+ continue;
+ if (event->key == menu->items[i].key) {
+ md->choice = i;
+ goto chosen;
+ }
+ }
+ switch (event->key & ~KEYC_MASK_FLAGS) {
+ case KEYC_UP:
+ case 'k':
+ if (old == -1)
+ old = 0;
+ do {
+ if (md->choice == -1 || md->choice == 0)
+ md->choice = count - 1;
+ else
+ md->choice--;
+ name = menu->items[md->choice].name;
+ } while ((name == NULL || *name == '-') && md->choice != old);
+ c->flags |= CLIENT_REDRAWOVERLAY;
+ return (0);
+ case KEYC_BSPACE:
+ if (~md->flags & MENU_TAB)
+ break;
+ return (1);
+ case '\011': /* Tab */
+ if (~md->flags & MENU_TAB)
+ break;
+ if (md->choice == count - 1)
+ return (1);
+ /* FALLTHROUGH */
+ case KEYC_DOWN:
+ case 'j':
+ if (old == -1)
+ old = 0;
+ do {
+ if (md->choice == -1 || md->choice == count - 1)
+ md->choice = 0;
+ else
+ md->choice++;
+ name = menu->items[md->choice].name;
+ } while ((name == NULL || *name == '-') && md->choice != old);
+ c->flags |= CLIENT_REDRAWOVERLAY;
+ return (0);
+ case 'g':
+ case KEYC_PPAGE:
+ case '\002': /* C-b */
+ if (md->choice > 5)
+ md->choice -= 5;
+ else
+ md->choice = 0;
+ while (md->choice != count && (name == NULL || *name == '-'))
+ md->choice++;
+ if (md->choice == count)
+ md->choice = -1;
+ c->flags |= CLIENT_REDRAWOVERLAY;
+ break;
+ case 'G':
+ case KEYC_NPAGE:
+ if (md->choice > count - 6)
+ md->choice = count - 1;
+ else
+ md->choice += 5;
+ while (md->choice != -1 && (name == NULL || *name == '-'))
+ md->choice--;
+ c->flags |= CLIENT_REDRAWOVERLAY;
+ break;
+ case '\006': /* C-f */
+ break;
+ case '\r':
+ goto chosen;
+ case '\033': /* Escape */
+ case '\003': /* C-c */
+ case '\007': /* C-g */
+ case 'q':
+ return (1);
+ }
+ return (0);
+
+chosen:
+ if (md->choice == -1)
+ return (1);
+ item = &menu->items[md->choice];
+ if (item->name == NULL || *item->name == '-') {
+ if (md->flags & MENU_STAYOPEN)
+ return (0);
+ return (1);
+ }
+ if (md->cb != NULL) {
+ md->cb(md->menu, md->choice, item->key, md->data);
+ md->cb = NULL;
+ return (1);
+ }
+
+ if (md->item != NULL)
+ event = cmdq_get_event(md->item);
+ else
+ event = NULL;
+ state = cmdq_new_state(&md->fs, event, 0);
+
+ status = cmd_parse_and_append(item->command, NULL, c, state, &error);
+ if (status == CMD_PARSE_ERROR) {
+ cmdq_append(c, cmdq_get_error(error));
+ free(error);
+ }
+ cmdq_free_state(state);
+
+ return (1);
+}
+
+struct menu_data *
+menu_prepare(struct menu *menu, int flags, struct cmdq_item *item, u_int px,
+ u_int py, struct client *c, struct cmd_find_state *fs, menu_choice_cb cb,
+ void *data)
+{
+ struct menu_data *md;
+ u_int i;
+ const char *name;
+
+ if (c->tty.sx < menu->width + 4 || c->tty.sy < menu->count + 2)
+ return (NULL);
+ if (px + menu->width + 4 > c->tty.sx)
+ px = c->tty.sx - menu->width - 4;
+ if (py + menu->count + 2 > c->tty.sy)
+ py = c->tty.sy - menu->count - 2;
+
+ md = xcalloc(1, sizeof *md);
+ md->item = item;
+ md->flags = flags;
+
+ if (fs != NULL)
+ cmd_find_copy_state(&md->fs, fs);
+ screen_init(&md->s, menu->width + 4, menu->count + 2, 0);
+ if (~md->flags & MENU_NOMOUSE)
+ md->s.mode |= (MODE_MOUSE_ALL|MODE_MOUSE_BUTTON);
+ md->s.mode &= ~MODE_CURSOR;
+
+ md->px = px;
+ md->py = py;
+
+ md->menu = menu;
+ if (md->flags & MENU_NOMOUSE) {
+ for (i = 0; i < menu->count; i++) {
+ name = menu->items[i].name;
+ if (name != NULL && *name != '-')
+ break;
+ }
+ if (i != menu->count)
+ md->choice = i;
+ else
+ md->choice = -1;
+ } else
+ md->choice = -1;
+
+ md->cb = cb;
+ md->data = data;
+ return (md);
+}
+
+int
+menu_display(struct menu *menu, int flags, struct cmdq_item *item, u_int px,
+ u_int py, struct client *c, struct cmd_find_state *fs, menu_choice_cb cb,
+ void *data)
+{
+ struct menu_data *md;
+
+ md = menu_prepare(menu, flags, item, px, py, c, fs, cb, data);
+ if (md == NULL)
+ return (-1);
+ server_client_set_overlay(c, 0, NULL, menu_mode_cb, menu_draw_cb,
+ menu_key_cb, menu_free_cb, NULL, md);
+ return (0);
+}