add --modifier alt|super to switch tab chords from Alt to Super

Standalone/DRM and labwc-style hosts (e.g. RPi5) keep Super free, while
GNOME steals it, so Alt stays the default.  The tab-management prefix
modifier (hold=overlay, Up/Down=cycle, Shift+Up/Down=reorder,
Delete=close) is now chosen at startup from the xkb keymap (Mod1 or
Mod4) with a raw wl_keyboard bit fallback when xkb init fails;
Ctrl+Shift+T is unchanged.
This commit is contained in:
Andrew Hamilton 2026-08-17 11:44:27 +10:00
parent f6c1a6b92e
commit e0af748a4b
3 changed files with 145 additions and 25 deletions

View file

@ -170,7 +170,9 @@ 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;
/* Modifier index of the tab-management prefix (Alt or Super/Mod4,
per --modifier). */
xkb_mod_index_t xkb_prefix_mod;
/* A client's fullscreen request is honored and mirrored onto the host
window, and vice versa. */
@ -279,7 +281,15 @@ static bool shutting_down;
so it may carry arguments. */
static const char *term_cmd = "alacritty";
static bool term_key_held; /* Ctrl+Shift+T currently down (repeat latch) */
static bool delete_key_held; /* Alt+Delete currently down (repeat latch) */
static bool delete_key_held; /* prefix+Delete currently down (repeat latch) */
/* The modifier prefix for the tab-management chords: hold = show overlay,
Up/Down = cycle, Shift+Up/Down = reorder, Delete = close. Defaults to
Alt; --modifier super selects Super (Mod4), which is a good fit for
standalone/DRM and labwc-style hosts that do not reserve it the way
GNOME does. */
enum tabbed_prefix { PREFIX_ALT, PREFIX_SUPER };
static enum tabbed_prefix tab_prefix = PREFIX_ALT;
/* ===================== Clipboard bridge ===================== */
@ -3324,8 +3334,12 @@ static void xkb_state_reload(void)
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);
server.xkb_prefix_mod = xkb_keymap_mod_get_index(server.xkb_keymap,
tab_prefix == PREFIX_SUPER ? XKB_MOD_NAME_MOD4
: XKB_MOD_NAME_ALT);
tabbed_log(TABBED_LOG_DEBUG, "tabbed-wlroots: prefix modifier index=%u "
"(%s)\n", (unsigned)server.xkb_prefix_mod,
tab_prefix == PREFIX_SUPER ? "Super/Mod4" : "Alt");
}
static void host_kbd_keymap(void *data, struct wl_keyboard *kbd, uint32_t format,
@ -3692,6 +3706,18 @@ static struct wl_listener standalone_cursor_motion_listener = {
.notify = standalone_cursor_motion,
};
/* Is the configured tab-management prefix (Alt, or Super/Mod4 with
--modifier super) currently pressed? Uses the xkb modifier index when a
keymap is compiled; with no keymap (xkb init failed) it falls back to the
raw wl_keyboard modifier bit: mod1/alt = 1<<3, mod4/super = 1<<6. */
static bool prefix_active(uint32_t mods_depressed) {
if (server.xkb_state)
return xkb_state_mod_index_is_active(server.xkb_state,
server.xkb_prefix_mod, XKB_STATE_MODS_EFFECTIVE);
return (mods_depressed & (tab_prefix == PREFIX_SUPER ? (1u << 6)
: (1u << 3))) != 0;
}
/* Shared key handling for both the host keyboard and native input devices:
tab-switch bindings + overlay visibility, with the caller forwarding the
key to the seat afterwards. `mods_depressed` uses the wl_keyboard modifier
@ -3710,10 +3736,7 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
: XKB_KEY_NoSymbol;
/* With no compiled keymap (xkb init failed) fall back to the raw
evdev keycodes / modifier bits for the hotkey matching below. */
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;
bool alt = prefix_active(mods_depressed);
if (alt != host.overlay_visible)
update_overlay_visible(alt);
char symbuf[32];
@ -3787,7 +3810,7 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
}
return true;
}
/* Alt+Delete closes the active tab. Grabbed so the focused client never
/* prefix+Delete closes the active tab. Grabbed so the focused client never
sees the key; a latch drops host-synthesized repeats so one hold can't
close a tab per repeat. The active surface already moved to the right
neighbor inside close_active_tab(), so the tab is gone this frame even
@ -3852,10 +3875,7 @@ static void process_modifiers(uint32_t mods_depressed, uint32_t mods_latched,
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;
bool alt = prefix_active(mods_depressed);
if (alt != host.overlay_visible)
update_overlay_visible(alt);
if (host.repeat_key && !alt)
@ -4992,9 +5012,10 @@ int main(int argc, char *argv[]) {
/* Ctrl+Shift+T opens a terminal named by $TERMINAL when set, else the
alacritty default. Every command-line argument is one tab launcher,
except --screen-mode WxH[@Hz], which selects the native output mode and
is consumed here. Handle all of this up front so a missing command (or
a bad request) can be reported and the process can exit before any
window is created. */
is consumed here, and --modifier alt|super, which picks the modifier
prefix for the tab-management chords. Handle all of this up front so a
missing command (or a bad request) can be reported and the process can
exit before any window is created. */
{
const char *env_term = getenv("TERMINAL");
if (env_term && env_term[0])
@ -5009,11 +5030,15 @@ int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
fprintf(stderr,
"usage: %s [--screen-mode WxH[@Hz]] [prog...]\n"
"usage: %s [--screen-mode WxH[@Hz]] [--modifier alt|super] "
"[prog...]\n"
" Select the native output mode (standalone only): "
"e.g. --screen-mode 1920x1080\n"
" or --screen-mode 1920x1080@60. A request nothing "
"supports prints every mode and exits.\n"
" --modifier picks the tab-management modifier "
"(default alt); 'super' suits standalone/DRM and "
"labwc-style hosts.\n"
" Each <prog> is one shell command; quote it when it "
"takes args, e.g. \"firefox -no-remote\".\n"
" With no arguments, a single terminal tab opens "
@ -5033,6 +5058,38 @@ int main(int argc, char *argv[]) {
val = argv[++i];
} else if (strncmp(argv[i], "--screen-mode=", 14) == 0) {
val = argv[i] + 14;
} else if (strcmp(argv[i], "--modifier") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "tabbed-wlroots: --modifier needs a value, "
"e.g. --modifier super\n");
free(appv);
return 2;
}
const char *m = argv[++i];
if (strcmp(m, "alt") == 0) {
tab_prefix = PREFIX_ALT;
} else if (strcmp(m, "super") == 0) {
tab_prefix = PREFIX_SUPER;
} else {
fprintf(stderr, "tabbed-wlroots: bad --modifier '%s' "
"(use 'alt' or 'super')\n", m);
free(appv);
return 2;
}
continue;
} else if (strncmp(argv[i], "--modifier=", 11) == 0) {
const char *m = argv[i] + 11;
if (strcmp(m, "alt") == 0) {
tab_prefix = PREFIX_ALT;
} else if (strcmp(m, "super") == 0) {
tab_prefix = PREFIX_SUPER;
} else {
fprintf(stderr, "tabbed-wlroots: bad --modifier '%s' "
"(use 'alt' or 'super')\n", m);
free(appv);
return 2;
}
continue;
}
if (val) {
if (!parse_screen_mode(val)) {
@ -5366,9 +5423,12 @@ int main(int argc, char *argv[]) {
launch_client(i);
}
const char *prefix = tab_prefix == PREFIX_SUPER ? "Super" : "Alt";
tabbed_log(TABBED_LOG_INFO, "tabbed-wlroots: running. "
"Alt+Up/Down switches tabs; Alt+Delete closes the active tab; "
"Ctrl+Shift+T opens a terminal (%s).\n", term_cmd);
"%s+Up/Down switches tabs; %s+Shift+Up/Down reorders; "
"%s+Delete closes the active tab; "
"Ctrl+Shift+T opens a terminal (%s).\n",
prefix, prefix, prefix, term_cmd);
/* Standalone event loop: no host display, so drive the server loop alone.
The native output's frame signal (or the headless 60Hz timer) wakes the