Alt+Tab / Alt+1 walks a window-level MRU stack, commits on release

Prefix+Tab and prefix+1 preview the session's windows most-recently-used
first: each press steps to the next window after the shown one in server.mru
order, wrapping at the bottom, without mutating the stack until the modifier
is released. Releasing after navigating commits the shown window to the
front; a bare hold leaves the order untouched. Focus changes with the
modifier up move the entered window to the front immediately.

Alt+1 is the chord that survives hosts like GNOME, which grab Alt+Tab,
Alt+Esc and Alt+Backtick (the latter via the metacity-era switch_group
default) before a nested compositor ever sees them; Alt+Tab is kept for
hosts that leave it free. Windows join the stack least-recent on allocation
and are removed on teardown; reachable stops mirror cycle_tab (live or
pending-claim, not closing). Holding the chord repeats via the existing
synthesized-repeat machinery.

Tests cover stepping, commit-on-release, the Alt+1 synonym, and hold-to-
repeat.
This commit is contained in:
Andrew Hamilton 2026-09-06 23:52:03 +10:00
parent ca7f1f2852
commit 1f35f90a95
3 changed files with 274 additions and 6 deletions

191
kick.c
View file

@ -302,6 +302,15 @@ struct kick_window {
window the tab is currently displaying, so this cache lets the overlay
log one line per actual change. */
char row_title[128];
/* Node in the global MRU window stack (server.mru, most-recent first).
Every window joins at the tail on allocation and bubbles to the front
when it becomes current -- except while the tab-management prefix is
held (overlay visible), when focus changes are only previews and the
commit happens on prefix release. mru_linked guards removal: a window
is linked from window_alloc until it is freed. */
struct wl_list mru_link;
bool mru_linked;
};
struct kick_server {
@ -494,6 +503,18 @@ struct kick_server {
/* Tabs, in overlay/cycle order: a list of kick_tab, each holding its
window list. */
struct wl_list tabs;
/* MRU window stack, most-recent first: a list of kick_window.mru_link
covering every live window (plus still-black pending placeholders, so
Alt+Tab/Alt+1 can land on them like Alt+Up/Down can). prefix+Tab/1 walk
down this order without mutating it; the shown window moves to the
front on prefix release (or immediately when focus changes with the
prefix up). */
struct wl_list mru;
/* The window that was current when the prefix went down (overlay shown),
and whether the user navigated anywhere since. On prefix release a
navigated session commits the then-current window to the MRU front. */
struct kick_window *mru_alt_anchor;
bool mru_alt_navigated;
/* wlr-layer-shell (waybar / panels). Layer surfaces render into their
own always-on-top scene tree (layer_tree) instead of being tabs, so a
@ -668,9 +689,10 @@ struct kick_host {
int repeat_dir;
/* What each repeat tick should do: REPEAT_CYCLE switches tabs,
REPEAT_REORDER moves the active tab within the list, REPEAT_SUBCYCLE
rotates the active tab's stacked windows. */
rotates the active tab's stacked windows, REPEAT_MRU steps down the
MRU window stack. */
enum { REPEAT_CYCLE, REPEAT_REORDER, REPEAT_SUBCYCLE,
REPEAT_MEMBER_MOVE } repeat_kind;
REPEAT_MEMBER_MOVE, REPEAT_MRU } repeat_kind;
};
static struct kick_server server;
@ -893,6 +915,12 @@ static int cmd_in_path(const char *cmd);
static void insert_tab_after(struct kick_tab *tab, struct kick_tab *anchor);
static void launch_client(struct kick_window *w);
static void launch_all_cli(void);
/* MRU window-stack bodies live with the focus helpers; called from
window_alloc/teardown, window_enter and the prefix+Tab/1 key handling. */
static void mru_move_to_front(struct kick_window *w);
static void mru_remove(struct kick_window *w);
static struct kick_window *mru_current(void);
static void mru_step(void);
#if HAVE_XWAYLAND
/* XWayland surface handlers (defined with the other toplevel handlers). */
@ -1278,6 +1306,9 @@ static void shutdown_cleanup(void) {
free(tab);
}
wl_list_init(&server.tabs);
wl_list_init(&server.mru);
server.mru_alt_anchor = NULL;
server.mru_alt_navigated = false;
/* Detach + free the standalone native keyboard devices. */
struct standalone_kbdev *kd, *kd_tmp;
@ -2877,6 +2908,16 @@ static void window_enter(struct kick_window *w) {
lock surface (which is focused directly, outside the tab model); the
lock restore path clears session_locked before refocusing. */
if (!w || server.session_locked) return;
/* MRU bookkeeping: with the prefix up the entered window is the new most
recent; with the prefix held it is only a preview -- the stack must
not move until the prefix is released, but the session counts as
navigated when the preview leaves its anchor. */
if (host.overlay_visible) {
if (w != server.mru_alt_anchor)
server.mru_alt_navigated = true;
} else {
mru_move_to_front(w);
}
struct kick_tab *tab = w->tab;
/* A genuine no-op: already focused AND already drawn. The black ->
mapped promotion below is the one same-window case with something
@ -3007,6 +3048,7 @@ static void remove_tab(struct kick_tab *tab) {
if (w->is_x11)
window_detach_x11_listeners(w);
#endif
mru_remove(w);
free(w->cmd);
free(w);
}
@ -3054,6 +3096,7 @@ static void window_destroy_common(struct kick_window *w) {
if (w->icon_wlr)
wlr_texture_destroy(w->icon_wlr);
icon_proto_release(w);
mru_remove(w);
free(w);
if (wl_list_empty(&tab->windows)) {
drop_tab(tab);
@ -3692,6 +3735,11 @@ static struct kick_window *window_alloc(struct kick_tab *tab) {
if (!w) return NULL;
w->tab = tab;
wl_list_insert(tab->windows.prev, &w->link);
/* New windows start least-recent: background arrivals must not hijack
Alt+Tab. The window bubbles to the front when it becomes current
(window_enter with the prefix up). */
wl_list_insert(server.mru.prev, &w->mru_link);
w->mru_linked = true;
return w;
}
@ -5081,6 +5129,78 @@ static void handle_shape_set(struct wl_listener *listener, void *data) {
apply_cursor_shape(ev->shape);
}
/* ===================== MRU window stack =====================
Alt+Tab / Alt+1 walk the windows most-recently-used-first: each press
previews the next window after the shown one in server.mru order, wrapping
at the bottom, without mutating the stack. The shown window moves to the
front when the prefix is released (see update_overlay_visible), or
immediately when focus changes with the prefix up (window_enter). */
static void mru_move_to_front(struct kick_window *w) {
if (!w || !w->mru_linked) return;
if (server.mru.next == &w->mru_link) return; /* already most recent */
wl_list_remove(&w->mru_link);
wl_list_insert(&server.mru, &w->mru_link);
}
static void mru_remove(struct kick_window *w) {
if (!w || !w->mru_linked) return;
wl_list_remove(&w->mru_link);
w->mru_linked = false;
/* The anchor is only compared by pointer on prefix release; never
dereferenced, but clear it so a destroyed anchor can't masquerade as
"no navigation happened" logic elsewhere. */
if (server.mru_alt_anchor == w)
server.mru_alt_anchor = NULL;
}
/* The window the user is currently looking at: the focused window of the
active tab (a still-black pending placeholder when the tab awaits its
client). */
static struct kick_window *mru_current(void) {
return tab_current_window(server.active_tab);
}
/* Whether a window is a reachable Alt+Tab stop: it holds (or awaits) a
surface and is not being closed. Mirrors tab_overlay_row/cycle_tab so
Alt+Tab lands everywhere Alt+Up/Down can. */
static bool mru_reachable(struct kick_window *w) {
if (!w || w->closing) return false;
return window_live(w) || w->pending_claim;
}
/* Preview the next window after the shown one in MRU order, wrapping to the
most recent. With a single reachable window this re-shows it (a no-op
that still counts as navigation, so release commits it to the front). */
static void mru_step(void) {
if (wl_list_empty(&server.mru)) return;
struct kick_window *cur = mru_current();
struct kick_window *order[512];
int n = 0;
struct kick_window *w;
wl_list_for_each(w, &server.mru, mru_link) {
if (n < 512 && mru_reachable(w))
order[n++] = w;
}
if (n == 0) return;
int idx = -1;
for (int i = 0; i < n; i++) {
if (order[i] == cur) { idx = i; break; }
}
/* idx < 0 (shown window not in the stack) falls through to order[0]:
the most recent window. Otherwise the first press from the top lands
on order[1], the second-from-top, as specified. */
struct kick_window *target = order[(idx + 1) % n];
if (target->tab != server.active_tab)
set_active_tab(target->tab);
/* set_active_tab enters the tab's current window, which may be a
different stacked member; pin the exact MRU target. Harmless when
target is already shown (window_enter no-ops on focused+enabled). */
window_enter(target);
server.mru_alt_navigated = true;
kick_log(KICK_LOG_INFO, "[mru] preview app=%d\n", tab_index(target->tab));
}
/* Switch to the next live tab in `dir` direction (Alt+Down: +1), wrapping
around the end of the tab list, skipping tabs that are mid-close. With
only the current tab live, does nothing. */
@ -5201,7 +5321,9 @@ static int kbd_repeat_cb(void *data) {
struct kick_window *w = tab_current_window(server.active_tab);
if (w)
reorder_window(w, host.repeat_dir);
} else
} else if (host.repeat_kind == REPEAT_MRU)
mru_step();
else
cycle_tab(host.repeat_dir);
if (host.repeat_timer)
wl_event_source_timer_update(host.repeat_timer,
@ -7960,6 +8082,27 @@ static void handle_window_meta(struct wl_listener *l, void *data) {
static void update_overlay_visible(bool alt) {
if (alt == host.overlay_visible) return;
if (alt) {
/* Prefix went down: snapshot the shown window. Every focus change
from here until release is a preview -- window_enter leaves the
MRU stack alone -- and marks the session navigated. */
server.mru_alt_anchor = mru_current();
server.mru_alt_navigated = false;
} else {
/* Prefix released: the window we are on becomes most recent, but
only if we actually moved. A bare prefix tap/hold that never
navigates must not reshuffle the stack. */
if (server.mru_alt_navigated) {
struct kick_window *cur = mru_current();
if (cur) {
mru_move_to_front(cur);
kick_log(KICK_LOG_INFO, "[mru] commit app=%d\n",
tab_index(cur->tab));
}
}
server.mru_alt_anchor = NULL;
server.mru_alt_navigated = false;
}
host.overlay_visible = alt;
/* The overlay draws as a second pass on top of the composited frame, so
while it is visible direct scanout must yield to the composite path:
@ -8141,6 +8284,36 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
}
return true;
}
/* prefix+Tab / prefix+1 walk the MRU window stack one step per press
(hold to keep walking via REPEAT_MRU): each press previews the next
window after the shown one, wrapping at the bottom, without touching
the stack itself -- the shown window moves to the front on prefix
release. Shift is ignored (no reverse direction): with Shift held
this still walks forward so no stray Tab/1 leaks to the client
mid-menu. Alt+1 is the reliable MRU chord on hosts that grab the
switcher keys: GNOME takes Alt+Tab, Alt+Esc AND Alt+Backtick (the
metacity-era switch_group default) before a nested compositor ever
sees them, but leaves prefix+1 free; Alt+Tab is kept for hosts that
don't grab it. Grabbed so the focused client never sees the chord. */
{
bool is_tab = server.xkb_state
? (sym == XKB_KEY_Tab || sym == XKB_KEY_ISO_Left_Tab)
: key == 15;
bool is_one = server.xkb_state
? (sym == XKB_KEY_1)
: key == 2;
if (alt && (is_tab || is_one)) {
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (host.repeat_key != key) {
mru_step();
kbd_repeat_start(key, 1, REPEAT_MRU);
}
} else {
kbd_repeat_cancel();
}
return true;
}
}
/* Alt+Left / Alt+Right rotate the active tab's windows: Right
steps forward to the next window, Left steps backward to the previous
one, wrapping at the ends. Grabbed (consumed) so the focused client
@ -10848,6 +11021,10 @@ int main(int argc, char *argv[]) {
"Keys (hold the modifier):\n"
" Up / Down Cycle tabs; hold to keep "
"cycling.\n"
" Tab / 1 Walk windows most-recently-used first; "
"hold to keep\n"
" walking. The stack only changes on "
"release.\n"
" Shift+Up / Shift+Down Reorder the active tab.\n"
" Delete Close the active window (or the tab when it is the only window).\n"
" Shift+T Open a new terminal tab "
@ -11603,6 +11780,9 @@ int main(int argc, char *argv[]) {
handle_new_toplevel puts each client on its own slot no matter which
connects first). */
wl_list_init(&server.tabs);
wl_list_init(&server.mru);
server.mru_alt_anchor = NULL;
server.mru_alt_navigated = false;
int focus_idx = start_cmd_index >= 0 ? start_cmd_index : total_apps - 1;
struct kick_tab *focus_tab = NULL;
@ -11662,10 +11842,11 @@ int main(int argc, char *argv[]) {
const char *prefix = tab_prefix == PREFIX_SUPER ? "Super" : "Alt";
kick_log(KICK_LOG_INFO, "kick: running. "
"%s+Up/Down switches tabs; %s+Shift+Up/Down reorders; "
"%s+Up/Down switches tabs; %s+Tab/%s+1 walks recent windows; "
"%s+Shift+Up/Down reorders; "
"%s+Delete closes the active window (or the tab when it's the only window); "
"%s+Shift+T opens a terminal (%s).\n",
prefix, prefix, prefix, prefix, term_cmd);
prefix, prefix, prefix, 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