#include "input.h"

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

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

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

#include <xdg-shell.h>

#define LOG_MODULE "input"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "commands.h"
#include "config.h"
#include "grid.h"
#include "keymap.h"
#include "kitty-keymap.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 "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);
        } else
                cmd_scrollback_up(term, amount);
        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);
        } else
            cmd_scrollback_down(term, amount);
        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_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_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);
        break;

    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:
        close(fd);
        return;

    case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1:
        break;

    default:
        LOG_WARN("unrecognized keymap format: %u", format);
        close(fd);
        return;
    }

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

    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);

    }

    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;

        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");
    }

    munmap(map_str, size);
    close(fd);

    key_binding_load_keymap(wayl->key_binding_manager, seat);
}

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 (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);

    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 (pressed) {
        if (term->unicode_mode.active) {
            unicode_mode_input(seat, term, sym);
            return;
        }

        else if (term->is_searching) {
            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 (should_repeat)
                start_repeater(seat, key);

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

#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))
                {
                    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))
            {
                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))
                {
                    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;

#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];
    pipe2(chan, O_CLOEXEC);

    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_tiled_top && !term->window->is_tiled_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_tiled_top && !term->window->is_tiled_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_tiled_bottom && !term->window->is_tiled_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_tiled_bottom && !term->window->is_tiled_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 CURSOR_SHAPE_LEFT_SIDE;
    else if (term->active_surface == TERM_SURF_BORDER_RIGHT)  return CURSOR_SHAPE_RIGHT_SIDE;
    else if (term->active_surface == TERM_SURF_BORDER_TOP)    return CURSOR_SHAPE_TOP_SIDE;
    else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) return CURSOR_SHAPE_BOTTOM_SIDE;
    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:
        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 == win->term);
        }

        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:
            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:
        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:
        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: {
        static const enum xdg_toplevel_resize_edge map[] = {
            [TERM_SURF_BORDER_LEFT] = XDG_TOPLEVEL_RESIZE_EDGE_LEFT,
            [TERM_SURF_BORDER_RIGHT] = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT,
            [TERM_SURF_BORDER_TOP] = XDG_TOPLEVEL_RESIZE_EDGE_TOP,
            [TERM_SURF_BORDER_BOTTOM] = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM,
        };

        if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) {
            enum xdg_toplevel_resize_edge resize_type;

            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
                resize_type = map[term->active_surface];

            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_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,
};
