A "Quit Vectrum Code?" dialog (Enter or a click on Quit quits, Escape or Cancel stays) guards every way out. On by default, as Close confirmation under Settings > Confirmations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Showing 5 changed files with 70 additions and 7 deletions.
| @@ -163,7 +163,8 @@ Gruvbox, Rosé Pine, Solarized and Dracula. | ||
| 163 | 163 | menu, the rich text composer, collapsing on scroll, the send shortcut (Enter, Ctrl+Enter |
| 164 | 164 | for multiline prompts, or Ctrl+Enter always), follow-up behavior, update checks, |
| 165 | 165 | continuing after restarts, background activity); Projects & threads; Confirmations |
| 166 | - (unpin, archive, delete, and the quit shortcut: Direct, Hold or Double press); the text | |
| 166 | + (unpin, archive, delete, and the quit shortcut: Direct, Hold or Double press, and a close | |
| 167 | + confirmation that Ctrl+Q, Alt+F4 and the window's close button go through); the text | |
| 167 | 168 | generation model; the version; Diagnostics (CPU, memory and every process the app |
| 168 | 169 | started, with interrupt and kill buttons) and the open source licenses; and the folded |
| 169 | 170 | Legacy features (plan mode, the context window ring, the per-project sidebar). |
| @@ -21,11 +21,14 @@ mod worktree; | ||
| 21 | 21 | |
| 22 | 22 | use std::{ |
| 23 | 23 | path::PathBuf, |
| 24 | - sync::Mutex, | |
| 24 | + sync::{ | |
| 25 | + Mutex, | |
| 26 | + atomic::{AtomicBool, Ordering}, | |
| 27 | + }, | |
| 25 | 28 | time::{Duration, Instant}, |
| 26 | 29 | }; |
| 27 | 30 | |
| 28 | - use gpui_kit::component::{WindowExt as _, notification::Notification}; | |
| 31 | + use gpui_kit::component::{WindowExt as _, button::ButtonVariant, notification::Notification}; | |
| 29 | 32 | use gpui_kit::*; |
| 30 | 33 | |
| 31 | 34 | use crate::ui::{icons::AppAssets, metal, workspace::Workspace}; |
| @@ -191,6 +194,8 @@ fn main() { | ||
| 191 | 194 | }; |
| 192 | 195 | gpui_kit::open_window(options, cx, |window, cx| { |
| 193 | 196 | metal::sync_appearance(window, cx); |
| 197 | + // Alt+F4 and the window manager's close button. | |
| 198 | + window.on_window_should_close(cx, |window, cx| !ask_to_close(window, cx)); | |
| 194 | 199 | cx.new(|cx| Workspace::new(project, window, cx)) |
| 195 | 200 | }) |
| 196 | 201 | .expect("failed to open the main window"); |
| @@ -198,17 +203,17 @@ fn main() { | ||
| 198 | 203 | }); |
| 199 | 204 | } |
| 200 | 205 | |
| 201 | - /// Quits, or with the Hold shortcut, asks for the keys to be held or pressed | |
| 202 | - /// again. | |
| 206 | + /// Quits (through the close confirmation), or with the Hold shortcut, asks | |
| 207 | + /// for the keys to be held or pressed again. | |
| 203 | 208 | fn quit(_: &Quit, cx: &mut App) { |
| 204 | 209 | static FIRST_PRESS: Mutex<Option<Instant>> = Mutex::new(None); |
| 205 | 210 | if settings::get(|settings| settings.quit_shortcut) == settings::QuitShortcut::Press { |
| 206 | - cx.quit(); | |
| 211 | + quit_or_ask(cx); | |
| 207 | 212 | return; |
| 208 | 213 | } |
| 209 | 214 | let mut first = FIRST_PRESS.lock().unwrap_or_else(|poisoned| poisoned.into_inner()); |
| 210 | 215 | if first.is_some_and(|at| at.elapsed() < QUIT_WINDOW) { |
| 211 | - cx.quit(); | |
| 216 | + quit_or_ask(cx); | |
| 212 | 217 | return; |
| 213 | 218 | } |
| 214 | 219 | *first = Some(Instant::now()); |
| @@ -222,3 +227,48 @@ fn quit(_: &Quit, cx: &mut App) { | ||
| 222 | 227 | }); |
| 223 | 228 | } |
| 224 | 229 | } |
| 230 | + | |
| 231 | + /// Quits, or asks first when closing needs a confirmation. Deferred, since a | |
| 232 | + /// key's action runs while its window is mid-update. | |
| 233 | + fn quit_or_ask(cx: &mut App) { | |
| 234 | + cx.defer(|cx| { | |
| 235 | + let Some(window) = cx.active_window().or_else(|| cx.windows().first().copied()) else { | |
| 236 | + cx.quit(); | |
| 237 | + return; | |
| 238 | + }; | |
| 239 | + if let Ok(false) = window.update(cx, |_, window, cx| ask_to_close(window, cx)) { | |
| 240 | + cx.quit(); | |
| 241 | + } | |
| 242 | + }); | |
| 243 | + } | |
| 244 | + | |
| 245 | + /// With Close confirmation on, opens the "Quit Vectrum Code?" dialog (Enter | |
| 246 | + /// or a click on Quit quits, Escape or Cancel stays) and returns true; the | |
| 247 | + /// caller then must not close. Returns false when closing needs no asking. | |
| 248 | + fn ask_to_close(window: &mut Window, cx: &mut App) -> bool { | |
| 249 | + /// The dialog is up: a held Ctrl+Q's key repeat or a second Alt+F4 | |
| 250 | + /// leaves it be rather than stacking another. | |
| 251 | + static ASKING: AtomicBool = AtomicBool::new(false); | |
| 252 | + if !settings::get(|settings| settings.confirm_close) { | |
| 253 | + return false; | |
| 254 | + } | |
| 255 | + if ASKING.load(Ordering::Relaxed) && window.has_active_dialog(cx) { | |
| 256 | + return true; | |
| 257 | + } | |
| 258 | + ASKING.store(true, Ordering::Relaxed); | |
| 259 | + window.open_alert_dialog(cx, |alert, _, _| { | |
| 260 | + alert | |
| 261 | + .title("Quit Vectrum Code?") | |
| 262 | + .description("Running agents and terminals stop. Press Enter to quit, or Escape to stay.") | |
| 263 | + .ok_text("Quit") | |
| 264 | + .ok_variant(ButtonVariant::Danger) | |
| 265 | + .cancel_text("Cancel") | |
| 266 | + .show_cancel(true) | |
| 267 | + .on_ok(|_, _, cx| { | |
| 268 | + cx.quit(); | |
| 269 | + true | |
| 270 | + }) | |
| 271 | + .on_close(|_, _, _| ASKING.store(false, Ordering::Relaxed)) | |
| 272 | + }); | |
| 273 | + true | |
| 274 | + } | |
| @@ -480,6 +480,8 @@ pub struct Settings { | ||
| 480 | 480 | pub confirm_settle: bool, |
| 481 | 481 | pub confirm_delete: bool, |
| 482 | 482 | pub quit_shortcut: QuitShortcut, |
| 483 | + /// Ctrl+Q, Alt+F4 and the window's close button ask before closing. | |
| 484 | + pub confirm_close: bool, | |
| 483 | 485 | |
| 484 | 486 | // ---- notifications and background work ---- |
| 485 | 487 | /// A desktop notification when a thread finishes or needs you while the |
| @@ -592,6 +594,7 @@ impl Default for Settings { | ||
| 592 | 594 | confirm_settle: false, |
| 593 | 595 | confirm_delete: true, |
| 594 | 596 | quit_shortcut: QuitShortcut::Hold, |
| 597 | + confirm_close: true, | |
| 595 | 598 | notify: false, |
| 596 | 599 | notify_sound: false, |
| 597 | 600 | in_app_notifications: false, |
| @@ -690,6 +693,7 @@ impl Settings { | ||
| 690 | 693 | ("Archive confirmation", self.confirm_settle != d.confirm_settle), |
| 691 | 694 | ("Delete confirmation", self.confirm_delete != d.confirm_delete), |
| 692 | 695 | ("Quit shortcut", self.quit_shortcut != d.quit_shortcut), |
| 696 | + ("Close confirmation", self.confirm_close != d.confirm_close), | |
| 693 | 697 | ("Text generation model", (&self.text_model, &self.text_reasoning) != (&d.text_model, &d.text_reasoning)), |
| 694 | 698 | ( |
| 695 | 699 | "Worktree branch naming", |
| @@ -502,6 +502,13 @@ impl SettingsView { | ||
| 502 | 502 | settings.quit_shortcut = shortcut |
| 503 | 503 | })), |
| 504 | 504 | ) |
| 505 | + .row( | |
| 506 | + row("Close confirmation") | |
| 507 | + .id("close-confirmation") | |
| 508 | + .description("Ask before Ctrl+Q, Alt+F4 or the window's close button closes the app.") | |
| 509 | + .reset(reset(&view, |s| s.confirm_close, |s, on| s.confirm_close = on)) | |
| 510 | + .control(switch("confirm-close", saved.confirm_close, &view, p, |settings, on| settings.confirm_close = on)), | |
| 511 | + ) | |
| 505 | 512 | .render(cx); |
| 506 | 513 | |
| 507 | 514 | // ---- Text generation ---- |
| @@ -152,6 +152,7 @@ const SETTINGS: &[(&str, &str)] = &[ | ||
| 152 | 152 | ("Settle confirmation", "General"), |
| 153 | 153 | ("Delete confirmation", "General"), |
| 154 | 154 | ("Quit shortcut", "General"), |
| 155 | + ("Close confirmation", "General"), | |
| 155 | 156 | ("Thread notifications", "Behavior"), |
| 156 | 157 | ("In-app notifications", "Behavior"), |
| 157 | 158 | ("Time format", "Behavior"), |