close the active window with Alt+Delete, not the whole tab
This commit is contained in:
parent
118f5c276a
commit
39329347f6
4 changed files with 93 additions and 15 deletions
12
README.md
12
README.md
|
|
@ -199,7 +199,7 @@ every surface individually reachable, which is kick's original model.
|
||||||
| `Alt+Down` | Next tab (hold to keep cycling) |
|
| `Alt+Down` | Next tab (hold to keep cycling) |
|
||||||
| `Alt+Shift+Up` | Move the active tab earlier in the list |
|
| `Alt+Shift+Up` | Move the active tab earlier in the list |
|
||||||
| `Alt+Shift+Down` | Move the active tab later in the list |
|
| `Alt+Shift+Down` | Move the active tab later in the list |
|
||||||
| `Alt+Delete` | Close the active tab (SIGTERM, SIGKILL fallback) |
|
| `Alt+Delete` | Close the active window (or the tab when it's the only window) |
|
||||||
| `Alt+Shift+T` | Open a new terminal tab next to the current one |
|
| `Alt+Shift+T` | Open a new terminal tab next to the current one |
|
||||||
| `` Alt+` `` | Cycle the active tab's tab-group windows (hold to keep cycling) |
|
| `` Alt+` `` | Cycle the active tab's tab-group windows (hold to keep cycling) |
|
||||||
|
|
||||||
|
|
@ -247,10 +247,12 @@ Both `Alt+Up/Down` and `Alt+Shift+Up/Down` are grabbed: kick consumes them
|
||||||
so the focused client never receives the chord (e.g. a terminal can't act on
|
so the focused client never receives the chord (e.g. a terminal can't act on
|
||||||
`Shift`+arrow while you reorder).
|
`Shift`+arrow while you reorder).
|
||||||
|
|
||||||
`Alt+Delete` closes the active tab. The tab disappears immediately (the tab
|
`Alt+Delete` closes the active window. When the tab holds more than one window
|
||||||
to its right becomes active, or the left neighbor if it was the rightmost);
|
(a tab group), only the focused window closes and the tab stays up; when it is
|
||||||
the client's process group gets a `SIGTERM`, escalated to `SIGKILL` after a
|
the tab's only window, the whole tab disappears immediately (the tab to its
|
||||||
short grace if it ignores the signal. A tab that closes on its own (its
|
right becomes active, or the left neighbor if it was the rightmost); the
|
||||||
|
client's process group gets a `SIGTERM`, escalated to `SIGKILL` after a short
|
||||||
|
grace if it ignores the signal. A window or tab that closes on its own (its
|
||||||
program exits) instead hands active off to the left neighbor.
|
program exits) instead hands active off to the left neighbor.
|
||||||
|
|
||||||
Function keys are **not** grabbed: `F9`/`F10`/`F11` (and anything else) are
|
Function keys are **not** grabbed: `F9`/`F10`/`F11` (and anything else) are
|
||||||
|
|
|
||||||
56
kick.c
56
kick.c
|
|
@ -2619,6 +2619,35 @@ static void close_tab(struct kick_app *app) {
|
||||||
wl_event_source_timer_update(app->close_timer, KICK_CLOSE_ESCALATE_MS);
|
wl_event_source_timer_update(app->close_timer, KICK_CLOSE_ESCALATE_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Alt+Delete on the active tab closes the active WINDOW, not the whole tab:
|
||||||
|
the focused stack member (a stacked tab-group window) is asked to close on
|
||||||
|
its own and the tab stays up; only when the tab holds a single window (its
|
||||||
|
wrapped main surface) does Delete take the whole tab down. The member's
|
||||||
|
own destroy path (member_destroy_common) unstacks it and refocuses the tab,
|
||||||
|
just as if the client had closed it directly. */
|
||||||
|
static void close_active_window(void) {
|
||||||
|
struct kick_app *app = server.active_app;
|
||||||
|
if (!app) return;
|
||||||
|
struct kick_tab_group_member *m =
|
||||||
|
(server.active_app == app) ? app->focus_member : NULL;
|
||||||
|
if (m && !m->is_main) {
|
||||||
|
/* A stacked tab-group window: ask it to close; its destroy handler
|
||||||
|
unstacks it and refocuses the tab. Either protocol path works. */
|
||||||
|
if (m->toplevel) {
|
||||||
|
wlr_xdg_toplevel_send_close(m->toplevel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#if HAVE_XWAYLAND
|
||||||
|
if (m->is_x11 && m->xsurface) {
|
||||||
|
wlr_xwayland_surface_close(m->xsurface);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
/* Member has no closable surface: fall through to the whole tab. */
|
||||||
|
}
|
||||||
|
close_tab(app);
|
||||||
|
}
|
||||||
|
|
||||||
static void handle_request_maximize(struct wl_listener *listener, void *data) {
|
static void handle_request_maximize(struct wl_listener *listener, void *data) {
|
||||||
(void)data;
|
(void)data;
|
||||||
struct kick_app *app = wl_container_of(listener, app, request_maximize);
|
struct kick_app *app = wl_container_of(listener, app, request_maximize);
|
||||||
|
|
@ -3565,8 +3594,16 @@ static void handle_xs_request_activate(struct wl_listener *listener,
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handle_xs_request_close(struct wl_listener *listener, void *data) {
|
static void handle_xs_request_close(struct wl_listener *listener, void *data) {
|
||||||
(void)data;
|
struct wlr_xwayland_surface *xs = data;
|
||||||
struct kick_app *app = wl_container_of(listener, app, xs_request_close);
|
struct kick_app *app = wl_container_of(listener, app, xs_request_close);
|
||||||
|
/* A stacked tab-group X11 window closing on its own only unstacks -- the
|
||||||
|
member destroy path handles it -- so leave it be. Only the main
|
||||||
|
surface's close tears the whole tab down, matching Alt+Delete and the
|
||||||
|
Wayland behavior. */
|
||||||
|
struct kick_tab_group_member *sub;
|
||||||
|
wl_list_for_each(sub, &app->group_members, link)
|
||||||
|
if (sub->is_x11 && sub->xsurface == xs && !sub->is_main)
|
||||||
|
return;
|
||||||
close_tab(app);
|
close_tab(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6121,11 +6158,12 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* prefix+Delete closes the active tab. Grabbed so the focused client never
|
/* prefix+Delete closes the active window (or the whole tab when it is the
|
||||||
sees the key; a latch drops host-synthesized repeats so one hold can't
|
only window). Grabbed so the focused client never sees the key; a latch
|
||||||
close a tab per repeat. The active surface already moved to the right
|
drops host-synthesized repeats so one hold can't fire it per repeat.
|
||||||
neighbor inside close_active_tab(), so the tab is gone this frame even
|
When the whole tab closes, the active surface moves to the right
|
||||||
before the client process exits. */
|
neighbor inside close_tab(), so the tab is gone this frame even before
|
||||||
|
the client process exits. */
|
||||||
{ bool is_delete = server.xkb_state
|
{ bool is_delete = server.xkb_state
|
||||||
? (sym == XKB_KEY_Delete)
|
? (sym == XKB_KEY_Delete)
|
||||||
: key == 111;
|
: key == 111;
|
||||||
|
|
@ -6134,7 +6172,7 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
|
||||||
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
||||||
if (!delete_key_held) {
|
if (!delete_key_held) {
|
||||||
delete_key_held = true;
|
delete_key_held = true;
|
||||||
close_tab(server.active_app);
|
close_active_window();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delete_key_held = false;
|
delete_key_held = false;
|
||||||
|
|
@ -7687,7 +7725,7 @@ int main(int argc, char *argv[]) {
|
||||||
" Up / Down Cycle tabs; hold to keep "
|
" Up / Down Cycle tabs; hold to keep "
|
||||||
"cycling.\n"
|
"cycling.\n"
|
||||||
" Shift+Up / Shift+Down Reorder the active tab.\n"
|
" Shift+Up / Shift+Down Reorder the active tab.\n"
|
||||||
" Delete Close 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 "
|
" Shift+T Open a new terminal tab "
|
||||||
"($TERMINAL).\n"
|
"($TERMINAL).\n"
|
||||||
" ` (grave) Cycle the active tab's "
|
" ` (grave) Cycle the active tab's "
|
||||||
|
|
@ -8266,7 +8304,7 @@ int main(int argc, char *argv[]) {
|
||||||
const char *prefix = tab_prefix == PREFIX_SUPER ? "Super" : "Alt";
|
const char *prefix = tab_prefix == PREFIX_SUPER ? "Super" : "Alt";
|
||||||
kick_log(KICK_LOG_INFO, "kick: running. "
|
kick_log(KICK_LOG_INFO, "kick: running. "
|
||||||
"%s+Up/Down switches tabs; %s+Shift+Up/Down reorders; "
|
"%s+Up/Down switches tabs; %s+Shift+Up/Down reorders; "
|
||||||
"%s+Delete closes the active tab; "
|
"%s+Delete closes the active window (or the tab when it's the only window); "
|
||||||
"%s+Shift+T opens a terminal (%s).\n",
|
"%s+Shift+T opens a terminal (%s).\n",
|
||||||
prefix, prefix, prefix, prefix, term_cmd);
|
prefix, prefix, prefix, prefix, term_cmd);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1833,6 +1833,34 @@ def test_sub_windows_stack_mode_row_title_follows_top():
|
||||||
s.proc.terminate()
|
s.proc.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.flaky
|
||||||
|
def test_alt_delete_closes_active_window_not_tab():
|
||||||
|
"""--tab-groups on: Alt+Delete on a tab that holds a stacked window closes
|
||||||
|
only that window -- the tab stays up (the compositor stays alive, no new
|
||||||
|
slot is claimed) and the stacked member unstacks instead of the tab being
|
||||||
|
torn down."""
|
||||||
|
if not os.environ.get("WAYLAND_DISPLAY"):
|
||||||
|
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
|
||||||
|
s = _nested_session(f"{WL} --extra '{GREEN}' '{RED}'",
|
||||||
|
extra_args=("--tab-groups", "on"))
|
||||||
|
try:
|
||||||
|
s.wait_any_active()
|
||||||
|
s.wait_for(r"\[sub\] stack app=0\b") # stacked window mapped
|
||||||
|
claims_before = CLAIM_RE.findall(s.read_log())
|
||||||
|
s.chord(ALT, DELETE) # close the active window
|
||||||
|
s.wait_for(r"\[sub\] unstack app=0\b") # only the window closed
|
||||||
|
time.sleep(0.5)
|
||||||
|
assert s.proc.poll() is None, \
|
||||||
|
"Alt+Delete tore the whole tab down instead of just the window"
|
||||||
|
claims_after = CLAIM_RE.findall(s.read_log())
|
||||||
|
assert claims_before == claims_after, \
|
||||||
|
f"Alt+Delete changed the tab set: {claims_before} -> {claims_after}"
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
if s.proc.poll() is None:
|
||||||
|
s.proc.terminate()
|
||||||
|
|
||||||
|
|
||||||
def test_alt_delete_activates_right_neighbor():
|
def test_alt_delete_activates_right_neighbor():
|
||||||
"""Alt+Delete closes the active tab, making the tab to its right active
|
"""Alt+Delete closes the active tab, making the tab to its right active
|
||||||
(SIGTERM, escalated to SIGKILL only if the client lingers). With three
|
(SIGTERM, escalated to SIGKILL only if the client lingers). With three
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,17 @@ static void xdg_toplevel_configure(void *data, struct xdg_toplevel *xp, int32_t
|
||||||
}
|
}
|
||||||
|
|
||||||
static void xdg_toplevel_close(void *data, struct xdg_toplevel *xp) {
|
static void xdg_toplevel_close(void *data, struct xdg_toplevel *xp) {
|
||||||
(void)data; (void)xp;
|
(void)data;
|
||||||
|
/* Closing the extra (second, tab-group) toplevel destroys just that
|
||||||
|
window so the client (and its main window) survive; closing the main
|
||||||
|
toplevel exits the client. */
|
||||||
|
if (xp == ex_toplevel) {
|
||||||
|
if (ex_toplevel) {
|
||||||
|
xdg_toplevel_destroy(ex_toplevel);
|
||||||
|
ex_toplevel = NULL;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue