#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 "config.h" #include "commands.h" #include "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 "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 bool execute_binding(struct seat *seat, struct terminal *term, enum bind_action_normal action, char *const *pipe_argv, uint32_t serial) { switch (action) { case BIND_ACTION_NONE: 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_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_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); 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: { if (pipe_argv == NULL) 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; 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, NULL, pipe_argv, pipe_fd[0], stdout_fd, stderr_fd)) 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: { xassert(!urls_mode_is_active(term)); enum url_action url_action = action == BIND_ACTION_SHOW_URLS_COPY ? URL_ACTION_COPY : URL_ACTION_LAUNCH; urls_collect(term, url_action, &term->urls); urls_assign_key_combos(term->conf, &term->urls); urls_render(term); return true; } case BIND_ACTION_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); return true; case BIND_ACTION_SELECT_BEGIN_BLOCK: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_BLOCK, false); return true; case BIND_ACTION_SELECT_EXTEND: selection_extend( seat, term, seat->mouse.col, seat->mouse.row, term->selection.kind); return true; case BIND_ACTION_SELECT_EXTEND_CHAR_WISE: if (term->selection.kind != SELECTION_BLOCK) { selection_extend( seat, term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE); return true; } return false; case BIND_ACTION_SELECT_WORD: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, false); return true; case BIND_ACTION_SELECT_WORD_WS: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, true); return true; case BIND_ACTION_SELECT_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 xkb_mod_mask_t conf_modifiers_to_mask(const struct seat *seat, const struct config_key_modifiers *modifiers) { xkb_mod_mask_t mods = 0; mods |= modifiers->shift << seat->kbd.mod_shift; mods |= modifiers->ctrl << seat->kbd.mod_ctrl; mods |= modifiers->alt << seat->kbd.mod_alt; mods |= modifiers->meta << seat->kbd.mod_meta; return mods; } static xkb_keycode_list_t key_codes_for_xkb_sym(struct xkb_keymap *keymap, xkb_keysym_t sym) { xkb_keycode_list_t key_codes = tll_init(); /* * Find all key codes that map to this symbol. * * This allows us to match bindings in other layouts * too. */ struct xkb_state *state = xkb_state_new(keymap); for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap); code <= xkb_keymap_max_keycode(keymap); code++) { if (xkb_state_key_get_one_sym(state, code) == sym) tll_push_back(key_codes, code); } xkb_state_unref(state); return key_codes; } static xkb_keysym_t maybe_repair_key_combo(const struct seat *seat, xkb_keysym_t sym, xkb_mod_mask_t mods) { /* * Detect combos containing a shifted symbol and the corresponding * modifier, and replace the shifted symbol with its unshifted * variant. * * For example, the combo is “Control+Shift+U”. In this case, * Shift is the modifier used to “shift” ‘u’ to ‘U’, after which * ‘Shift’ will have been “consumed”. Since we filter out consumed * modifiers when matching key combos, this key combo will never * trigger (we will never be able to match the ‘Shift’ modifier). * * There are two correct variants of the above key combo: * - “Control+U” (upper case ‘U’) * - “Control+Shift+u” (lower case ‘u’) * * What we do here is, for each key *code*, check if there are any * (shifted) levels where it produces ‘sym’. If there are, check * *which* sets of modifiers are needed to produce it, and compare * with ‘mods’. * * If there is at least one common modifier, it means ‘sym’ is a * “shifted” symbol, with the corresponding shifting modifier * explicitly included in the key combo. I.e. the key combo will * never trigger. * * We then proceed and “repair” the key combo by replacing ‘sym’ * with the corresponding unshifted symbol. * * To reduce the noise, we ignore all key codes where the shifted * symbol is the same as the unshifted symbol. */ for (xkb_keycode_t code = xkb_keymap_min_keycode(seat->kbd.xkb_keymap); code <= xkb_keymap_max_keycode(seat->kbd.xkb_keymap); code++) { xkb_layout_index_t layout_idx = xkb_state_key_get_layout(seat->kbd.xkb_state, code); /* Get all unshifted symbols for this key */ const xkb_keysym_t *base_syms = NULL; size_t base_count = xkb_keymap_key_get_syms_by_level( seat->kbd.xkb_keymap, code, layout_idx, 0, &base_syms); if (base_count == 0 || sym == base_syms[0]) { /* No unshifted symbols, or unshifted symbol is same as ‘sym’ */ continue; } /* Name of the unshifted symbol, for logging */ char base_name[100]; xkb_keysym_get_name(base_syms[0], base_name, sizeof(base_name)); /* Iterate all shift levels */ for (xkb_level_index_t level_idx = 1; level_idx < xkb_keymap_num_levels_for_key( seat->kbd.xkb_keymap, code, layout_idx); level_idx++) { /* Get all symbols for current shift level */ const xkb_keysym_t *shifted_syms = NULL; size_t shifted_count = xkb_keymap_key_get_syms_by_level( seat->kbd.xkb_keymap, code, layout_idx, level_idx, &shifted_syms); for (size_t i = 0; i < shifted_count; i++) { if (shifted_syms[i] != sym) continue; /* Get modifier sets that produces the current shift level */ xkb_mod_mask_t mod_masks[16]; size_t mod_mask_count = xkb_keymap_key_get_mods_for_level( seat->kbd.xkb_keymap, code, layout_idx, level_idx, mod_masks, ALEN(mod_masks)); /* Check if key combo’s modifier set intersects */ for (size_t j = 0; j < mod_mask_count; j++) { if ((mod_masks[j] & mods) != mod_masks[j]) continue; char combo[64] = {0}; for (int k = 0; k < sizeof(xkb_mod_mask_t) * 8; k++) { if (!(mods & (1u << k))) continue; const char *mod_name = xkb_keymap_mod_get_name( seat->kbd.xkb_keymap, k); strcat(combo, mod_name); strcat(combo, "+"); } size_t len = strlen(combo); xkb_keysym_get_name( sym, &combo[len], sizeof(combo) - len); LOG_WARN( "%s: combo with both explicit modifier and shifted symbol " "(level=%d, mod-mask=0x%08x), " "replacing with %s", combo, level_idx, mod_masks[j], base_name); /* Replace with unshifted symbol */ return base_syms[0]; } } } } return sym; } static void convert_key_binding(const struct seat *seat, const struct config_key_binding *conf_binding, key_binding_list_t *bindings) { xkb_mod_mask_t mods = conf_modifiers_to_mask(seat, &conf_binding->modifiers); xkb_keysym_t sym = maybe_repair_key_combo(seat, conf_binding->sym, mods); struct key_binding binding = { .mods = mods, .sym = sym, .key_codes = key_codes_for_xkb_sym(seat->kbd.xkb_keymap, sym), .action = conf_binding->action, .pipe_argv = conf_binding->pipe.argv, }; tll_push_back(*bindings, binding); } static void convert_key_bindings(const struct config *conf, struct seat *seat) { for (size_t i = 0; i < conf->bindings.key.count; i++) { const struct config_key_binding *binding = &conf->bindings.key.arr[i]; if (binding->action == BIND_ACTION_NONE) continue; convert_key_binding(seat, binding, &seat->kbd.bindings.key); } } static void convert_search_bindings(const struct config *conf, struct seat *seat) { for (size_t i = 0; i < conf->bindings.search.count; i++) { const struct config_key_binding *binding = &conf->bindings.search.arr[i]; if (binding->action == BIND_ACTION_SEARCH_NONE) continue; convert_key_binding(seat, binding, &seat->kbd.bindings.search); } } static void convert_url_bindings(const struct config *conf, struct seat *seat) { for (size_t i = 0; i < conf->bindings.url.count; i++) { const struct config_key_binding *binding = &conf->bindings.url.arr[i]; #if 0 if (binding->action == BIND_ACTION_URL_NONE) continue; #endif convert_key_binding(seat, binding, &seat->kbd.bindings.url); } } static void convert_mouse_binding(struct seat *seat, const struct config_mouse_binding *conf_binding) { struct mouse_binding binding = { .action = conf_binding->action, .mods = conf_modifiers_to_mask(seat, &conf_binding->modifiers), .button = conf_binding->button, .count = conf_binding->count, .pipe_argv = conf_binding->pipe.argv, }; tll_push_back(seat->mouse.bindings, binding); } static void convert_mouse_bindings(const struct config *conf, struct seat *seat) { for (size_t i = 0; i < conf->bindings.mouse.count; i++) { const struct config_mouse_binding *binding = &conf->bindings.mouse.arr[i]; if (binding->action == BIND_ACTION_NONE) continue; convert_mouse_binding(seat, binding); } } 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; } tll_foreach(seat->kbd.bindings.key, it) tll_free(it->item.key_codes); tll_free(seat->kbd.bindings.key); tll_foreach(seat->kbd.bindings.search, it) tll_free(it->item.key_codes); tll_free(seat->kbd.bindings.search); tll_free(seat->mouse.bindings); /* 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_meta = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_LOGO); 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); convert_key_bindings(wayl->conf, seat); convert_search_bindings(wayl->conf, seat); convert_url_bindings(wayl->conf, seat); convert_mouse_bindings(wayl->conf, 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); 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); if (seat->kbd.xkb == NULL) return; 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); if (seat->kbd.xkb == NULL) return; 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.meta = 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++) { if (info[j].modifiers != MOD_ANY && info[j].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(strcmp(info->seq, "\033[27;6;9~") == 0); } static void key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, uint32_t key, uint32_t state) { if (seat->kbd.xkb == NULL || seat->kbd.xkb_keymap == NULL || seat->kbd.xkb_state == NULL) { return; } const xkb_mod_mask_t ctrl = 1 << seat->kbd.mod_ctrl; const xkb_mod_mask_t alt = 1 << seat->kbd.mod_alt; const xkb_mod_mask_t shift = 1 << seat->kbd.mod_shift; const xkb_mod_mask_t meta = 1 << seat->kbd.mod_meta; if (state == XKB_KEY_UP) { stop_repeater(seat, key); return; } bool should_repeat = 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 (state == XKB_KEY_DOWN && term->conf->mouse.hide_when_typing && /* TODO: better way to detect modifiers */ sym != XKB_KEY_Shift_L && sym != XKB_KEY_Shift_R && sym != XKB_KEY_Control_L && sym != XKB_KEY_Control_R && sym != XKB_KEY_Alt_L && sym != XKB_KEY_Alt_R && sym != XKB_KEY_ISO_Level3_Shift && sym != XKB_KEY_Super_L && sym != XKB_KEY_Super_R && sym != XKB_KEY_Meta_L && sym != XKB_KEY_Meta_R && sym != XKB_KEY_Menu) { 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) { xkb_compose_state_feed(seat->kbd.xkb_compose_state, sym); compose_status = xkb_compose_state_get_status( seat->kbd.xkb_compose_state); } if (compose_status == XKB_COMPOSE_COMPOSING) { /* TODO: goto maybe_repeat? */ return; } xkb_mod_mask_t mods = xkb_state_serialize_mods( seat->kbd.xkb_state, XKB_STATE_MODS_DEPRESSED); xkb_mod_mask_t consumed = xkb_state_key_get_consumed_mods( seat->kbd.xkb_state, key); xkb_mod_mask_t significant = ctrl | alt | shift | meta; mods &= significant; consumed &= significant; 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); if (term->is_searching) { if (should_repeat) start_repeater(seat, key); search_input( seat, term, 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, key, sym, mods, consumed, raw_syms, raw_count, serial); return; } #if 0 for (size_t i = 0; i < 32; i++) { if (mods & (1 << 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)); #endif 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); /* * User configurable bindings */ tll_foreach(seat->kbd.bindings.key, it) { const struct key_binding *bind = &it->item; /* Match translated symbol */ if (bind->sym == sym && bind->mods == (mods & ~consumed) && execute_binding( seat, term, bind->action, bind->pipe_argv, serial)) { goto maybe_repeat; } if (bind->mods != mods) continue; /* Match untranslated symbols */ for (size_t i = 0; i < raw_count; i++) { if (bind->sym == raw_syms[i] && execute_binding( seat, term, bind->action, bind->pipe_argv, serial)) { goto maybe_repeat; } } /* Match raw key code */ tll_foreach(bind->key_codes, code) { if (code->item == key && execute_binding( seat, term, bind->action, bind->pipe_argv, serial)) { goto maybe_repeat; } } } /* * Keys generating escape sequences */ 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.meta ? MOD_META : MOD_NONE; const struct key_data *keymap; if (sym == XKB_KEY_Escape && keymap_mods == MOD_NONE && term->modify_escape_key) { static const struct key_data esc = {.seq = "\033[27;1;27~"}; keymap = &esc; } else keymap = keymap_lookup(term, sym, keymap_mods); if (keymap != NULL) { term_to_slave(term, keymap->seq, strlen(keymap->seq)); term_reset_view(term); selection_cancel(term); goto maybe_repeat; } if (compose_status == XKB_COMPOSE_CANCELLED) goto maybe_repeat; /* * Compose, and maybe emit "normal" character */ xassert(seat->kbd.xkb_compose_state != NULL || compose_status != XKB_COMPOSE_COMPOSED); int count = compose_status == XKB_COMPOSE_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); if (count <= 0) goto maybe_repeat; /* 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); compose_status == XKB_COMPOSE_COMPOSED ? xkb_compose_state_get_utf8( seat->kbd.xkb_compose_state, (char *)utf8, count + 1) : xkb_state_key_get_utf8( seat->kbd.xkb_state, key, (char *)utf8, count + 1); if (seat->kbd.xkb_compose_state != NULL) xkb_compose_state_reset(seat->kbd.xkb_compose_state); #define is_control_key(x) ((x) >= 0x40 && (x) <= 0x7f) #define IS_CTRL(x) ((x) < 0x20 || ((x) >= 0x7f && (x) <= 0x9f)) if ((keymap_mods & MOD_CTRL) && !is_control_key(sym) && (count == 1 && !IS_CTRL(utf8[0])) && sym < 256) { 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 < sizeof(mod_param_map) / sizeof(mod_param_map[0])); int modify_param = mod_param_map[keymap_mods]; xassert(modify_param != 0); char reply[1024]; size_t n = xsnprintf(reply, sizeof(reply), "\x1b[27;%d;%d~", modify_param, sym); term_to_slave(term, reply, n); } else { if (mods & 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 an 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 wchar_t wc = 0x80 | utf8[0]; char utf8[8]; mbstate_t ps = {0}; size_t chars = wcrtomb(utf8, wc, &ps); if (chars != (size_t)-1) term_to_slave(term, utf8, chars); else term_to_slave(term, utf8, count); } else { /* Alt ignored */ term_to_slave(term, utf8, count); } } else term_to_slave(term, utf8, count); } if (utf8 != buf) free(utf8); term_reset_view(term); selection_cancel(term); 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 = xkb_state_mod_index_is_active( seat->kbd.xkb_state, seat->kbd.mod_shift, XKB_STATE_MODS_DEPRESSED); seat->kbd.alt = xkb_state_mod_index_is_active( seat->kbd.xkb_state, seat->kbd.mod_alt, XKB_STATE_MODS_DEPRESSED); seat->kbd.ctrl = xkb_state_mod_index_is_active( seat->kbd.xkb_state, seat->kbd.mod_ctrl, XKB_STATE_MODS_DEPRESSED); seat->kbd.meta = xkb_state_mod_index_is_active( seat->kbd.xkb_state, seat->kbd.mod_meta, XKB_STATE_MODS_DEPRESSED); } 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))); } static const char * xcursor_for_csd_border(struct terminal *term, int x, int y) { if (is_top_left(term, x, y)) return XCURSOR_TOP_LEFT_CORNER; else if (is_top_right(term, x, y)) return XCURSOR_TOP_RIGHT_CORNER; else if (is_bottom_left(term, x, y)) return XCURSOR_BOTTOM_LEFT_CORNER; else if (is_bottom_right(term, x, y)) return XCURSOR_BOTTOM_RIGHT_CORNER; else if (term->active_surface == TERM_SURF_BORDER_LEFT) return XCURSOR_LEFT_SIDE; else if (term->active_surface == TERM_SURF_BORDER_RIGHT) return XCURSOR_RIGHT_SIDE; else if (term->active_surface == TERM_SURF_BORDER_TOP) return XCURSOR_TOP_SIDE; else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) return XCURSOR_BOTTOM_SIDE; else { BUG("Unreachable"); return NULL; } } 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) { xassert(surface != NULL); if (surface == NULL) { /* Seen on mutter-3.38 */ return; } struct seat *seat = data; struct wl_window *win = wl_surface_get_user_data(surface); struct terminal *term = win->term; seat->pointer.serial = serial; seat->pointer.hidden = false; LOG_DBG("pointer-enter: pointer=%p, serial=%u, surface = %p, new-moused = %p", (void *)wl_pointer, serial, (void *)surface, (void *)term); /* Scale may have changed */ wayl_reload_xcursor_theme(seat, term->scale); seat->mouse_focus = term; int x = wl_fixed_to_int(surface_x) * term->scale; int y = wl_fixed_to_int(surface_y) * term->scale; switch ((term->active_surface = term_surface_kind(term, surface))) { case TERM_SURF_GRID: { /* * 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; term_xcursor_update_for_seat(term, seat); break; } case TERM_SURF_SEARCH: case TERM_SURF_SCROLLBACK_INDICATOR: case TERM_SURF_RENDER_TIMER: case TERM_SURF_JUMP_LABEL: case TERM_SURF_TITLE: render_xcursor_set(seat, term, XCURSOR_LEFT_PTR); break; case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: render_xcursor_set(seat, term, xcursor_for_csd_border(term, x, y)); break; case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: case TERM_SURF_BUTTON_CLOSE: render_xcursor_set(seat, term, XCURSOR_LEFT_PTR); 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; 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; seat->pointer.xcursor = NULL; } /* Reset mouse state */ seat->mouse.x = seat->mouse.y = 0; seat->mouse.col = seat->mouse.row = 0; 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)); seat->mouse.axis_aggregated = 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; term_xcursor_update_for_seat(old_moused, seat); switch (active_surface) { case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: case TERM_SURF_BUTTON_CLOSE: if (old_moused->is_shutting_down) break; render_refresh_csd(old_moused); break; case TERM_SURF_NONE: case TERM_SURF_GRID: case TERM_SURF_SEARCH: case TERM_SURF_SCROLLBACK_INDICATOR: case TERM_SURF_RENDER_TIMER: case TERM_SURF_JUMP_LABEL: 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; struct wayland *wayl = seat->wayl; struct terminal *term = seat->mouse_focus; 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; 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_SEARCH: case TERM_SURF_SCROLLBACK_INDICATOR: case TERM_SURF_RENDER_TIMER: case TERM_SURF_JUMP_LABEL: 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: render_xcursor_set(seat, term, xcursor_for_csd_border(term, x, y)); 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)); term_xcursor_update_for_seat(term, seat); /* 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; /* 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.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 && ((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->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 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); struct seat *seat = data; struct wayland *wayl = seat->wayl; struct terminal *term = seat->mouse_focus; 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) { /* Time since last click */ struct timeval now, since_last; gettimeofday(&now, NULL); timersub(&now, &seat->mouse.last_time, &since_last); if (seat->mouse.last_released_button == button && since_last.tv_sec == 0 && since_last.tv_usec <= 300 * 1000) { seat->mouse.count++; } else seat->mouse.count = 1; #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 (!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"); 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 (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); } } } 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_SEARCH: case TERM_SURF_SCROLLBACK_INDICATOR: case TERM_SURF_RENDER_TIMER: case TERM_SURF_JUMP_LABEL: 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)) { if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) { /* Seat has keyboard - use mouse bindings *with* modifiers */ xkb_mod_mask_t mods = xkb_state_serialize_mods( seat->kbd.xkb_state, XKB_STATE_MODS_DEPRESSED); /* Ignore Shift when matching modifiers, since it is * used to enable selection in mouse grabbing client * applications */ mods &= ~(1 << seat->kbd.mod_shift); const struct mouse_binding *match = NULL; tll_foreach(seat->mouse.bindings, it) { const struct mouse_binding *binding = &it->item; if (binding->button != button) { /* Wrong button */ continue; } if (binding->mods != mods) { /* Modifier mismatch */ continue; } if (binding->count > seat->mouse.count) { /* Not correct click count */ continue; } if (match == NULL || binding->count > match->count) match = binding; } if (match != NULL) { consumed = execute_binding( seat, term, match->action, match->pipe_argv, serial); } } else { /* Seat does NOT have a keyboard - use mouse bindings *without* modifiers */ const struct config_mouse_binding *match = NULL; const struct config *conf = seat->wayl->conf; for (size_t i = 0; i < conf->bindings.mouse.count; i++) { const struct config_mouse_binding *binding = &conf->bindings.mouse.arr[i]; if (binding->button != button) { /* Wrong button */ continue; } if (binding->count > seat->mouse.count) { /* Incorrect click count */ continue; } const struct config_key_modifiers no_mods = {0}; if (memcmp(&binding->modifiers, &no_mods, sizeof(no_mods)) != 0) { /* Binding has modifiers */ continue; } if (match == NULL || binding->count > match->count) match = binding; } if (match != NULL) { consumed = execute_binding( seat, term, match->action, match->pipe.argv, serial); } } } 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->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->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; 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) { struct terminal *term = seat->mouse_focus; xassert(term != NULL); int button = amount < 0 ? BTN_BACK : BTN_FORWARD; amount = abs(amount); if (term->mouse_tracking == MOUSE_NONE) { if (term->grid == &term->alt) { if (term->alt_scrolling) alternate_scroll(seat, amount, button); } else { if (button == BTN_BACK) cmd_scrollback_up(term, amount); else cmd_scrollback_down(term, amount); } } else if (!term_mouse_grabbed(term, seat) && 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->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); } term_mouse_up( term, button, seat->mouse.row, seat->mouse.col, seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); } } static void wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) { if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) return; struct seat *seat = data; if (seat->mouse.have_discrete) return; xassert(seat->mouse_focus != NULL); /* * Aggregate scrolled amount until we get at least 1.0 * * Without this, very slow scrolling will never actually scroll * anything. */ seat->mouse.axis_aggregated += seat->wayl->conf->scrollback.multiplier * wl_fixed_to_double(value); if (fabs(seat->mouse.axis_aggregated) < seat->mouse_focus->cell_height) return; int lines = seat->mouse.axis_aggregated / seat->mouse_focus->cell_height; mouse_scroll(seat, lines); seat->mouse.axis_aggregated -= (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) { if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) return; struct seat *seat = data; seat->mouse.have_discrete = true; mouse_scroll(seat, seat->wayl->conf->scrollback.multiplier * discrete); } static void wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) { struct seat *seat = data; 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) { if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) return; struct seat *seat = data; seat->mouse.axis_aggregated = 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, };