mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 00:15:06 +00:00
fix(settings): put the password and key passphrase back on the SSH host editor (#1130)
* fix(settings): put the password and key passphrase back on the SSH host editor The rebuilt settings window drew the host form from scratch and left out the credential rows. Everything behind them survived — the form still read the keychain on open, counted a typed secret as a change, and filed it on save — but with no box on screen there was no way to give a host a password short of connecting and waiting to be asked. The editor now shows a masked Password row (with a reveal toggle) after Port, and a Key passphrase row after IdentityFile. Each appears only when the host's auth method would actually use it, the same split the connection path makes: Password for Auto/Password, the passphrase for Auto/PublicKey once IdentityFile names a key that exists on this machine. * test(settings): the SSH editor's credential rows follow the auth method
This commit is contained in:
@@ -1466,6 +1466,20 @@ impl SshProfileForm {
|
||||
self.password.read(cx).value().as_ref() != self.loaded_password
|
||||
|| self.passphrase.read(cx).value().as_ref() != self.loaded_passphrase
|
||||
}
|
||||
|
||||
/// Which credential boxes the method actually uses — the same split
|
||||
/// `ssh_connect::build_spec_inner` makes when it decides what to hand the
|
||||
/// daemon. A password box under "Agent" would be a secret that is stored
|
||||
/// and then never offered.
|
||||
fn wants_password(&self) -> bool {
|
||||
matches!(self.auth, AuthMode::Auto | AuthMode::Password)
|
||||
}
|
||||
|
||||
/// A passphrase is filed against a key's contents, so the box only shows
|
||||
/// once the form names a key that is actually on this machine.
|
||||
fn wants_passphrase(&self) -> bool {
|
||||
matches!(self.auth, AuthMode::Auto | AuthMode::PublicKey) && self.loaded_key.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// What saving does to the keychain for the password field: which entry to
|
||||
@@ -3928,6 +3942,39 @@ mod gpui_tests {
|
||||
});
|
||||
}
|
||||
|
||||
/// The credential rows follow the auth method the same way connecting
|
||||
/// does: a password only for methods that offer one, a passphrase never
|
||||
/// for a method that does not try keys. (Whether Auto / PublicKey show the
|
||||
/// passphrase depends on the default keys in this machine's `~/.ssh`, so
|
||||
/// that half is not pinned here.)
|
||||
#[gpui::test]
|
||||
fn the_credential_rows_follow_the_auth_method(cx: &mut TestAppContext) {
|
||||
use crate::core::ssh_profile::{AuthMode, SshProfile};
|
||||
crate::core::config::pin_test_config_dir();
|
||||
let (app, mut vcx) = harness(cx);
|
||||
for (auth, password, keys) in [
|
||||
(AuthMode::Auto, true, true),
|
||||
(AuthMode::Password, true, false),
|
||||
(AuthMode::PublicKey, false, true),
|
||||
(AuthMode::Agent, false, false),
|
||||
] {
|
||||
let mut profile = SshProfile::new("creds");
|
||||
profile.auth = auth;
|
||||
let id = profile.id;
|
||||
app.update_in(&mut vcx, |app, window, cx| {
|
||||
cx.global_mut::<Config>().ssh_profiles.push(profile);
|
||||
app.open_ssh_profile_in_settings(id, window, cx);
|
||||
let form = app.active_settings().unwrap().ssh_form.as_ref().unwrap();
|
||||
assert_eq!(form.wants_password(), password, "{auth:?}");
|
||||
if !keys {
|
||||
assert!(!form.wants_passphrase(), "{auth:?} never tries a key");
|
||||
}
|
||||
app.cancel_ssh_form(cx);
|
||||
});
|
||||
vcx.run_until_parked();
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn failed_settings_writes_remain_visible_until_retry_succeeds(cx: &mut TestAppContext) {
|
||||
crate::core::config::pin_test_config_dir();
|
||||
|
||||
@@ -721,13 +721,42 @@ impl Tty7App {
|
||||
.max_w(px(FORM_FIELD_W)),
|
||||
)
|
||||
};
|
||||
// The secrets are not ssh_config keywords, so they are the only two
|
||||
// labels here that are translated. Neither reaches the config file:
|
||||
// `save_ssh_form_secrets` files them in the system keychain.
|
||||
let secret = |label: &'static str, input: &Entity<InputState>| {
|
||||
let focused = self.settings_input_focused(input, cx);
|
||||
h_flex()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.w(px(SSH_LABEL_W))
|
||||
.flex_shrink_0()
|
||||
.text_size(fs(12.))
|
||||
.text_color(tk.k45)
|
||||
.child(label),
|
||||
)
|
||||
.child(
|
||||
kit::secret_field(input, focused, &tk, cx)
|
||||
.bg(tk.page)
|
||||
.text_size(fs(11.5))
|
||||
.flex_1()
|
||||
.max_w(px(FORM_FIELD_W)),
|
||||
)
|
||||
};
|
||||
let fields = v_flex()
|
||||
.gap(px(6.))
|
||||
.child(field("Alias", &form.name, false))
|
||||
.child(field("HostName", &form.host, host_error.is_some()))
|
||||
.child(field("User", &form.user, false))
|
||||
.child(field("Port", &form.port, errors.port.is_some()))
|
||||
.when(form.wants_password(), |v| {
|
||||
v.child(secret(t(L10nKey::SettingsPassword), &form.password))
|
||||
})
|
||||
.child(field("IdentityFile", &form.identity_files, false))
|
||||
.when(form.wants_passphrase(), |v| {
|
||||
v.child(secret(t(L10nKey::SettingsKeyPassphrase), &form.passphrase))
|
||||
})
|
||||
.child(field("ProxyJump", &form.jump, errors.jump.is_some()));
|
||||
|
||||
v_flex()
|
||||
|
||||
+23
-1
@@ -488,6 +488,27 @@ pub(crate) fn text_field(
|
||||
invalid: bool,
|
||||
tk: &Tk,
|
||||
cx: &App,
|
||||
) -> Div {
|
||||
field_box(input, focused, invalid, false, tk, cx)
|
||||
}
|
||||
|
||||
/// A [`text_field`] for a masked input, with the eye that reveals it.
|
||||
pub(crate) fn secret_field(
|
||||
input: &gpui::Entity<InputState>,
|
||||
focused: bool,
|
||||
tk: &Tk,
|
||||
cx: &App,
|
||||
) -> Div {
|
||||
field_box(input, focused, false, true, tk, cx)
|
||||
}
|
||||
|
||||
fn field_box(
|
||||
input: &gpui::Entity<InputState>,
|
||||
focused: bool,
|
||||
invalid: bool,
|
||||
reveal: bool,
|
||||
tk: &Tk,
|
||||
cx: &App,
|
||||
) -> Div {
|
||||
let ring_color = if invalid {
|
||||
tk.danger
|
||||
@@ -519,7 +540,8 @@ pub(crate) fn text_field(
|
||||
// The input's own `text_sm` beat the 12px set on this
|
||||
// box, so the value stood a size above every dropdown
|
||||
// label in the same column.
|
||||
.text_size(fs(12.)),
|
||||
.text_size(fs(12.))
|
||||
.when(reveal, |i| i.mask_toggle()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user