#include "input.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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)) 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->urls); urls_assign_key_combos(term->conf, &term->urls); urls_render(term); 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(seat); 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_compose_state != NULL) { xkb_compose_state_unref(seat->kbd.xkb_compose_state); seat->kbd.xkb_compose_state = NULL; } if (seat->kbd.xkb_compose_table != NULL) { xkb_compose_table_unref(seat->kbd.xkb_compose_table); seat->kbd.xkb_compose_table = NULL; } 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; } if (seat->kbd.xkb != NULL) { xkb_context_unref(seat->kbd.xkb); seat->kbd.xkb = 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: return; case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: break; default: LOG_WARN("unrecognized keymap format: %u", format); 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--; seat->kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); 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); /* Compose (dead keys) */ 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) { LOG_WARN("failed to instantiate compose table; dead keys will not work"); } else { seat->kbd.xkb_compose_state = xkb_compose_state_new( seat->kbd.xkb_compose_table, XKB_COMPOSE_STATE_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; if (composed && 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 consumed = 0; 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, ctx->key, false); consumed = xkb_state_key_get_consumed_mods2( seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_GTK); #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 locked modifiers being filtered out */ get_current_modifiers(seat, &mods, NULL, ctx->key, false); /* Re-retrieve the consumed modifiers using the GTK mode, to better match kitty. */ consumed = xkb_state_key_get_consumed_mods2( seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_GTK); } mods &= seat->kbd.kitty_significant; consumed &= seat->kbd.kitty_significant; const xkb_mod_mask_t effective = mods & ~consumed; const xkb_mod_mask_t caps_num = (seat->kbd.mod_caps != XKB_MOD_INVALID ? 1 << seat->kbd.mod_caps : 0) | (seat->kbd.mod_num != XKB_MOD_INVALID ? 1 << seat->kbd.mod_num : 0); bool is_text = count > 0 && utf32 != NULL && (effective & ~caps_num) == 0; for (size_t i = 0; utf32[i] != U'\0'; i++) { if (!iswprint(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 (effective == 0) { switch (sym) { case XKB_KEY_Return: term_to_slave(term, "\r", 1); return true; case XKB_KEY_BackSpace: term_to_slave(term, "\x7f", 1); return true; case XKB_KEY_Tab: 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++; int key = -1, alternate = -1, base = -1; char final; if (info != NULL) { if (!info->is_modifier || report_all_as_escapes) { key = info->key; final = info->final; } } else { /* * Use keysym (typically its Unicode codepoint value). * * 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) */ /* Get the key's shift level */ xkb_level_index_t lvl = xkb_state_key_get_level( seat->kbd.xkb_state, ctx->key, ctx->layout); /* And get all modifier combinations that, combined with * the pressed key, results in the current shift level */ xkb_mod_mask_t masks[32]; size_t mask_count = xkb_keymap_key_get_mods_for_level( seat->kbd.xkb_keymap, ctx->key, ctx->layout, lvl, masks, ALEN(masks)); /* Check modifier combinations - if a combination has * modifiers not in our set of 'significant' modifiers, * use key sym as-is */ bool use_level0_sym = true; for (size_t i = 0; i < mask_count; i++) { if ((masks[i] & ~seat->kbd.kitty_significant) > 0) { use_level0_sym = false; break; } } xkb_keysym_t sym_to_use = use_level0_sym && ctx->level0_syms.count > 0 ? ctx->level0_syms.syms[0] : sym; if (composed) key = utf32[0]; /* TODO: what if there are multiple codepoints? */ else { key = xkb_keysym_to_utf32(sym_to_use); if (key == 0) return false; /* The *shifted* key. May be the same as the unshifted * key - if so, this is filtered out below, when * emitting the CSI */ alternate = xkb_keysym_to_utf32(sym); } /* 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]); final = 'u'; } if (key < 0) return false; 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; 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; 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; } 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 (seat->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 0 for (size_t i = 0; i < 32; i++) { if (mods & (1u << i)) { LOG_INFO("%s", xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); } } #endif #if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG char sym_name[100]; xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); LOG_DBG("%s (%u/0x%x): seat=%s, term=%p, serial=%u, " "mods=0x%08x, consumed=0x%08x, repeats=%d", sym_name, sym, sym, seat->name, (void *)term, serial, mods, consumed, should_repeat); #endif /* * User configurable bindings */ if (pressed) { tll_foreach(bindings->key, it) { const struct key_binding *bind = &it->item; /* Match translated symbol */ if (bind->k.sym == sym && bind->mods == (mods & ~consumed) && execute_binding(seat, term, bind, serial, 1)) { LOG_WARN("matched translated symbol"); goto maybe_repeat; } if (bind->mods != mods) continue; /* Match untranslated symbols */ for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i] && execute_binding(seat, term, bind, serial, 1)) { LOG_WARN("matched untranslated symbol"); goto maybe_repeat; } } /* Match raw key code */ tll_foreach(bind->k.key_codes, code) { if (code->item == key && execute_binding(seat, term, bind, serial, 1)) { LOG_WARN("matched raw key code"); 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) { 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; LOG_DBG("modifiers: depressed=0x%x, latched=0x%x, locked=0x%x, group=%u", mods_depressed, mods_latched, mods_locked, group); 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); } 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 || x >= term->width - term->margins.right) seat->mouse.col = -1; else seat->mouse.col = (x - term->margins.left) / term->cell_width; if (y < term->margins.top || y >= term->height - term->margins.bottom) seat->mouse.row = -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 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; seat->pointer.hidden = false; seat->mouse.x = x; seat->mouse.y = y; term_xcursor_update_for_seat(term, seat); enum term_surface surf_kind = term->active_surface; int button = 0; bool send_to_client = false; 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: case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: case TERM_SURF_BUTTON_CLOSE: 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; /* * While the seat's mouse coordinates must always be on the * grid, or -1, we allow updating the selection even when the * mouse is outside the grid (could also be outside the * terminal window). */ int selection_col; int selection_row; if (x < term->margins.left) { seat->mouse.col = -1; selection_col = 0; } else if (x >= term->width - term->margins.right) { seat->mouse.col = -1; selection_col = term->cols - 1; } else { seat->mouse.col = (x - term->margins.left) / term->cell_width; selection_col = seat->mouse.col; } if (y < term->margins.top) { seat->mouse.row = -1; selection_row = 0; } else if (y >= term->height - term->margins.bottom) { seat->mouse.row = -1; selection_row = term->rows - 1; } else { seat->mouse.row = (y - term->margins.top) / term->cell_height; selection_row = seat->mouse.row; } /* * If client is receiving events (because the button was * pressed while the cursor was inside the grid area), then * make sure it receives valid coordinates. */ if (send_to_client) { seat->mouse.col = selection_col; seat->mouse.row = selection_row; } xassert(seat->mouse.col == -1 || (seat->mouse.col >= 0 && seat->mouse.col < term->cols)); xassert(seat->mouse.row == -1 || (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 = 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, selection_col); } if (term->selection.ongoing && ( cursor_is_on_new_cell || term->selection.coords.end.row < 0)) { selection_update(term, selection_col, selection_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 && state == WL_POINTER_BUTTON_STATE_PRESSED) xdg_toplevel_set_minimized(term->window->xdg_toplevel); break; case TERM_SURF_BUTTON_MAXIMIZE: if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { 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 && state == WL_POINTER_BUTTON_STATE_PRESSED) 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_BACK : BTN_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 float 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, uint32_t axis, int32_t 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); } 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, }; 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, };