From 78e5dabb14fdca13489710e78397c0c5621229d7 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 8 Aug 2026 02:41:44 +0800 Subject: [PATCH] fix(settings): stop Escape from throwing away an unsaved connection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editing an SSH profile and pressing Escape closed Settings and discarded the form. `dirty` was already computed — it is what enables Save — but only to grey out a button, never to ask. Escape, the close button and ⌘, now ask when the form holds unsaved edits, with Keep Editing as the default. Every other caller of close_settings runs as the tail of something the user explicitly chose (Connect, Save & Connect, quick connect) and closes as before. --- src/ui/app.rs | 2 +- src/ui/i18n/en.rs | 5 +++++ src/ui/i18n/ja.rs | 3 +++ src/ui/i18n/mod.rs | 6 ++++++ src/ui/i18n/zh.rs | 3 +++ src/ui/settings.rs | 42 ++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index bf5c4880..f9637a14 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -3601,7 +3601,7 @@ impl Tty7App { fn toggle_settings(&mut self, window: &mut Window, cx: &mut Context) { if self.settings.is_some() { - self.close_settings(window, cx); + self.close_settings_checked(window, cx); return; } self.remember_active_pane(window, cx); diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index f8767753..07d6515d 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -181,6 +181,11 @@ pub fn translate_en(key: L10nKey) -> &'static str { "Ask for confirmation before closing a tab or pane with a live SSH session." } L10nKey::SettingsNewHost => "New host", + L10nKey::SettingsDiscardChangesTitle => "Discard unsaved changes?", + L10nKey::SettingsDiscardChangesBody => { + "The connection you are editing has changes that were never saved." + } + L10nKey::SettingsKeepEditing => "Keep Editing", L10nKey::SettingsName => "Name", L10nKey::SettingsNameDesc => "A label for this connection.", L10nKey::SettingsHost => "Host", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index 2641042d..817046ca 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -179,6 +179,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { "アクティブな SSH セッションのあるタブやペインを閉じる前に確認を求めます" } L10nKey::SettingsNewHost => "新規ホスト", + L10nKey::SettingsDiscardChangesTitle => "保存していない変更を破棄しますか?", + L10nKey::SettingsDiscardChangesBody => "編集中の接続に、まだ保存していない変更があります。", + L10nKey::SettingsKeepEditing => "編集を続ける", L10nKey::SettingsName => "名前", L10nKey::SettingsNameDesc => "この接続の表示名", L10nKey::SettingsHost => "ホスト名", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index add96591..ad43fc75 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -192,6 +192,9 @@ pub enum L10nKey { WarnBeforeClosing, SettingsWarnBeforeClosingDesc, SettingsNewHost, + SettingsDiscardChangesTitle, + SettingsDiscardChangesBody, + SettingsKeepEditing, SettingsName, SettingsNameDesc, SettingsHost, @@ -1227,6 +1230,9 @@ mod tests { L10nKey::WarnBeforeClosing, L10nKey::SettingsWarnBeforeClosingDesc, L10nKey::SettingsNewHost, + L10nKey::SettingsDiscardChangesTitle, + L10nKey::SettingsDiscardChangesBody, + L10nKey::SettingsKeepEditing, L10nKey::SettingsName, L10nKey::SettingsNameDesc, L10nKey::SettingsHost, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index cd02070f..cf82ee8c 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -159,6 +159,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { "在关闭带有活动 SSH 会话的标签页或窗格前请求确认。" } L10nKey::SettingsNewHost => "新主机", + L10nKey::SettingsDiscardChangesTitle => "丢弃未保存的改动?", + L10nKey::SettingsDiscardChangesBody => "你正在编辑的连接有还没保存的改动。", + L10nKey::SettingsKeepEditing => "继续编辑", L10nKey::SettingsName => "名称", L10nKey::SettingsNameDesc => "此连接的标签。", L10nKey::SettingsHost => "主机", diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 3a893306..a23e3558 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -894,7 +894,7 @@ impl Tty7App { .track_focus(&focus_handle) .on_key_down(cx.listener(|this, ev: &KeyDownEvent, window, cx| { if ev.keystroke.key.as_str() == "escape" { - this.close_settings(window, cx); + this.close_settings_checked(window, cx); } })) .child(sidebar) @@ -933,7 +933,7 @@ impl Tty7App { .h(px(TILE_SIZE)) .rounded_lg() .on_click(cx.listener(|this, _, window, cx| { - this.close_settings(window, cx) + this.close_settings_checked(window, cx) })), ), ) @@ -2480,6 +2480,44 @@ impl Tty7App { cx.notify(); } + /// Whether the SSH profile form on screen holds edits that were never + /// saved. Save is enabled off exactly this, so closing on it is the same + /// question the button already answers. + pub(crate) fn ssh_form_dirty(&self, cx: &App) -> bool { + let Some(form) = self.active_settings().and_then(|s| s.ssh_form.as_ref()) else { + return false; + }; + let saved = cx + .global::() + .ssh_profiles + .iter() + .find(|p| p.id == form.editing) + .cloned(); + self.ssh_form_collect(cx) != saved + } + + /// Closing from Escape or the X is the user leaving; every other caller + /// closes as the tail of something they explicitly chose, and has already + /// saved or does not care. + pub(crate) fn close_settings_checked(&mut self, window: &mut Window, cx: &mut Context) { + if !self.ssh_form_dirty(cx) { + self.close_settings(window, cx); + return; + } + let answer = window.prompt( + gpui::PromptLevel::Warning, + t(L10nKey::SettingsDiscardChangesTitle), + Some(t(L10nKey::SettingsDiscardChangesBody)), + &[t(L10nKey::SettingsKeepEditing), t(L10nKey::EditorDiscard)], + cx, + ); + cx.spawn_in(window, async move |this, cx| { + let Ok(1) = answer.await else { return }; + let _ = this.update_in(cx, |this, window, cx| this.close_settings(window, cx)); + }) + .detach(); + } + pub(crate) fn save_and_connect_profile(&mut self, window: &mut Window, cx: &mut Context) { if let Some(id) = self.save_editing_profile(cx) { self.close_settings(window, cx);