Vectrum
Sign In

Ask before Ctrl+Q, Alt+F4 or the close button quits

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>
JjohannSocommitted 1 parent 3d9f92ecommitcb860d1

Showing 5 changed files with 70 additions and 7 deletions.

+2−1README.mdView File
@@ -163,7 +163,8 @@ Gruvbox, Rosé Pine, Solarized and Dracula.
163163 menu, the rich text composer, collapsing on scroll, the send shortcut (Enter, Ctrl+Enter
164164 for multiline prompts, or Ctrl+Enter always), follow-up behavior, update checks,
165165 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
167168 generation model; the version; Diagnostics (CPU, memory and every process the app
168169 started, with interrupt and kill buttons) and the open source licenses; and the folded
169170 Legacy features (plan mode, the context window ring, the per-project sidebar).
+56−6src/main.rsView File
@@ -21,11 +21,14 @@ mod worktree;
2121
2222 use std::{
2323 path::PathBuf,
24- sync::Mutex,
24+ sync::{
25+ Mutex,
26+ atomic::{AtomicBool, Ordering},
27+ },
2528 time::{Duration, Instant},
2629 };
2730
28- use gpui_kit::component::{WindowExt as _, notification::Notification};
31+ use gpui_kit::component::{WindowExt as _, button::ButtonVariant, notification::Notification};
2932 use gpui_kit::*;
3033
3134 use crate::ui::{icons::AppAssets, metal, workspace::Workspace};
@@ -191,6 +194,8 @@ fn main() {
191194 };
192195 gpui_kit::open_window(options, cx, |window, cx| {
193196 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));
194199 cx.new(|cx| Workspace::new(project, window, cx))
195200 })
196201 .expect("failed to open the main window");
@@ -198,17 +203,17 @@ fn main() {
198203 });
199204 }
200205
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.
203208 fn quit(_: &Quit, cx: &mut App) {
204209 static FIRST_PRESS: Mutex<Option<Instant>> = Mutex::new(None);
205210 if settings::get(|settings| settings.quit_shortcut) == settings::QuitShortcut::Press {
206- cx.quit();
211+ quit_or_ask(cx);
207212 return;
208213 }
209214 let mut first = FIRST_PRESS.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
210215 if first.is_some_and(|at| at.elapsed() < QUIT_WINDOW) {
211- cx.quit();
216+ quit_or_ask(cx);
212217 return;
213218 }
214219 *first = Some(Instant::now());
@@ -222,3 +227,48 @@ fn quit(_: &Quit, cx: &mut App) {
222227 });
223228 }
224229 }
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+ }
+4−0src/settings.rsView File
@@ -480,6 +480,8 @@ pub struct Settings {
480480 pub confirm_settle: bool,
481481 pub confirm_delete: bool,
482482 pub quit_shortcut: QuitShortcut,
483+ /// Ctrl+Q, Alt+F4 and the window's close button ask before closing.
484+ pub confirm_close: bool,
483485
484486 // ---- notifications and background work ----
485487 /// A desktop notification when a thread finishes or needs you while the
@@ -592,6 +594,7 @@ impl Default for Settings {
592594 confirm_settle: false,
593595 confirm_delete: true,
594596 quit_shortcut: QuitShortcut::Hold,
597+ confirm_close: true,
595598 notify: false,
596599 notify_sound: false,
597600 in_app_notifications: false,
@@ -690,6 +693,7 @@ impl Settings {
690693 ("Archive confirmation", self.confirm_settle != d.confirm_settle),
691694 ("Delete confirmation", self.confirm_delete != d.confirm_delete),
692695 ("Quit shortcut", self.quit_shortcut != d.quit_shortcut),
696+ ("Close confirmation", self.confirm_close != d.confirm_close),
693697 ("Text generation model", (&self.text_model, &self.text_reasoning) != (&d.text_model, &d.text_reasoning)),
694698 (
695699 "Worktree branch naming",
+7−0src/ui/settings/general.rsView File
@@ -502,6 +502,13 @@ impl SettingsView {
502502 settings.quit_shortcut = shortcut
503503 })),
504504 )
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+ )
505512 .render(cx);
506513
507514 // ---- Text generation ----
+1−0src/ui/workspace/palette.rsView File
@@ -152,6 +152,7 @@ const SETTINGS: &[(&str, &str)] = &[
152152 ("Settle confirmation", "General"),
153153 ("Delete confirmation", "General"),
154154 ("Quit shortcut", "General"),
155+ ("Close confirmation", "General"),
155156 ("Thread notifications", "Behavior"),
156157 ("In-app notifications", "Behavior"),
157158 ("Time format", "Behavior"),