feat(launcher): show configured SSH host names in launcher rows

The new-tab launcher, Quick Jump and the command-palette SSH rows used
to render the raw destination (user@host) as their label, ignoring the
user-configured "主机名称" (host name) from SSH settings.

- Add ssh_host_labels() alongside ssh_host_icon_ids() and pass the
  label map into shell_palette_rows().
- Share a single ssh_host_display() rule across the launcher, Quick
  Jump and palette rows: a named host shows its alias as the label with
  the real destination as the metadata hint; unnamed hosts keep the
  previous label=destination, hint="SSH" presentation.
- Search now matches both the alias and the destination.

Tests: launcher rows with and without a configured name, including the
existing no-label fallback.
This commit is contained in:
ZiChuanShanFeng committed 2026-09-24 00:28:44 +08:00
1 parent 1d26c2dc98
commit 3577d7a0e5
6 files changed
+88 -10

No files matched your search

+10
View File
@@ -636,6 +636,15 @@ fn ssh_host_icon_ids(data_dir: &Path) -> std::collections::HashMap<String, Strin
.unwrap_or_default()
}
/// 地址 → 展示名(用户在 SSH 设置里起的"主机名称"),与 `ssh_host_icon_ids`
/// 同一缓存策略:启动器 / Quick Jump / 命令面板在打开时各读一次,编辑保存后
/// 由下一轮打开刷新。没起名的主机不在这里,展示端回落地址本身。
fn ssh_host_labels(data_dir: &Path) -> std::collections::HashMap<String, String> {
crate::ssh_profiles::SshProfiles::load(&data_dir.join("ssh_profiles.json"))
.map(|profiles| profiles.labels())
.unwrap_or_default()
}
/// 标签的用户可编辑元数据:重命名与色标(旧壳 `TabEntry::custom_name` /
/// `custom_color` 的对应物,字段与共享 session v4 的 `TabSession` 同名同义,
/// 所以导出/恢复不需要转换层)。
@@ -2229,6 +2238,7 @@ impl NebulaWorkspace {
.map(|store| store.as_config_profiles())
.unwrap_or_default(),
crate::gpui_shell::ssh_hosts::SshHostLists::load().merged(),
&ssh_host_labels(&crate::display::nebula_data_dir()),
&default_shell_id,
language,
window.scale_factor().max(0.5),
@@ -38,6 +38,7 @@ fn open(cx: &mut TestAppContext) -> (Entity<NebulaWorkspace>, VisualTestContext)
vec![shell("cmd"), shell("pwsh")],
vec![],
["test-host".into()],
&std::collections::HashMap::new(),
"cmd",
workspace_ui_language(),
1.0,
+10 -3
View File
@@ -99,6 +99,7 @@ impl NebulaWorkspace {
// 启动器混排(旧壳 ⌘K 裁定):SSH 主机与命令同列,置顶/隐藏
// 次序由共享 merge 权威裁定。
let ssh_icons = ssh_host_icon_ids(&crate::display::nebula_data_dir());
let ssh_labels = ssh_host_labels(&crate::display::nebula_data_dir());
rows.extend(
crate::gpui_shell::ssh_hosts::SshHostLists::load().merged().into_iter().map(
|host| {
@@ -106,13 +107,19 @@ impl NebulaWorkspace {
ssh_icons.get(&host).map(String::as_str),
)
.glyph;
let (label, hint) = shell_picker::ssh_host_display(
ssh_labels.get(&host).map(String::as_str),
&host,
);
let search = format!("{label} {host} ssh host remote lianjie 连接")
.to_lowercase();
WorkspacePaletteRow {
group_order: usize::MAX,
group: language.pick("SSH 主机", "SSH HOSTS").to_owned(),
label: host.clone(),
hint: "SSH".to_owned(),
label,
hint,
hint_style: WorkspacePaletteHintStyle::Metadata,
search: format!("{host} ssh host remote lianjie 连接").to_lowercase(),
search,
action: WorkspacePaletteAction::LaunchSshHost(host),
icon: None,
icon_glyph: Some(glyph),
@@ -142,19 +142,23 @@ fn directory_rows(language: crate::display::UiLanguage) -> Vec<WorkspacePaletteR
fn ssh_rows(language: crate::display::UiLanguage) -> Vec<WorkspacePaletteRow> {
let group = language.pick("SSH 主机", "SSH hosts");
let icons = ssh_host_icon_ids(&crate::display::nebula_data_dir());
let labels = ssh_host_labels(&crate::display::nebula_data_dir());
crate::gpui_shell::ssh_hosts::SshHostLists::load()
.merged()
.into_iter()
.map(|host| {
let glyph =
crate::display::ui::os_icons::resolve(icons.get(&host).map(String::as_str)).glyph;
let (label, hint) =
super::shell_picker::ssh_host_display(labels.get(&host).map(String::as_str), &host);
let search = format!("{label} {host} ssh host remote 远程 连接").to_lowercase();
WorkspacePaletteRow {
group_order: 3,
group: group.to_owned(),
label: host.clone(),
hint: "SSH".to_owned(),
label,
hint,
hint_style: super::WorkspacePaletteHintStyle::Metadata,
search: format!("{host} ssh host remote 远程 连接").to_lowercase(),
search,
action: WorkspacePaletteAction::LaunchSshHost(host),
icon: None,
icon_glyph: Some(glyph),
@@ -15,6 +15,20 @@ fn fallback_shell_glyph(id: &str, has_brand: bool) -> Option<char> {
crate::shell_detect::icon_for_id(id).chars().next()
}
/// SSH 主机的展示名与次要信息,与旧壳 `SshRow` 同口径:
///
/// - 用户在 SSH 设置里起过"主机名称":label 显示别名,hint 给真实连接地址;
/// - 没起名:回落地址本身当 label,hint 保持 "SSH" 类型标签,避免重复。
pub(super) fn ssh_host_display(
label: Option<&str>,
host: &str,
) -> (String, String) {
match label {
Some(label) if !label.trim().is_empty() => (label.trim().to_owned(), host.to_owned()),
_ => (host.to_owned(), "SSH".to_owned()),
}
}
/// 新建终端弹窗的行:已检测 shell + SSH 主机,分组对照旧壳
/// `CommandPalette::open_profiles`(推荐 / 所有 Shell / SSH 主机)。
/// 三点菜单与 Ctrl+K 打开的是这份列表,不是通用命令面板。
@@ -22,6 +36,7 @@ pub(super) fn shell_palette_rows(
shells: Vec<crate::shell_detect::DetectedShell>,
profiles: Vec<crate::config::ui_config::Profile>,
ssh_hosts: impl IntoIterator<Item = String>,
ssh_labels: &std::collections::HashMap<String, String>,
default_shell_id: &str,
language: crate::display::UiLanguage,
scale_factor: f32,
@@ -88,13 +103,16 @@ pub(super) fn shell_palette_rows(
rows.extend(ssh_hosts.into_iter().map(|host| {
let glyph =
crate::display::ui::os_icons::resolve(ssh_icons.get(&host).map(String::as_str)).glyph;
let (label, hint) =
ssh_host_display(ssh_labels.get(&host).map(String::as_str), &host);
let search = format!("{label} {host} ssh host remote lianjie 连接").to_lowercase();
WorkspacePaletteRow {
group_order: 2,
group: ssh_group.to_owned(),
label: host.clone(),
hint: "SSH".to_owned(),
label,
hint,
hint_style: WorkspacePaletteHintStyle::Metadata,
search: format!("{host} ssh host remote lianjie 连接").to_lowercase(),
search,
action: WorkspacePaletteAction::LaunchSshHost(host),
icon: None,
icon_glyph: Some(glyph),
+39 -1
View File
@@ -557,6 +557,7 @@ fn shell_palette_puts_the_default_shell_first() {
shells.clone(),
Vec::new(),
["box.example".to_owned()],
&std::collections::HashMap::new(),
"nu",
language,
1.0,
@@ -583,7 +584,7 @@ fn shell_palette_puts_the_default_shell_first() {
assert_eq!(rows[4].group, "SSH 主机");
// 默认 id 没在检测结果里(WSL 发行版被卸载等):不置顶也不 panic。
let rows = shell_palette_rows(shells, Vec::new(), None::<String>, "wsl:Ghost", language, 1.0);
let rows = shell_palette_rows(shells, Vec::new(), None::<String>, &std::collections::HashMap::new(), "wsl:Ghost", language, 1.0);
assert!(matches!(
&rows[0].action,
WorkspacePaletteAction::LaunchShell(shell) if shell.id == "pwsh"
@@ -591,6 +592,41 @@ fn shell_palette_puts_the_default_shell_first() {
assert_eq!(rows[0].group, "所有 Shell");
}
/// 启动器 SSH 行的"主机名称":起过名的显示别名、hint 给真实地址;没起名
/// 回落地址本身、hint 保持 "SSH"。搜索词必须同时覆盖别名与地址。
#[test]
fn shell_palette_ssh_rows_show_the_configured_host_name() {
let labels = [("root@box.example".to_owned(), "生产机".to_owned())].into();
let rows = shell_palette_rows(
Vec::new(),
Vec::new(),
["root@box.example".to_owned(), "plain.example".to_owned()],
&labels,
"cmd",
crate::display::UiLanguage::ZhCn,
1.0,
);
assert_eq!(rows.len(), 2);
let named = &rows[0];
assert_eq!(named.label, "生产机");
assert_eq!(named.hint, "root@box.example");
assert!(named.search.contains("生产机"));
assert!(named.search.contains("root@box.example"));
assert!(matches!(
&named.action,
WorkspacePaletteAction::LaunchSshHost(host) if host == "root@box.example"
));
let plain = &rows[1];
assert_eq!(plain.label, "plain.example");
assert_eq!(plain.hint, "SSH");
assert!(matches!(
&plain.action,
WorkspacePaletteAction::LaunchSshHost(host) if host == "plain.example"
));
}
/// 回归锁:新建终端弹窗里,没有品牌贴图的 shell 不能空着——必须回落到按 id
/// 取字的 Nerd Font 字形。之前这一列只填了 `icon`,于是 zsh、csh、ksh 这些
/// 整行左边什么都没有。
@@ -609,6 +645,7 @@ fn shell_palette_falls_back_to_an_id_glyph_when_brand_art_is_absent() {
shells,
Vec::new(),
None::<String>,
&std::collections::HashMap::new(),
"zsh",
crate::display::UiLanguage::ZhCn,
1.0,
@@ -650,6 +687,7 @@ fn shell_palette_includes_imported_terminal_profiles() {
Vec::new(),
vec![profile],
None::<String>,
&std::collections::HashMap::new(),
&id,
crate::display::UiLanguage::ZhCn,
1.0,