#include "input.h"

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <locale.h>
#include <signal.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/mman.h>
#include <sys/timerfd.h>
#include <threads.h>
#include <unistd.h>
#include <wctype.h>

#include <linux/input-event-codes.h>

#include <xkbcommon/xkbcommon-compose.h>
#include <xkbcommon/xkbcommon-keysyms.h>
#include <xkbcommon/xkbcommon.h>

#include <xdg-shell.h>

#define LOG_MODULE "input"
#define LOG_ENABLE_DBG 0
#include "commands.h"
#include "config.h"
#include "grid.h"
#include "keymap.h"
#include "kitty-keymap.h"
#include "log.h"
#include "macros.h"
#include "quirks.h"
#include "render.h"
#include "search.h"
#include "selection.h"
#include "spawn.h"
#include "terminal.h"
#include "tokenize.h"
#include "unicode-mode.h"
#include "url-mode.h"
#include "util.h"
#include "vt.h"
#include "xkbcommon-vmod.h"
#include "xmalloc.h"
#include "xsnprintf.h"

struct pipe_context {
  char *text;
  size_t idx;
  size_t left;
};

static bool fdm_write_pipe(struct fdm *fdm, int fd, int events, void *data) {
  struct pipe_context *ctx = data;

  if (events & EPOLLHUP)
    goto pipe_closed;

  xassert(events & EPOLLOUT);
  ssize_t written = write(fd, &ctx->text[ctx->idx], ctx->left);

  if (written < 0) {
    LOG_WARN("failed to write to pipe: %s", strerror(errno));
    goto pipe_closed;
  }

  xassert(written <= ctx->left);
  ctx->idx += written;
  ctx->left -= written;

  if (ctx->left == 0)
    goto pipe_closed;

  return true;

pipe_closed:
  free(ctx->text);
  free(ctx);
  fdm_del(fdm, fd);
  return true;
}

static void alternate_scroll(struct seat *seat, int amount, int button);

static bool execute_binding(struct seat *seat, struct terminal *term,
                            const struct key_binding *binding, uint32_t serial,
                            int amount) {
  const enum bind_action_normal action = binding->action;

  switch (action) {
  case BIND_ACTION_NONE:
    return true;

  case BIND_ACTION_NOOP:
    return true;

  case BIND_ACTION_SCROLLBACK_UP_PAGE:
    if (term->grid == &term->normal) {
      cmd_scrollback_up(term, term->rows);
      return true;
    }
    break;

  case BIND_ACTION_SCROLLBACK_UP_HALF_PAGE:
    if (term->grid == &term->normal) {
      cmd_scrollback_up(term, max(term->rows / 2, 1));
      return true;
    }
    break;

  case BIND_ACTION_SCROLLBACK_UP_LINE:
    if (term->grid == &term->normal) {
      cmd_scrollback_up(term, 1);
      return true;
    }
    break;

  case BIND_ACTION_SCROLLBACK_UP_MOUSE:
    if (term->grid == &term->alt) {
      if (term->alt_scrolling) {
        alternate_scroll(seat, amount, BTN_BACK);
        return true;
      }
    } else {
      cmd_scrollback_up(term, amount);
      return true;
    }
    break;

  case BIND_ACTION_SCROLLBACK_DOWN_PAGE:
    if (term->grid == &term->normal) {
      cmd_scrollback_down(term, term->rows);
      return true;
    }
    break;

  case BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE:
    if (term->grid == &term->normal) {
      cmd_scrollback_down(term, max(term->rows / 2, 1));
      return true;
    }
    break;

  case BIND_ACTION_SCROLLBACK_DOWN_LINE:
    if (term->grid == &term->normal) {
      cmd_scrollback_down(term, 1);
      return true;
    }
    break;

  case BIND_ACTION_SCROLLBACK_DOWN_MOUSE:
    if (term->grid == &term->alt) {
      if (term->alt_scrolling) {
        alternate_scroll(seat, amount, BTN_FORWARD);
        return true;
      }
    } else {
      cmd_scrollback_down(term, amount);
      return true;
    }
    break;

  case BIND_ACTION_SCROLLBACK_HOME:
    if (term->grid == &term->normal) {
      cmd_scrollback_up(term, term->grid->num_rows);
      return true;
    }
    break;

  case BIND_ACTION_SCROLLBACK_END:
    if (term->grid == &term->normal) {
      cmd_scrollback_down(term, term->grid->num_rows);
      return true;
    }
    break;

  case BIND_ACTION_CLIPBOARD_COPY:
    selection_to_clipboard(seat, term, serial);
    return true;

  case BIND_ACTION_CLIPBOARD_PASTE:
    selection_from_clipboard(seat, term, serial);
    term_reset_view(term);
    return true;

  case BIND_ACTION_PRIMARY_PASTE:
    selection_from_primary(seat, term);
    term_reset_view(term);
    return true;

  case BIND_ACTION_SEARCH_START:
    search_begin(term);
    return true;

  case BIND_ACTION_FONT_SIZE_UP:
    term_font_size_increase(term);
    return true;

  case BIND_ACTION_FONT_SIZE_DOWN:
    term_font_size_decrease(term);
    return true;

  case BIND_ACTION_FONT_SIZE_RESET:
    term_font_size_reset(term);
    return true;

  case BIND_ACTION_SPAWN_TERMINAL:
    term_spawn_new(term);
    return true;

  case BIND_ACTION_MINIMIZE:
    xdg_toplevel_set_minimized(term->window->xdg_toplevel);
    return true;

  case BIND_ACTION_MAXIMIZE:
    if (term->window->is_fullscreen)
      xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel);
    if (term->window->is_maximized)
      xdg_toplevel_unset_maximized(term->window->xdg_toplevel);
    else
      xdg_toplevel_set_maximized(term->window->xdg_toplevel);
    return true;

  case BIND_ACTION_FULLSCREEN:
    if (term->window->is_fullscreen)
      xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel);
    else
      xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL);
    return true;

  case BIND_ACTION_PIPE_SCROLLBACK:
    if (term->grid == &term->alt)
      break;
    /* FALLTHROUGH */
  case BIND_ACTION_PIPE_VIEW:
  case BIND_ACTION_PIPE_SELECTED:
  case BIND_ACTION_PIPE_COMMAND_OUTPUT: {
    if (binding->aux->type != BINDING_AUX_PIPE)
      return true;

    struct pipe_context *ctx = NULL;

    int pipe_fd[2] = {-1, -1};
    int stdout_fd = -1;
    int stderr_fd = -1;

    char *text = NULL;
    size_t len = 0;

    if (pipe(pipe_fd) < 0) {
      LOG_ERRNO("failed to create pipe");
      goto pipe_err;
    }

    stdout_fd = open("/dev/null", O_WRONLY);
    stderr_fd = open("/dev/null", O_WRONLY);

    if (stdout_fd < 0 || stderr_fd < 0) {
      LOG_ERRNO("failed to open /dev/null");
      goto pipe_err;
    }

    bool success;
    switch (action) {
    case BIND_ACTION_PIPE_SCROLLBACK:
      success = term_scrollback_to_text(term, &text, &len);
      break;

    case BIND_ACTION_PIPE_VIEW:
      success = term_view_to_text(term, &text, &len);
      break;

    case BIND_ACTION_PIPE_SELECTED:
      text = selection_to_text(term);
      success = text != NULL;
      len = text != NULL ? strlen(text) : 0;
      break;

    case BIND_ACTION_PIPE_COMMAND_OUTPUT:
      success = term_command_output_to_text(term, &text, &len);
      break;

    default:
      BUG("Unhandled action type");
      success = false;
      break;
    }

    if (!success)
      goto pipe_err;

    /* Make write-end non-blocking; required by the FDM */
    {
      int flags = fcntl(pipe_fd[1], F_GETFL);
      if (flags < 0 || fcntl(pipe_fd[1], F_SETFL, flags | O_NONBLOCK) < 0) {
        LOG_ERRNO("failed to make write-end of pipe non-blocking");
        goto pipe_err;
      }
    }

    /* Make sure write-end is closed on exec() - or the spawned
     * program may not terminate*/
    {
      int flags = fcntl(pipe_fd[1], F_GETFD);
      if (flags < 0 || fcntl(pipe_fd[1], F_SETFD, flags | FD_CLOEXEC) < 0) {
        LOG_ERRNO("failed to set FD_CLOEXEC on writeend of pipe");
        goto pipe_err;
      }
    }

    if (spawn(term->reaper, term->cwd, binding->aux->pipe.args, pipe_fd[0],
              stdout_fd, stderr_fd, NULL, NULL, NULL) < 0)
      goto pipe_err;

    /* Close read end */
    close(pipe_fd[0]);

    ctx = xmalloc(sizeof(*ctx));
    *ctx = (struct pipe_context){
        .text = text,
        .left = len,
    };

    /* Asynchronously write the output to the pipe */
    if (!fdm_add(term->fdm, pipe_fd[1], EPOLLOUT, &fdm_write_pipe, ctx))
      goto pipe_err;

    return true;

  pipe_err:
    if (stdout_fd >= 0)
      close(stdout_fd);
    if (stderr_fd >= 0)
      close(stderr_fd);
    if (pipe_fd[0] >= 0)
      close(pipe_fd[0]);
    if (pipe_fd[1] >= 0)
      close(pipe_fd[1]);
    free(text);
    free(ctx);
    return true;
  }

  case BIND_ACTION_SHOW_URLS_COPY:
  case BIND_ACTION_SHOW_URLS_LAUNCH:
  case BIND_ACTION_SHOW_URLS_PERSISTENT: {
    xassert(!urls_mode_is_active(term));

    enum url_action url_action =
        action == BIND_ACTION_SHOW_URLS_COPY     ? URL_ACTION_COPY
        : action == BIND_ACTION_SHOW_URLS_LAUNCH ? URL_ACTION_LAUNCH
                                                 : URL_ACTION_PERSISTENT;

    urls_collect(term, url_action, &term->conf->url.preg, true, &term->urls);
    urls_assign_key_combos(term->conf, &term->urls);
    urls_render(term, &term->conf->url.launch);
    return true;
  }

  case BIND_ACTION_TEXT_BINDING:
    xassert(binding->aux->type == BINDING_AUX_TEXT);
    term_to_slave(term, binding->aux->text.data, binding->aux->text.len);
    return true;

  case BIND_ACTION_PROMPT_PREV: {
    if (term->grid != &term->normal)
      return false;

    struct grid *grid = term->grid;
    const int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows);

    /* Check each row from current view-1 (that is, the first
     * currently not visible row), up to, and including, the
     * scrollback start */
    for (int r_sb_rel =
             grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, grid->view) -
             1;
         r_sb_rel >= 0; r_sb_rel--) {
      const int r_abs =
          grid_row_sb_to_abs_precalc_sb_start(grid, sb_start, r_sb_rel);

      const struct row *row = grid->rows[r_abs];
      xassert(row != NULL);

      if (!row->shell_integration.prompt_marker)
        continue;

      grid->view = r_abs;
      term_damage_view(term);
      render_refresh(term);
      break;
    }

    return true;
  }

  case BIND_ACTION_PROMPT_NEXT: {
    if (term->grid != &term->normal)
      return false;

    struct grid *grid = term->grid;
    const int num_rows = grid->num_rows;

    if (grid->view == grid->offset) {
      /* Already at the bottom */
      return true;
    }

    /* Check each row from view+1, to the bottom of the scrollback */
    for (int r_abs = (grid->view + 1) & (num_rows - 1);;
         r_abs = (r_abs + 1) & (num_rows - 1)) {
      const struct row *row = grid->rows[r_abs];
      xassert(row != NULL);

      if (!row->shell_integration.prompt_marker) {
        if (r_abs == grid->offset + term->rows - 1) {
          /* We've reached the bottom of the scrollback */
          break;
        }
        continue;
      }

      int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows);
      int ofs_sb_rel =
          grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, grid->offset);
      int new_view_sb_rel =
          grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, r_abs);

      new_view_sb_rel = min(ofs_sb_rel, new_view_sb_rel);
      grid->view =
          grid_row_sb_to_abs_precalc_sb_start(grid, sb_start, new_view_sb_rel);

      term_damage_view(term);
      render_refresh(term);
      break;
    }

    return true;
  }

  case BIND_ACTION_UNICODE_INPUT:
    unicode_mode_activate(term);
    return true;

  case BIND_ACTION_QUIT:
    term_shutdown(term);
    return true;

  case BIND_ACTION_TAB_NEW:
    term_tab_new(term, 0, NULL, NULL, term->shutdown.cb,
                 term->shutdown.cb_data);
    return true;

  case BIND_ACTION_TAB_CLOSE:
    term_tab_close(term);
    return true;

  case BIND_ACTION_TAB_NEXT: {
    struct wl_window *win = term->window;
    if (win->tab_count > 1)
      term_tab_switch(win, (win->active_tab + 1) % win->tab_count);
    return true;
  }

  case BIND_ACTION_TAB_PREV: {
    struct wl_window *win = term->window;
    if (win->tab_count > 1)
      term_tab_switch(win, win->active_tab == 0 ? win->tab_count - 1
                                                : win->active_tab - 1);
    return true;
  }

  case BIND_ACTION_TAB_1:
  case BIND_ACTION_TAB_2:
  case BIND_ACTION_TAB_3:
  case BIND_ACTION_TAB_4:
  case BIND_ACTION_TAB_5:
  case BIND_ACTION_TAB_6:
  case BIND_ACTION_TAB_7:
  case BIND_ACTION_TAB_8:
  case BIND_ACTION_TAB_9: {
    size_t idx = (size_t)(action - BIND_ACTION_TAB_1);
    struct wl_window *win = term->window;
    if (idx < win->tab_count)
      term_tab_switch(win, idx);
    return true;
  }

  case BIND_ACTION_TAB_OVERVIEW:
    tab_overview_toggle(term->window);
    return true;

  case BIND_ACTION_SESSION_SAVE:
    search_begin_session(term, SEARCH_MODE_SESSION_SAVE);
    return true;

  case BIND_ACTION_SESSION_LOAD:
    search_begin_session(term, SEARCH_MODE_SESSION_LOAD);
    return true;

  case BIND_ACTION_SESSION_SAVE_SECURE:
    search_begin_session(term, SEARCH_MODE_SESSION_SAVE_SECURE_NAME);
    return true;

  case BIND_ACTION_REGEX_LAUNCH:
  case BIND_ACTION_REGEX_COPY:
    if (binding->aux->type != BINDING_AUX_REGEX)
      return true;

    tll_foreach(term->conf->custom_regexes, it) {
      const struct custom_regex *regex = &it->item;

      if (streq(regex->name, binding->aux->regex_name)) {
        xassert(!urls_mode_is_active(term));

        enum url_action url_action = action == BIND_ACTION_REGEX_LAUNCH
                                         ? URL_ACTION_LAUNCH
                                         : URL_ACTION_COPY;

        if (regex->regex == NULL) {
          LOG_ERR("regex:%s has no regex defined", regex->name);
          return true;
        }
        if (url_action == URL_ACTION_LAUNCH &&
            regex->launch.argv.args == NULL) {
          LOG_ERR("regex:%s has no launch command defined", regex->name);
          return true;
        }

        urls_collect(term, url_action, &regex->preg, false, &term->urls);
        urls_assign_key_combos(term->conf, &term->urls);
        urls_render(term, &regex->launch);
        return true;
      }
    }

    LOG_ERR("no regex section named '%s' defined in the configuration",
            binding->aux->regex_name);

    return true;

  case BIND_ACTION_THEME_SWITCH_1:
  case BIND_ACTION_THEME_SWITCH_DARK:
    term_theme_switch_to_dark(term);
    return true;

  case BIND_ACTION_THEME_SWITCH_2:
  case BIND_ACTION_THEME_SWITCH_LIGHT:
    term_theme_switch_to_light(term);
    return true;

  case BIND_ACTION_THEME_TOGGLE:
    term_theme_toggle(term);
    return true;

  case BIND_ACTION_SELECT_BEGIN:
    selection_start(term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE,
                    false);
    return true;

  case BIND_ACTION_SELECT_BEGIN_BLOCK:
    selection_start(term, seat->mouse.col, seat->mouse.row, SELECTION_BLOCK,
                    false);
    return true;

  case BIND_ACTION_SELECT_EXTEND:
    selection_extend(seat, term, seat->mouse.col, seat->mouse.row,
                     term->selection.kind);
    return true;

  case BIND_ACTION_SELECT_EXTEND_CHAR_WISE:
    if (term->selection.kind != SELECTION_BLOCK) {
      selection_extend(seat, term, seat->mouse.col, seat->mouse.row,
                       SELECTION_CHAR_WISE);
      return true;
    }
    return false;

  case BIND_ACTION_SELECT_WORD:
    selection_start(term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE,
                    false);
    return true;

  case BIND_ACTION_SELECT_WORD_WS:
    selection_start(term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE,
                    true);
    return true;

  case BIND_ACTION_SELECT_QUOTE:
    selection_start(term, seat->mouse.col, seat->mouse.row,
                    SELECTION_QUOTE_WISE, false);
    return true;

  case BIND_ACTION_SELECT_ROW:
    selection_start(term, seat->mouse.col, seat->mouse.row, SELECTION_LINE_WISE,
                    false);
    return true;

  case BIND_ACTION_COUNT:
    BUG("Invalid action type");
    return false;
  }

  return false;
}

static void keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard,
                            uint32_t format, int32_t fd, uint32_t size) {
  LOG_DBG("keyboard_keymap: keyboard=%p (format=%u, size=%u)",
          (void *)wl_keyboard, format, size);

  struct seat *seat = data;
  struct wayland *wayl = seat->wayl;

  /*
   * Free old keymap state
   */

  if (seat->kbd.xkb_keymap != NULL) {
    xkb_keymap_unref(seat->kbd.xkb_keymap);
    seat->kbd.xkb_keymap = NULL;
  }
  if (seat->kbd.xkb_state != NULL) {
    xkb_state_unref(seat->kbd.xkb_state);
    seat->kbd.xkb_state = NULL;
  }

  key_binding_unload_keymap(wayl->key_binding_manager, seat);

  /* Verify keymap is in a format we understand */
  switch ((enum wl_keyboard_keymap_format)format) {
  case WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP:
    goto err;

  case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1:
    break;

  default:
    LOG_WARN("unrecognized keymap format: %u", format);
    goto err;
  }

  char *map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
  if (map_str == MAP_FAILED) {
    LOG_ERRNO("failed to mmap keyboard keymap");
    goto err;
  }

  while (map_str[size - 1] == '\0')
    size--;

  if (seat->kbd.xkb != NULL) {
    seat->kbd.xkb_keymap = xkb_keymap_new_from_buffer(
        seat->kbd.xkb, map_str, size, XKB_KEYMAP_FORMAT_TEXT_V1,
        XKB_KEYMAP_COMPILE_NO_FLAGS);
  }

  munmap(map_str, size);

  if (seat->kbd.xkb_keymap != NULL) {
    seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap);

    seat->kbd.mod_shift =
        xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_SHIFT);
    seat->kbd.mod_alt =
        xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_ALT);
    seat->kbd.mod_ctrl =
        xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CTRL);
    seat->kbd.mod_super =
        xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_LOGO);
    seat->kbd.mod_caps =
        xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CAPS);
    seat->kbd.mod_num =
        xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_NUM);

    /* Significant modifiers in the legacy keyboard protocol */
    seat->kbd.legacy_significant = 0;
    if (seat->kbd.mod_shift != XKB_MOD_INVALID)
      seat->kbd.legacy_significant |= 1 << seat->kbd.mod_shift;
    if (seat->kbd.mod_alt != XKB_MOD_INVALID)
      seat->kbd.legacy_significant |= 1 << seat->kbd.mod_alt;
    if (seat->kbd.mod_ctrl != XKB_MOD_INVALID)
      seat->kbd.legacy_significant |= 1 << seat->kbd.mod_ctrl;
    if (seat->kbd.mod_super != XKB_MOD_INVALID)
      seat->kbd.legacy_significant |= 1 << seat->kbd.mod_super;

    /* Significant modifiers in the kitty keyboard protocol */
    seat->kbd.kitty_significant = seat->kbd.legacy_significant;
    if (seat->kbd.mod_caps != XKB_MOD_INVALID)
      seat->kbd.kitty_significant |= 1 << seat->kbd.mod_caps;
    if (seat->kbd.mod_num != XKB_MOD_INVALID)
      seat->kbd.kitty_significant |= 1 << seat->kbd.mod_num;

    /*
     * Create a mask of all "virtual" modifiers. Some compositors
     * add these *in addition* to the "real" modifiers (Mod1,
     * Mod2, etc).
     *
     * Since our modifier logic (both for internal shortcut
     * processing, and e.g. the kitty keyboard protocol) makes
     * very few assumptions on available modifiers, which keys map
     * to which modifier etc, the presence of virtual modifiers
     * causes various things to break.
     *
     * For example, if a foot shortcut is Mod1+b (i.e. Alt+b), it
     * won't match if the compositor _also_ sets the Alt modifier
     * (the corresponding shortcut in foot would be Alt+Mod1+b).
     *
     * See https://codeberg.org/dnkl/foot/issues/2009
     *
     * Mutter (GNOME) is known to set the virtual modifiers in
     * addtiion to the real modifiers.
     *
     * As far as I know, there's no compositor that _only_ sets
     * virtual modifiers (don't think that's even legal...?)
     */
    {
      xkb_mod_index_t alt =
          xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_ALT);
      xkb_mod_index_t meta =
          xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_META);
      xkb_mod_index_t super =
          xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SUPER);
      xkb_mod_index_t hyper =
          xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_HYPER);
      xkb_mod_index_t num_lock =
          xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_NUM);
      xkb_mod_index_t scroll_lock =
          xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SCROLL);
      xkb_mod_index_t level_three =
          xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL3);
      xkb_mod_index_t level_five =
          xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL5);

      xkb_mod_index_t ignore = 0;

      if (alt != XKB_MOD_INVALID)
        ignore |= 1 << alt;
      if (meta != XKB_MOD_INVALID)
        ignore |= 1 << meta;
      if (super != XKB_MOD_INVALID)
        ignore |= 1 << super;
      if (hyper != XKB_MOD_INVALID)
        ignore |= 1 << hyper;
      if (num_lock != XKB_MOD_INVALID)
        ignore |= 1 << num_lock;
      if (scroll_lock != XKB_MOD_INVALID)
        ignore |= 1 << scroll_lock;
      if (level_three != XKB_MOD_INVALID)
        ignore |= 1 << level_three;
      if (level_five != XKB_MOD_INVALID)
        ignore |= 1 << level_five;

      seat->kbd.virtual_modifiers = ignore;
    }

    seat->kbd.key_arrow_up = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "UP");
    seat->kbd.key_arrow_down =
        xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN");
  }

  key_binding_load_keymap(wayl->key_binding_manager, seat);

err:
  close(fd);
}

static void keyboard_enter(void *data, struct wl_keyboard *wl_keyboard,
                           uint32_t serial, struct wl_surface *surface,
                           struct wl_array *keys) {
  xassert(surface != NULL);
  xassert(serial != 0);

  struct seat *seat = data;
  struct wl_window *win = wl_surface_get_user_data(surface);
  struct terminal *term = win->term;

  LOG_DBG("%s: keyboard_enter: keyboard=%p, serial=%u, surface=%p", seat->name,
          (void *)wl_keyboard, serial, (void *)surface);

  term_kbd_focus_in(term);
  seat->kbd_focus = term;
  seat->kbd.serial = serial;
}

static bool start_repeater(struct seat *seat, uint32_t key) {
  if (seat->kbd.repeat.dont_re_repeat)
    return true;

  if (seat->kbd.repeat.rate == 0)
    return true;

  struct itimerspec t = {
      .it_value = {.tv_sec = 0, .tv_nsec = seat->kbd.repeat.delay * 1000000},
      .it_interval = {.tv_sec = 0,
                      .tv_nsec = 1000000000 / seat->kbd.repeat.rate},
  };

  if (t.it_value.tv_nsec >= 1000000000) {
    t.it_value.tv_sec += t.it_value.tv_nsec / 1000000000;
    t.it_value.tv_nsec %= 1000000000;
  }
  if (t.it_interval.tv_nsec >= 1000000000) {
    t.it_interval.tv_sec += t.it_interval.tv_nsec / 1000000000;
    t.it_interval.tv_nsec %= 1000000000;
  }
  if (timerfd_settime(seat->kbd.repeat.fd, 0, &t, NULL) < 0) {
    LOG_ERRNO("%s: failed to arm keyboard repeat timer", seat->name);
    return false;
  }

  seat->kbd.repeat.key = key;
  return true;
}

static bool stop_repeater(struct seat *seat, uint32_t key) {
  if (key != -1 && key != seat->kbd.repeat.key)
    return true;

  if (timerfd_settime(seat->kbd.repeat.fd, 0, &(struct itimerspec){{0}}, NULL) <
      0) {
    LOG_ERRNO("%s: failed to disarm keyboard repeat timer", seat->name);
    return false;
  }

  return true;
}

static void keyboard_leave(void *data, struct wl_keyboard *wl_keyboard,
                           uint32_t serial, struct wl_surface *surface) {
  struct seat *seat = data;

  LOG_DBG("keyboard_leave: keyboard=%p, serial=%u, surface=%p",
          (void *)wl_keyboard, serial, (void *)surface);

  xassert(seat->kbd_focus == NULL || surface == NULL || /* Seen on Sway 1.2 */
          ((const struct wl_window *)wl_surface_get_user_data(surface))->term ==
              seat->kbd_focus);

  struct terminal *old_focused = seat->kbd_focus;
  seat->kbd_focus = NULL;

  stop_repeater(seat, -1);
  seat->kbd.shift = false;
  seat->kbd.alt = false;
  seat->kbd.ctrl = false;
  seat->kbd.super = false;

  if (seat->kbd.xkb_compose_state != NULL)
    xkb_compose_state_reset(seat->kbd.xkb_compose_state);

  if (seat->kbd.xkb_state != NULL && seat->kbd.xkb_keymap != NULL) {
    const xkb_layout_index_t layout_count =
        xkb_keymap_num_layouts(seat->kbd.xkb_keymap);

    for (xkb_layout_index_t i = 0; i < layout_count; i++)
      xkb_state_update_mask(seat->kbd.xkb_state, 0, 0, 0, i, i, i);
  }

  if (old_focused != NULL) {
    seat->pointer.hidden = false;
    term_xcursor_update_for_seat(old_focused, seat);
    term_kbd_focus_out(old_focused);
  } else {
    /*
     * Sway bug - under certain conditions we get a
     * keyboard_leave() (and keyboard_key()) without first having
     * received a keyboard_enter()
     */
    LOG_WARN("compositor sent keyboard_leave event without a keyboard_enter "
             "event: surface=%p",
             (void *)surface);
  }
}

static const struct key_data *keymap_data_for_sym(xkb_keysym_t sym,
                                                  size_t *count) {
  switch (sym) {
  case XKB_KEY_Escape:
    *count = ALEN(key_escape);
    return key_escape;
  case XKB_KEY_Return:
    *count = ALEN(key_return);
    return key_return;
  case XKB_KEY_ISO_Left_Tab:
    *count = ALEN(key_iso_left_tab);
    return key_iso_left_tab;
  case XKB_KEY_Tab:
    *count = ALEN(key_tab);
    return key_tab;
  case XKB_KEY_BackSpace:
    *count = ALEN(key_backspace);
    return key_backspace;
  case XKB_KEY_Up:
    *count = ALEN(key_up);
    return key_up;
  case XKB_KEY_Down:
    *count = ALEN(key_down);
    return key_down;
  case XKB_KEY_Right:
    *count = ALEN(key_right);
    return key_right;
  case XKB_KEY_Left:
    *count = ALEN(key_left);
    return key_left;
  case XKB_KEY_Home:
    *count = ALEN(key_home);
    return key_home;
  case XKB_KEY_End:
    *count = ALEN(key_end);
    return key_end;
  case XKB_KEY_Insert:
    *count = ALEN(key_insert);
    return key_insert;
  case XKB_KEY_Delete:
    *count = ALEN(key_delete);
    return key_delete;
  case XKB_KEY_Page_Up:
    *count = ALEN(key_pageup);
    return key_pageup;
  case XKB_KEY_Page_Down:
    *count = ALEN(key_pagedown);
    return key_pagedown;
  case XKB_KEY_F1:
    *count = ALEN(key_f1);
    return key_f1;
  case XKB_KEY_F2:
    *count = ALEN(key_f2);
    return key_f2;
  case XKB_KEY_F3:
    *count = ALEN(key_f3);
    return key_f3;
  case XKB_KEY_F4:
    *count = ALEN(key_f4);
    return key_f4;
  case XKB_KEY_F5:
    *count = ALEN(key_f5);
    return key_f5;
  case XKB_KEY_F6:
    *count = ALEN(key_f6);
    return key_f6;
  case XKB_KEY_F7:
    *count = ALEN(key_f7);
    return key_f7;
  case XKB_KEY_F8:
    *count = ALEN(key_f8);
    return key_f8;
  case XKB_KEY_F9:
    *count = ALEN(key_f9);
    return key_f9;
  case XKB_KEY_F10:
    *count = ALEN(key_f10);
    return key_f10;
  case XKB_KEY_F11:
    *count = ALEN(key_f11);
    return key_f11;
  case XKB_KEY_F12:
    *count = ALEN(key_f12);
    return key_f12;
  case XKB_KEY_F13:
    *count = ALEN(key_f13);
    return key_f13;
  case XKB_KEY_F14:
    *count = ALEN(key_f14);
    return key_f14;
  case XKB_KEY_F15:
    *count = ALEN(key_f15);
    return key_f15;
  case XKB_KEY_F16:
    *count = ALEN(key_f16);
    return key_f16;
  case XKB_KEY_F17:
    *count = ALEN(key_f17);
    return key_f17;
  case XKB_KEY_F18:
    *count = ALEN(key_f18);
    return key_f18;
  case XKB_KEY_F19:
    *count = ALEN(key_f19);
    return key_f19;
  case XKB_KEY_F20:
    *count = ALEN(key_f20);
    return key_f20;
  case XKB_KEY_F21:
    *count = ALEN(key_f21);
    return key_f21;
  case XKB_KEY_F22:
    *count = ALEN(key_f22);
    return key_f22;
  case XKB_KEY_F23:
    *count = ALEN(key_f23);
    return key_f23;
  case XKB_KEY_F24:
    *count = ALEN(key_f24);
    return key_f24;
  case XKB_KEY_F25:
    *count = ALEN(key_f25);
    return key_f25;
  case XKB_KEY_F26:
    *count = ALEN(key_f26);
    return key_f26;
  case XKB_KEY_F27:
    *count = ALEN(key_f27);
    return key_f27;
  case XKB_KEY_F28:
    *count = ALEN(key_f28);
    return key_f28;
  case XKB_KEY_F29:
    *count = ALEN(key_f29);
    return key_f29;
  case XKB_KEY_F30:
    *count = ALEN(key_f30);
    return key_f30;
  case XKB_KEY_F31:
    *count = ALEN(key_f31);
    return key_f31;
  case XKB_KEY_F32:
    *count = ALEN(key_f32);
    return key_f32;
  case XKB_KEY_F33:
    *count = ALEN(key_f33);
    return key_f33;
  case XKB_KEY_F34:
    *count = ALEN(key_f34);
    return key_f34;
  case XKB_KEY_F35:
    *count = ALEN(key_f35);
    return key_f35;
  case XKB_KEY_KP_Up:
    *count = ALEN(key_kp_up);
    return key_kp_up;
  case XKB_KEY_KP_Down:
    *count = ALEN(key_kp_down);
    return key_kp_down;
  case XKB_KEY_KP_Right:
    *count = ALEN(key_kp_right);
    return key_kp_right;
  case XKB_KEY_KP_Left:
    *count = ALEN(key_kp_left);
    return key_kp_left;
  case XKB_KEY_KP_Begin:
    *count = ALEN(key_kp_begin);
    return key_kp_begin;
  case XKB_KEY_KP_Home:
    *count = ALEN(key_kp_home);
    return key_kp_home;
  case XKB_KEY_KP_End:
    *count = ALEN(key_kp_end);
    return key_kp_end;
  case XKB_KEY_KP_Insert:
    *count = ALEN(key_kp_insert);
    return key_kp_insert;
  case XKB_KEY_KP_Delete:
    *count = ALEN(key_kp_delete);
    return key_kp_delete;
  case XKB_KEY_KP_Page_Up:
    *count = ALEN(key_kp_pageup);
    return key_kp_pageup;
  case XKB_KEY_KP_Page_Down:
    *count = ALEN(key_kp_pagedown);
    return key_kp_pagedown;
  case XKB_KEY_KP_Enter:
    *count = ALEN(key_kp_enter);
    return key_kp_enter;
  case XKB_KEY_KP_Divide:
    *count = ALEN(key_kp_divide);
    return key_kp_divide;
  case XKB_KEY_KP_Multiply:
    *count = ALEN(key_kp_multiply);
    return key_kp_multiply;
  case XKB_KEY_KP_Subtract:
    *count = ALEN(key_kp_subtract);
    return key_kp_subtract;
  case XKB_KEY_KP_Add:
    *count = ALEN(key_kp_add);
    return key_kp_add;
  case XKB_KEY_KP_Separator:
    *count = ALEN(key_kp_separator);
    return key_kp_separator;
  case XKB_KEY_KP_Decimal:
    *count = ALEN(key_kp_decimal);
    return key_kp_decimal;
  case XKB_KEY_KP_0:
    *count = ALEN(key_kp_0);
    return key_kp_0;
  case XKB_KEY_KP_1:
    *count = ALEN(key_kp_1);
    return key_kp_1;
  case XKB_KEY_KP_2:
    *count = ALEN(key_kp_2);
    return key_kp_2;
  case XKB_KEY_KP_3:
    *count = ALEN(key_kp_3);
    return key_kp_3;
  case XKB_KEY_KP_4:
    *count = ALEN(key_kp_4);
    return key_kp_4;
  case XKB_KEY_KP_5:
    *count = ALEN(key_kp_5);
    return key_kp_5;
  case XKB_KEY_KP_6:
    *count = ALEN(key_kp_6);
    return key_kp_6;
  case XKB_KEY_KP_7:
    *count = ALEN(key_kp_7);
    return key_kp_7;
  case XKB_KEY_KP_8:
    *count = ALEN(key_kp_8);
    return key_kp_8;
  case XKB_KEY_KP_9:
    *count = ALEN(key_kp_9);
    return key_kp_9;
  }

  return NULL;
}

static const struct key_data *
keymap_lookup(struct terminal *term, xkb_keysym_t sym, enum modifier mods) {
  size_t count;
  const struct key_data *info = keymap_data_for_sym(sym, &count);

  if (info == NULL)
    return NULL;

  const enum cursor_keys cursor_keys_mode = term->cursor_keys_mode;
  const enum keypad_keys keypad_keys_mode =
      term->num_lock_modifier ? KEYPAD_NUMERICAL : term->keypad_keys_mode;

  LOG_DBG("keypad mode: %d", keypad_keys_mode);

  for (size_t j = 0; j < count; j++) {
    enum modifier modifiers = info[j].modifiers;

    if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE1) {
      if (term->modify_other_keys_2)
        continue;
      modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE1;
    }
    if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE2) {
      if (!term->modify_other_keys_2)
        continue;
      modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE2;
    }

    if (modifiers != MOD_ANY && modifiers != mods)
      continue;

    if (info[j].cursor_keys_mode != CURSOR_KEYS_DONTCARE &&
        info[j].cursor_keys_mode != cursor_keys_mode)
      continue;

    if (info[j].keypad_keys_mode != KEYPAD_DONTCARE &&
        info[j].keypad_keys_mode != keypad_keys_mode)
      continue;

    return &info[j];
  }

  return NULL;
}

UNITTEST {
  struct terminal term = {
      .num_lock_modifier = false,
      .keypad_keys_mode = KEYPAD_NUMERICAL,
      .cursor_keys_mode = CURSOR_KEYS_NORMAL,
  };

  const struct key_data *info =
      keymap_lookup(&term, XKB_KEY_ISO_Left_Tab, MOD_SHIFT | MOD_CTRL);
  xassert(info != NULL);
  xassert(streq(info->seq, "\033[27;6;9~"));
}

UNITTEST {
  struct terminal term = {
      .modify_other_keys_2 = false,
  };

  const struct key_data *info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT);
  xassert(info != NULL);
  xassert(streq(info->seq, "\033\r"));

  term.modify_other_keys_2 = true;
  info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT);
  xassert(info != NULL);
  xassert(streq(info->seq, "\033[27;3;13~"));
}

void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective,
                           xkb_mod_mask_t *consumed, uint32_t key,
                           bool filter_locked) {
  if (unlikely(seat->kbd.xkb_state == NULL)) {
    if (effective != NULL)
      *effective = 0;
    if (consumed != NULL)
      *consumed = 0;
  }

  else {
    const xkb_mod_mask_t locked =
        xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED);

    if (effective != NULL) {
      *effective = xkb_state_serialize_mods(seat->kbd.xkb_state,
                                            XKB_STATE_MODS_EFFECTIVE);

      if (filter_locked)
        *effective &= ~locked;
    }

    if (consumed != NULL) {
      *consumed = xkb_state_key_get_consumed_mods2(seat->kbd.xkb_state, key,
                                                   XKB_CONSUMED_MODE_XKB);

      if (filter_locked)
        *consumed &= ~locked;
    }
  }
}

struct kbd_ctx {
  xkb_layout_index_t layout;
  xkb_keycode_t key;
  xkb_keysym_t sym;

  struct {
    const xkb_keysym_t *syms;
    size_t count;
  } level0_syms;

  xkb_mod_mask_t mods;
  xkb_mod_mask_t consumed;

  struct {
    const uint8_t *buf;
    size_t count;
  } utf8;
  uint32_t *utf32;

  enum xkb_compose_status compose_status;
  enum wl_keyboard_key_state key_state;
};

static bool legacy_kbd_protocol(struct seat *seat, struct terminal *term,
                                const struct kbd_ctx *ctx) {
  if (ctx->key_state != WL_KEYBOARD_KEY_STATE_PRESSED)
    return false;
  if (ctx->compose_status == XKB_COMPOSE_COMPOSING)
    return false;

  enum modifier keymap_mods = MOD_NONE;
  keymap_mods |= seat->kbd.shift ? MOD_SHIFT : MOD_NONE;
  keymap_mods |= seat->kbd.alt ? MOD_ALT : MOD_NONE;
  keymap_mods |= seat->kbd.ctrl ? MOD_CTRL : MOD_NONE;
  keymap_mods |= seat->kbd.super ? MOD_META : MOD_NONE;

  const xkb_keysym_t sym = ctx->sym;
  const size_t count = ctx->utf8.count;
  const uint8_t *const utf8 = ctx->utf8.buf;

  const struct key_data *keymap = keymap_lookup(term, sym, keymap_mods);
  if (keymap != NULL) {
    term_to_slave(term, keymap->seq, strlen(keymap->seq));
    return true;
  }

  if (count == 0)
    return false;

#define is_control_key(x) ((x) >= 0x40 && (x) <= 0x7f)
#define IS_CTRL(x) ((x) < 0x20 || ((x) >= 0x7f && (x) <= 0x9f))

  // LOG_DBG("term->modify_other_keys=%d, count=%zu, is_ctrl=%d (utf8=0x%02x),
  // sym=%d", term->modify_other_keys_2, count, IS_CTRL(utf8[0]), utf8[0], sym);

  bool ctrl_is_in_effect = (keymap_mods & MOD_CTRL) != 0;
  bool ctrl_seq = is_control_key(sym) || (count == 1 && IS_CTRL(utf8[0]));

  bool modify_other_keys2_in_effect = false;

  if (term->modify_other_keys_2) {
    /*
     * Try to mimic XTerm's behavior, when holding shift:
     *
     *   - if other modifiers are pressed (e.g. Alt), emit a CSI escape
     *   - upper-case symbols A-Z are encoded as an CSI escape
     *   - other upper-case symbols (e.g 'Ö') or emitted as is
     *   - non-upper cased symbols are _mostly_ emitted as is (foot
     *     always emits as is)
     *
     * Examples (assuming Swedish layout):
     *   - Shift-a ('A') emits a CSI
     *   - Shift-, (';') emits ';'
     *   - Shift-Alt-, (Alt-;) emits a CSI
     *   - Shift-ö ('Ö') emits 'Ö'
     */

    /* Any modifiers, besides shift active? */
    const xkb_mod_mask_t shift_mask = 1 << seat->kbd.mod_shift;
    if ((ctx->mods & ~shift_mask & seat->kbd.legacy_significant) != 0)
      modify_other_keys2_in_effect = true;

    else {
      const xkb_layout_index_t layout_idx =
          xkb_state_key_get_layout(seat->kbd.xkb_state, ctx->key);

      /*
       * Get pressed key's base symbol.
       *   - for 'A' (shift-a), that's 'a'
       *   - for ';' (shift-,), that's ','
       */
      const xkb_keysym_t *base_syms = NULL;
      size_t base_count = xkb_keymap_key_get_syms_by_level(
          seat->kbd.xkb_keymap, ctx->key, layout_idx, 0, &base_syms);

      /* Check if base symbol(s) is a-z. If so, emit CSI */
      const xkb_keysym_t lower_cased_sym = xkb_keysym_to_lower(ctx->sym);
      for (size_t i = 0; i < base_count; i++) {
        const xkb_keysym_t s = base_syms[i];
        if (lower_cased_sym == s && s >= XKB_KEY_a && s <= XKB_KEY_z) {
          modify_other_keys2_in_effect = true;
          break;
        }
      }
    }
  }

  if (keymap_mods != MOD_NONE &&
      (modify_other_keys2_in_effect || (ctrl_is_in_effect && !ctrl_seq))) {
    static const int mod_param_map[32] = {
        [MOD_SHIFT] = 2,
        [MOD_ALT] = 3,
        [MOD_SHIFT | MOD_ALT] = 4,
        [MOD_CTRL] = 5,
        [MOD_SHIFT | MOD_CTRL] = 6,
        [MOD_ALT | MOD_CTRL] = 7,
        [MOD_SHIFT | MOD_ALT | MOD_CTRL] = 8,
        [MOD_META] = 9,
        [MOD_META | MOD_SHIFT] = 10,
        [MOD_META | MOD_ALT] = 11,
        [MOD_META | MOD_SHIFT | MOD_ALT] = 12,
        [MOD_META | MOD_CTRL] = 13,
        [MOD_META | MOD_SHIFT | MOD_CTRL] = 14,
        [MOD_META | MOD_ALT | MOD_CTRL] = 15,
        [MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL] = 16,
    };

    xassert(keymap_mods < ALEN(mod_param_map));
    int modify_param = mod_param_map[keymap_mods];
    xassert(modify_param != 0);

    char reply[32];
    size_t n =
        xsnprintf(reply, sizeof(reply), "\x1b[27;%d;%d~", modify_param, sym);
    term_to_slave(term, reply, n);
  }

  else if (keymap_mods & MOD_ALT) {
    /*
     * When the alt modifier is pressed, we do one out of three things:
     *
     *  1. we prefix the output bytes with ESC
     *  2. we set the 8:th bit in the output byte
     *  3. we ignore the alt modifier
     *
     * #1 is configured with \E[?1036, and is on by default
     *
     * If #1 has been disabled, we use #2, *if* it's a single byte
     * we're emitting. Since this is a UTF-8 terminal, we then
     * UTF8-encode the 8-bit character. #2 is configured with
     * \E[?1034, and is on by default.
     *
     * Lastly, if both #1 and #2 have been disabled, the alt
     * modifier is ignored.
     */
    if (term->meta.esc_prefix) {
      term_to_slave(term, "\x1b", 1);
      term_to_slave(term, utf8, count);
    }

    else if (term->meta.eight_bit && count == 1) {
      const char32_t wc = 0x80 | utf8[0];

      char utf8_meta[MB_CUR_MAX];
      size_t chars = c32rtomb(utf8_meta, wc, &(mbstate_t){0});

      if (chars != (size_t)-1)
        term_to_slave(term, utf8_meta, chars);
      else
        term_to_slave(term, utf8, count);
    }

    else {
      /* Alt ignored */
      term_to_slave(term, utf8, count);
    }
  } else
    term_to_slave(term, utf8, count);

  return true;
}

UNITTEST {
  /* Verify the kitty keymap is sorted */
  xkb_keysym_t last = 0;
  for (size_t i = 0; i < ALEN(kitty_keymap); i++) {
    const struct kitty_key_data *e = &kitty_keymap[i];
    xassert(e->sym > last);
    last = e->sym;
  }
}

static int kitty_search(const void *_key, const void *_e) {
  const xkb_keysym_t *key = _key;
  const struct kitty_key_data *e = _e;
  return *key - e->sym;
}

static bool kitty_kbd_protocol(struct seat *seat, struct terminal *term,
                               const struct kbd_ctx *ctx) {
  const bool repeating = seat->kbd.repeat.dont_re_repeat;
  const bool pressed =
      ctx->key_state == WL_KEYBOARD_KEY_STATE_PRESSED && !repeating;
  const bool released = ctx->key_state == WL_KEYBOARD_KEY_STATE_RELEASED;
  const bool composing = ctx->compose_status == XKB_COMPOSE_COMPOSING;
  const bool composed = ctx->compose_status == XKB_COMPOSE_COMPOSED;

  const enum kitty_kbd_flags flags =
      term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx];

  const bool disambiguate = flags & KITTY_KBD_DISAMBIGUATE;
  const bool report_events = flags & KITTY_KBD_REPORT_EVENT;
  const bool report_alternate = flags & KITTY_KBD_REPORT_ALTERNATE;
  const bool report_all_as_escapes = flags & KITTY_KBD_REPORT_ALL;

  if (!report_events && released)
    return false;

  /* TODO: should we even bother with this, or just say it's not supported? */
  if (!disambiguate && !report_all_as_escapes && pressed)
    return legacy_kbd_protocol(seat, term, ctx);

  const xkb_keysym_t sym = ctx->sym;
  const uint32_t *utf32 = ctx->utf32;
  const uint8_t *const utf8 = ctx->utf8.buf;
  const size_t count = ctx->utf8.count;

  /* Lookup sym in the pre-defined keysym table */
  const struct kitty_key_data *info =
      bsearch(&sym, kitty_keymap, ALEN(kitty_keymap), sizeof(kitty_keymap[0]),
              &kitty_search);
  xassert(info == NULL || info->sym == sym);

  xkb_mod_mask_t mods = 0;
  xkb_mod_mask_t locked = 0;
  xkb_mod_mask_t consumed = ctx->consumed;

  if (info != NULL && info->is_modifier) {
    /*
     * Special-case modifier keys.
     *
     * Normally, the "current" XKB state reflects the state
     * *before* the current key event. In other words, the
     * modifiers for key events that affect the modifier state
     * (e.g. one of the control keys, or shift keys etc) does
     * *not* include the key itself.
     *
     * Put another way, if you press "control", the modifier set
     * is empty in the key press event, but contains "ctrl" in the
     * release event.
     *
     * The kitty protocol mandates the modifier list contain the
     * key itself, in *both* the press and release event.
     *
     * We handle this by updating the XKB state to *include* the
     * current key, retrieve the set of modifiers (including the
     * set of consumed modifiers), and then revert the XKB update.
     */
    xkb_state_update_key(seat->kbd.xkb_state, ctx->key,
                         pressed ? XKB_KEY_DOWN : XKB_KEY_UP);

    get_current_modifiers(seat, &mods, NULL, 0, false);

    locked =
        xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED);
    consumed = xkb_state_key_get_consumed_mods2(seat->kbd.xkb_state, ctx->key,
                                                XKB_CONSUMED_MODE_XKB);

#if 0
        /*
         * TODO: according to the XKB docs, state updates should
         * always be in pairs: each press should be followed by a
         * release. However, doing this just breaks the xkb state.
         *
         * *Not* pairing the above press/release with a corresponding
         * release/press appears to do exactly what we want.
         */
        xkb_state_update_key(
            seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_UP : XKB_KEY_DOWN);
#endif
  } else {
    /* Same as ctx->mods, but *without* filtering locked modifiers */
    get_current_modifiers(seat, &mods, NULL, 0, false);
    locked =
        xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED);
  }

  mods &= seat->kbd.kitty_significant;
  consumed &= seat->kbd.kitty_significant;

  /*
   * A note on locked modifiers; they *are* a part of the protocol,
   * and *should* be included in the modifier set reported in the
   * key event.
   *
   * However, *only* if the key would result in a CSIu *without* the
   * locked modifier being enabled
   *
   * Translated: if *another* modifier is active, or if
   * report-all-keys-as-escapes is enabled, then we include the
   * locked modifier in the key event.
   *
   * But, if the key event would result in plain text output without
   * the locked modifier, then we "ignore" the locked modifier and
   * emit plain text anyway.
   */

  bool is_text =
      count > 0 && utf32 != NULL && (mods & ~locked & ~consumed) == 0;
  for (size_t i = 0; utf32[i] != U'\0'; i++) {
    if (!isc32print(utf32[i])) {
      is_text = false;
      break;
    }
  }

  const bool report_associated_text =
      (flags & KITTY_KBD_REPORT_ASSOCIATED) && is_text && !released;

  if (composing) {
    /* We never emit anything while composing, *except* modifiers
     * (and only in report-all-keys-as-escape-codes mode) */
    if (info != NULL && info->is_modifier)
      goto emit_escapes;

    return false;
  }

  if (report_all_as_escapes)
    goto emit_escapes;

  if ((mods & ~locked & ~consumed) == 0) {
    switch (sym) {
    case XKB_KEY_Return:
      if (!released)
        term_to_slave(term, "\r", 1);
      return true;

    case XKB_KEY_BackSpace:
      if (!released)
        term_to_slave(term, "\x7f", 1);
      return true;

    case XKB_KEY_Tab:
      if (!released)
        term_to_slave(term, "\t", 1);
      return true;
    }
  }

  /* Plain-text without modifiers, or commposed text, is emitted as-is */
  if (is_text && !released) {
    term_to_slave(term, utf8, count);
    return true;
  }

emit_escapes:;
  unsigned int encoded_mods = 0;
  if (seat->kbd.mod_shift != XKB_MOD_INVALID)
    encoded_mods |= mods & (1 << seat->kbd.mod_shift) ? (1 << 0) : 0;
  if (seat->kbd.mod_alt != XKB_MOD_INVALID)
    encoded_mods |= mods & (1 << seat->kbd.mod_alt) ? (1 << 1) : 0;
  if (seat->kbd.mod_ctrl != XKB_MOD_INVALID)
    encoded_mods |= mods & (1 << seat->kbd.mod_ctrl) ? (1 << 2) : 0;
  if (seat->kbd.mod_super != XKB_MOD_INVALID)
    encoded_mods |= mods & (1 << seat->kbd.mod_super) ? (1 << 3) : 0;
  if (seat->kbd.mod_caps != XKB_MOD_INVALID)
    encoded_mods |= mods & (1 << seat->kbd.mod_caps) ? (1 << 6) : 0;
  if (seat->kbd.mod_num != XKB_MOD_INVALID)
    encoded_mods |= mods & (1 << seat->kbd.mod_num) ? (1 << 7) : 0;
  encoded_mods++;

  /*
   * Figure out the main, alternate and base key codes.
   *
   * The main key is the unshifted version of the generated symbol,
   * the alternate key is the shifted version, and base is the
   * (unshifted) key assuming the default layout.
   *
   * For example, the user presses shift+a, then:
   *   - unshifted = 'a'
   *   - shifted = 'A'
   *   - base = 'a'
   *
   * Base will in many cases be the same as the unshifted key, but
   * may differ if the active keyboard layout is non-ASCII (examples
   * would be russian, or alternative layouts like neo etc).
   *
   * The shifted key is what we get from XKB, i.e. the resulting key
   * from all active modifiers, plus the pressed key.
   */
  int unshifted = -1, shifted = -1, base = -1;
  char final;

  if (info != NULL) {
    /* Use code from lookup table (cursor keys, enter, tab etc)*/
    if (!info->is_modifier || report_all_as_escapes) {
      shifted = info->key;
      final = info->final;
    }
  } else {
    /* Use keysym (typically its Unicode codepoint value) */

    if (composed)
      shifted = utf32[0]; /* TODO: what if there are multiple codepoints? */
    else
      shifted = xkb_keysym_to_utf32(sym);

    final = 'u';
  }

  if (shifted <= 0)
    return false;

  /* Base layout key. I.e the symbol the pressed key produces in
   * the base/default layout (layout idx 0) */
  const xkb_keysym_t *base_syms;
  int base_sym_count = xkb_keymap_key_get_syms_by_level(
      seat->kbd.xkb_keymap, ctx->key, 0, 0, &base_syms);

  if (base_sym_count > 0)
    base = xkb_keysym_to_utf32(base_syms[0]);

  /*
   * If the keysym is shifted, use its unshifted codepoint
   * instead. In other words, ctrl+a and ctrl+shift+a should both
   * use the same value for 'key' (97 - i.a. 'a').
   *
   * However, don't do this if a non-significant modifier was used
   * to generate the symbol. This is needed since we cannot encode
   * non-significant modifiers, and thus the "extra" modifier(s)
   * would get lost.
   *
   * Example:
   *
   * the Swedish layout has '2', QUOTATION MARK ("double quote"),
   * '@', and '²' on the same key. '2' is the base symbol.
   *
   * Shift+2 results in QUOTATION MARK
   * AltGr+2 results in '@'
   * AltGr+Shift+2 results in '²'
   *
   * The kitty kbd protocol can't encode AltGr. So, if we always
   * used the base symbol ('2'), Alt+Shift+2 would result in the
   * same escape sequence as AltGr+Alt+Shift+2.
   *
   * (yes, this matches what kitty does, as of 0.23.1)
   */
  const bool use_level0_sym = (ctx->mods & ~seat->kbd.kitty_significant) == 0 &&
                              ctx->level0_syms.count > 0;

  unshifted =
      use_level0_sym ? xkb_keysym_to_utf32(ctx->level0_syms.syms[0]) : 0;

  xassert(encoded_mods >= 1);

  char event[4];
  if (report_events /*&& !pressed*/) {
    /* Note: this deviates slightly from Kitty, which omits the
     * ":1" subparameter for key press events */
    event[0] = ':';
    event[1] = '0' + (pressed ? 1 : repeating ? 2 : 3);
    event[2] = '\0';
  } else
    event[0] = '\0';

  char buf[128], *p = buf;
  size_t left = sizeof(buf);
  size_t bytes;

  const int key =
      unshifted > 0 && isc32print(unshifted) && !composed ? unshifted : shifted;
  const int alternate = shifted;

  if (final == 'u' || final == '~') {
    bytes = snprintf(p, left, "\x1b[%u", key);
    p += bytes;
    left -= bytes;

    if (report_alternate) {
      bool emit_alternate = alternate > 0 && alternate != key;
      bool emit_base =
          base > 0 && base != key && base != alternate && isc32print(base);

      if (emit_alternate) {
        bytes = snprintf(p, left, ":%u", alternate);
        p += bytes;
        left -= bytes;
      }

      if (emit_base) {
        bytes = snprintf(p, left, "%s:%u", !emit_alternate ? ":" : "", base);
        p += bytes;
        left -= bytes;
      }
    }

    bool emit_mods = encoded_mods > 1 || event[0] != '\0';

    if (emit_mods) {
      bytes = snprintf(p, left, ";%u%s", encoded_mods, event);
      p += bytes;
      left -= bytes;
    }

    if (report_associated_text) {
      bytes = snprintf(p, left, "%s;%u", !emit_mods ? ";" : "", utf32[0]);
      p += bytes;
      left -= bytes;

      /* Additional text codepoints */
      if (utf32[0] != U'\0') {
        for (size_t i = 1; utf32[i] != U'\0'; i++) {
          bytes = snprintf(p, left, ":%u", utf32[i]);
          p += bytes;
          left -= bytes;
        }
      }
    }

    bytes = snprintf(p, left, "%c", final);
    p += bytes;
    left -= bytes;
  } else {
    if (encoded_mods > 1 || event[0] != '\0') {
      bytes = snprintf(p, left, "\x1b[1;%u%s%c", encoded_mods, event, final);
      p += bytes;
      left -= bytes;
    } else {
      bytes = snprintf(p, left, "\x1b[%c", final);
      p += bytes;
      left -= bytes;
    }
  }

  return term_to_slave(term, buf, sizeof(buf) - left);
}

/* Copied from libxkbcommon (internal function) */
static bool keysym_is_modifier(xkb_keysym_t keysym) {
  return (keysym >= XKB_KEY_Shift_L && keysym <= XKB_KEY_Hyper_R) ||
         /* libX11 only goes up to XKB_KEY_ISO_Level5_Lock. */
         (keysym >= XKB_KEY_ISO_Lock &&
          keysym <= XKB_KEY_ISO_Last_Group_Lock) ||
         keysym == XKB_KEY_Mode_switch || keysym == XKB_KEY_Num_Lock;
}

#if defined(_DEBUG)
static void modifier_string(xkb_mod_mask_t mods, size_t sz,
                            char mod_str[static sz], const struct seat *seat) {
  if (sz == 0)
    return;

  mod_str[0] = '\0';

  for (size_t i = 0; i < sizeof(xkb_mod_mask_t) * 8; i++) {
    if (!(mods & (1u << i)))
      continue;

    strcat(mod_str, xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i));
    strcat(mod_str, "+");
  }

  if (mod_str[0] != '\0') {
    /* Strip the last '+' */
    mod_str[strlen(mod_str) - 1] = '\0';
  }

  if (mod_str[0] == '\0') {
    strcpy(mod_str, "<none>");
  }
}
#endif

static void key_press_release(struct seat *seat, struct terminal *term,
                              uint32_t serial, uint32_t key, uint32_t state) {
  xassert(serial != 0);

  seat->kbd.serial = serial;
  if (seat->kbd.xkb == NULL || seat->kbd.xkb_keymap == NULL ||
      seat->kbd.xkb_state == NULL) {
    return;
  }

  const bool pressed = state == WL_KEYBOARD_KEY_STATE_PRESSED;
  // const bool repeated = pressed && seat->kbd.repeat.dont_re_repeat;
  const bool released = state == WL_KEYBOARD_KEY_STATE_RELEASED;

  if (released)
    stop_repeater(seat, key);

  if (pressed)
    seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1;

  bool should_repeat =
      pressed && xkb_keymap_key_repeats(seat->kbd.xkb_keymap, key);

  xkb_keysym_t sym = xkb_state_key_get_one_sym(seat->kbd.xkb_state, key);

  if (pressed && term->conf->mouse.hide_when_typing &&
      !keysym_is_modifier(sym)) {
    seat->pointer.hidden = true;
    term_xcursor_update_for_seat(term, seat);
  }

  enum xkb_compose_status compose_status = XKB_COMPOSE_NOTHING;

  if (seat->kbd.xkb_compose_state != NULL) {
    if (pressed)
      xkb_compose_state_feed(seat->kbd.xkb_compose_state, sym);
    compose_status = xkb_compose_state_get_status(seat->kbd.xkb_compose_state);
  }

  const bool composed = compose_status == XKB_COMPOSE_COMPOSED;

  xkb_mod_mask_t mods, consumed;
  get_current_modifiers(seat, &mods, &consumed, key, true);

  xkb_layout_index_t layout_idx =
      xkb_state_key_get_layout(seat->kbd.xkb_state, key);

  const xkb_keysym_t *raw_syms = NULL;
  size_t raw_count = xkb_keymap_key_get_syms_by_level(seat->kbd.xkb_keymap, key,
                                                      layout_idx, 0, &raw_syms);

  const struct key_binding_set *bindings =
      key_binding_for(seat->wayl->key_binding_manager, term->conf, seat);
  xassert(bindings != NULL);

  if (term->unicode_mode.active) {
    if (pressed)
      unicode_mode_input(seat, term, sym);
    return;
  }

  else if (term->is_searching) {
    if (pressed) {
      if (should_repeat)
        start_repeater(seat, key);

      search_input(seat, term, bindings, key, sym, mods, consumed, raw_syms,
                   raw_count, serial);
    }
    return;
  }

  else if (urls_mode_is_active(term)) {
    if (pressed) {
      if (should_repeat)
        start_repeater(seat, key);

      urls_input(seat, term, bindings, key, sym, mods, consumed, raw_syms,
                 raw_count, serial);
    }
    return;
  }

  /* Tab overview: intercept navigation + activation while open. The
   * configured tab-overview binding still fires (handled below), so the
   * user can toggle it back off with the same key. */
  if (term->window != NULL && term->window->tab_overview_state.target_open &&
      pressed) {
    struct wl_window *win = term->window;
    __typeof__(win->tab_overview_state) *st = &win->tab_overview_state;
    const size_t n = win->tab_count;

    bool handled = false;
    switch (sym) {
    case XKB_KEY_Escape:
      tab_overview_toggle(win);
      handled = true;
      break;
    case XKB_KEY_Return:
    case XKB_KEY_KP_Enter:
      if (st->sel_idx < n && st->sel_idx != win->active_tab)
        term_tab_switch(win, st->sel_idx);
      tab_overview_close_instant(win);
      handled = true;
      break;
    case XKB_KEY_Left:
      if (n > 0)
        st->sel_idx = (st->sel_idx == 0) ? n - 1 : st->sel_idx - 1;
      render_refresh(term);
      handled = true;
      break;
    case XKB_KEY_Right:
      if (n > 0)
        st->sel_idx = (st->sel_idx + 1) % n;
      render_refresh(term);
      handled = true;
      break;
    case XKB_KEY_Up:
    case XKB_KEY_Down: {
      /* Step by one row in the cached card layout */
      if (st->card_count >= 2) {
        int cur_y = st->cards[st->sel_idx].y;
        int cur_x = st->cards[st->sel_idx].x;
        int best = -1;
        int best_d = INT_MAX;
        for (size_t i = 0; i < st->card_count; i++) {
          if (i == st->sel_idx)
            continue;
          int dy = st->cards[i].y - cur_y;
          if (sym == XKB_KEY_Up && dy >= 0)
            continue;
          if (sym == XKB_KEY_Down && dy <= 0)
            continue;
          int dx = st->cards[i].x - cur_x;
          int d = dx * dx + dy * dy;
          if (d < best_d) {
            best_d = d;
            best = (int)i;
          }
        }
        if (best >= 0)
          st->sel_idx = (size_t)best;
      }
      render_refresh(term);
      handled = true;
      break;
    }
    default:
      if (sym >= XKB_KEY_1 && sym <= XKB_KEY_9) {
        size_t idx = (size_t)(sym - XKB_KEY_1);
        if (idx < n) {
          if (idx != win->active_tab)
            term_tab_switch(win, idx);
          tab_overview_close_instant(win);
        }
        handled = true;
      }
      break;
    }

    if (handled) {
      seat->kbd.last_shortcut_sym = sym;
      /* Don't try repeat; navigation is one-shot */
      return;
    }

    /* Fall through to binding match so the user's tab-overview key
     * can still toggle off, but swallow everything else. */
  }

#if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG
  char sym_name[100];
  xkb_keysym_get_name(sym, sym_name, sizeof(sym_name));

  char active_mods_str[256] = {0};
  char consumed_mods_str[256] = {0};
  char locked_mods_str[256] = {0};

  const xkb_mod_mask_t locked =
      xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED);

  modifier_string(mods, sizeof(active_mods_str), active_mods_str, seat);
  modifier_string(consumed, sizeof(consumed_mods_str), consumed_mods_str, seat);
  modifier_string(locked, sizeof(locked_mods_str), locked_mods_str, seat);

  LOG_DBG("%s: %s (%u/0x%x), seat=%s, term=%p, serial=%u, "
          "mods=%s (0x%08x), consumed=%s (0x%08x), locked=%s (0x%08x), "
          "repeats=%d",
          pressed ? "pressed" : "released", sym_name, sym, sym, seat->name,
          (void *)term, serial, active_mods_str, mods, consumed_mods_str,
          consumed, locked_mods_str, locked, should_repeat);
#endif

  /*
   * User configurable bindings
   */
  if (pressed) {
    /* Match untranslated symbols */
    tll_foreach(bindings->key, it) {
      const struct key_binding *bind = &it->item;

      if (bind->mods != mods || bind->mods == 0)
        continue;

      for (size_t i = 0; i < raw_count; i++) {
        if (bind->k.sym == raw_syms[i] &&
            execute_binding(seat, term, bind, serial, 1)) {
          seat->kbd.last_shortcut_sym = sym;
          goto maybe_repeat;
        }
      }
    }

    /* Match translated symbol */
    tll_foreach(bindings->key, it) {
      const struct key_binding *bind = &it->item;

      if (bind->k.sym == sym && bind->mods == (mods & ~consumed) &&
          execute_binding(seat, term, bind, serial, 1)) {
        seat->kbd.last_shortcut_sym = sym;
        goto maybe_repeat;
      }
    }

    /* Match raw key code */
    tll_foreach(bindings->key, it) {
      const struct key_binding *bind = &it->item;

      if (bind->mods != mods || bind->mods == 0)
        continue;

      tll_foreach(bind->k.key_codes, code) {
        if (code->item == key && execute_binding(seat, term, bind, serial, 1)) {
          seat->kbd.last_shortcut_sym = sym;
          goto maybe_repeat;
        }
      }
    }
  }

  /* Overview is modal: don't pass un-bound keys through to the PTY */
  if (term->window != NULL && (term->window->tab_overview_state.target_open ||
                               term->window->tab_overview_state.visible)) {
    return;
  }

  if (released && seat->kbd.last_shortcut_sym == sym) {
    /*
     * Don't process a release event, if it corresponds to a
     * triggered shortcut.
     *
     * 1. If we consumed a key (press) event, we shouldn't emit an
     *    escape for its release event.
     * 2. Ignoring the incorrectness of doing so; this also caused
     *    us to reset the viewport.
     *
     * Background: if the kitty keyboard protocol was enabled,
     * then the viewport was instantly reset to the bottom, after
     * scrolling up.
     */
    // seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1;
    goto maybe_repeat;
  }

  /*
   * Keys generating escape sequences
   */

  /*
   * Compose, and maybe emit "normal" character
   */

  xassert(seat->kbd.xkb_compose_state != NULL || !composed);

  if (compose_status == XKB_COMPOSE_CANCELLED)
    goto maybe_repeat;

  int count =
      composed
          ? xkb_compose_state_get_utf8(seat->kbd.xkb_compose_state, NULL, 0)
          : xkb_state_key_get_utf8(seat->kbd.xkb_state, key, NULL, 0);

  /* Buffer for translated key. Use a static buffer in most cases,
   * and use a malloc:ed buffer when necessary */
  uint8_t buf[32];
  uint8_t *utf8 = count < sizeof(buf) ? buf : xmalloc(count + 1);
  uint32_t *utf32 = NULL;

  if (composed) {
    xkb_compose_state_get_utf8(seat->kbd.xkb_compose_state, (char *)utf8,
                               count + 1);

    if (count > 0)
      utf32 = ambstoc32((const char *)utf8);
  } else {
    xkb_state_key_get_utf8(seat->kbd.xkb_state, key, (char *)utf8, count + 1);

    utf32 = xcalloc(2, sizeof(utf32[0]));
    utf32[0] = xkb_state_key_get_utf32(seat->kbd.xkb_state, key);
  }

  struct kbd_ctx ctx = {
      .layout = layout_idx,
      .key = key,
      .sym = sym,
      .level0_syms =
          {
              .syms = raw_syms,
              .count = raw_count,
          },
      .mods = mods,
      .consumed = consumed,
      .utf8 =
          {
              .buf = utf8,
              .count = count,
          },
      .utf32 = utf32,
      .compose_status = compose_status,
      .key_state = state,
  };

  bool handled = term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx] != 0
                     ? kitty_kbd_protocol(seat, term, &ctx)
                     : legacy_kbd_protocol(seat, term, &ctx);

  if (composed && released)
    xkb_compose_state_reset(seat->kbd.xkb_compose_state);

  if (utf8 != buf)
    free(utf8);

  if (handled && !keysym_is_modifier(sym)) {
    term_reset_view(term);
    selection_cancel(term);
  }

  free(utf32);

maybe_repeat:
  clock_gettime(term->wl->presentation_clock_id, &term->render.input_time);

  if (should_repeat)
    start_repeater(seat, key);
}

static void keyboard_key(void *data, struct wl_keyboard *wl_keyboard,
                         uint32_t serial, uint32_t time, uint32_t key,
                         uint32_t state) {
  struct seat *seat = data;
  key_press_release(seat, seat->kbd_focus, serial, key + 8, state);
}

static void keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard,
                               uint32_t serial, uint32_t mods_depressed,
                               uint32_t mods_latched, uint32_t mods_locked,
                               uint32_t group) {
  struct seat *seat = data;

  mods_depressed &= ~seat->kbd.virtual_modifiers;
  mods_latched &= ~seat->kbd.virtual_modifiers;
  mods_locked &= ~seat->kbd.virtual_modifiers;

#if defined(_DEBUG)
  char depressed[256];
  char latched[256];
  char locked[256];

  modifier_string(mods_depressed, sizeof(depressed), depressed, seat);
  modifier_string(mods_latched, sizeof(latched), latched, seat);
  modifier_string(mods_locked, sizeof(locked), locked, seat);

  LOG_DBG(
      "modifiers: depressed=%s (0x%x), latched=%s (0x%x), locked=%s (0x%x), "
      "group=%u",
      depressed, mods_depressed, latched, mods_latched, locked, mods_locked,
      group);
#endif

  if (seat->kbd.xkb_state != NULL) {
    xkb_state_update_mask(seat->kbd.xkb_state, mods_depressed, mods_latched,
                          mods_locked, 0, 0, group);

    /* Update state of modifiers we're interested in for e.g mouse events */
    seat->kbd.shift = seat->kbd.mod_shift != XKB_MOD_INVALID
                          ? xkb_state_mod_index_is_active(
                                seat->kbd.xkb_state, seat->kbd.mod_shift,
                                XKB_STATE_MODS_EFFECTIVE)
                          : false;
    seat->kbd.alt = seat->kbd.mod_alt != XKB_MOD_INVALID
                        ? xkb_state_mod_index_is_active(
                              seat->kbd.xkb_state, seat->kbd.mod_alt,
                              XKB_STATE_MODS_EFFECTIVE)
                        : false;
    seat->kbd.ctrl = seat->kbd.mod_ctrl != XKB_MOD_INVALID
                         ? xkb_state_mod_index_is_active(
                               seat->kbd.xkb_state, seat->kbd.mod_ctrl,
                               XKB_STATE_MODS_EFFECTIVE)
                         : false;
    seat->kbd.super = seat->kbd.mod_super != XKB_MOD_INVALID
                          ? xkb_state_mod_index_is_active(
                                seat->kbd.xkb_state, seat->kbd.mod_super,
                                XKB_STATE_MODS_EFFECTIVE)
                          : false;
  }

  if (seat->kbd_focus && seat->kbd_focus->active_surface == TERM_SURF_GRID)
    term_xcursor_update_for_seat(seat->kbd_focus, seat);
}

UNITTEST {
  int chan[2];
  xassert(pipe2(chan, O_CLOEXEC) == 0);

  xassert(chan[0] >= 0);
  xassert(chan[1] >= 0);

  struct config conf = {0};
  struct grid grid = {0};

  struct terminal term = {
      .conf = &conf,
      .grid = &grid,
      .ptmx = chan[1],
      .selection =
          {
              .coords =
                  {
                      .start = {-1, -1},
                      .end = {-1, -1},
                  },
              .auto_scroll =
                  {
                      .fd = -1,
                  },
          },
  };

  struct key_binding_manager *key_binding_manager = key_binding_manager_new();

  struct wayland wayl = {
      .key_binding_manager = key_binding_manager,
      .terms = tll_init(),
  };

  struct seat seat = {
      .wayl = &wayl,
      .name = "unittest",
  };

  tll_push_back(wayl.terms, &term);
  term.wl = &wayl;

  seat.kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
  xassert(seat.kbd.xkb != NULL);

  grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE;

  /* Swedish keymap */
  {
    seat.kbd.xkb_keymap = xkb_keymap_new_from_names(
        seat.kbd.xkb, &(struct xkb_rule_names){.layout = "se"},
        XKB_KEYMAP_COMPILE_NO_FLAGS);
    if (seat.kbd.xkb_keymap == NULL) {
      /* Skip test */
      goto no_keymap;
    }

    seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap);
    xassert(seat.kbd.xkb_state != NULL);

    seat.kbd.mod_shift =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT);
    seat.kbd.mod_alt =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT);
    seat.kbd.mod_ctrl =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL);
    seat.kbd.mod_super =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO);
    seat.kbd.mod_caps =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS);
    seat.kbd.mod_num =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM);

    /* Significant modifiers in the legacy keyboard protocol */
    seat.kbd.legacy_significant = 0;
    if (seat.kbd.mod_shift != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift;
    if (seat.kbd.mod_alt != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt;
    if (seat.kbd.mod_ctrl != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl;
    if (seat.kbd.mod_super != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super;

    /* Significant modifiers in the kitty keyboard protocol */
    seat.kbd.kitty_significant = seat.kbd.legacy_significant;
    if (seat.kbd.mod_caps != XKB_MOD_INVALID)
      seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps;
    if (seat.kbd.mod_num != XKB_MOD_INVALID)
      seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num;

    key_binding_new_for_seat(key_binding_manager, &seat);
    key_binding_load_keymap(key_binding_manager, &seat);

    {
      xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_ctrl;
      keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0);
      key_press_release(&seat, &term, 1337, KEY_A + 8,
                        WL_KEYBOARD_KEY_STATE_PRESSED);

      char escape[64] = {0};
      ssize_t count = read(chan[0], escape, sizeof(escape));

      /* key: 97 = 'a', alternate: 65 = 'A', base: N/A, mods: 6 = ctrl+shift */
      const char expected_ctrl_shift_a[] = "\033[97:65;6u";
      xassert(count == strlen(expected_ctrl_shift_a));
      xassert(streq(escape, expected_ctrl_shift_a));

      key_press_release(&seat, &term, 1337, KEY_A + 8,
                        WL_KEYBOARD_KEY_STATE_RELEASED);
    }

    {
      xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt;
      keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0);
      key_press_release(&seat, &term, 1337, KEY_2 + 8,
                        WL_KEYBOARD_KEY_STATE_PRESSED);

      char escape[64] = {0};
      ssize_t count = read(chan[0], escape, sizeof(escape));

      /* key;. 50 = '2', alternate: 34 = '"', base: N/A, 4 = alt+shift */
      const char expected_alt_shift_2[] = "\033[50:34;4u";
      xassert(count == strlen(expected_alt_shift_2));
      xassert(streq(escape, expected_alt_shift_2));

      key_press_release(&seat, &term, 1337, KEY_2 + 8,
                        WL_KEYBOARD_KEY_STATE_RELEASED);
    }

    {
      xkb_mod_index_t alt_gr =
          xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, "Mod5");
      xassert(alt_gr != XKB_MOD_INVALID);

      xkb_mod_mask_t mods =
          1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt | 1u << alt_gr;
      keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0);
      key_press_release(&seat, &term, 1337, KEY_2 + 8,
                        WL_KEYBOARD_KEY_STATE_PRESSED);

      char escape[64] = {0};
      ssize_t count = read(chan[0], escape, sizeof(escape));

      /* key; 178 = '²', alternate: N/A, base: 50 = '2', 4 = alt+shift (AltGr
       * not part of the protocol) */
      const char expected_altgr_alt_shift_2[] = "\033[178::50;4u";
      xassert(count == strlen(expected_altgr_alt_shift_2));
      xassert(streq(escape, expected_altgr_alt_shift_2));

      key_press_release(&seat, &term, 1337, KEY_2 + 8,
                        WL_KEYBOARD_KEY_STATE_RELEASED);
    }

    {
      xkb_mod_mask_t mods = 1u << seat.kbd.mod_alt;
      keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0);
      key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8,
                        WL_KEYBOARD_KEY_STATE_PRESSED);

      char escape[64] = {0};
      ssize_t count = read(chan[0], escape, sizeof(escape));

      /* key; 127 = <backspace>, alternate: N/A, base: N/A, 3 = alt */
      const char expected_alt_backspace[] = "\033[127;3u";
      xassert(count == strlen(expected_alt_backspace));
      xassert(streq(escape, expected_alt_backspace));

      key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8,
                        WL_KEYBOARD_KEY_STATE_RELEASED);
    }

    {
      xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl;
      keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0);
      key_press_release(&seat, &term, 1337, KEY_ENTER + 8,
                        WL_KEYBOARD_KEY_STATE_PRESSED);

      char escape[64] = {0};
      ssize_t count = read(chan[0], escape, sizeof(escape));

      /* key; 13 = <enter>, alternate: N/A, base: N/A, 5 = ctrl */
      const char expected_ctrl_enter[] = "\033[13;5u";
      xassert(count == strlen(expected_ctrl_enter));
      xassert(streq(escape, expected_ctrl_enter));

      key_press_release(&seat, &term, 1337, KEY_ENTER + 8,
                        WL_KEYBOARD_KEY_STATE_RELEASED);
    }

    {
      xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl;
      keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0);
      key_press_release(&seat, &term, 1337, KEY_TAB + 8,
                        WL_KEYBOARD_KEY_STATE_PRESSED);

      char escape[64] = {0};
      ssize_t count = read(chan[0], escape, sizeof(escape));

      /* key; 9 = <tab>, alternate: N/A, base: N/A, 5 = ctrl */
      const char expected_ctrl_tab[] = "\033[9;5u";
      xassert(count == strlen(expected_ctrl_tab));
      xassert(streq(escape, expected_ctrl_tab));

      key_press_release(&seat, &term, 1337, KEY_TAB + 8,
                        WL_KEYBOARD_KEY_STATE_RELEASED);
    }

    {
      xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl | 1u << seat.kbd.mod_shift;
      keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0);
      key_press_release(&seat, &term, 1337, KEY_LEFT + 8,
                        WL_KEYBOARD_KEY_STATE_PRESSED);

      char escape[64] = {0};
      ssize_t count = read(chan[0], escape, sizeof(escape));

      const char expected_ctrl_shift_left[] = "\033[1;6D";
      xassert(count == strlen(expected_ctrl_shift_left));
      xassert(streq(escape, expected_ctrl_shift_left));

      key_press_release(&seat, &term, 1337, KEY_LEFT + 8,
                        WL_KEYBOARD_KEY_STATE_RELEASED);
    }
    key_binding_unload_keymap(key_binding_manager, &seat);
    key_binding_remove_seat(key_binding_manager, &seat);

    xkb_state_unref(seat.kbd.xkb_state);
    xkb_keymap_unref(seat.kbd.xkb_keymap);

    seat.kbd.xkb_state = NULL;
    seat.kbd.xkb_keymap = NULL;
  }

  /* de(neo) keymap */
  {
    seat.kbd.xkb_keymap = xkb_keymap_new_from_names(
        seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us,de(neo)"},
        XKB_KEYMAP_COMPILE_NO_FLAGS);

    if (seat.kbd.xkb_keymap == NULL) {
      /* Skip test */
      goto no_keymap;
    }

    seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap);
    xassert(seat.kbd.xkb_state != NULL);

    seat.kbd.mod_shift =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT);
    seat.kbd.mod_alt =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT);
    seat.kbd.mod_ctrl =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL);
    seat.kbd.mod_super =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO);
    seat.kbd.mod_caps =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS);
    seat.kbd.mod_num =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM);

    /* Significant modifiers in the legacy keyboard protocol */
    seat.kbd.legacy_significant = 0;
    if (seat.kbd.mod_shift != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift;
    if (seat.kbd.mod_alt != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt;
    if (seat.kbd.mod_ctrl != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl;
    if (seat.kbd.mod_super != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super;

    /* Significant modifiers in the kitty keyboard protocol */
    seat.kbd.kitty_significant = seat.kbd.legacy_significant;
    if (seat.kbd.mod_caps != XKB_MOD_INVALID)
      seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps;
    if (seat.kbd.mod_num != XKB_MOD_INVALID)
      seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num;

    key_binding_new_for_seat(key_binding_manager, &seat);
    key_binding_load_keymap(key_binding_manager, &seat);

    {
      /*
       * In the de(neo) layout, the Y key generates 'k'. This
       * means we should get a key+alternate that indicates 'k',
       * but a base key that is 'y'.
       */
      xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt;
      keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1);
      key_press_release(&seat, &term, 1337, KEY_Y + 8,
                        WL_KEYBOARD_KEY_STATE_PRESSED);

      char escape[64] = {0};
      ssize_t count = read(chan[0], escape, sizeof(escape));

      /* key: 107 = 'k', alternate: 75 = 'K', base: 121 = 'y', mods: 4 =
       * alt+shift */
      const char expected_alt_shift_y[] = "\033[107:75:121;4u";
      xassert(count == strlen(expected_alt_shift_y));
      xassert(streq(escape, expected_alt_shift_y));

      key_press_release(&seat, &term, 1337, KEY_Y + 8,
                        WL_KEYBOARD_KEY_STATE_RELEASED);
    }

    key_binding_unload_keymap(key_binding_manager, &seat);
    key_binding_remove_seat(key_binding_manager, &seat);

    xkb_state_unref(seat.kbd.xkb_state);
    xkb_keymap_unref(seat.kbd.xkb_keymap);

    seat.kbd.xkb_state = NULL;
    seat.kbd.xkb_keymap = NULL;
  }

  /* us(intl) keymap */
  {
    seat.kbd.xkb_keymap = xkb_keymap_new_from_names(
        seat.kbd.xkb,
        &(struct xkb_rule_names){.layout = "us", .variant = "intl"},
        XKB_KEYMAP_COMPILE_NO_FLAGS);

    if (seat.kbd.xkb_keymap == NULL) {
      /* Skip test */
      goto no_keymap;
    }

    seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap);
    xassert(seat.kbd.xkb_state != NULL);

    seat.kbd.xkb_compose_table = xkb_compose_table_new_from_locale(
        seat.kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS);
    if (seat.kbd.xkb_compose_table == NULL)
      goto no_keymap;

    seat.kbd.xkb_compose_state = xkb_compose_state_new(
        seat.kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS);
    if (seat.kbd.xkb_compose_state == NULL) {
      xkb_compose_table_unref(seat.kbd.xkb_compose_table);
      goto no_keymap;
    }

    seat.kbd.mod_shift =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT);
    seat.kbd.mod_alt =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT);
    seat.kbd.mod_ctrl =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL);
    seat.kbd.mod_super =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO);
    seat.kbd.mod_caps =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS);
    seat.kbd.mod_num =
        xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM);

    /* Significant modifiers in the legacy keyboard protocol */
    seat.kbd.legacy_significant = 0;
    if (seat.kbd.mod_shift != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift;
    if (seat.kbd.mod_alt != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt;
    if (seat.kbd.mod_ctrl != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl;
    if (seat.kbd.mod_super != XKB_MOD_INVALID)
      seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super;

    /* Significant modifiers in the kitty keyboard protocol */
    seat.kbd.kitty_significant = seat.kbd.legacy_significant;
    if (seat.kbd.mod_caps != XKB_MOD_INVALID)
      seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps;
    if (seat.kbd.mod_num != XKB_MOD_INVALID)
      seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num;

    key_binding_new_for_seat(key_binding_manager, &seat);
    key_binding_load_keymap(key_binding_manager, &seat);

    {
      /*
       * Test the compose sequence "shift+', shift+space"
       *
       * Should result in a double quote, but a regression
       * caused it to instead emit a space. See #1987
       *
       * Note: "shift+', space" also results in a double quote,
       * but never regressed to a space.
       */
      grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE;
      xkb_compose_state_reset(seat.kbd.xkb_compose_state);

      xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift;
      keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1);

      key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8,
                        WL_KEYBOARD_KEY_STATE_PRESSED);
      key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8,
                        WL_KEYBOARD_KEY_STATE_RELEASED);

      key_press_release(&seat, &term, 1337, KEY_SPACE + 8,
                        WL_KEYBOARD_KEY_STATE_PRESSED);

      char escape[64] = {0};
      ssize_t count = read(chan[0], escape, sizeof(escape));

      /* key: 34 = '"', alternate: N/A, base: N/A, mods: 2 = shift */
      const char expected_shift_apostrophe[] = "\033[34;2u";
      xassert(count == strlen(expected_shift_apostrophe));
      xassert(streq(escape, expected_shift_apostrophe));

      key_press_release(&seat, &term, 1337, KEY_SPACE + 8,
                        WL_KEYBOARD_KEY_STATE_RELEASED);

      grid.kitty_kbd.flags[0] =
          KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE;
    }

    key_binding_unload_keymap(key_binding_manager, &seat);
    key_binding_remove_seat(key_binding_manager, &seat);

    xkb_compose_state_unref(seat.kbd.xkb_compose_state);
    xkb_compose_table_unref(seat.kbd.xkb_compose_table);

    xkb_state_unref(seat.kbd.xkb_state);
    xkb_keymap_unref(seat.kbd.xkb_keymap);

    seat.kbd.xkb_state = NULL;
    seat.kbd.xkb_keymap = NULL;
  }

no_keymap:
  xkb_context_unref(seat.kbd.xkb);
  key_binding_manager_destroy(key_binding_manager);

  tll_free(wayl.terms);
  close(chan[0]);
  close(chan[1]);
}

static void keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard,
                                 int32_t rate, int32_t delay) {
  struct seat *seat = data;
  LOG_DBG("keyboard repeat: rate=%d, delay=%d", rate, delay);
  seat->kbd.repeat.rate = rate;
  seat->kbd.repeat.delay = delay;
}

const struct wl_keyboard_listener keyboard_listener = {
    .keymap = &keyboard_keymap,
    .enter = &keyboard_enter,
    .leave = &keyboard_leave,
    .key = &keyboard_key,
    .modifiers = &keyboard_modifiers,
    .repeat_info = &keyboard_repeat_info,
};

void input_repeat(struct seat *seat, uint32_t key) {
  /* Should be cleared as soon as we loose focus */
  xassert(seat->kbd_focus != NULL);
  struct terminal *term = seat->kbd_focus;

  key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN);
}

static bool is_top_left(const struct terminal *term, int x, int y) {
  int csd_border_size = term->conf->csd.border_width;
  return ((!term->window->is_constrained_top &&
           !term->window->is_constrained_left) &&
          ((term->active_surface == TERM_SURF_BORDER_LEFT &&
            y < 10 * term->scale) ||
           (term->active_surface == TERM_SURF_BORDER_TOP &&
            x < (10 + csd_border_size) * term->scale)));
}

static bool is_top_right(const struct terminal *term, int x, int y) {
  int csd_border_size = term->conf->csd.border_width;
  return ((!term->window->is_constrained_top &&
           !term->window->is_constrained_right) &&
          ((term->active_surface == TERM_SURF_BORDER_RIGHT &&
            y < 10 * term->scale) ||
           (term->active_surface == TERM_SURF_BORDER_TOP &&
            x > term->width + 1 * csd_border_size * term->scale -
                    10 * term->scale)));
}

static bool is_bottom_left(const struct terminal *term, int x, int y) {
  int csd_title_size = term->conf->csd.title_height;
  int csd_border_size = term->conf->csd.border_width;
  return ((!term->window->is_constrained_bottom &&
           !term->window->is_constrained_left) &&
          ((term->active_surface == TERM_SURF_BORDER_LEFT &&
            y > csd_title_size * term->scale + term->height) ||
           (term->active_surface == TERM_SURF_BORDER_BOTTOM &&
            x < (10 + csd_border_size) * term->scale)));
}

static bool is_bottom_right(const struct terminal *term, int x, int y) {
  int csd_title_size = term->conf->csd.title_height;
  int csd_border_size = term->conf->csd.border_width;
  return ((!term->window->is_constrained_bottom &&
           !term->window->is_constrained_right) &&
          ((term->active_surface == TERM_SURF_BORDER_RIGHT &&
            y > csd_title_size * term->scale + term->height) ||
           (term->active_surface == TERM_SURF_BORDER_BOTTOM &&
            x > term->width + 1 * csd_border_size * term->scale -
                    10 * term->scale)));
}

enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y) {
  if (is_top_left(term, x, y))
    return CURSOR_SHAPE_TOP_LEFT_CORNER;
  else if (is_top_right(term, x, y))
    return CURSOR_SHAPE_TOP_RIGHT_CORNER;
  else if (is_bottom_left(term, x, y))
    return CURSOR_SHAPE_BOTTOM_LEFT_CORNER;
  else if (is_bottom_right(term, x, y))
    return CURSOR_SHAPE_BOTTOM_RIGHT_CORNER;

  else if (term->active_surface == TERM_SURF_BORDER_LEFT)
    return !term->window->is_constrained_left ? CURSOR_SHAPE_LEFT_SIDE
                                              : CURSOR_SHAPE_LEFT_PTR;

  else if (term->active_surface == TERM_SURF_BORDER_RIGHT)
    return !term->window->is_constrained_right ? CURSOR_SHAPE_RIGHT_SIDE
                                               : CURSOR_SHAPE_LEFT_PTR;

  else if (term->active_surface == TERM_SURF_BORDER_TOP)
    return !term->window->is_constrained_top ? CURSOR_SHAPE_TOP_SIDE
                                             : CURSOR_SHAPE_LEFT_PTR;

  else if (term->active_surface == TERM_SURF_BORDER_BOTTOM)
    return !term->window->is_constrained_bottom ? CURSOR_SHAPE_BOTTOM_SIDE
                                                : CURSOR_SHAPE_LEFT_PTR;

  else {
    BUG("Unreachable");
    return CURSOR_SHAPE_NONE;
  }
}

static void mouse_button_state_reset(struct seat *seat) {
  tll_free(seat->mouse.buttons);
  seat->mouse.count = 0;
  seat->mouse.last_released_button = 0;
  memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time));
}

static void mouse_coord_pixel_to_cell(struct seat *seat,
                                      const struct terminal *term, int x,
                                      int y) {
  /*
   * Translate x,y pixel coordinate to a cell coordinate, or -1
   * if the cursor is outside the grid. I.e. if it is inside the
   * margins.
   */
  if (x < term->margins.left)
    seat->mouse.col = 0;
  else if (x >= term->width - term->margins.right)
    seat->mouse.col = term->cols - 1;
  else
    seat->mouse.col = (x - term->margins.left) / term->cell_width;

  if (y < term->margins.top)
    seat->mouse.row = 0;
  else if (y >= term->height - term->margins.bottom)
    seat->mouse.row = term->rows - 1;
  else
    seat->mouse.row = (y - term->margins.top) / term->cell_height;
}

static bool touch_is_active(const struct seat *seat) {
  if (seat->wl_touch == NULL) {
    return false;
  }

  switch (seat->touch.state) {
  case TOUCH_STATE_IDLE:
  case TOUCH_STATE_INHIBITED:
    return false;

  case TOUCH_STATE_HELD:
  case TOUCH_STATE_DRAGGING:
  case TOUCH_STATE_SCROLLING:
    return true;
  }

  BUG("Bad touch state: %d", seat->touch.state);
  return false;
}

static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer,
                             uint32_t serial, struct wl_surface *surface,
                             wl_fixed_t surface_x, wl_fixed_t surface_y) {
  if (unlikely(surface == NULL)) {
    /* Seen on mutter-3.38 */
    LOG_WARN("compositor sent pointer_enter event with a NULL surface");
    return;
  }

  struct seat *seat = data;

  struct wl_window *win = wl_surface_get_user_data(surface);
  struct terminal *term = win->term;

  seat->mouse_focus = term;
  term->active_surface = term_surface_kind(term, surface);

  if (touch_is_active(seat))
    return;

  int x = wl_fixed_to_int(surface_x) * term->scale;
  int y = wl_fixed_to_int(surface_y) * term->scale;

  seat->pointer.serial = serial;
  seat->pointer.hidden = false;
  seat->mouse.x = x;
  seat->mouse.y = y;

  LOG_DBG(
      "pointer-enter: pointer=%p, serial=%u, surface = %p, new-moused = %p, "
      "x=%d, y=%d",
      (void *)wl_pointer, serial, (void *)surface, (void *)term, x, y);

  xassert(tll_length(seat->mouse.buttons) == 0);

  wayl_reload_xcursor_theme(seat, term->scale); /* Scale may have changed */
  term_xcursor_update_for_seat(term, seat);

  switch (term->active_surface) {
  case TERM_SURF_GRID: {
    mouse_coord_pixel_to_cell(seat, term, x, y);
    break;
  }

  case TERM_SURF_TITLE:
  case TERM_SURF_BORDER_LEFT:
  case TERM_SURF_BORDER_RIGHT:
  case TERM_SURF_BORDER_TOP:
  case TERM_SURF_BORDER_BOTTOM:
  case TERM_SURF_TAB_BAR:
  case TERM_SURF_TAB_OVERVIEW:
    break;

  case TERM_SURF_BUTTON_MINIMIZE:
  case TERM_SURF_BUTTON_MAXIMIZE:
  case TERM_SURF_BUTTON_CLOSE:
    render_refresh_csd(term);
    break;

  case TERM_SURF_NONE:
    BUG("Invalid surface type");
    break;
  }
}

static void wl_pointer_leave(void *data, struct wl_pointer *wl_pointer,
                             uint32_t serial, struct wl_surface *surface) {
  struct seat *seat = data;

  if (seat->wl_touch != NULL) {
    switch (seat->touch.state) {
    case TOUCH_STATE_IDLE:
      break;

    case TOUCH_STATE_INHIBITED:
      seat->touch.state = TOUCH_STATE_IDLE;
      break;

    case TOUCH_STATE_HELD:
    case TOUCH_STATE_DRAGGING:
    case TOUCH_STATE_SCROLLING:
      return;
    }
  }

  struct terminal *old_moused = seat->mouse_focus;

  LOG_DBG(
      "%s: pointer-leave: pointer=%p, serial=%u, surface = %p, old-moused = %p",
      seat->name, (void *)wl_pointer, serial, (void *)surface,
      (void *)old_moused);

  seat->pointer.hidden = false;

  if (seat->pointer.xcursor_callback != NULL) {
    /* A cursor frame callback may never be called if the pointer leaves our
     * surface */
    wl_callback_destroy(seat->pointer.xcursor_callback);
    seat->pointer.xcursor_callback = NULL;
    seat->pointer.xcursor_pending = false;
  }

  /* Reset last-set-xcursor, to ensure we update it on a pointer-enter event */
  seat->pointer.shape = CURSOR_SHAPE_NONE;

  /* Reset mouse state */
  seat->mouse.x = seat->mouse.y = 0;
  seat->mouse.col = seat->mouse.row = 0;
  mouse_button_state_reset(seat);
  for (size_t i = 0; i < ALEN(seat->mouse.aggregated); i++)
    seat->mouse.aggregated[i] = 0.0;
  seat->mouse.have_discrete = false;

  seat->mouse_focus = NULL;
  if (old_moused == NULL) {
    LOG_WARN("compositor sent pointer_leave event without a pointer_enter "
             "event: surface=%p",
             (void *)surface);
  } else {
    if (surface != NULL) {
      /* Sway 1.4 sends this event with a NULL surface when we destroy the
       * window */
      const struct wl_window UNUSED *win = wl_surface_get_user_data(surface);
      xassert(old_moused->window == win);
    }

    enum term_surface active_surface = old_moused->active_surface;

    old_moused->active_surface = TERM_SURF_NONE;

    switch (active_surface) {
    case TERM_SURF_BUTTON_MINIMIZE:
    case TERM_SURF_BUTTON_MAXIMIZE:
    case TERM_SURF_BUTTON_CLOSE:
      if (old_moused->shutdown.in_progress)
        break;

      render_refresh_csd(old_moused);
      break;

    case TERM_SURF_GRID:
      selection_finalize(seat, old_moused, seat->pointer.serial);
      break;

    case TERM_SURF_NONE:
    case TERM_SURF_TITLE:
    case TERM_SURF_BORDER_LEFT:
    case TERM_SURF_BORDER_RIGHT:
    case TERM_SURF_BORDER_TOP:
    case TERM_SURF_BORDER_BOTTOM:
    case TERM_SURF_TAB_BAR:
    case TERM_SURF_TAB_OVERVIEW:
      break;
    }
  }
}

static bool pointer_is_on_button(const struct terminal *term,
                                 const struct seat *seat,
                                 enum csd_surface csd_surface) {
  if (seat->mouse.x < 0)
    return false;
  if (seat->mouse.y < 0)
    return false;

  struct csd_data info = get_csd_data(term, csd_surface);
  if (seat->mouse.x > info.width)
    return false;

  if (seat->mouse.y > info.height)
    return false;

  return true;
}

static void wl_pointer_motion(void *data, struct wl_pointer *wl_pointer,
                              uint32_t time, wl_fixed_t surface_x,
                              wl_fixed_t surface_y) {
  struct seat *seat = data;

  /* Touch-emulated pointer events have wl_pointer == NULL. */
  if (wl_pointer != NULL && touch_is_active(seat))
    return;

  struct wayland *wayl = seat->wayl;
  struct terminal *term = seat->mouse_focus;

  if (unlikely(term == NULL)) {
    /* Typically happens when the compositor sent a pointer enter
     * event with a NULL surface - see wl_pointer_enter().
     *
     * In this case, we never set seat->mouse_focus (since we
     * can't map the enter event to a specific window). */
    return;
  }

  struct wl_window *win = term->window;

  LOG_DBG("pointer_motion: pointer=%p, x=%d, y=%d", (void *)wl_pointer,
          wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y));

  xassert(term != NULL);

  int x = wl_fixed_to_int(surface_x) * term->scale;
  int y = wl_fixed_to_int(surface_y) * term->scale;

  enum term_surface surf_kind = term->active_surface;
  int button = 0;
  bool send_to_client = false;
  bool is_on_button = false;

  /* If current surface is a button, check if pointer was on it
   *before* the motion event */
  switch (surf_kind) {
  case TERM_SURF_BUTTON_MINIMIZE:
    is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE);
    break;

  case TERM_SURF_BUTTON_MAXIMIZE:
    is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE);
    break;

  case TERM_SURF_BUTTON_CLOSE:
    is_on_button = pointer_is_on_button(term, seat, CSD_SURF_CLOSE);
    break;

  case TERM_SURF_NONE:
  case TERM_SURF_GRID:
  case TERM_SURF_TITLE:
  case TERM_SURF_BORDER_LEFT:
  case TERM_SURF_BORDER_RIGHT:
  case TERM_SURF_BORDER_TOP:
  case TERM_SURF_BORDER_BOTTOM:
  case TERM_SURF_TAB_BAR:
  case TERM_SURF_TAB_OVERVIEW:
    break;
  }

  seat->pointer.hidden = false;
  seat->mouse.x = x;
  seat->mouse.y = y;

  term_xcursor_update_for_seat(term, seat);

  if (tll_length(seat->mouse.buttons) > 0) {
    const struct button_tracker *tracker = &tll_front(seat->mouse.buttons);
    surf_kind = tracker->surf_kind;
    button = tracker->button;
    send_to_client = tracker->send_to_client;
  }

  switch (surf_kind) {
  case TERM_SURF_NONE:
    break;

  case TERM_SURF_BUTTON_MINIMIZE:
    if (pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) != is_on_button)
      render_refresh_csd(term);
    break;

  case TERM_SURF_BUTTON_MAXIMIZE:
    if (pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) != is_on_button)
      render_refresh_csd(term);
    break;

  case TERM_SURF_BUTTON_CLOSE:
    if (pointer_is_on_button(term, seat, CSD_SURF_CLOSE) != is_on_button)
      render_refresh_csd(term);
    break;

  case TERM_SURF_TITLE:
    /* We've started a 'move' timer, but user started dragging
     * right away - abort the timer and initiate the actual move
     * right away */
    if (button == BTN_LEFT && win->csd.move_timeout_fd != -1) {
      fdm_del(wayl->fdm, win->csd.move_timeout_fd);
      win->csd.move_timeout_fd = -1;
      xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial);
    }
    break;

  case TERM_SURF_BORDER_LEFT:
  case TERM_SURF_BORDER_RIGHT:
  case TERM_SURF_BORDER_TOP:
  case TERM_SURF_BORDER_BOTTOM:
  case TERM_SURF_TAB_BAR:
    break;

  case TERM_SURF_TAB_OVERVIEW: {
    const int new_hover = tab_overview_hit_test(win, x, y);
    if (new_hover != win->tab_overview_state.hover_idx) {
      win->tab_overview_state.hover_idx = new_hover;
      render_refresh(term);
    }
    break;
  }

  case TERM_SURF_GRID: {
    int old_col = seat->mouse.col;
    int old_row = seat->mouse.row;

    mouse_coord_pixel_to_cell(seat, term, seat->mouse.x, seat->mouse.y);

    xassert(seat->mouse.col >= 0 && seat->mouse.col < term->cols);
    xassert(seat->mouse.row >= 0 && seat->mouse.row < term->rows);

    /* Cursor has moved to a different cell since last time */
    bool cursor_is_on_new_cell =
        old_col != seat->mouse.col || old_row != seat->mouse.row;

    if (cursor_is_on_new_cell) {
      /* Prevent multiple/different mouse bindings from
       * triggering if the mouse has moved "too much" (to
       * another cell) */
      seat->mouse.count = 0;
    }

    /* Cursor is inside the grid, i.e. *not* in the margins */
    const bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0;

    enum selection_scroll_direction auto_scroll_direction =
        term->selection.coords.end.row < 0        ? SELECTION_SCROLL_NOT
        : y < term->margins.top                   ? SELECTION_SCROLL_UP
        : y > term->height - term->margins.bottom ? SELECTION_SCROLL_DOWN
                                                  : SELECTION_SCROLL_NOT;

    if (auto_scroll_direction == SELECTION_SCROLL_NOT)
      selection_stop_scroll_timer(term);

    /* Update selection */
    if (!term->is_searching) {
      if (auto_scroll_direction != SELECTION_SCROLL_NOT) {
        /*
         * Start 'selection auto-scrolling'
         *
         * The speed of the scrolling is proportional to the
         * distance between the mouse and the grid; the
         * further away the mouse is, the faster we scroll.
         *
         * Note that the speed is measured in 'intervals (in
         * ns) between each timed scroll of a single line'.
         *
         * Thus, the further away the mouse is, the smaller
         * interval value we use.
         */

        int distance = auto_scroll_direction == SELECTION_SCROLL_UP
                           ? term->margins.top - y
                           : y - (term->height - term->margins.bottom);

        xassert(distance > 0);
        int divisor =
            distance * term->conf->scrollback.multiplier / term->scale;

        selection_start_scroll_timer(term,
                                     400000000 / (divisor > 0 ? divisor : 1),
                                     auto_scroll_direction, seat->mouse.col);
      }

      if (term->selection.ongoing &&
          (cursor_is_on_new_cell ||
           (term->selection.coords.end.row < 0 &&
            seat->mouse.x >= term->margins.left &&
            seat->mouse.x < term->width - term->margins.right &&
            seat->mouse.y >= term->margins.top &&
            seat->mouse.y < term->height - term->margins.bottom))) {
        selection_update(term, seat->mouse.col, seat->mouse.row);
      }
    }

    /* Send mouse event to client application */
    if (!term_mouse_grabbed(term, seat) &&
        (cursor_is_on_new_cell || term->mouse_reporting == MOUSE_SGR_PIXELS) &&
        ((button == 0 && cursor_is_on_grid) ||
         (button != 0 && send_to_client))) {
      xassert(seat->mouse.col < term->cols);
      xassert(seat->mouse.row < term->rows);

      term_mouse_motion(term, button, seat->mouse.row, seat->mouse.col,
                        seat->mouse.y - term->margins.top,
                        seat->mouse.x - term->margins.left, seat->kbd.shift,
                        seat->kbd.alt, seat->kbd.ctrl);
    }
    break;
  }
  }
}

static bool fdm_csd_move(struct fdm *fdm, int fd, int events, void *data) {
  struct seat *seat = data;
  fdm_del(fdm, fd);

  if (seat->mouse_focus == NULL) {
    LOG_WARN("%s: CSD move timeout triggered, but seat's has no mouse focused "
             "terminal",
             seat->name);
    return true;
  }

  struct wl_window *win = seat->mouse_focus->window;

  win->csd.move_timeout_fd = -1;
  xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial);
  return true;
}

static const struct key_binding *
match_mouse_binding(const struct seat *seat, const struct terminal *term,
                    int button) {
  if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) {
    /* Seat has keyboard - use mouse bindings *with* modifiers */

    const struct key_binding_set *bindings =
        key_binding_for(term->wl->key_binding_manager, term->conf, seat);
    xassert(bindings != NULL);

    xkb_mod_mask_t mods;
    get_current_modifiers(seat, &mods, NULL, 0, true);

    /* Ignore selection override modifiers when
     * matching modifiers */
    mods &= ~bindings->selection_overrides;

    const struct key_binding *match = NULL;

    tll_foreach(bindings->mouse, it) {
      const struct key_binding *binding = &it->item;

      if (binding->m.button != button) {
        /* Wrong button */
        continue;
      }

      if (binding->mods != mods) {
        /* Modifier mismatch */
        continue;
      }

      if (binding->m.count > seat->mouse.count) {
        /* Not correct click count */
        continue;
      }

      if (match == NULL || binding->m.count > match->m.count)
        match = binding;
    }

    return match;
  }

  else {
    /* Seat does NOT have a keyboard - use mouse bindings *without*
     * modifiers */
    const struct config_key_binding *match = NULL;
    const struct config *conf = term->conf;

    for (size_t i = 0; i < conf->bindings.mouse.count; i++) {
      const struct config_key_binding *binding = &conf->bindings.mouse.arr[i];

      if (binding->m.button != button) {
        /* Wrong button */
        continue;
      }

      if (binding->m.count > seat->mouse.count) {
        /* Incorrect click count */
        continue;
      }

      if (tll_length(binding->modifiers) > 0) {
        /* Binding has modifiers */
        continue;
      }

      if (match == NULL || binding->m.count > match->m.count)
        match = binding;
    }

    if (match != NULL) {
      static struct key_binding bind;
      bind.action = match->action;
      bind.aux = &match->aux;
      return &bind;
    }

    return NULL;
  }

  BUG("should not get here");
}

static void wl_pointer_button(void *data, struct wl_pointer *wl_pointer,
                              uint32_t serial, uint32_t time, uint32_t button,
                              uint32_t state) {
  LOG_DBG("BUTTON: pointer=%p, serial=%u, button=%x, state=%u",
          (void *)wl_pointer, serial, button, state);

  xassert(serial != 0);

  struct seat *seat = data;

  /* Touch-emulated pointer events have wl_pointer == NULL. */
  if (wl_pointer != NULL && touch_is_active(seat))
    return;

  struct wayland *wayl = seat->wayl;
  struct terminal *term = seat->mouse_focus;

  seat->pointer.serial = serial;
  seat->pointer.hidden = false;

  xassert(term != NULL);

  enum term_surface surf_kind = TERM_SURF_NONE;
  bool send_to_client = false;

  if (state == WL_POINTER_BUTTON_STATE_PRESSED) {
    if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_IDLE) {
      seat->touch.state = TOUCH_STATE_INHIBITED;
    }

    /* Time since last click */
    struct timespec now, since_last;
    clock_gettime(CLOCK_MONOTONIC, &now);
    timespec_sub(&now, &seat->mouse.last_time, &since_last);

    if (seat->mouse.last_released_button == button && since_last.tv_sec == 0 &&
        since_last.tv_nsec <= 300 * 1000 * 1000) {
      seat->mouse.count++;
    } else
      seat->mouse.count = 1;

    /*
     * Workaround GNOME bug
     *
     * Dragging the window, then stopping the drag (releasing the
     * mouse button), *without* moving the mouse, and then
     * clicking twice, waiting for the CSD timer, and finally
     * clicking once more, results in the following sequence
     * (keyboard and other irrelevant events filtered out, unless
     * they're needed to prove a point):
     *
     * dbg: input.c:1551: cancelling drag timer, moving window
     * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=873,
     * surface=0x6070000036d0 dbg: input.c:1432: seat0: pointer-leave:
     * pointer=0x607000003660, serial=874, surface = 0x6070000396e0, old-moused
     * = 0x622000006100
     *
     * --> drag stopped here
     *
     * --> LMB clicked first time after the drag (generates the
     *     enter event on *release*, but no button events)
     * dbg: input.c:1360: pointer-enter: pointer=0x607000003660, serial=876,
     * surface = 0x6070000396e0, new-moused = 0x622000006100
     *
     * --> LMB clicked, and held until the timer times out, second
     *     time after the drag
     * dbg: input.c:1712: BUTTON: pointer=0x607000003660, serial=877,
     * button=110, state=1 dbg: input.c:1806: starting move timer dbg:
     * input.c:1692: move timer timed out dbg: input.c:759: keyboard_leave:
     * keyboard=0x607000003580, serial=878, surface=0x6070000036d0
     *
     * --> NOTE: ^^ no pointer leave event this time, only the
     *     keyboard leave
     *
     * --> LMB clicked one last time
     * dbg: input.c:697: seat0: keyboard_enter: keyboard=0x607000003580,
     * serial=879, surface=0x6070000036d0 dbg: input.c:1712: BUTTON:
     * pointer=0x607000003660, serial=880, button=110, state=1 err:
     * input.c:1741: BUG in wl_pointer_button(): assertion failed:
     * 'it->item.button != button'
     *
     * What are we seeing?
     *
     * - GNOME does *not* send a pointer *enter* event after the drag
     *   has stopped
     * - The second drag does *not* generate a pointer *leave* event
     * - The missing leave event means we're still tracking LMB as
     *   being held down in our seat struct.
     * - This leads to an assert (debug builds) when LMB is clicked
     *   again (seat's button list already contains LMB).
     *
     * Note: I've also observed variants of the above
     */
    tll_foreach(seat->mouse.buttons, it) {
      if (it->item.button == button) {
        LOG_WARN("multiple button press events for button %d "
                 "(compositor bug?)",
                 button);
        tll_remove(seat->mouse.buttons, it);
        break;
      }
    }

#if defined(_DEBUG)
    tll_foreach(seat->mouse.buttons, it) xassert(it->item.button != button);
#endif

    /*
     * Remember which surface "owns" this button, so that we can
     * send motion and button release events to that surface, even
     * if the pointer is no longer over it.
     */
    tll_push_back(seat->mouse.buttons,
                  ((struct button_tracker){.button = button,
                                           .surf_kind = term->active_surface,
                                           .send_to_client = false}));

    seat->mouse.last_time = now;

    surf_kind = term->active_surface;
    send_to_client = false; /* For now, may be set to true if a binding consumes
                               the button */
  } else {
    bool UNUSED have_button = false;
    tll_foreach(seat->mouse.buttons, it) {
      if (it->item.button == button) {
        have_button = true;
        surf_kind = it->item.surf_kind;
        send_to_client = it->item.send_to_client;
        tll_remove(seat->mouse.buttons, it);
        break;
      }
    }

    if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_INHIBITED) {
      if (tll_length(seat->mouse.buttons) == 0) {
        seat->touch.state = TOUCH_STATE_IDLE;
      }
    }

    if (!have_button) {
      /*
       * Seen on Sway with slurp
       *
       *  1. Run slurp
       *  2. Press, and hold left mouse button
       *  3. Press escape, to cancel slurp
       *  4. Release mouse button
       *  5. BAM!
       */
      LOG_WARN("stray button release event (compositor bug?)");
      return;
    }

    seat->mouse.last_released_button = button;
  }

  switch (surf_kind) {
  case TERM_SURF_TITLE:
    if (state == WL_POINTER_BUTTON_STATE_PRESSED) {

      struct wl_window *win = term->window;

      /* Toggle maximized state on double-click */
      if (term->conf->csd.double_click_to_maximize && button == BTN_LEFT &&
          seat->mouse.count == 2) {
        if (win->is_maximized)
          xdg_toplevel_unset_maximized(win->xdg_toplevel);
        else
          xdg_toplevel_set_maximized(win->xdg_toplevel);
      }

      else if (button == BTN_LEFT && win->csd.move_timeout_fd < 0) {
        const struct itimerspec timeout = {
            .it_value = {.tv_nsec = 200000000},
        };

        int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
        if (fd >= 0 && timerfd_settime(fd, 0, &timeout, NULL) == 0 &&
            fdm_add(wayl->fdm, fd, EPOLLIN, &fdm_csd_move, seat)) {
          win->csd.move_timeout_fd = fd;
          win->csd.serial = serial;
        } else {
          LOG_ERRNO("failed to configure XDG toplevel move timer FD");
          if (fd >= 0)
            close(fd);
        }
      }

      if (button == BTN_RIGHT && tll_length(seat->mouse.buttons) == 1) {
        const struct csd_data info = get_csd_data(term, CSD_SURF_TITLE);
        xdg_toplevel_show_window_menu(
            win->xdg_toplevel, seat->wl_seat, seat->pointer.serial,
            seat->mouse.x + info.x, seat->mouse.y + info.y);
      }
    }

    else if (state == WL_POINTER_BUTTON_STATE_RELEASED) {
      struct wl_window *win = term->window;
      if (win->csd.move_timeout_fd >= 0) {
        fdm_del(wayl->fdm, win->csd.move_timeout_fd);
        win->csd.move_timeout_fd = -1;
      }
    }
    return;

  case TERM_SURF_BORDER_LEFT:
  case TERM_SURF_BORDER_RIGHT:
  case TERM_SURF_BORDER_TOP:
  case TERM_SURF_BORDER_BOTTOM: {
    if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) {
      enum xdg_toplevel_resize_edge resize_type = XDG_TOPLEVEL_RESIZE_EDGE_NONE;

      int x = seat->mouse.x;
      int y = seat->mouse.y;

      if (is_top_left(term, x, y))
        resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT;
      else if (is_top_right(term, x, y))
        resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT;
      else if (is_bottom_left(term, x, y))
        resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT;
      else if (is_bottom_right(term, x, y))
        resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT;
      else {
        if (term->active_surface == TERM_SURF_BORDER_LEFT &&
            !term->window->is_constrained_left) {
          resize_type = XDG_TOPLEVEL_RESIZE_EDGE_LEFT;
        }

        else if (term->active_surface == TERM_SURF_BORDER_RIGHT &&
                 !term->window->is_constrained_right) {
          resize_type = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT;
        }

        else if (term->active_surface == TERM_SURF_BORDER_TOP &&
                 !term->window->is_constrained_top) {
          resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP;
        }

        else if (term->active_surface == TERM_SURF_BORDER_BOTTOM &&
                 !term->window->is_constrained_bottom) {
          resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM;
        }
      }

      if (resize_type != XDG_TOPLEVEL_RESIZE_EDGE_NONE) {
        xdg_toplevel_resize(term->window->xdg_toplevel, seat->wl_seat, serial,
                            resize_type);
      }
    }
    return;
  }

  case TERM_SURF_BUTTON_MINIMIZE:
    if (button == BTN_LEFT &&
        pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) &&
        state == WL_POINTER_BUTTON_STATE_RELEASED) {
      xdg_toplevel_set_minimized(term->window->xdg_toplevel);
    }
    break;

  case TERM_SURF_BUTTON_MAXIMIZE:
    if (button == BTN_LEFT &&
        pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) &&
        state == WL_POINTER_BUTTON_STATE_RELEASED) {
      if (term->window->is_maximized)
        xdg_toplevel_unset_maximized(term->window->xdg_toplevel);
      else
        xdg_toplevel_set_maximized(term->window->xdg_toplevel);
    }
    break;

  case TERM_SURF_BUTTON_CLOSE:
    if (button == BTN_LEFT &&
        pointer_is_on_button(term, seat, CSD_SURF_CLOSE) &&
        state == WL_POINTER_BUTTON_STATE_RELEASED) {
      term_shutdown(term);
    }
    break;

  case TERM_SURF_GRID: {
    search_cancel(term);
    urls_reset(term);

    bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0;

    switch (state) {
    case WL_POINTER_BUTTON_STATE_PRESSED: {
      bool consumed = false;

      if (cursor_is_on_grid && term_mouse_grabbed(term, seat)) {
        const struct key_binding *match =
            match_mouse_binding(seat, term, button);

        if (match != NULL)
          consumed = execute_binding(seat, term, match, serial, 1);
      }

      send_to_client = !consumed && cursor_is_on_grid;

      if (send_to_client)
        tll_back(seat->mouse.buttons).send_to_client = true;

      if (send_to_client && !term_mouse_grabbed(term, seat) &&
          cursor_is_on_grid) {
        term_mouse_down(term, button, seat->mouse.row, seat->mouse.col,
                        seat->mouse.y - term->margins.top,
                        seat->mouse.x - term->margins.left, seat->kbd.shift,
                        seat->kbd.alt, seat->kbd.ctrl);
      }
      break;
    }

    case WL_POINTER_BUTTON_STATE_RELEASED:
      selection_finalize(seat, term, serial);

      if (send_to_client && !term_mouse_grabbed(term, seat)) {
        term_mouse_up(term, button, seat->mouse.row, seat->mouse.col,
                      seat->mouse.y - term->margins.top,
                      seat->mouse.x - term->margins.left, seat->kbd.shift,
                      seat->kbd.alt, seat->kbd.ctrl);
      }
      break;
    }
    break;
  }

  case TERM_SURF_TAB_BAR: {
    if (state != WL_POINTER_BUTTON_STATE_PRESSED)
      break;
    if (button != BTN_LEFT)
      break;

    size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y);
    if (idx == SIZE_MAX || idx >= term->window->tab_count)
      break;
    if (idx == term->window->active_tab)
      break;
    term_tab_switch(term->window, idx);
    break;
  }

  case TERM_SURF_TAB_OVERVIEW: {
    if (state != WL_POINTER_BUTTON_STATE_PRESSED)
      break;
    struct wl_window *win = term->window;
    if (button == BTN_LEFT) {
      int idx = tab_overview_hit_test(win, seat->mouse.x, seat->mouse.y);
      if (idx >= 0 && (size_t)idx < win->tab_count) {
        if ((size_t)idx != win->active_tab)
          term_tab_switch(win, (size_t)idx);
        tab_overview_close_instant(win);
      } else {
        tab_overview_toggle(win);
      }
    } else if (button == BTN_RIGHT) {
      tab_overview_toggle(win);
    }
    break;
  }

  case TERM_SURF_NONE:
    BUG("Invalid surface type");
    break;
  }
}

static void alternate_scroll(struct seat *seat, int amount, int button) {
  if (seat->wl_keyboard == NULL)
    return;

  /* Should be cleared in leave event */
  xassert(seat->mouse_focus != NULL);
  struct terminal *term = seat->mouse_focus;

  assert(button == BTN_BACK || button == BTN_FORWARD);

  xkb_keycode_t key =
      button == BTN_BACK ? seat->kbd.key_arrow_up : seat->kbd.key_arrow_down;

  for (int i = 0; i < amount; i++)
    key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN);
  key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_UP);
}

static void mouse_scroll(struct seat *seat, int amount,
                         enum wl_pointer_axis axis) {
  struct terminal *term = seat->mouse_focus;
  xassert(term != NULL);

  int button = axis == WL_POINTER_AXIS_VERTICAL_SCROLL
                   ? amount < 0 ? BTN_WHEEL_BACK : BTN_WHEEL_FORWARD
               : amount < 0 ? BTN_WHEEL_LEFT
                            : BTN_WHEEL_RIGHT;
  amount = abs(amount);

  if (term_mouse_grabbed(term, seat)) {
    seat->mouse.count = 1;

    const struct key_binding *match = match_mouse_binding(seat, term, button);

    if (match != NULL)
      execute_binding(seat, term, match, seat->pointer.serial, amount);

    seat->mouse.last_released_button = button;
  }

  else if (seat->mouse.col >= 0 && seat->mouse.row >= 0) {
    xassert(seat->mouse.col < term->cols);
    xassert(seat->mouse.row < term->rows);

    for (int i = 0; i < amount; i++) {
      term_mouse_down(term, button, seat->mouse.row, seat->mouse.col,
                      seat->mouse.y - term->margins.top,
                      seat->mouse.x - term->margins.left, seat->kbd.shift,
                      seat->kbd.alt, seat->kbd.ctrl);
    }

    term_mouse_up(term, button, seat->mouse.row, seat->mouse.col,
                  seat->mouse.y - term->margins.top,
                  seat->mouse.x - term->margins.left, seat->kbd.shift,
                  seat->kbd.alt, seat->kbd.ctrl);
  }
}

static double mouse_scroll_multiplier(const struct terminal *term,
                                      const struct seat *seat) {
  return (term->grid == &term->normal ||
          (term_mouse_grabbed(term, seat) && term->alt_scrolling))
             ? term->conf->scrollback.multiplier
             : 1.0;
}

static void wl_pointer_axis(void *data, struct wl_pointer *wl_pointer,
                            uint32_t time, uint32_t axis, wl_fixed_t value) {
  struct seat *seat = data;

  if (touch_is_active(seat))
    return;

  if (seat->mouse.have_discrete)
    return;

  xassert(seat->mouse_focus != NULL);
  xassert(axis < ALEN(seat->mouse.aggregated));

  const struct terminal *term = seat->mouse_focus;

  /*
   * Aggregate scrolled amount until we get at least 1.0
   *
   * Without this, very slow scrolling will never actually scroll
   * anything.
   */
  seat->mouse.aggregated[axis] +=
      mouse_scroll_multiplier(term, seat) * wl_fixed_to_double(value);
  if (fabs(seat->mouse.aggregated[axis]) < seat->mouse_focus->cell_height)
    return;

  int lines = seat->mouse.aggregated[axis] / seat->mouse_focus->cell_height;
  mouse_scroll(seat, lines, axis);
  seat->mouse.aggregated[axis] -=
      (double)lines * seat->mouse_focus->cell_height;
}

static void wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer,
                                     enum wl_pointer_axis axis,
                                     int32_t discrete) {
  LOG_DBG("axis_discrete: %d", discrete);
  struct seat *seat = data;

  if (touch_is_active(seat))
    return;

  seat->mouse.have_discrete = true;
  int amount = discrete;

  if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) {
    /* Treat mouse wheel left/right as regular buttons */
  } else
    amount *= mouse_scroll_multiplier(seat->mouse_focus, seat);

  mouse_scroll(seat, amount, axis);
}

#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION)
static void wl_pointer_axis_value120(void *data, struct wl_pointer *wl_pointer,
                                     enum wl_pointer_axis axis,
                                     int32_t value120) {
  LOG_DBG("axis_value120: %d -> %.2f", value120, (float)value120 / 120.);

  struct seat *seat = data;

  if (touch_is_active(seat))
    return;

  seat->mouse.have_discrete = true;

  /*
   * 120 corresponds to a single "low-res" scroll step.
   *
   * When doing high-res scrolling, take the scrollback.multiplier,
   * and calculate how many degrees there are per line.
   *
   * For example, with scrollback.multiplier = 3, we have 120 / 3 == 40.
   *
   * Then, accumulate high-res scroll events, until we have *at
   * least* that much. Translate the accumulated value to number of
   * lines, and scroll.
   *
   * Subtract the "used" degrees from the accumulated value, and
   * keep what's left (this value will always be less than the
   * per-line value).
   */
  const double multiplier = mouse_scroll_multiplier(seat->mouse_focus, seat);
  const double per_line = 120. / multiplier;

  seat->mouse.aggregated_120[axis] += (double)value120;

  if (fabs(seat->mouse.aggregated_120[axis]) < per_line)
    return;

  int lines = (int)(seat->mouse.aggregated_120[axis] / per_line);
  mouse_scroll(seat, lines, axis);
  seat->mouse.aggregated_120[axis] -= (double)lines * per_line;
}
#endif

static void wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) {
  struct seat *seat = data;

  if (touch_is_active(seat))
    return;

  seat->mouse.have_discrete = false;
}

static void wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer,
                                   uint32_t axis_source) {}

static void wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer,
                                 uint32_t time, uint32_t axis) {
  struct seat *seat = data;

  if (touch_is_active(seat))
    return;

  xassert(axis < ALEN(seat->mouse.aggregated));
  seat->mouse.aggregated[axis] = 0.;
}

const struct wl_pointer_listener pointer_listener = {
    .enter = &wl_pointer_enter,
    .leave = &wl_pointer_leave,
    .motion = &wl_pointer_motion,
    .button = &wl_pointer_button,
    .axis = &wl_pointer_axis,
    .frame = &wl_pointer_frame,
    .axis_source = &wl_pointer_axis_source,
    .axis_stop = &wl_pointer_axis_stop,
    .axis_discrete = &wl_pointer_axis_discrete,
#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION)
    .axis_value120 = &wl_pointer_axis_value120,
#endif
};

static bool touch_to_scroll(struct seat *seat, struct terminal *term,
                            wl_fixed_t surface_x, wl_fixed_t surface_y) {
  bool coord_updated = false;

  int y = wl_fixed_to_int(surface_y) * term->scale;
  int rows = (y - seat->mouse.y) / term->cell_height;
  if (rows != 0) {
    mouse_scroll(seat, -rows, WL_POINTER_AXIS_VERTICAL_SCROLL);
    seat->mouse.y += rows * term->cell_height;
    coord_updated = true;
  }

  int x = wl_fixed_to_int(surface_x) * term->scale;
  int cols = (x - seat->mouse.x) / term->cell_width;
  if (cols != 0) {
    mouse_scroll(seat, -cols, WL_POINTER_AXIS_HORIZONTAL_SCROLL);
    seat->mouse.x += cols * term->cell_width;
    coord_updated = true;
  }

  return coord_updated;
}

static void wl_touch_down(void *data, struct wl_touch *wl_touch,
                          uint32_t serial, uint32_t time,
                          struct wl_surface *surface, int32_t id,
                          wl_fixed_t surface_x, wl_fixed_t surface_y) {
  struct seat *seat = data;

  if (seat->touch.state != TOUCH_STATE_IDLE)
    return;

  struct wl_window *win = wl_surface_get_user_data(surface);
  struct terminal *term = win->term;

  LOG_DBG("touch_down: touch=%p, x=%d, y=%d", (void *)wl_touch,
          wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y));

  int x = wl_fixed_to_int(surface_x) * term->scale;
  int y = wl_fixed_to_int(surface_y) * term->scale;

  seat->mouse.x = x;
  seat->mouse.y = y;
  mouse_coord_pixel_to_cell(seat, term, x, y);

  seat->touch.state = TOUCH_STATE_HELD;
  seat->touch.serial = serial;
  seat->touch.time = time + term->conf->touch.long_press_delay;
  seat->touch.surface = surface;
  seat->touch.surface_kind = term_surface_kind(term, surface);
  seat->touch.id = id;
}

static void wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial,
                        uint32_t time, int32_t id) {
  struct seat *seat = data;

  if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id)
    return;

  LOG_DBG("touch_up: touch=%p", (void *)wl_touch);

  struct wl_window *win = wl_surface_get_user_data(seat->touch.surface);
  struct terminal *term = win->term;

  struct terminal *old_term = seat->mouse_focus;
  enum term_surface old_active_surface = term->active_surface;
  seat->mouse_focus = term;
  term->active_surface = seat->touch.surface_kind;

  switch (seat->touch.state) {
  case TOUCH_STATE_HELD:
    wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT,
                      WL_POINTER_BUTTON_STATE_PRESSED);
    /* fallthrough */
  case TOUCH_STATE_DRAGGING:
    wl_pointer_button(seat, NULL, serial, time, BTN_LEFT,
                      WL_POINTER_BUTTON_STATE_RELEASED);
    /* fallthrough */
  case TOUCH_STATE_SCROLLING:
    term->active_surface = TERM_SURF_NONE;
    seat->touch.state = TOUCH_STATE_IDLE;
    break;

  case TOUCH_STATE_INHIBITED:
  case TOUCH_STATE_IDLE:
    BUG("Bad touch state: %d", seat->touch.state);
    break;
  }

  seat->mouse_focus = old_term;
  term->active_surface = old_active_surface;
}

static void wl_touch_motion(void *data, struct wl_touch *wl_touch,
                            uint32_t time, int32_t id, wl_fixed_t surface_x,
                            wl_fixed_t surface_y) {
  struct seat *seat = data;
  if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id)
    return;

  LOG_DBG("touch_motion: touch=%p, x=%d, y=%d", (void *)wl_touch,
          wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y));

  struct wl_window *win = wl_surface_get_user_data(seat->touch.surface);
  struct terminal *term = win->term;

  struct terminal *old_term = seat->mouse_focus;
  enum term_surface old_active_surface = term->active_surface;
  seat->mouse_focus = term;
  term->active_surface = seat->touch.surface_kind;

  switch (seat->touch.state) {
  case TOUCH_STATE_HELD:
    if (time <= seat->touch.time && term->active_surface == TERM_SURF_GRID) {
      if (touch_to_scroll(seat, term, surface_x, surface_y))
        seat->touch.state = TOUCH_STATE_SCROLLING;
      break;
    } else {
      wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT,
                        WL_POINTER_BUTTON_STATE_PRESSED);
      seat->touch.state = TOUCH_STATE_DRAGGING;
      /* fallthrough */
    }
  case TOUCH_STATE_DRAGGING:
    wl_pointer_motion(seat, NULL, time, surface_x, surface_y);
    break;
  case TOUCH_STATE_SCROLLING:
    touch_to_scroll(seat, term, surface_x, surface_y);
    break;

  case TOUCH_STATE_INHIBITED:
  case TOUCH_STATE_IDLE:
    BUG("Bad touch state: %d", seat->touch.state);
    break;
  }

  seat->mouse_focus = old_term;
  term->active_surface = old_active_surface;
}

static void wl_touch_frame(void *data, struct wl_touch *wl_touch) {}

static void wl_touch_cancel(void *data, struct wl_touch *wl_touch) {
  struct seat *seat = data;
  if (seat->touch.state == TOUCH_STATE_INHIBITED)
    return;

  seat->touch.state = TOUCH_STATE_IDLE;
}

const struct wl_touch_listener touch_listener = {
    .down = wl_touch_down,
    .up = wl_touch_up,
    .motion = wl_touch_motion,
    .frame = wl_touch_frame,
    .cancel = wl_touch_cancel,
};
