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:
parent
ca7f1f2852
commit
1f35f90a95
3 changed files with 274 additions and 6 deletions
191
kick.c
191
kick.c
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue