mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
Merge pull request #206 from l0ng-ai/feat/confirm-window-close
feat(settings): let the last-window close confirmation be turned off
This commit is contained in:
@@ -184,6 +184,17 @@ pub struct Config {
|
||||
/// `true` by that hint; there is no UI to reset it (nor a reason to).
|
||||
#[serde(default)]
|
||||
pub workspace_detach_hint_seen: bool,
|
||||
/// Ask before closing the *last* window (the close that also quits the app).
|
||||
/// On by default, which is the behavior every build so far has had.
|
||||
///
|
||||
/// The prompt was only ever a teaching device, not a safety net: ⌘Q, the
|
||||
/// tray's Quit and the palette's Quit all leave without asking, and nothing
|
||||
/// is lost either way — the panes keep running in the daemon. So once the
|
||||
/// user has learned that (Settings states it permanently under "How sessions
|
||||
/// work"), being asked on every quit is pure friction. Off makes the last
|
||||
/// window close exactly like any other: detach the workspace, quit.
|
||||
#[serde(default = "default_true")]
|
||||
pub confirm_window_close: bool,
|
||||
/// How the terminal bell (BEL / `^G`) is signalled. Defaults to a brief
|
||||
/// visual flash (the current behavior).
|
||||
#[serde(default, deserialize_with = "de_lenient")]
|
||||
@@ -619,6 +630,7 @@ impl Default for Config {
|
||||
restore_session: true,
|
||||
show_tray_icon: true,
|
||||
workspace_detach_hint_seen: false,
|
||||
confirm_window_close: true,
|
||||
// Visual flash preserves the pre-config behavior (the bell always
|
||||
// flashed); opting into None/Audible is a deliberate change.
|
||||
bell: BellMode::Visual,
|
||||
@@ -1037,6 +1049,31 @@ mod tests {
|
||||
assert_eq!(back.ssh_profile_frecency.get(&id).unwrap().count, 4);
|
||||
}
|
||||
|
||||
/// Opt-*out*, unlike most flags here: a config written before this setting
|
||||
/// existed must keep the prompt, or an update would silently take away the
|
||||
/// one thing telling people their sessions survive a quit.
|
||||
#[test]
|
||||
fn confirm_window_close_defaults_on_and_round_trips() {
|
||||
assert!(Config::default().confirm_window_close);
|
||||
|
||||
let old: Config = serde_json::from_str(r#"{"font_size": 15.0}"#).unwrap();
|
||||
assert!(old.confirm_window_close);
|
||||
|
||||
let off: Config = serde_json::from_str(r#"{"confirm_window_close": false}"#).unwrap();
|
||||
assert!(!off.confirm_window_close);
|
||||
let json = serde_json::to_string(&off).unwrap();
|
||||
let back: Config = serde_json::from_str(&json).unwrap();
|
||||
assert!(!back.confirm_window_close);
|
||||
|
||||
// ...and a key this build has never heard of — a config last written by
|
||||
// a newer tty7, or hand-edited — must be ignored rather than failing the
|
||||
// whole parse, which `Config::load` would swallow into *defaults*: the
|
||||
// opt-out would come back on with nothing said.
|
||||
let newer: Config =
|
||||
serde_json::from_str(r#"{"confirm_window_close": false, "not_a_setting": 7}"#).unwrap();
|
||||
assert!(!newer.confirm_window_close);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_follow_system_defaults_and_round_trips() {
|
||||
// Old configs (no follow-system keys) must land on off + the built-in
|
||||
|
||||
+20
-7
@@ -1089,10 +1089,14 @@ impl Tty7App {
|
||||
//
|
||||
// The last window is different: closing it also quits the app (a
|
||||
// windowless process left in the Dock no longer responds to being
|
||||
// clicked — #147), so that one keeps the reassuring prompt. We veto the
|
||||
// immediate close (return `false`), show it, and quit only if the user
|
||||
// picks "Close"; a one-shot flag lets that post-confirm close through
|
||||
// instead of looping the prompt.
|
||||
// clicked — #147), so that one keeps the reassuring prompt by default.
|
||||
// We veto the immediate close (return `false`), show it, and quit only
|
||||
// if the user picks "Close"; a one-shot flag lets that post-confirm
|
||||
// close through instead of looping the prompt.
|
||||
//
|
||||
// `confirm_window_close` turns the prompt off for users who have learned
|
||||
// the model — it is teaching, not protection (⌘Q never asked), so it has
|
||||
// to be escapable.
|
||||
let close_confirmed = std::rc::Rc::new(std::cell::Cell::new(false));
|
||||
let weak_app = cx.weak_entity();
|
||||
window.on_window_should_close(cx, move |window, cx| {
|
||||
@@ -1104,9 +1108,11 @@ impl Tty7App {
|
||||
.upgrade()
|
||||
.is_some_and(|app| app.read(cx).tabs.is_empty());
|
||||
|
||||
// Any window but the last, or an empty one with nothing to
|
||||
// reassure about: detach and go. Prompting here would be friction.
|
||||
if !last_window || empty {
|
||||
// Any window but the last, an empty one with nothing to reassure
|
||||
// about, or a user who has turned the prompt off: detach and go.
|
||||
// Prompting here would be friction.
|
||||
let confirm = cx.global::<Config>().confirm_window_close;
|
||||
if !last_window || empty || !confirm {
|
||||
if let Some(app) = weak_app.upgrade() {
|
||||
app.update(cx, |app, cx| app.detach_workspace(cx));
|
||||
}
|
||||
@@ -2623,6 +2629,13 @@ impl Tty7App {
|
||||
self.update_config(cx, |cfg| cfg.show_tray_icon = on);
|
||||
}
|
||||
|
||||
/// Toggle the "Close Window?" prompt on the last window. The close handler
|
||||
/// reads the flag when it fires, so this applies to the very next ⌘W with no
|
||||
/// restart and nothing to push to open windows.
|
||||
pub(crate) fn set_confirm_window_close(&mut self, on: bool, cx: &mut Context<Self>) {
|
||||
self.update_config(cx, |cfg| cfg.confirm_window_close = on);
|
||||
}
|
||||
|
||||
// ── Input / Mouse setters ───────────────────────────────────────────────
|
||||
|
||||
/// Takes effect on the next keystroke — the terminal reads the flag per
|
||||
|
||||
@@ -344,6 +344,14 @@ fn settings_search_entries() -> &'static [SearchEntry] {
|
||||
title: "Restore last layout",
|
||||
keywords: "restore session previous tabs splits reopen launch startup layout",
|
||||
},
|
||||
SearchEntry {
|
||||
section: WindowTabs,
|
||||
title: "Confirm before closing the last window",
|
||||
// Both spellings of the chord: the prompt this turns off is reached
|
||||
// by ⌘W on macOS and Ctrl-W everywhere else, and the user types
|
||||
// whichever one their own keyboard just used.
|
||||
keywords: "close quit confirm prompt dialog ask again warn last window cmd-w ctrl-w",
|
||||
},
|
||||
SearchEntry {
|
||||
section: WindowTabs,
|
||||
title: "Show tray icon",
|
||||
@@ -3441,6 +3449,7 @@ impl Tty7App {
|
||||
let restore_session = cfg.restore_session;
|
||||
let remember_window_size = cfg.remember_window_size;
|
||||
let show_tray_icon = cfg.show_tray_icon;
|
||||
let confirm_window_close = cfg.confirm_window_close;
|
||||
let tab_bar_idx = match cfg.tab_bar_position {
|
||||
TabBarPosition::Top => 0,
|
||||
TabBarPosition::Left => 1,
|
||||
@@ -3505,6 +3514,10 @@ impl Tty7App {
|
||||
.checked(remember_window_size)
|
||||
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_remember_window_size(*on, cx)))
|
||||
.into_any_element();
|
||||
let confirm_close_switch = crate::ui::theme::switch("wt-confirm-window-close", cx)
|
||||
.checked(confirm_window_close)
|
||||
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_confirm_window_close(*on, cx)))
|
||||
.into_any_element();
|
||||
let tray_switch = crate::ui::theme::switch("wt-tray-icon", cx)
|
||||
.checked(show_tray_icon)
|
||||
.on_click(cx.listener(|this, on: &bool, _w, cx| this.set_show_tray_icon(*on, cx)))
|
||||
@@ -3590,6 +3603,16 @@ impl Tty7App {
|
||||
restore_switch,
|
||||
cx,
|
||||
))
|
||||
// Phrased around what stays true either way: the prompt is there to
|
||||
// teach that closing isn't ending, so the row that turns it off is
|
||||
// the last chance to say so.
|
||||
.child(self.settings_row(
|
||||
"Confirm before closing the last window",
|
||||
"Ask first, since that close also quits tty7. Off closes straight away — \
|
||||
either way your shells keep running in the background.",
|
||||
confirm_close_switch,
|
||||
cx,
|
||||
))
|
||||
.child(self.settings_row(
|
||||
"Show tray icon",
|
||||
"Keep a status item in the system tray / menu bar: it signals when a \
|
||||
@@ -4551,6 +4574,28 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The close-confirmation toggle is the one people go looking for *after*
|
||||
/// the dialog has annoyed them, so it has to be reachable by what they'd
|
||||
/// type in that moment — not just by its own title.
|
||||
#[test]
|
||||
fn close_confirmation_toggle_is_findable() {
|
||||
// Not a bare "confirm": SSH's own close warning owns that word just as
|
||||
// legitimately, and the nav's per-section counts are what disambiguate.
|
||||
for query in [
|
||||
"ask again",
|
||||
"closing the last window",
|
||||
"dialog",
|
||||
"cmd-w",
|
||||
"ctrl-w",
|
||||
] {
|
||||
assert_eq!(
|
||||
best_matching_section(query).map(|s| s.profile_label()),
|
||||
Some(SettingsSection::WindowTabs.profile_label()),
|
||||
"query {query:?} should land on Window & Tabs"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The index names rows, so a title that no longer matches the rendered row
|
||||
/// sends the user to the right page and then leaves them hunting. This
|
||||
/// pins the ones that had drifted (the index said "Working directory"; the
|
||||
@@ -4560,6 +4605,7 @@ mod tests {
|
||||
for title in [
|
||||
"Start in",
|
||||
"Restore last layout",
|
||||
"Confirm before closing the last window",
|
||||
"Terminal bell",
|
||||
"Report mouse to apps",
|
||||
"Open files with",
|
||||
|
||||
Reference in New Issue
Block a user