Cycle tabs with Alt+Up/Down matched by keysym

Ctrl+PageUp/PageDown and Ctrl+Tab are needed by real apps (browser tabs,
terminal scrollback), and a tabbed-wlroots hotkey consumes the combo before
the nested client ever sees it.  Alt+Up/Down is free on both the
GNOME host (no wm/mutter/shell binding) and labwc, so the keys reach the
compositor instead of being grabbed or swallowed. Alt+Up/Down is used in
some apps but in minor ways. I tried to use Alt+PageUp/PageDown but it
fails on some laptops like mine.
This commit is contained in:
Andrew Hamilton 2026-08-14 08:48:28 +10:00
parent a86a21d76e
commit 54c3c99e1d
3 changed files with 90 additions and 30 deletions

View file

@ -116,6 +116,8 @@ struct tabbed_server {
struct xkb_context *xkb_ctx;
struct xkb_keymap *xkb_keymap;
struct xkb_state *xkb_state;
xkb_mod_index_t xkb_alt_mod;
};
/* ===================== Host-side (nested) state ===================== */
@ -359,6 +361,7 @@ static void shutdown_cleanup(void) {
}
free(server.apps);
server.apps = NULL;
if (server.xkb_state) xkb_state_unref(server.xkb_state);
if (server.xkb_keymap) xkb_keymap_unref(server.xkb_keymap);
if (server.xkb_ctx) xkb_context_unref(server.xkb_ctx);
bridge_cleanup();
@ -1818,6 +1821,23 @@ static const struct zxdg_toplevel_decoration_v1_listener host_decoration_listene
/* ---------- host keyboard ---------- */
/* (Re)build the xkb_state used to translate raw evdev keycodes into
keysyms for host hotkey matching. The state is derived from whatever
keymap is current (host-provided or fallback), so the hotkeys work on
any keyboard/layout without hardcoded keycodes. */
static void xkb_state_reload(void)
{
if (server.xkb_state) {
xkb_state_unref(server.xkb_state);
server.xkb_state = NULL;
}
if (!server.xkb_keymap)
return;
server.xkb_state = xkb_state_new(server.xkb_keymap);
server.xkb_alt_mod =
xkb_keymap_mod_get_index(server.xkb_keymap, XKB_MOD_NAME_ALT);
}
static void host_kbd_keymap(void *data, struct wl_keyboard *kbd, uint32_t format,
int32_t fd, uint32_t size)
{
@ -1865,6 +1885,7 @@ static void host_kbd_keymap(void *data, struct wl_keyboard *kbd, uint32_t format
if (server.xkb_keymap)
xkb_keymap_unref(server.xkb_keymap);
server.xkb_keymap = km;
xkb_state_reload();
tabbed_log(TABBED_LOG_DEBUG, "tabbed-wlroots: host keymap parsed OK\n");
} else {
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: failed to parse host "
@ -2030,11 +2051,29 @@ static void host_kbd_key(void *data, struct wl_keyboard *kbd, uint32_t serial,
return;
(void)kbd;
bridge.kbd_serial = serial;
if (server.xkb_state)
xkb_state_update_key(server.xkb_state, key + 8,
state == WL_KEYBOARD_KEY_STATE_PRESSED
? XKB_KEY_DOWN : XKB_KEY_UP);
xkb_keysym_t sym = server.xkb_state
? xkb_state_key_get_one_sym(server.xkb_state, key + 8)
: XKB_KEY_NoSymbol;
/* With no compiled keymap (xkb init failed) fall back to the raw
evdev keycodes. */
bool keysym_fallback = !server.xkb_state;
bool alt = server.xkb_state
? xkb_state_mod_index_is_active(server.xkb_state,
server.xkb_alt_mod, XKB_STATE_MODS_EFFECTIVE)
: (host.host_mods_depressed & (1u << 3)) != 0;
char symbuf[32];
xkb_keysym_get_name(sym, symbuf, sizeof(symbuf));
tabbed_log(TABBED_LOG_DEBUG,
"[kbd] key=%u state=%u ctrl=%d repeat_key=%u\n", key, state,
(host.host_mods_depressed & (1u << 2)) != 0, host.repeat_key);
"[kbd] key=%u state=%u sym=%s ctrl=%d alt=%d repeat_key=%u\n",
key, state, sym ? symbuf : "-",
(host.host_mods_depressed & (1u << 2)) != 0, alt, host.repeat_key);
/* F11 toggles the host window fullscreen. */
if (state == WL_KEYBOARD_KEY_STATE_PRESSED && key == 87) {
if (state == WL_KEYBOARD_KEY_STATE_PRESSED &&
(sym == XKB_KEY_F11 || (keysym_fallback && key == 87))) {
host.fullscreen = !host.fullscreen;
tabbed_log(TABBED_LOG_INFO, "host fullscreen=%d\n", host.fullscreen);
if (host.fullscreen)
@ -2045,26 +2084,36 @@ static void host_kbd_key(void *data, struct wl_keyboard *kbd, uint32_t serial,
}
/* Temporary: F9/F10 frame dumps for the grey-band diagnosis. */
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (key == 67) /* F9 */ {
if (sym == XKB_KEY_F9 || (keysym_fallback && key == 67)) {
dump_scene_req = true;
needs_redraw = true;
return;
}
if (key == 68) /* F10 */ {
if (sym == XKB_KEY_F10 || (keysym_fallback && key == 68)) {
dump_host_req = true;
needs_redraw = true;
return;
}
}
/* Ctrl+PageUp / Ctrl+PageDown cycle tabs. The host sends a single
physical press, so repeats are synthesized by kbd_repeat_start();
subsequent host presses of an already-held key are ignored so a
host-sent repeat never cycles twice. */
bool ctrl = (host.host_mods_depressed & (1u << 2)) != 0;
if (ctrl && (key == 104 || key == 109)) {
/* Alt+Up / Alt+Down cycle tabs. The host sends a single physical
press, so repeats are synthesized by kbd_repeat_start(); subsequent
host presses of an already-held key are ignored so a host-sent
repeat never cycles twice. Plain arrow keys (not Fn+arrow) are used
because many laptop firmwares swallow Alt+Fn+PageUp (the Fn key
reports as XF86WakeUp there), which makes Alt+PageUp/PageDown
unreliable. */
bool prev, next;
if (server.xkb_state) {
prev = sym == XKB_KEY_Up;
next = sym == XKB_KEY_Down;
} else {
prev = key == 103;
next = key == 108;
}
if (alt && (prev || next)) {
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (host.repeat_key != key) {
int dir = key == 109 ? 1 : -1;
int dir = next ? 1 : -1;
cycle_tab(dir);
kbd_repeat_start(key, dir);
}
@ -2095,7 +2144,14 @@ static void host_kbd_modifiers(void *data, struct wl_keyboard *kbd, uint32_t ser
return;
(void)kbd;
host.host_mods_depressed = mods_depressed;
if (host.repeat_key && !(mods_depressed & (1u << 2)))
if (server.xkb_state)
xkb_state_update_mask(server.xkb_state, mods_depressed,
mods_latched, mods_locked, group, group, group);
bool alt = server.xkb_state
? xkb_state_mod_index_is_active(server.xkb_state,
server.xkb_alt_mod, XKB_STATE_MODS_EFFECTIVE)
: (mods_depressed & (1u << 3)) != 0;
if (host.repeat_key && !alt)
kbd_repeat_cancel();
wlr_keyboard_notify_modifiers(&server.keyboard, mods_depressed,
mods_latched, mods_locked, group);
@ -2887,8 +2943,10 @@ int main(int argc, char *argv[]) {
};
server.xkb_keymap = xkb_keymap_new_from_names(
server.xkb_ctx, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
if (server.xkb_keymap)
if (server.xkb_keymap) {
wlr_keyboard_set_keymap(&server.keyboard, server.xkb_keymap);
xkb_state_reload();
}
}
wlr_seat_set_keyboard(server.seat, &server.keyboard);
@ -2976,7 +3034,7 @@ int main(int argc, char *argv[]) {
}
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: running. "
"Ctrl+PageUp/PageDown switches tabs.\n");
"Alt+Up/Down switches tabs.\n");
/* Main event loop: service both the server and the host displays */
int server_fd = wl_event_loop_get_fd(server.loop);