mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
fix(ssh): give a failed one-off connection a way forward, and stop a switcher row eliding to a lone ellipsis
Edit connection… on an ad-hoc `user@host` opened Settings on "Nothing selected". The comment beside it already said a one-off is not editable and that offering it would open an empty page; the code under that comment only checked that the spec's uuid *parsed*, and every connection carries one — a transient profile is handed a fresh uuid on its way to the daemon. Both places that ask this question now ask it the same way, through `saved_profile_of`. A one-off that failed gets Save as Host… instead: the form opens prefilled from the address that failed, which is where a mistyped hostname actually gets corrected (#438). A saved host keeps Edit connection… and still opens its own profile. The switcher's second line put the workspace's timestamp in a box with an auto flex basis, so it started at its content width and then absorbed every pixel the row was over — collapsing to its zero minimum and leaving a bare "…" where the time goes. Rows with *less* to say lost more of it: `java-box · …` sat beside `local · ~/repo/025/tty7 ·…`. A zero basis that grows into what is left ellipsizes only what genuinely does not fit.
This commit is contained in:
+24
-8
@@ -182,11 +182,14 @@ impl Tty7App {
|
||||
_ => None,
|
||||
};
|
||||
// A saved connection is editable; a one-off `user@host` is not, and
|
||||
// offering to edit one would open an empty page.
|
||||
// offering to edit one opened Settings on "Nothing selected" — this
|
||||
// comment already said that was the thing to avoid, and the code under
|
||||
// it only checked that the spec's uuid *parsed*. Every connection has
|
||||
// one. Ask the same question `unsaved_ssh_session` asks.
|
||||
let profiles = &cx.global::<crate::core::config::Config>().ssh_profiles;
|
||||
let profile = view
|
||||
.ssh_spec()
|
||||
.and_then(|s| s.profile_id.clone())
|
||||
.and_then(|id| uuid::Uuid::parse_str(&id).ok());
|
||||
.and_then(|spec| crate::ui::ssh_connect::saved_profile_of(&spec, profiles));
|
||||
|
||||
let theme = cx.theme();
|
||||
|
||||
@@ -251,15 +254,28 @@ impl Tty7App {
|
||||
cx.listener(|this, _, window, cx| this.restart_ssh_session(window, cx)),
|
||||
),
|
||||
)
|
||||
.children(profile.map(|id| {
|
||||
Button::new("ssh-edit-profile")
|
||||
.label(crate::ui::i18n::t(crate::ui::i18n::L10nKey::SshEditProfile))
|
||||
.child(match profile {
|
||||
// A saved host: the form that opens is the one this connection
|
||||
// was dialled from, so a wrong hostname or password is fixed
|
||||
// where it lives.
|
||||
Some(id) => Button::new("ssh-edit-profile")
|
||||
.label(t(L10nKey::SshEditProfile))
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.open_ssh_profile_in_settings(id, window, cx)
|
||||
}))
|
||||
}));
|
||||
})),
|
||||
// A one-off: there is no host to open, but the address that
|
||||
// failed is worth keeping — the form opens prefilled from the
|
||||
// live spec, which is where the typo gets corrected (#438).
|
||||
None => Button::new("ssh-save-as-host")
|
||||
.label(t(L10nKey::SshSaveAsHost))
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.save_ssh_session_as_host(window, cx)
|
||||
})),
|
||||
});
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
|
||||
@@ -917,6 +917,7 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::ForwardPanelTitle => "Forwards",
|
||||
L10nKey::ForwardDisconnected => "Disconnected",
|
||||
L10nKey::ForwardDisconnectedFrom => "Disconnected from {host}",
|
||||
L10nKey::SshSaveAsHost => "Save as Host…",
|
||||
L10nKey::SshEditProfile => "Edit connection…",
|
||||
L10nKey::ForwardTooltipAdd => "Add forward",
|
||||
L10nKey::ForwardTooltipTurnOff => "Switch off — keeps the rule",
|
||||
|
||||
@@ -967,6 +967,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::ForwardPanelTitle => "ポートフォワード",
|
||||
L10nKey::ForwardDisconnected => "切断済み",
|
||||
L10nKey::ForwardDisconnectedFrom => "{host} から切断されました",
|
||||
L10nKey::SshSaveAsHost => "ホストとして保存…",
|
||||
L10nKey::SshEditProfile => "接続を編集…",
|
||||
L10nKey::ForwardTooltipAdd => "フォワードを追加",
|
||||
L10nKey::ForwardTooltipTurnOff => "無効にする(ルールは残す)",
|
||||
|
||||
@@ -678,6 +678,7 @@ l10n_keys! {
|
||||
ForwardDisconnected,
|
||||
ForwardDisconnectedFrom,
|
||||
SshEditProfile,
|
||||
SshSaveAsHost,
|
||||
ForwardTooltipAdd,
|
||||
ForwardTooltipRemove,
|
||||
ForwardTooltipTurnOff,
|
||||
|
||||
@@ -871,6 +871,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::ForwardPanelTitle => "端口转发",
|
||||
L10nKey::ForwardDisconnected => "已断开",
|
||||
L10nKey::ForwardDisconnectedFrom => "与 {host} 的连接已断开",
|
||||
L10nKey::SshSaveAsHost => "保存为主机…",
|
||||
L10nKey::SshEditProfile => "编辑连接…",
|
||||
L10nKey::ForwardTooltipAdd => "添加转发",
|
||||
L10nKey::ForwardTooltipTurnOff => "停用(保留规则)",
|
||||
|
||||
+44
-9
@@ -155,15 +155,7 @@ impl Tty7App {
|
||||
) -> Option<Box<NativeSshSpec>> {
|
||||
let spec = self.focused_pane_view(window, cx)?.read(cx).ssh_spec()?;
|
||||
let profiles = &cx.global::<Config>().ssh_profiles;
|
||||
// A transient profile is handed a fresh uuid on its way to the daemon,
|
||||
// so an id alone does not mean a host was saved — only one that still
|
||||
// resolves does.
|
||||
let saved = spec
|
||||
.profile_id
|
||||
.as_deref()
|
||||
.and_then(|s| uuid::Uuid::parse_str(s).ok())
|
||||
.is_some_and(|id| profiles.iter().any(|p| p.id == id));
|
||||
(!saved).then_some(spec)
|
||||
saved_profile_of(&spec, profiles).is_none().then_some(spec)
|
||||
}
|
||||
|
||||
/// Keep the connection in front of you: the form opens on everything the
|
||||
@@ -244,6 +236,21 @@ impl Tty7App {
|
||||
}
|
||||
}
|
||||
|
||||
/// The saved host a live connection came from, if it still exists.
|
||||
///
|
||||
/// A transient profile is handed a fresh uuid on its way to the daemon, so
|
||||
/// *every* spec carries a `profile_id` and an id alone does not mean anything
|
||||
/// was saved. Only one that still resolves does — and the two places that ask
|
||||
/// (whether to offer Save Connection as Host, and whether the failure strip's
|
||||
/// button edits a host or offers to keep one) have to agree, or an ad-hoc
|
||||
/// connection gets an Edit button that opens Settings on nothing.
|
||||
pub(crate) fn saved_profile_of(spec: &NativeSshSpec, profiles: &[SshProfile]) -> Option<Uuid> {
|
||||
spec.profile_id
|
||||
.as_deref()
|
||||
.and_then(|s| Uuid::parse_str(s).ok())
|
||||
.filter(|id| profiles.iter().any(|p| p.id == *id))
|
||||
}
|
||||
|
||||
/// Saved hosts, most likely first: whatever has been connected to often and
|
||||
/// recently, then alphabetically for everything nobody has used yet. Shared by
|
||||
/// the palette and the new-tab menu so the same host leads both lists.
|
||||
@@ -659,6 +666,34 @@ mod tests {
|
||||
assert_eq!(spec.password, None);
|
||||
}
|
||||
|
||||
/// Both the palette (Save Connection as Host) and the failure strip (Edit
|
||||
/// connection… vs Save as Host…) branch on this. They used to answer it
|
||||
/// separately, and the strip's copy of the test only checked that the uuid
|
||||
/// parsed — which every connection's does — so an ad-hoc `user@host` wore
|
||||
/// an Edit button that opened Settings on "Nothing selected".
|
||||
#[test]
|
||||
fn only_a_uuid_that_still_names_a_host_counts_as_saved() {
|
||||
let store = InMemoryCredentialStore::new();
|
||||
let saved = profile("prod-web", "10.0.0.5", "deploy");
|
||||
let profiles = vec![saved.clone()];
|
||||
|
||||
let spec = build_native_ssh_spec(&saved, &profiles, &store, true);
|
||||
assert_eq!(saved_profile_of(&spec, &profiles), Some(saved.id));
|
||||
|
||||
// What `quick_connect` and the palette's address box build: a profile
|
||||
// that never reached the config, carrying a uuid of its own.
|
||||
let adhoc = profile("", "nosuchhost.invalid", "root");
|
||||
let spec = build_native_ssh_spec(&adhoc, &profiles, &store, true);
|
||||
assert!(
|
||||
spec.profile_id.is_some(),
|
||||
"the spec does carry an id — that is exactly why the id alone is not the test"
|
||||
);
|
||||
assert_eq!(saved_profile_of(&spec, &profiles), None);
|
||||
|
||||
// And a host that was saved and has since been deleted.
|
||||
assert_eq!(saved_profile_of(&spec, &[]), None);
|
||||
}
|
||||
|
||||
/// The pane wears this until the remote shell titles itself, so a host
|
||||
/// nobody bothered to name still says where it went (#438). Every host
|
||||
/// imported from `~/.ssh/config` used to arrive nameless, and every one
|
||||
|
||||
+11
-1
@@ -2313,8 +2313,18 @@ impl Tty7App {
|
||||
.child(host_label),
|
||||
)
|
||||
.when(!when_path.is_empty(), |line| {
|
||||
// `flex_1`, not a bare `min_w_0`: with an auto
|
||||
// basis this box takes its content width as its
|
||||
// starting size and then absorbs every pixel
|
||||
// the row is over — which collapsed it to its
|
||||
// zero minimum and left a lone "…" where the
|
||||
// timestamp goes. Rows with *less* to say lost
|
||||
// more of it: `java-box · …` beside
|
||||
// `local · ~/repo/025/tty7 ·…`. A zero basis
|
||||
// that grows into what is left ellipsizes only
|
||||
// what genuinely does not fit.
|
||||
line.child(div().flex_shrink_0().child("·"))
|
||||
.child(div().min_w_0().truncate().child(when_path))
|
||||
.child(div().flex_1().min_w_0().truncate().child(when_path))
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user