/* $OpenBSD$ */

/*
* Copyright (c) 2020 Nicholas Marriott <[email protected]>
*
* 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 <ctype.h>
#include <stdlib.h>
#include <string.h>

#include "tmux.h"

static struct screen    *window_customize_init(struct window_mode_entry *,
                            struct cmd_find_state *, struct args *);
static void              window_customize_free(struct window_mode_entry *);
static void              window_customize_resize(struct window_mode_entry *,
                             u_int, u_int);
static void              window_customize_key(struct window_mode_entry *,
                            struct client *, struct session *,
                            struct winlink *, key_code, struct mouse_event *);

#define WINDOW_CUSTOMIZE_DEFAULT_FORMAT \
       "#{?is_option," \
               "#{?option_is_global,,#[reverse](#{option_scope})#[default] }" \
               "#[ignore]" \
               "#{option_value}#{?option_unit, #{option_unit},}" \
       "," \
               "#{key}" \
       "}"

static const struct menu_item window_customize_menu_items[] = {
       { "Select", '\r', NULL },
       { "Expand", KEYC_RIGHT, NULL },
       { "", KEYC_NONE, NULL },
       { "Tag", 't', NULL },
       { "Tag All", '\024', NULL },
       { "Tag None", 'T', NULL },
       { "", KEYC_NONE, NULL },
       { "Cancel", 'q', NULL },

       { NULL, KEYC_NONE, NULL }
};

const struct window_mode window_customize_mode = {
       .name = "options-mode",
       .default_format = WINDOW_CUSTOMIZE_DEFAULT_FORMAT,

       .init = window_customize_init,
       .free = window_customize_free,
       .resize = window_customize_resize,
       .key = window_customize_key,
};

enum window_customize_scope {
       WINDOW_CUSTOMIZE_NONE,
       WINDOW_CUSTOMIZE_KEY,
       WINDOW_CUSTOMIZE_SERVER,
       WINDOW_CUSTOMIZE_GLOBAL_SESSION,
       WINDOW_CUSTOMIZE_SESSION,
       WINDOW_CUSTOMIZE_GLOBAL_WINDOW,
       WINDOW_CUSTOMIZE_WINDOW,
       WINDOW_CUSTOMIZE_PANE
};

enum window_customize_change {
       WINDOW_CUSTOMIZE_UNSET,
       WINDOW_CUSTOMIZE_RESET,
};

struct window_customize_itemdata {
       struct window_customize_modedata        *data;
       enum window_customize_scope              scope;

       char                                    *table;
       key_code                                 key;

       struct options                          *oo;
       char                                    *name;
       int                                      idx;
};

struct window_customize_modedata {
       struct window_pane                       *wp;
       int                                       dead;
       int                                       references;

       struct mode_tree_data                    *data;
       char                                     *format;
       int                                       hide_global;

       struct window_customize_itemdata        **item_list;
       u_int                                     item_size;

       struct cmd_find_state                     fs;
       enum window_customize_change              change;
};

static uint64_t
window_customize_get_tag(struct options_entry *o, int idx,
   const struct options_table_entry *oe)
{
       uint64_t        offset;

       if (oe == NULL)
               return ((uintptr_t)o);
       offset = ((const char *)oe - (const char *)options_table) / sizeof *options_table;
       return ((2ULL << 62)|(offset << 32)|((idx + 1) << 1)|1);
}

static struct options *
window_customize_get_tree(enum window_customize_scope scope,
   struct cmd_find_state *fs)
{
       switch (scope) {
       case WINDOW_CUSTOMIZE_NONE:
       case WINDOW_CUSTOMIZE_KEY:
               return (NULL);
       case WINDOW_CUSTOMIZE_SERVER:
               return (global_options);
       case WINDOW_CUSTOMIZE_GLOBAL_SESSION:
               return (global_s_options);
       case WINDOW_CUSTOMIZE_SESSION:
               return (fs->s->options);
       case WINDOW_CUSTOMIZE_GLOBAL_WINDOW:
               return (global_w_options);
       case WINDOW_CUSTOMIZE_WINDOW:
               return (fs->w->options);
       case WINDOW_CUSTOMIZE_PANE:
               return (fs->wp->options);
       }
       return (NULL);
}

static int
window_customize_check_item(struct window_customize_modedata *data,
   struct window_customize_itemdata *item, struct cmd_find_state *fsp)
{
       struct cmd_find_state   fs;

       if (fsp == NULL)
               fsp = &fs;

       if (cmd_find_valid_state(&data->fs))
               cmd_find_copy_state(fsp, &data->fs);
       else
               cmd_find_from_pane(fsp, data->wp, 0);
       return (item->oo == window_customize_get_tree(item->scope, fsp));
}

static int
window_customize_get_key(struct window_customize_itemdata *item,
   struct key_table **ktp, struct key_binding **bdp)
{
       struct key_table        *kt;
       struct key_binding      *bd;

       kt = key_bindings_get_table(item->table, 0);
       if (kt == NULL)
               return (0);
       bd = key_bindings_get(kt, item->key);
       if (bd == NULL)
               return (0);

       if (ktp != NULL)
               *ktp = kt;
       if (bdp != NULL)
               *bdp = bd;
       return (1);
}

static char *
window_customize_scope_text(enum window_customize_scope scope,
   struct cmd_find_state *fs)
{
       char    *s;
       u_int    idx;

       switch (scope) {
       case WINDOW_CUSTOMIZE_PANE:
               window_pane_index(fs->wp, &idx);
               xasprintf(&s, "pane %u", idx);
               break;
       case WINDOW_CUSTOMIZE_SESSION:
               xasprintf(&s, "session %s", fs->s->name);
               break;
       case WINDOW_CUSTOMIZE_WINDOW:
               xasprintf(&s, "window %u", fs->wl->idx);
               break;
       default:
               s = xstrdup("");
               break;
       }
       return (s);
}

static struct window_customize_itemdata *
window_customize_add_item(struct window_customize_modedata *data)
{
       struct window_customize_itemdata        *item;

       data->item_list = xreallocarray(data->item_list, data->item_size + 1,
           sizeof *data->item_list);
       item = data->item_list[data->item_size++] = xcalloc(1, sizeof *item);
       return (item);
}

static void
window_customize_free_item(struct window_customize_itemdata *item)
{
       free(item->table);
       free(item->name);
       free(item);
}

static void
window_customize_build_array(struct window_customize_modedata *data,
   struct mode_tree_item *top, enum window_customize_scope scope,
   struct options_entry *o, struct format_tree *ft)
{
       const struct options_table_entry        *oe = options_table_entry(o);
       struct options                          *oo = options_owner(o);
       struct window_customize_itemdata        *item;
       struct options_array_item               *ai;
       char                                    *name, *value, *text;
       u_int                                    idx;
       uint64_t                                 tag;

       ai = options_array_first(o);
       while (ai != NULL) {
               idx = options_array_item_index(ai);

               xasprintf(&name, "%s[%u]", options_name(o), idx);
               format_add(ft, "option_name", "%s", name);
               value = options_to_string(o, idx, 0);
               format_add(ft, "option_value", "%s", value);

               item = window_customize_add_item(data);
               item->scope = scope;
               item->oo = oo;
               item->name = xstrdup(options_name(o));
               item->idx = idx;

               text = format_expand(ft, data->format);
               tag = window_customize_get_tag(o, idx, oe);
               mode_tree_add(data->data, top, item, tag, name, text, -1);
               free(text);

               free(name);
               free(value);

               ai = options_array_next(ai);
       }
}

static void
window_customize_build_option(struct window_customize_modedata *data,
   struct mode_tree_item *top, enum window_customize_scope scope,
   struct options_entry *o, struct format_tree *ft,
   const char *filter, struct cmd_find_state *fs)
{
       const struct options_table_entry        *oe = options_table_entry(o);
       struct options                          *oo = options_owner(o);
       const char                              *name = options_name(o);
       struct window_customize_itemdata        *item;
       char                                    *text, *expanded, *value;
       int                                      global = 0, array = 0;
       uint64_t                                 tag;

       if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK))
               return;
       if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY))
               array = 1;

       if (scope == WINDOW_CUSTOMIZE_SERVER ||
           scope == WINDOW_CUSTOMIZE_GLOBAL_SESSION ||
           scope == WINDOW_CUSTOMIZE_GLOBAL_WINDOW)
               global = 1;
       if (data->hide_global && global)
               return;

       format_add(ft, "option_name", "%s", name);
       format_add(ft, "option_is_global", "%d", global);
       format_add(ft, "option_is_array", "%d", array);

       text = window_customize_scope_text(scope, fs);
       format_add(ft, "option_scope", "%s", text);
       free(text);

       if (oe != NULL && oe->unit != NULL)
               format_add(ft, "option_unit", "%s", oe->unit);
       else
               format_add(ft, "option_unit", "%s", "");

       if (!array) {
               value = options_to_string(o, -1, 0);
               format_add(ft, "option_value", "%s", value);
               free(value);
       }

       if (filter != NULL) {
               expanded = format_expand(ft, filter);
               if (!format_true(expanded)) {
                       free(expanded);
                       return;
               }
               free(expanded);
       }
       item = window_customize_add_item(data);
       item->oo = oo;
       item->scope = scope;
       item->name = xstrdup(name);
       item->idx = -1;

       if (array)
               text = NULL;
       else
               text = format_expand(ft, data->format);
       tag = window_customize_get_tag(o, -1, oe);
       top = mode_tree_add(data->data, top, item, tag, name, text, 0);
       free(text);

       if (array)
               window_customize_build_array(data, top, scope, o, ft);
}

static void
window_customize_find_user_options(struct options *oo, const char ***list,
   u_int *size)
{
       struct options_entry    *o;
       const char              *name;
       u_int                    i;

       o = options_first(oo);
       while (o != NULL) {
               name = options_name(o);
               if (*name != '@') {
                       o = options_next(o);
                       continue;
               }
               for (i = 0; i < *size; i++) {
                       if (strcmp((*list)[i], name) == 0)
                               break;
               }
               if (i != *size) {
                       o = options_next(o);
                       continue;
               }
               *list = xreallocarray(*list, (*size) + 1, sizeof **list);
               (*list)[(*size)++] = name;

               o = options_next(o);
       }
}

static void
window_customize_build_options(struct window_customize_modedata *data,
   const char *title, uint64_t tag,
   enum window_customize_scope scope0, struct options *oo0,
   enum window_customize_scope scope1, struct options *oo1,
   enum window_customize_scope scope2, struct options *oo2,
   struct format_tree *ft, const char *filter, struct cmd_find_state *fs)
{
       struct mode_tree_item            *top;
       struct options_entry             *o = NULL, *loop;
       const char                      **list = NULL, *name;
       u_int                             size = 0, i;
       enum window_customize_scope       scope;

       top = mode_tree_add(data->data, NULL, NULL, tag, title, NULL, 0);
       mode_tree_no_tag(top);

       /*
        * We get the options from the first tree, but build it using the
        * values from the other two. Any tree can have user options so we need
        * to build a separate list of them.
        */

       window_customize_find_user_options(oo0, &list, &size);
       if (oo1 != NULL)
               window_customize_find_user_options(oo1, &list, &size);
       if (oo2 != NULL)
               window_customize_find_user_options(oo2, &list, &size);

       for (i = 0; i < size; i++) {
               if (oo2 != NULL)
                       o = options_get(oo2, list[i]);
               if (o == NULL && oo1 != NULL)
                       o = options_get(oo1, list[i]);
               if (o == NULL)
                       o = options_get(oo0, list[i]);
               if (options_owner(o) == oo2)
                       scope = scope2;
               else if (options_owner(o) == oo1)
                       scope = scope1;
               else
                       scope = scope0;
               window_customize_build_option(data, top, scope, o, ft, filter,
                   fs);
       }
       free(list);

       loop = options_first(oo0);
       while (loop != NULL) {
               name = options_name(loop);
               if (*name == '@') {
                       loop = options_next(loop);
                       continue;
               }
               if (oo2 != NULL)
                       o = options_get(oo2, name);
               else if (oo1 != NULL)
                       o = options_get(oo1, name);
               else
                       o = loop;
               if (options_owner(o) == oo2)
                       scope = scope2;
               else if (options_owner(o) == oo1)
                       scope = scope1;
               else
                       scope = scope0;
               window_customize_build_option(data, top, scope, o, ft, filter,
                   fs);
               loop = options_next(loop);
       }
}

static void
window_customize_build_keys(struct window_customize_modedata *data,
   struct key_table *kt, struct format_tree *ft, const char *filter,
   struct cmd_find_state *fs, u_int number)
{
       struct mode_tree_item                   *top, *child, *mti;
       struct window_customize_itemdata        *item;
       struct key_binding                      *bd;
       char                                    *title, *text, *tmp, *expanded;
       const char                              *flag;
       uint64_t                                 tag;

       tag = (1ULL << 62)|((uint64_t)number << 54)|1;

       xasprintf(&title, "Key Table - %s", kt->name);
       top = mode_tree_add(data->data, NULL, NULL, tag, title, NULL, 0);
       mode_tree_no_tag(top);
       free(title);

       ft = format_create_from_state(NULL, NULL, fs);
       format_add(ft, "is_option", "0");
       format_add(ft, "is_key", "1");

       bd = key_bindings_first(kt);
       while (bd != NULL) {
               format_add(ft, "key", "%s", key_string_lookup_key(bd->key, 0));
               if (bd->note != NULL)
                       format_add(ft, "key_note", "%s", bd->note);
               if (filter != NULL) {
                       expanded = format_expand(ft, filter);
                       if (!format_true(expanded)) {
                               free(expanded);
                               continue;
                       }
                       free(expanded);
               }

               item = window_customize_add_item(data);
               item->scope = WINDOW_CUSTOMIZE_KEY;
               item->table = xstrdup(kt->name);
               item->key = bd->key;
               item->name = xstrdup(key_string_lookup_key(item->key, 0));
               item->idx = -1;

               expanded = format_expand(ft, data->format);
               child = mode_tree_add(data->data, top, item, (uintptr_t)bd,
                   expanded, NULL, 0);
               free(expanded);

               tmp = cmd_list_print(bd->cmdlist, 0);
               xasprintf(&text, "#[ignore]%s", tmp);
               free(tmp);
               mti = mode_tree_add(data->data, child, item,
                   tag|(bd->key << 3)|(0 << 1)|1, "Command", text, -1);
               mode_tree_draw_as_parent(mti);
               mode_tree_no_tag(mti);
               free(text);

               if (bd->note != NULL)
                       xasprintf(&text, "#[ignore]%s", bd->note);
               else
                       text = xstrdup("");
               mti = mode_tree_add(data->data, child, item,
                   tag|(bd->key << 3)|(1 << 1)|1, "Note", text, -1);
               mode_tree_draw_as_parent(mti);
               mode_tree_no_tag(mti);
               free(text);

               if (bd->flags & KEY_BINDING_REPEAT)
                       flag = "on";
               else
                       flag = "off";
               mti = mode_tree_add(data->data, child, item,
                   tag|(bd->key << 3)|(2 << 1)|1, "Repeat", flag, -1);
               mode_tree_draw_as_parent(mti);
               mode_tree_no_tag(mti);

               bd = key_bindings_next(kt, bd);
       }

       format_free(ft);
}

static void
window_customize_build(void *modedata,
   __unused struct mode_tree_sort_criteria *sort_crit, __unused uint64_t *tag,
   const char *filter)
{
       struct window_customize_modedata        *data = modedata;
       struct cmd_find_state                    fs;
       struct format_tree                      *ft;
       u_int                                    i;
       struct key_table                        *kt;

       for (i = 0; i < data->item_size; i++)
               window_customize_free_item(data->item_list[i]);
       free(data->item_list);
       data->item_list = NULL;
       data->item_size = 0;

       if (cmd_find_valid_state(&data->fs))
               cmd_find_copy_state(&fs, &data->fs);
       else
               cmd_find_from_pane(&fs, data->wp, 0);

       ft = format_create_from_state(NULL, NULL, &fs);
       format_add(ft, "is_option", "1");
       format_add(ft, "is_key", "0");

       window_customize_build_options(data, "Server Options",
           (3ULL << 62)|(OPTIONS_TABLE_SERVER << 1)|1,
           WINDOW_CUSTOMIZE_SERVER, global_options,
           WINDOW_CUSTOMIZE_NONE, NULL,
           WINDOW_CUSTOMIZE_NONE, NULL,
           ft, filter, &fs);
       window_customize_build_options(data, "Session Options",
           (3ULL << 62)|(OPTIONS_TABLE_SESSION << 1)|1,
           WINDOW_CUSTOMIZE_GLOBAL_SESSION, global_s_options,
           WINDOW_CUSTOMIZE_SESSION, fs.s->options,
           WINDOW_CUSTOMIZE_NONE, NULL,
           ft, filter, &fs);
       window_customize_build_options(data, "Window & Pane Options",
           (3ULL << 62)|(OPTIONS_TABLE_WINDOW << 1)|1,
           WINDOW_CUSTOMIZE_GLOBAL_WINDOW, global_w_options,
           WINDOW_CUSTOMIZE_WINDOW, fs.w->options,
           WINDOW_CUSTOMIZE_PANE, fs.wp->options,
           ft, filter, &fs);

       format_free(ft);
       ft = format_create_from_state(NULL, NULL, &fs);

       i = 0;
       kt = key_bindings_first_table();
       while (kt != NULL) {
               if (!RB_EMPTY(&kt->key_bindings)) {
                       window_customize_build_keys(data, kt, ft, filter, &fs,
                           i);
                       if (++i == 256)
                               break;
               }
               kt = key_bindings_next_table(kt);
       }

       format_free(ft);
}

static void
window_customize_draw_key(__unused struct window_customize_modedata *data,
   struct window_customize_itemdata *item, struct screen_write_ctx *ctx,
   u_int sx, u_int sy)
{
       struct screen           *s = ctx->s;
       u_int                    cx = s->cx, cy = s->cy;
       struct key_table        *kt;
       struct key_binding      *bd, *default_bd;
       const char              *note, *period = "";
       char                    *cmd, *default_cmd;

       if (item == NULL || !window_customize_get_key(item, &kt, &bd))
               return;

       note = bd->note;
       if (note == NULL)
               note = "There is no note for this key.";
       if (*note != '\0' && note[strlen (note) - 1] != '.')
               period = ".";
       if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, "%s%s",
           note, period))
               return;
       screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */
       if (s->cy >= cy + sy - 1)
               return;

       if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
           &grid_default_cell, "This key is in the %s table.", kt->name))
               return;
       if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
           &grid_default_cell, "This key %s repeat.",
           (bd->flags & KEY_BINDING_REPEAT) ? "does" : "does not"))
               return;
       screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */
       if (s->cy >= cy + sy - 1)
               return;

       cmd = cmd_list_print(bd->cmdlist, 0);
       if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
           &grid_default_cell, "Command: %s", cmd)) {
               free(cmd);
               return;
       }
       default_bd = key_bindings_get_default(kt, bd->key);
       if (default_bd != NULL) {
               default_cmd = cmd_list_print(default_bd->cmdlist, 0);
               if (strcmp(cmd, default_cmd) != 0 &&
                   !screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
                   &grid_default_cell, "The default is: %s", default_cmd)) {
                       free(default_cmd);
                       free(cmd);
                       return;
               }
               free(default_cmd);
       }
       free(cmd);
}

static void
window_customize_draw_option(struct window_customize_modedata *data,
   struct window_customize_itemdata *item, struct screen_write_ctx *ctx,
   u_int sx, u_int sy)
{
       struct screen                            *s = ctx->s;
       u_int                                     cx = s->cx, cy = s->cy;
       int                                       idx;
       struct options_entry                     *o, *parent;
       struct options                           *go, *wo;
       const struct options_table_entry         *oe;
       struct grid_cell                          gc;
       const char                              **choice, *text, *name;
       const char                               *space = "", *unit = "";
       char                                     *value = NULL, *expanded;
       char                                     *default_value = NULL;
       char                                      choices[256] = "";
       struct cmd_find_state                     fs;
       struct format_tree                       *ft;

       if (!window_customize_check_item(data, item, &fs))
               return;
       name = item->name;
       idx = item->idx;

       o = options_get(item->oo, name);
       if (o == NULL)
               return;
       oe = options_table_entry(o);

       if (oe != NULL && oe->unit != NULL) {
               space = " ";
               unit = oe->unit;
       }
       ft = format_create_from_state(NULL, NULL, &fs);

       if (oe == NULL || oe->text == NULL)
               text = "This option doesn't have a description.";
       else
               text = oe->text;
       if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, "%s",
           text))
               goto out;
       screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */
       if (s->cy >= cy + sy - 1)
               goto out;

       if (oe == NULL)
               text = "user";
       else if ((oe->scope & (OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE)) ==
           (OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE))
               text = "window and pane";
       else if (oe->scope & OPTIONS_TABLE_WINDOW)
               text = "window";
       else if (oe->scope & OPTIONS_TABLE_SESSION)
               text = "session";
       else
               text = "server";
       if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
           &grid_default_cell, "This is a %s option.", text))
               goto out;
       if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
               if (idx != -1) {
                       if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy),
                           0, &grid_default_cell,
                           "This is an array option, index %u.", idx))
                               goto out;
               } else {
                       if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy),
                           0, &grid_default_cell, "This is an array option."))
                               goto out;
               }
               if (idx == -1)
                       goto out;
       }
       screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */
       if (s->cy >= cy + sy - 1)
               goto out;

       value = options_to_string(o, idx, 0);
       if (oe != NULL && idx == -1) {
               default_value = options_default_to_string(oe);
               if (strcmp(default_value, value) == 0) {
                       free(default_value);
                       default_value = NULL;
               }
       }
       if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
           &grid_default_cell, "Option value: %s%s%s", value, space, unit))
               goto out;
       if (oe == NULL || oe->type == OPTIONS_TABLE_STRING) {
               expanded = format_expand(ft, value);
               if (strcmp(expanded, value) != 0) {
                       if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy),
                           0, &grid_default_cell, "This expands to: %s",
                           expanded))
                               goto out;
               }
               free(expanded);
       }
       if (oe != NULL && oe->type == OPTIONS_TABLE_CHOICE) {
               for (choice = oe->choices; *choice != NULL; choice++) {
                       strlcat(choices, *choice, sizeof choices);
                       strlcat(choices, ", ", sizeof choices);
               }
               choices[strlen(choices) - 2] = '\0';
               if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
                   &grid_default_cell, "Available values are: %s",
                   choices))
                       goto out;
       }
       if (oe != NULL && oe->type == OPTIONS_TABLE_COLOUR) {
               if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 1,
                   &grid_default_cell, "This is a colour option: "))
                       goto out;
               memcpy(&gc, &grid_default_cell, sizeof gc);
               gc.fg = options_get_number(item->oo, name);
               if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &gc,
                   "EXAMPLE"))
                       goto out;
       }
       if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_STYLE)) {
               if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 1,
                   &grid_default_cell, "This is a style option: "))
                       goto out;
               style_apply(&gc, item->oo, name, ft);
               if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &gc,
                   "EXAMPLE"))
                       goto out;
       }
       if (default_value != NULL) {
               if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0,
                   &grid_default_cell, "The default is: %s%s%s", default_value,
                   space, unit))
                       goto out;
       }

       screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */
       if (s->cy > cy + sy - 1)
               goto out;
       if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
               wo = NULL;
               go = NULL;
       } else {
               switch (item->scope) {
               case WINDOW_CUSTOMIZE_PANE:
                       wo = options_get_parent(item->oo);
                       go = options_get_parent(wo);
                       break;
               case WINDOW_CUSTOMIZE_WINDOW:
               case WINDOW_CUSTOMIZE_SESSION:
                       wo = NULL;
                       go = options_get_parent(item->oo);
                       break;
               default:
                       wo = NULL;
                       go = NULL;
                       break;
               }
       }
       if (wo != NULL && options_owner(o) != wo) {
               parent = options_get_only(wo, name);
               if (parent != NULL) {
                       value = options_to_string(parent, -1 , 0);
                       if (!screen_write_text(ctx, s->cx, sx,
                           sy - (s->cy - cy), 0, &grid_default_cell,
                           "Window value (from window %u): %s%s%s", fs.wl->idx,
                           value, space, unit))
                               goto out;
               }
       }
       if (go != NULL && options_owner(o) != go) {
               parent = options_get_only(go, name);
               if (parent != NULL) {
                       value = options_to_string(parent, -1 , 0);
                       if (!screen_write_text(ctx, s->cx, sx,
                           sy - (s->cy - cy), 0, &grid_default_cell,
                           "Global value: %s%s%s", value, space, unit))
                               goto out;
               }
       }

out:
       free(value);
       free(default_value);
       format_free(ft);
}

static void
window_customize_draw(void *modedata, void *itemdata,
   struct screen_write_ctx *ctx, u_int sx, u_int sy)
{
       struct window_customize_modedata        *data = modedata;
       struct window_customize_itemdata        *item = itemdata;

       if (item == NULL)
               return;

       if (item->scope == WINDOW_CUSTOMIZE_KEY)
               window_customize_draw_key(data, item, ctx, sx, sy);
       else
               window_customize_draw_option(data, item, ctx, sx, sy);
}

static void
window_customize_menu(void *modedata, struct client *c, key_code key)
{
       struct window_customize_modedata        *data = modedata;
       struct window_pane                      *wp = data->wp;
       struct window_mode_entry                *wme;

       wme = TAILQ_FIRST(&wp->modes);
       if (wme == NULL || wme->data != modedata)
               return;
       window_customize_key(wme, c, NULL, NULL, key, NULL);
}

static u_int
window_customize_height(__unused void *modedata, __unused u_int height)
{
       return (12);
}

static struct screen *
window_customize_init(struct window_mode_entry *wme, struct cmd_find_state *fs,
   struct args *args)
{
       struct window_pane                      *wp = wme->wp;
       struct window_customize_modedata        *data;
       struct screen                           *s;

       wme->data = data = xcalloc(1, sizeof *data);
       data->wp = wp;
       data->references = 1;

       memcpy(&data->fs, fs, sizeof data->fs);

       if (args == NULL || !args_has(args, 'F'))
               data->format = xstrdup(WINDOW_CUSTOMIZE_DEFAULT_FORMAT);
       else
               data->format = xstrdup(args_get(args, 'F'));

       data->data = mode_tree_start(wp, args, window_customize_build,
           window_customize_draw, NULL, window_customize_menu,
           window_customize_height, NULL, data, window_customize_menu_items,
           NULL, 0, &s);
       mode_tree_zoom(data->data, args);

       mode_tree_build(data->data);
       mode_tree_draw(data->data);

       return (s);
}

static void
window_customize_destroy(struct window_customize_modedata *data)
{
       u_int   i;

       if (--data->references != 0)
               return;

       for (i = 0; i < data->item_size; i++)
               window_customize_free_item(data->item_list[i]);
       free(data->item_list);

       free(data->format);

       free(data);
}

static void
window_customize_free(struct window_mode_entry *wme)
{
       struct window_customize_modedata *data = wme->data;

       if (data == NULL)
               return;

       data->dead = 1;
       mode_tree_free(data->data);
       window_customize_destroy(data);
}

static void
window_customize_resize(struct window_mode_entry *wme, u_int sx, u_int sy)
{
       struct window_customize_modedata        *data = wme->data;

       mode_tree_resize(data->data, sx, sy);
}

static void
window_customize_free_callback(void *modedata)
{
       window_customize_destroy(modedata);
}

static void
window_customize_free_item_callback(void *itemdata)
{
       struct window_customize_itemdata        *item = itemdata;
       struct window_customize_modedata        *data = item->data;

       window_customize_free_item(item);
       window_customize_destroy(data);
}

static int
window_customize_set_option_callback(struct client *c, void *itemdata,
   const char *s, __unused int done)
{
       struct window_customize_itemdata        *item = itemdata;
       struct window_customize_modedata        *data = item->data;
       struct options_entry                    *o;
       const struct options_table_entry        *oe;
       struct options                          *oo = item->oo;
       const char                              *name = item->name;
       char                                    *cause;
       int                                      idx = item->idx;

       if (s == NULL || *s == '\0' || data->dead)
               return (0);
       if (item == NULL || !window_customize_check_item(data, item, NULL))
               return (0);
       o = options_get(oo, name);
       if (o == NULL)
               return (0);
       oe = options_table_entry(o);

       if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
               if (idx == -1) {
                       for (idx = 0; idx < INT_MAX; idx++) {
                               if (options_array_get(o, idx) == NULL)
                                       break;
                       }
               }
               if (options_array_set(o, idx, s, 0, &cause) != 0)
                       goto fail;
       } else {
               if (options_from_string(oo, oe, name, s, 0, &cause) != 0)
                       goto fail;
       }

       options_push_changes(item->name);
       mode_tree_build(data->data);
       mode_tree_draw(data->data);
       data->wp->flags |= PANE_REDRAW;

       return (0);

fail:
       *cause = toupper((u_char)*cause);
       status_message_set(c, -1, 1, 0, "%s", cause);
       free(cause);
       return (0);
}

static void
window_customize_set_option(struct client *c,
   struct window_customize_modedata *data,
   struct window_customize_itemdata *item, int global, int pane)
{
       struct options_entry                    *o;
       const struct options_table_entry        *oe;
       struct options                          *oo;
       struct window_customize_itemdata        *new_item;
       int                                      flag, idx = item->idx;
       enum window_customize_scope              scope = WINDOW_CUSTOMIZE_NONE;
       u_int                                    choice;
       const char                              *name = item->name, *space = "";
       char                                    *prompt, *value, *text;
       struct cmd_find_state                    fs;

       if (item == NULL || !window_customize_check_item(data, item, &fs))
               return;
       o = options_get(item->oo, name);
       if (o == NULL)
               return;

       oe = options_table_entry(o);
       if (oe != NULL && ~oe->scope & OPTIONS_TABLE_PANE)
               pane = 0;
       if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
               scope = item->scope;
               oo = item->oo;
       } else {
               if (global) {
                       switch (item->scope) {
                       case WINDOW_CUSTOMIZE_NONE:
                       case WINDOW_CUSTOMIZE_KEY:
                       case WINDOW_CUSTOMIZE_SERVER:
                       case WINDOW_CUSTOMIZE_GLOBAL_SESSION:
                       case WINDOW_CUSTOMIZE_GLOBAL_WINDOW:
                               scope = item->scope;
                               break;
                       case WINDOW_CUSTOMIZE_SESSION:
                               scope = WINDOW_CUSTOMIZE_GLOBAL_SESSION;
                               break;
                       case WINDOW_CUSTOMIZE_WINDOW:
                       case WINDOW_CUSTOMIZE_PANE:
                               scope = WINDOW_CUSTOMIZE_GLOBAL_WINDOW;
                               break;
                       }
               } else {
                       switch (item->scope) {
                       case WINDOW_CUSTOMIZE_NONE:
                       case WINDOW_CUSTOMIZE_KEY:
                       case WINDOW_CUSTOMIZE_SERVER:
                       case WINDOW_CUSTOMIZE_SESSION:
                               scope = item->scope;
                               break;
                       case WINDOW_CUSTOMIZE_WINDOW:
                       case WINDOW_CUSTOMIZE_PANE:
                               if (pane)
                                       scope = WINDOW_CUSTOMIZE_PANE;
                               else
                                       scope = WINDOW_CUSTOMIZE_WINDOW;
                               break;
                       case WINDOW_CUSTOMIZE_GLOBAL_SESSION:
                               scope = WINDOW_CUSTOMIZE_SESSION;
                               break;
                       case WINDOW_CUSTOMIZE_GLOBAL_WINDOW:
                               if (pane)
                                       scope = WINDOW_CUSTOMIZE_PANE;
                               else
                                       scope = WINDOW_CUSTOMIZE_WINDOW;
                               break;
                       }
               }
               if (scope == item->scope)
                       oo = item->oo;
               else
                       oo = window_customize_get_tree(scope, &fs);
       }

       if (oe != NULL && oe->type == OPTIONS_TABLE_FLAG) {
               flag = options_get_number(oo, name);
               options_set_number(oo, name, !flag);
       } else if (oe != NULL && oe->type == OPTIONS_TABLE_CHOICE) {
               choice = options_get_number(oo, name);
               if (oe->choices[choice + 1] == NULL)
                       choice = 0;
               else
                       choice++;
               options_set_number(oo, name, choice);
       } else {
               text = window_customize_scope_text(scope, &fs);
               if (*text != '\0')
                       space = ", for ";
               else if (scope != WINDOW_CUSTOMIZE_SERVER)
                       space = ", global";
               if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
                       if (idx == -1) {
                               xasprintf(&prompt, "(%s[+]%s%s) ", name, space,
                                   text);
                       } else {
                               xasprintf(&prompt, "(%s[%d]%s%s) ", name, idx,
                                   space, text);
                       }
               } else
                       xasprintf(&prompt, "(%s%s%s) ", name, space, text);
               free(text);

               value = options_to_string(o, idx, 0);

               new_item = xcalloc(1, sizeof *new_item);
               new_item->data = data;
               new_item->scope = scope;
               new_item->oo = oo;
               new_item->name = xstrdup(name);
               new_item->idx = idx;

               data->references++;
               status_prompt_set(c, NULL, prompt, value,
                   window_customize_set_option_callback,
                   window_customize_free_item_callback, new_item,
                   PROMPT_NOFORMAT, PROMPT_TYPE_COMMAND);

               free(prompt);
               free(value);
       }
}

static void
window_customize_unset_option(struct window_customize_modedata *data,
   struct window_customize_itemdata *item)
{
       struct options_entry    *o;

       if (item == NULL || !window_customize_check_item(data, item, NULL))
               return;

       o = options_get(item->oo, item->name);
       if (o == NULL)
               return;
       if (item->idx != -1 && item == mode_tree_get_current(data->data))
               mode_tree_up(data->data, 0);
       options_remove_or_default(o, item->idx, NULL);
}

static void
window_customize_reset_option(struct window_customize_modedata *data,
   struct window_customize_itemdata *item)
{
       struct options          *oo;
       struct options_entry    *o;

       if (item == NULL || !window_customize_check_item(data, item, NULL))
               return;
       if (item->idx != -1)
               return;

       oo = item->oo;
       while (oo != NULL) {
               o = options_get_only(item->oo, item->name);
               if (o != NULL)
                       options_remove_or_default(o, -1, NULL);
               oo = options_get_parent(oo);
       }
}

static int
window_customize_set_command_callback(struct client *c, void *itemdata,
   const char *s, __unused int done)
{
       struct window_customize_itemdata        *item = itemdata;
       struct window_customize_modedata        *data = item->data;
       struct key_binding                      *bd;
       struct cmd_parse_result                 *pr;
       char                                    *error;

       if (s == NULL || *s == '\0' || data->dead)
               return (0);
       if (item == NULL || !window_customize_get_key(item, NULL, &bd))
               return (0);

       pr = cmd_parse_from_string(s, NULL);
       switch (pr->status) {
       case CMD_PARSE_ERROR:
               error = pr->error;
               goto fail;
       case CMD_PARSE_SUCCESS:
               break;
       }
       cmd_list_free(bd->cmdlist);
       bd->cmdlist = pr->cmdlist;

       mode_tree_build(data->data);
       mode_tree_draw(data->data);
       data->wp->flags |= PANE_REDRAW;

       return (0);

fail:
       *error = toupper((u_char)*error);
       status_message_set(c, -1, 1, 0, "%s", error);
       free(error);
       return (0);
}

static int
window_customize_set_note_callback(__unused struct client *c, void *itemdata,
   const char *s, __unused int done)
{
       struct window_customize_itemdata        *item = itemdata;
       struct window_customize_modedata        *data = item->data;
       struct key_binding                      *bd;

       if (s == NULL || *s == '\0' || data->dead)
               return (0);
       if (item == NULL || !window_customize_get_key(item, NULL, &bd))
               return (0);

       free(__UNCONST(bd->note));
       bd->note = xstrdup(s);

       mode_tree_build(data->data);
       mode_tree_draw(data->data);
       data->wp->flags |= PANE_REDRAW;

       return (0);
}

static void
window_customize_set_key(struct client *c,
   struct window_customize_modedata *data,
   struct window_customize_itemdata *item)
{
       key_code                                 key = item->key;
       struct key_binding                      *bd;
       const char                              *s;
       char                                    *prompt, *value;
       struct window_customize_itemdata        *new_item;

       if (item == NULL || !window_customize_get_key(item, NULL, &bd))
               return;

       s = mode_tree_get_current_name(data->data);
       if (strcmp(s, "Repeat") == 0)
               bd->flags ^= KEY_BINDING_REPEAT;
       else if (strcmp(s, "Command") == 0) {
               xasprintf(&prompt, "(%s) ", key_string_lookup_key(key, 0));
               value = cmd_list_print(bd->cmdlist, 0);

               new_item = xcalloc(1, sizeof *new_item);
               new_item->data = data;
               new_item->scope = item->scope;
               new_item->table = xstrdup(item->table);
               new_item->key = key;

               data->references++;
               status_prompt_set(c, NULL, prompt, value,
                   window_customize_set_command_callback,
                   window_customize_free_item_callback, new_item,
                   PROMPT_NOFORMAT, PROMPT_TYPE_COMMAND);
               free(prompt);
               free(value);
       } else if (strcmp(s, "Note") == 0) {
               xasprintf(&prompt, "(%s) ", key_string_lookup_key(key, 0));

               new_item = xcalloc(1, sizeof *new_item);
               new_item->data = data;
               new_item->scope = item->scope;
               new_item->table = xstrdup(item->table);
               new_item->key = key;

               data->references++;
               status_prompt_set(c, NULL, prompt,
                   (bd->note == NULL ? "" : bd->note),
                   window_customize_set_note_callback,
                   window_customize_free_item_callback, new_item,
                   PROMPT_NOFORMAT, PROMPT_TYPE_COMMAND);
               free(prompt);
       }
}

static void
window_customize_unset_key(struct window_customize_modedata *data,
   struct window_customize_itemdata *item)
{
       struct key_table        *kt;
       struct key_binding      *bd;

       if (item == NULL || !window_customize_get_key(item, &kt, &bd))
               return;

       if (item == mode_tree_get_current(data->data)) {
               mode_tree_collapse_current(data->data);
               mode_tree_up(data->data, 0);
       }
       key_bindings_remove(kt->name, bd->key);
}

static void
window_customize_reset_key(struct window_customize_modedata *data,
   struct window_customize_itemdata *item)
{
       struct key_table        *kt;
       struct key_binding      *dd, *bd;

       if (item == NULL || !window_customize_get_key(item, &kt, &bd))
               return;

       dd = key_bindings_get_default(kt, bd->key);
       if (dd != NULL && bd->cmdlist == dd->cmdlist)
               return;
       if (dd == NULL && item == mode_tree_get_current(data->data)) {
               mode_tree_collapse_current(data->data);
               mode_tree_up(data->data, 0);
       }
       key_bindings_reset(kt->name, bd->key);
}

static void
window_customize_change_each(void *modedata, void *itemdata,
   __unused struct client *c, __unused key_code key)
{
       struct window_customize_modedata        *data = modedata;
       struct window_customize_itemdata        *item = itemdata;

       switch (data->change) {
       case WINDOW_CUSTOMIZE_UNSET:
               if (item->scope == WINDOW_CUSTOMIZE_KEY)
                       window_customize_unset_key(data, item);
               else
                       window_customize_unset_option(data, item);
               break;
       case WINDOW_CUSTOMIZE_RESET:
               if (item->scope == WINDOW_CUSTOMIZE_KEY)
                       window_customize_reset_key(data, item);
               else
                       window_customize_reset_option(data, item);
               break;
       }
       if (item->scope != WINDOW_CUSTOMIZE_KEY)
               options_push_changes(item->name);
}

static int
window_customize_change_current_callback(__unused struct client *c,
   void *modedata, const char *s, __unused int done)
{
       struct window_customize_modedata        *data = modedata;
       struct window_customize_itemdata        *item;

       if (s == NULL || *s == '\0' || data->dead)
               return (0);
       if (tolower((u_char) s[0]) != 'y' || s[1] != '\0')
               return (0);

       item = mode_tree_get_current(data->data);
       switch (data->change) {
       case WINDOW_CUSTOMIZE_UNSET:
               if (item->scope == WINDOW_CUSTOMIZE_KEY)
                       window_customize_unset_key(data, item);
               else
                       window_customize_unset_option(data, item);
               break;
       case WINDOW_CUSTOMIZE_RESET:
               if (item->scope == WINDOW_CUSTOMIZE_KEY)
                       window_customize_reset_key(data, item);
               else
                       window_customize_reset_option(data, item);
               break;
       }
       if (item->scope != WINDOW_CUSTOMIZE_KEY)
               options_push_changes(item->name);
       mode_tree_build(data->data);
       mode_tree_draw(data->data);
       data->wp->flags |= PANE_REDRAW;

       return (0);
}

static int
window_customize_change_tagged_callback(struct client *c, void *modedata,
   const char *s, __unused int done)
{
       struct window_customize_modedata        *data = modedata;

       if (s == NULL || *s == '\0' || data->dead)
               return (0);
       if (tolower((u_char) s[0]) != 'y' || s[1] != '\0')
               return (0);

       mode_tree_each_tagged(data->data, window_customize_change_each, c,
           KEYC_NONE, 0);
       mode_tree_build(data->data);
       mode_tree_draw(data->data);
       data->wp->flags |= PANE_REDRAW;

       return (0);
}

static void
window_customize_key(struct window_mode_entry *wme, struct client *c,
   __unused struct session *s, __unused struct winlink *wl, key_code key,
   struct mouse_event *m)
{
       struct window_pane                      *wp = wme->wp;
       struct window_customize_modedata        *data = wme->data;
       struct window_customize_itemdata        *item, *new_item;
       int                                      finished, idx;
       char                                    *prompt;
       u_int                                    tagged;

       item = mode_tree_get_current(data->data);
       finished = mode_tree_key(data->data, c, &key, m, NULL, NULL);
       if (item != (new_item = mode_tree_get_current(data->data)))
               item = new_item;

       switch (key) {
       case '\r':
       case 's':
               if (item == NULL)
                       break;
               if (item->scope == WINDOW_CUSTOMIZE_KEY)
                       window_customize_set_key(c, data, item);
               else {
                       window_customize_set_option(c, data, item, 0, 1);
                       options_push_changes(item->name);
               }
               mode_tree_build(data->data);
               break;
       case 'w':
               if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY)
                       break;
               window_customize_set_option(c, data, item, 0, 0);
               options_push_changes(item->name);
               mode_tree_build(data->data);
               break;
       case 'S':
       case 'W':
               if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY)
                       break;
               window_customize_set_option(c, data, item, 1, 0);
               options_push_changes(item->name);
               mode_tree_build(data->data);
               break;
       case 'd':
               if (item == NULL || item->idx != -1)
                       break;
               xasprintf(&prompt, "Reset %s to default? ", item->name);
               data->references++;
               data->change = WINDOW_CUSTOMIZE_RESET;
               status_prompt_set(c, NULL, prompt, "",
                   window_customize_change_current_callback,
                   window_customize_free_callback, data,
                   PROMPT_SINGLE|PROMPT_NOFORMAT, PROMPT_TYPE_COMMAND);
               free(prompt);
               break;
       case 'D':
               tagged = mode_tree_count_tagged(data->data);
               if (tagged == 0)
                       break;
               xasprintf(&prompt, "Reset %u tagged to default? ", tagged);
               data->references++;
               data->change = WINDOW_CUSTOMIZE_RESET;
               status_prompt_set(c, NULL, prompt, "",
                   window_customize_change_tagged_callback,
                   window_customize_free_callback, data,
                   PROMPT_SINGLE|PROMPT_NOFORMAT, PROMPT_TYPE_COMMAND);
               free(prompt);
               break;
       case 'u':
               if (item == NULL)
                       break;
               idx = item->idx;
               if (idx != -1)
                       xasprintf(&prompt, "Unset %s[%d]? ", item->name, idx);
               else
                       xasprintf(&prompt, "Unset %s? ", item->name);
               data->references++;
               data->change = WINDOW_CUSTOMIZE_UNSET;
               status_prompt_set(c, NULL, prompt, "",
                   window_customize_change_current_callback,
                   window_customize_free_callback, data,
                   PROMPT_SINGLE|PROMPT_NOFORMAT, PROMPT_TYPE_COMMAND);
               free(prompt);
               break;
       case 'U':
               tagged = mode_tree_count_tagged(data->data);
               if (tagged == 0)
                       break;
               xasprintf(&prompt, "Unset %u tagged? ", tagged);
               data->references++;
               data->change = WINDOW_CUSTOMIZE_UNSET;
               status_prompt_set(c, NULL, prompt, "",
                   window_customize_change_tagged_callback,
                   window_customize_free_callback, data,
                   PROMPT_SINGLE|PROMPT_NOFORMAT, PROMPT_TYPE_COMMAND);
               free(prompt);
               break;
       case 'H':
               data->hide_global = !data->hide_global;
               mode_tree_build(data->data);
               break;
       }
       if (finished)
               window_pane_reset_mode(wp);
       else {
               mode_tree_draw(data->data);
               wp->flags |= PANE_REDRAW;
       }
}