add Alt+ backtick to cycle a tab's stacked windows backward

This commit is contained in:
Andrew Hamilton 2026-08-22 08:19:08 +10:00
parent 1ef4e4423c
commit 753b37ee7e
2 changed files with 83 additions and 2 deletions

View file

@ -196,6 +196,7 @@ model.
| `Alt+Shift+Down` | Move the active tab later in the list |
| `Alt+Delete` | Close the active tab (SIGTERM, SIGKILL fallback) |
| `Alt+Shift+T` | Open a new terminal tab next to the current one |
| `` Alt+` `` | Cycle the active tab's stacked windows (hold to keep cycling) |
The tab-management prefix is `Alt` by default. Pass `--modifier super` to
use `Super` (Windows/Meta key, Mod4) instead: `Super` hold shows the

84
kick.c
View file

@ -458,8 +458,9 @@ struct kick_host {
uint32_t repeat_key;
int repeat_dir;
/* What each repeat tick should do: REPEAT_CYCLE switches tabs,
REPEAT_REORDER moves the active tab within the list. */
enum { REPEAT_CYCLE, REPEAT_REORDER } repeat_kind;
REPEAT_REORDER moves the active tab within the list, REPEAT_SUBCYCLE
rotates the active tab's stacked windows. */
enum { REPEAT_CYCLE, REPEAT_REORDER, REPEAT_SUBCYCLE } repeat_kind;
};
static struct kick_server server;
@ -2905,6 +2906,60 @@ static void sub_refocus_owner(struct kick_app *app) {
kick_request_redraw();
}
/* Rotate the active tab's window stack one step in `dir` (-1 = backward):
the rotation visits every mapped stacked sub-window in creation order,
then the main surface, then wraps. Backward from a window means the
one stacked just before it; backward from the oldest is the main
surface; backward from the main surface wraps to the newest. The list
order itself is never mutated -- focus_sub alone tracks where the
rotation sits, so repeated steps walk the windows deterministically.
Returns false when the tab has no mapped sub-windows (nothing to
rotate), leaving the key free to pass through to the client. */
static bool cycle_stack(struct kick_app *app, int dir) {
struct kick_sub_window *subs[16];
struct kick_sub_window *sub;
int n = 0;
wl_list_for_each(sub, &app->sub_windows, link)
if (sub->mapped && n < 16) subs[n++] = sub;
if (n == 0)
return false;
/* Position of the shown window in the rotation [w0..w_{n-1}, MAIN]:
-1 when the main surface is showing (or nothing is yet). */
int cur = -1;
for (int i = 0; i < n; i++)
if (subs[i] == app->focus_sub) { cur = i; break; }
int next = (cur + dir) % (n + 1);
if (next < 0) next += n + 1;
if (next == n) {
/* MAIN's turn: drop keyboard focus from the stacked sub;
show_current() below reveals the main surface and hides every
sub, making it drawn and reachable again. */
if (app->focus_sub) {
sub_set_activated(app->focus_sub, false);
app->focus_sub = NULL;
}
if (app_has_window(app)) {
app_set_activated(app, true);
struct wlr_keyboard *kbd = wlr_seat_get_keyboard(server.seat);
if (!kbd) kbd = &server.keyboard;
wlr_seat_keyboard_notify_enter(server.seat,
app_surface(app),
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
}
} else {
sub_enter(subs[next]);
}
show_current(app);
kick_log(KICK_LOG_INFO, "[sub] cycle app=%d\n", app_index(app));
kick_request_redraw();
return true;
}
static void handle_sub_commit(struct wl_listener *listener, void *data) {
(void)data;
struct kick_sub_window *sub = wl_container_of(listener, sub, commit);
@ -4158,6 +4213,8 @@ static int kbd_repeat_cb(void *data) {
return 0;
if (host.repeat_kind == REPEAT_REORDER)
reorder_active_tab(host.repeat_dir);
else if (host.repeat_kind == REPEAT_SUBCYCLE)
cycle_stack(server.active_app, host.repeat_dir);
else
cycle_tab(host.repeat_dir);
if (host.repeat_timer)
@ -5999,6 +6056,27 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
}
return true;
}
/* Alt+` rotates the active tab's stacked windows backward: from the
shown window to the one added just before it, wrapping from the
oldest through the main surface back to the newest. Consumed only
when the tab actually has stacked windows, so a client that binds
Alt+` itself still sees the key elsewhere. */
{ bool is_grave = server.xkb_state
? (sym == XKB_KEY_grave)
: key == 41;
if (alt && is_grave && server.active_app &&
!wl_list_empty(&server.active_app->sub_windows)) {
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
if (host.repeat_key != key) {
cycle_stack(server.active_app, -1);
kbd_repeat_start(key, -1, REPEAT_SUBCYCLE);
}
} else {
kbd_repeat_cancel();
}
return true;
}
}
/* 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
@ -7561,6 +7639,8 @@ int main(int argc, char *argv[]) {
" Delete Close the active tab.\n"
" Shift+T Open a new terminal tab "
"($TERMINAL).\n"
" ` (grave) Cycle the tab's stacked "
"windows; hold to keep cycling.\n"
" Ctrl+Alt+F1..F12 Switch virtual terminals "
"(standalone only).\n",
argv[0], argv[0]);