confirm-before-quit: ask Y/Enter (quit) or N/Escape (cancel) for Ctrl+Alt+Q and host close when more than one window is open
This commit is contained in:
parent
a8e8106761
commit
fd7ed766ef
2 changed files with 271 additions and 16 deletions
193
kick.c
193
kick.c
|
|
@ -492,6 +492,13 @@ static const char *term_cmd = "alacritty";
|
||||||
static bool term_key_held; /* Alt+Shift+T currently down (repeat latch) */
|
static bool term_key_held; /* Alt+Shift+T currently down (repeat latch) */
|
||||||
static bool delete_key_held; /* prefix+Delete currently down (repeat latch) */
|
static bool delete_key_held; /* prefix+Delete currently down (repeat latch) */
|
||||||
static bool f11_key_held; /* prefix+F11 currently down (repeat latch) */
|
static bool f11_key_held; /* prefix+F11 currently down (repeat latch) */
|
||||||
|
static bool quit_key_held; /* Ctrl+Alt+Q currently down (repeat latch) */
|
||||||
|
|
||||||
|
/* Confirm-before-quit: set when a quit was requested (host window close or
|
||||||
|
Ctrl+Alt+Q) with more than one live window open. While set, a modal
|
||||||
|
panel over the composited frame asks Y/Enter (quit) or N/Escape (keep
|
||||||
|
running) and every key is swallowed so focused clients never see them. */
|
||||||
|
static bool confirm_quit_pending;
|
||||||
|
|
||||||
/* The modifier prefix for the tab-management chords: hold = show overlay,
|
/* The modifier prefix for the tab-management chords: hold = show overlay,
|
||||||
Up/Down = cycle, Shift+Up/Down = reorder, Delete = close. Defaults to
|
Up/Down = cycle, Shift+Up/Down = reorder, Delete = close. Defaults to
|
||||||
|
|
@ -862,6 +869,22 @@ static bool window_live(struct kick_window *w) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Live windows across every tab, for the confirm-before-quit count: a window
|
||||||
|
that holds (or is about to hold) a surface and is not already closing.
|
||||||
|
Still-black pending placeholders have no surface yet, so they do not count
|
||||||
|
-- there is nothing to lose by quitting for them. */
|
||||||
|
static int live_window_count(void) {
|
||||||
|
int n = 0;
|
||||||
|
struct kick_tab *t;
|
||||||
|
wl_list_for_each(t, &server.tabs, link) {
|
||||||
|
struct kick_window *w;
|
||||||
|
wl_list_for_each(w, &t->windows, link)
|
||||||
|
if (window_live(w) && !w->closing)
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
/* The first (oldest) window of a tab, or NULL. The tab's launcher identity
|
/* The first (oldest) window of a tab, or NULL. The tab's launcher identity
|
||||||
(cmd, pending_claim) lives on it. */
|
(cmd, pending_claim) lives on it. */
|
||||||
static struct kick_window *tab_first_window(struct kick_tab *tab) {
|
static struct kick_window *tab_first_window(struct kick_tab *tab) {
|
||||||
|
|
@ -1929,8 +1952,58 @@ static void overlay_pass_icon_tex(struct wlr_render_pass *pass,
|
||||||
};
|
};
|
||||||
wlr_render_pass_add_texture(pass, &o);
|
wlr_render_pass_add_texture(pass, &o);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Confirm-before-quit modal, drawn in place of the Alt tab list while a quit
|
||||||
|
is pending: a centered panel with the live-window count and the y/n keys.
|
||||||
|
Same styling language as the overlay -- same backdrop, rim strokes, and
|
||||||
|
glyph atlas. */
|
||||||
|
static void draw_confirm_quit_pass(struct wlr_render_pass *pass) {
|
||||||
|
int n = live_window_count();
|
||||||
|
char l1[96], l2[96];
|
||||||
|
snprintf(l1, sizeof(l1), "Quit kick? %d window%s open.", n,
|
||||||
|
n == 1 ? "" : "s");
|
||||||
|
snprintf(l2, sizeof(l2), "[Y]es quit [N]o keep running");
|
||||||
|
|
||||||
|
int margin = 8, boxpad = 10;
|
||||||
|
int row_h = host.overlay_font_h > 0 ? host.overlay_font_h + 8 : 18;
|
||||||
|
int row_gap = 6;
|
||||||
|
int w1 = overlay_text_width(l1), w2 = overlay_text_width(l2);
|
||||||
|
int box_w = (w1 > w2 ? w1 : w2) + 2 * boxpad;
|
||||||
|
int box_h = 2 * row_h + row_gap + 2 * boxpad;
|
||||||
|
if (box_w > host.width - 2 * margin) box_w = host.width - 2 * margin;
|
||||||
|
if (box_h > host.height - 2 * margin) box_h = host.height - 2 * margin;
|
||||||
|
int box_x = (host.width - box_w) / 2;
|
||||||
|
int box_y = (host.height - box_h) / 2;
|
||||||
|
|
||||||
|
int bw = 1;
|
||||||
|
overlay_pass_rect(pass, box_x + bw, box_y + bw,
|
||||||
|
box_w - 2 * bw, box_h - 2 * bw,
|
||||||
|
0.07f, 0.07f, 0.09f, 1.0f);
|
||||||
|
overlay_pass_rect(pass, box_x, box_y, box_w, bw, 0.45f, 0.45f, 0.48f, 1.0f);
|
||||||
|
overlay_pass_rect(pass, box_x, box_y + box_h - bw, box_w, bw,
|
||||||
|
0.45f, 0.45f, 0.48f, 1.0f);
|
||||||
|
overlay_pass_rect(pass, box_x, box_y, bw, box_h, 0.45f, 0.45f, 0.48f, 1.0f);
|
||||||
|
overlay_pass_rect(pass, box_x + box_w - bw, box_y, bw, box_h,
|
||||||
|
0.45f, 0.45f, 0.48f, 1.0f);
|
||||||
|
|
||||||
|
int row_inner = box_w - 2 * boxpad;
|
||||||
|
for (int r = 0; r < 2; r++) {
|
||||||
|
const char *s = r == 0 ? l1 : l2;
|
||||||
|
int tw = overlay_text_width(s);
|
||||||
|
int x = box_x + boxpad + (row_inner - tw) / 2;
|
||||||
|
int y = box_y + boxpad + r * (row_h + row_gap);
|
||||||
|
overlay_pass_text(pass, s, x, y + (row_h - host.overlay_font_h) / 2,
|
||||||
|
0.95f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void draw_tab_overlay_pass(struct wlr_render_pass *pass) {
|
static void draw_tab_overlay_pass(struct wlr_render_pass *pass) {
|
||||||
if (!host.overlay_visible || wl_list_empty(&server.tabs)) return;
|
if (!host.overlay_visible && !confirm_quit_pending) return;
|
||||||
|
if (confirm_quit_pending) {
|
||||||
|
draw_confirm_quit_pass(pass);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (wl_list_empty(&server.tabs)) return;
|
||||||
|
|
||||||
int margin = 8, boxpad = 6;
|
int margin = 8, boxpad = 6;
|
||||||
int icon_sz = OVERLAY_ICON_SZ, icon_gap = 8, member_gap = 12;
|
int icon_sz = OVERLAY_ICON_SZ, icon_gap = 8, member_gap = 12;
|
||||||
|
|
@ -5684,11 +5757,54 @@ static void host_xdg_toplevel_configure(void *data, struct xdg_toplevel *t,
|
||||||
host.configured = true;
|
host.configured = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Common quit tail for the user-requested exits: drop the host window
|
||||||
|
(nested; guarded no-op standalone), tear every client and window down,
|
||||||
|
and exit cleanly. */
|
||||||
|
static void do_quit(void) {
|
||||||
|
host_teardown_window();
|
||||||
|
shutdown_cleanup();
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show or dismiss the confirm-before-quit modal. The panel is drawn by the
|
||||||
|
same second pass as the Alt overlay, so the same dance applies: while it
|
||||||
|
is up, direct scanout must yield to the composite path (attach-render
|
||||||
|
lock) and the swapchain buffers must be forced to repaint, or the modal
|
||||||
|
would bake into a reused buffer like the overlay does. */
|
||||||
|
static void set_confirm_quit(bool on) {
|
||||||
|
if (on == confirm_quit_pending) return;
|
||||||
|
confirm_quit_pending = on;
|
||||||
|
if (server.output)
|
||||||
|
wlr_output_lock_attach_render(server.output,
|
||||||
|
on || host.overlay_visible);
|
||||||
|
if (server.standalone)
|
||||||
|
standalone_overlay_damage();
|
||||||
|
else if (server.scene_output)
|
||||||
|
wlr_damage_ring_add_whole(&server.scene_output->damage_ring);
|
||||||
|
kick_request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A quit was requested (host window close, or Ctrl+Alt+Q). With more than
|
||||||
|
one live window quitting would lose work, so raise the confirm modal and
|
||||||
|
wait for Y/N; a single window (or none) has nothing to lose and quits
|
||||||
|
straight away. */
|
||||||
|
static void quit_requested(void) {
|
||||||
|
int n = live_window_count();
|
||||||
|
kick_log(KICK_LOG_INFO, "kick: quit requested (%d window%s)\n",
|
||||||
|
n, n == 1 ? "" : "s");
|
||||||
|
if (n <= 1) {
|
||||||
|
do_quit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
kick_log(KICK_LOG_INFO, "[confirm] quit: %d windows open "
|
||||||
|
"(Y=quit, N=cancel)\n", n);
|
||||||
|
set_confirm_quit(true);
|
||||||
|
}
|
||||||
|
|
||||||
static void host_xdg_toplevel_close(void *data, struct xdg_toplevel *t) {
|
static void host_xdg_toplevel_close(void *data, struct xdg_toplevel *t) {
|
||||||
(void)data; (void)t;
|
(void)data; (void)t;
|
||||||
kick_log(KICK_LOG_INFO, "kick: window closed\n");
|
kick_log(KICK_LOG_INFO, "kick: window closed\n");
|
||||||
shutdown_cleanup();
|
quit_requested();
|
||||||
exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const struct xdg_toplevel_listener host_xdg_toplevel_listener = {
|
static const struct xdg_toplevel_listener host_xdg_toplevel_listener = {
|
||||||
|
|
@ -5778,9 +5894,7 @@ static void host_frame_configure(struct libdecor_frame *frame,
|
||||||
static void host_frame_close(struct libdecor_frame *frame, void *data) {
|
static void host_frame_close(struct libdecor_frame *frame, void *data) {
|
||||||
(void)frame; (void)data;
|
(void)frame; (void)data;
|
||||||
kick_log(KICK_LOG_INFO, "kick: window closed\n");
|
kick_log(KICK_LOG_INFO, "kick: window closed\n");
|
||||||
host_teardown_window();
|
quit_requested();
|
||||||
shutdown_cleanup();
|
|
||||||
exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void host_frame_commit(struct libdecor_frame *frame, void *data) {
|
static void host_frame_commit(struct libdecor_frame *frame, void *data) {
|
||||||
|
|
@ -6268,12 +6382,38 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
|
||||||
bool alt = prefix_active(mods_depressed);
|
bool alt = prefix_active(mods_depressed);
|
||||||
if (alt != host.overlay_visible)
|
if (alt != host.overlay_visible)
|
||||||
update_overlay_visible(alt);
|
update_overlay_visible(alt);
|
||||||
|
bool ctrl = (mods_depressed & (1u << 2)) != 0;
|
||||||
char symbuf[32];
|
char symbuf[32];
|
||||||
xkb_keysym_get_name(sym, symbuf, sizeof(symbuf));
|
xkb_keysym_get_name(sym, symbuf, sizeof(symbuf));
|
||||||
kick_log(KICK_LOG_DEBUG,
|
kick_log(KICK_LOG_DEBUG,
|
||||||
"[kbd] key=%u state=%u sym=%s ctrl=%d alt=%d repeat_key=%u\n",
|
"[kbd] key=%u state=%u sym=%s ctrl=%d alt=%d repeat_key=%u\n",
|
||||||
key, state, sym ? symbuf : "-",
|
key, state, sym ? symbuf : "-",
|
||||||
(mods_depressed & (1u << 2)) != 0, alt, host.repeat_key);
|
(mods_depressed & (1u << 2)) != 0, alt, host.repeat_key);
|
||||||
|
/* Confirm-before-quit modal active: every key is swallowed so focused
|
||||||
|
clients never see them. Y/Enter quits on the press (the first press
|
||||||
|
only -- the exit runs before any repeat could re-fire); N/Escape
|
||||||
|
cancels, and repeats just re-cancel the already-cleared state. */
|
||||||
|
if (confirm_quit_pending) {
|
||||||
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
||||||
|
bool yes = server.xkb_state
|
||||||
|
? (sym == XKB_KEY_y || sym == XKB_KEY_Y ||
|
||||||
|
sym == XKB_KEY_Return || sym == XKB_KEY_KP_Enter)
|
||||||
|
: (key == 21 || key == 28);
|
||||||
|
bool no = server.xkb_state
|
||||||
|
? (sym == XKB_KEY_n || sym == XKB_KEY_N ||
|
||||||
|
sym == XKB_KEY_Escape)
|
||||||
|
: (key == 49 || key == 1);
|
||||||
|
if (yes) {
|
||||||
|
kick_log(KICK_LOG_INFO, "kick: quit confirmed\n");
|
||||||
|
set_confirm_quit(false);
|
||||||
|
do_quit();
|
||||||
|
} else if (no) {
|
||||||
|
kick_log(KICK_LOG_INFO, "kick: quit cancelled\n");
|
||||||
|
set_confirm_quit(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
/* Ctrl+Alt+F1..F12 switch virtual terminals in standalone mode. wlroots
|
/* Ctrl+Alt+F1..F12 switch virtual terminals in standalone mode. wlroots
|
||||||
has no built-in handler; the compositor must notice the key and ask the
|
has no built-in handler; the compositor must notice the key and ask the
|
||||||
session (libseat/logind) to activate another VT. Both the press and
|
session (libseat/logind) to activate another VT. Both the press and
|
||||||
|
|
@ -6388,6 +6528,27 @@ static bool process_key_event(uint32_t time, uint32_t key, uint32_t state,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Ctrl+Alt+Q quits kick (both the host close and this chord share
|
||||||
|
quit_requested): with more than one window open a modal asks before
|
||||||
|
anything is torn down. Grabbed so the focused client never sees the
|
||||||
|
chord; a latch drops host-synthesized repeats so one hold can't fire
|
||||||
|
the request (or the confirm) more than once. */
|
||||||
|
{ bool is_q = server.xkb_state
|
||||||
|
? (sym == XKB_KEY_q || sym == XKB_KEY_Q)
|
||||||
|
: key == 16;
|
||||||
|
if (alt && ctrl && is_q) {
|
||||||
|
kbd_repeat_cancel();
|
||||||
|
if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
||||||
|
if (!quit_key_held) {
|
||||||
|
quit_key_held = true;
|
||||||
|
quit_requested();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quit_key_held = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
/* prefix+Delete closes the active window (or the whole tab when it is the
|
/* prefix+Delete closes the active window (or the whole tab when it is the
|
||||||
only window). Grabbed so the focused client never sees the key; a latch
|
only window). Grabbed so the focused client never sees the key; a latch
|
||||||
drops host-synthesized repeats so one hold can't fire it per repeat.
|
drops host-synthesized repeats so one hold can't fire it per repeat.
|
||||||
|
|
@ -7712,7 +7873,7 @@ static void render_standalone(void) {
|
||||||
dump_scene_buffer(state.buffer);
|
dump_scene_buffer(state.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host.overlay_visible) {
|
if (host.overlay_visible || confirm_quit_pending) {
|
||||||
struct wlr_render_pass *pass = wlr_renderer_begin_buffer_pass(
|
struct wlr_render_pass *pass = wlr_renderer_begin_buffer_pass(
|
||||||
server.renderer, state.buffer, NULL);
|
server.renderer, state.buffer, NULL);
|
||||||
if (pass) {
|
if (pass) {
|
||||||
|
|
@ -7794,7 +7955,7 @@ static void render_and_present(void) {
|
||||||
|
|
||||||
/* Overlay joins the frame before the blit, drawn exactly as standalone
|
/* Overlay joins the frame before the blit, drawn exactly as standalone
|
||||||
draws it -- one renderer for both modes; the blit stays a pure copy. */
|
draws it -- one renderer for both modes; the blit stays a pure copy. */
|
||||||
if (host.overlay_visible) {
|
if (host.overlay_visible || confirm_quit_pending) {
|
||||||
struct wlr_render_pass *pass = wlr_renderer_begin_buffer_pass(
|
struct wlr_render_pass *pass = wlr_renderer_begin_buffer_pass(
|
||||||
server.renderer, state.buffer, NULL);
|
server.renderer, state.buffer, NULL);
|
||||||
if (pass) {
|
if (pass) {
|
||||||
|
|
@ -8300,12 +8461,16 @@ int main(int argc, char *argv[]) {
|
||||||
" Delete Close the active window (or the tab when it is the only window).\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"
|
||||||
" Left / Right Cycle the active tab's "
|
" Left / Right Cycle the active tab's "
|
||||||
"tab-group windows (Left = previous, Right = next); hold "
|
"tab-group windows (Left = previous, Right = next); hold "
|
||||||
"to keep cycling.\n"
|
"to keep cycling.\n"
|
||||||
" Ctrl+Alt+F1..F12 Switch virtual terminals "
|
" Ctrl+Alt+F1..F12 Switch virtual terminals "
|
||||||
"(standalone only).\n",
|
"(standalone only).\n"
|
||||||
argv[0], argv[0]);
|
" Ctrl+Alt+Q Quit kick: asks for "
|
||||||
|
"confirmation when more than\n"
|
||||||
|
" one window is open "
|
||||||
|
"(Y/Enter quits, N/Escape keeps running).\n",
|
||||||
|
argv[0], argv[0]);
|
||||||
free(appv);
|
free(appv);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,13 +62,16 @@ _ensure_built()
|
||||||
# host_kbd_key in kick.c). Names, not numbers, in tests.
|
# host_kbd_key in kick.c). Names, not numbers, in tests.
|
||||||
F = 33
|
F = 33
|
||||||
F11 = 87
|
F11 = 87
|
||||||
|
Q = 16
|
||||||
|
Y = 21
|
||||||
|
N = 49
|
||||||
|
ESC = 1
|
||||||
|
ENTER = 28
|
||||||
UP = 103
|
UP = 103
|
||||||
DOWN = 108
|
DOWN = 108
|
||||||
RIGHT = 106
|
RIGHT = 106
|
||||||
LEFT = 105
|
LEFT = 105
|
||||||
DELETE = 111
|
DELETE = 111
|
||||||
LEFT = 105
|
|
||||||
RIGHT = 106
|
|
||||||
KEY_PRESSED = 1
|
KEY_PRESSED = 1
|
||||||
KEY_RELEASED = 0
|
KEY_RELEASED = 0
|
||||||
|
|
||||||
|
|
@ -1982,6 +1985,93 @@ def test_alt_delete_closes_active_window_not_tab():
|
||||||
s.proc.terminate()
|
s.proc.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_exit(s, code, timeout=10.0):
|
||||||
|
"""Wait for the compositor process to exit, asserting its status."""
|
||||||
|
since = time.monotonic()
|
||||||
|
while time.monotonic() - since < timeout:
|
||||||
|
rc = s.proc.poll()
|
||||||
|
if rc is not None:
|
||||||
|
assert rc == code, \
|
||||||
|
f"compositor exited with {rc}, expected {code}"
|
||||||
|
return
|
||||||
|
time.sleep(0.1)
|
||||||
|
pytest.fail("compositor never exited (expected returncode "
|
||||||
|
f"{code}) after quit")
|
||||||
|
|
||||||
|
|
||||||
|
def test_quit_confirm_multi_window():
|
||||||
|
"""Ctrl+Alt+Q with more than one window open raises the confirm modal
|
||||||
|
instead of quitting; Y/Enter confirms and quits cleanly with 0."""
|
||||||
|
if not os.environ.get("WAYLAND_DISPLAY"):
|
||||||
|
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
|
||||||
|
s = _nested_session(f"{WL} '{RED}'", f"{WL} '{GREEN}'")
|
||||||
|
try:
|
||||||
|
s.wait_any_active()
|
||||||
|
s.wait_for(r"\[claim\] app=0\b")
|
||||||
|
s.wait_for(r"\[claim\] app=1\b")
|
||||||
|
s.chord(CTRL | ALT, Q)
|
||||||
|
s.wait_for(r"\[confirm\] quit: 2 windows open ")
|
||||||
|
assert s.proc.poll() is None, \
|
||||||
|
"Ctrl+Alt+Q quit behind the confirm modal"
|
||||||
|
s.tap(Y) # confirm
|
||||||
|
s.wait_for(r"kick: quit confirmed\b")
|
||||||
|
_wait_exit(s, 0)
|
||||||
|
assert "kick: quit cancelled" not in s.read_log()
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
if s.proc.poll() is None:
|
||||||
|
s.proc.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_quit_cancel_keeps_running():
|
||||||
|
"""Ctrl+Alt+Q confirmation can be declined: N and Escape both dismiss it,
|
||||||
|
the compositor keeps running, and a later quit request re-asks."""
|
||||||
|
if not os.environ.get("WAYLAND_DISPLAY"):
|
||||||
|
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
|
||||||
|
s = _nested_session(f"{WL} '{RED}'", f"{WL} '{GREEN}'")
|
||||||
|
try:
|
||||||
|
s.wait_any_active()
|
||||||
|
s.wait_for(r"\[claim\] app=1\b")
|
||||||
|
|
||||||
|
s.chord(CTRL | ALT, Q)
|
||||||
|
s.wait_for(r"\[confirm\] quit: 2 windows open ")
|
||||||
|
s.tap(N) # decline
|
||||||
|
s.wait_for(r"kick: quit cancelled")
|
||||||
|
assert s.proc.poll() is None, "N after Ctrl+Alt+Q quit anyway"
|
||||||
|
|
||||||
|
s.chord(CTRL | ALT, Q) # re-ask, decline again via Escape
|
||||||
|
s.wait_for(r"\[confirm\] quit: 2 windows open ")
|
||||||
|
s.tap(ESC)
|
||||||
|
s.wait_for(r"kick: quit cancelled")
|
||||||
|
assert s.proc.poll() is None, "Escape after Ctrl+Alt+Q quit anyway"
|
||||||
|
|
||||||
|
assert s.cycle_ack(+1) is not None, "tab cycling broke after cancel"
|
||||||
|
assert s.proc.poll() is None, "tab cycling broke after cancelled quit"
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
if s.proc.poll() is None:
|
||||||
|
s.proc.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_quit_single_window_no_confirm():
|
||||||
|
"""With a single window there is nothing to lose, so Ctrl+Alt+Q quits
|
||||||
|
immediately -- no confirm modal, clean exit 0."""
|
||||||
|
if not os.environ.get("WAYLAND_DISPLAY"):
|
||||||
|
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
|
||||||
|
s = _nested_session(f"{WL} '{RED}'")
|
||||||
|
try:
|
||||||
|
s.wait_any_active()
|
||||||
|
s.wait_for(r"\[claim\] app=0\b")
|
||||||
|
s.chord(CTRL | ALT, Q)
|
||||||
|
_wait_exit(s, 0)
|
||||||
|
assert "[confirm] quit:" not in s.read_log(), \
|
||||||
|
"single-window quit unnecessarily asked for confirmation"
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
if s.proc.poll() is None:
|
||||||
|
s.proc.terminate()
|
||||||
|
|
||||||
|
|
||||||
def test_alt_left_right_cycle_tab_group_members():
|
def test_alt_left_right_cycle_tab_group_members():
|
||||||
"""--tab-groups on: Alt+Left / Alt+Right cycle the active tab's stacked
|
"""--tab-groups on: Alt+Left / Alt+Right cycle the active tab's stacked
|
||||||
windows (Left = previous, Right = next) and the tab stays up; the chord is
|
windows (Left = previous, Right = next) and the tab stays up; the chord is
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue