From cddeec78b3a5943e0ee37dd45235368b7ada9429 Mon Sep 17 00:00:00 2001 From: YinBuLiao Date: Wed, 23 Sep 2026 15:22:24 +0800 Subject: [PATCH 1/3] fix(macos): open terminal links with Command-click --- nebula_app/i18n/de-DE.json | 2 + nebula_app/i18n/en-US.json | 2 + nebula_app/i18n/es-ES.json | 2 + nebula_app/i18n/fr-FR.json | 2 + nebula_app/i18n/it-IT.json | 2 + nebula_app/i18n/ja-JP.json | 2 + nebula_app/i18n/ko-KR.json | 2 + nebula_app/i18n/pt-BR.json | 2 + nebula_app/i18n/ru-RU.json | 2 + nebula_app/i18n/zh-CN.json | 2 + nebula_app/i18n/zh-TW.json | 2 + nebula_app/src/display/hint.rs | 16 +- .../src/gpui_shell/terminal/osc_links.rs | 29 ++- nebula_app/src/gpui_shell/terminal/view.rs | 2 +- .../src/gpui_shell/terminal/view/pointer.rs | 65 ++++--- .../gpui_shell/terminal/view/pointer/tests.rs | 175 ++++++++++++++++++ 16 files changed, 274 insertions(+), 35 deletions(-) diff --git a/nebula_app/i18n/de-DE.json b/nebula_app/i18n/de-DE.json index af6269f..a68ccae 100644 --- a/nebula_app/i18n/de-DE.json +++ b/nebula_app/i18n/de-DE.json @@ -45,6 +45,8 @@ } }, "common": { + "link_command_click": "Command+Linksklick zum Öffnen", + "link_ctrl_click": "Strg+Linksklick zum Öffnen", "settings": "Einstellungen", "language": "Sprache", "about": "Über", diff --git a/nebula_app/i18n/en-US.json b/nebula_app/i18n/en-US.json index c74b422..706623c 100644 --- a/nebula_app/i18n/en-US.json +++ b/nebula_app/i18n/en-US.json @@ -209,6 +209,8 @@ } }, "common": { + "link_command_click": "Command+left click to open", + "link_ctrl_click": "Ctrl+left click to open", "settings": "Settings", "language": "Language", "about": "About", diff --git a/nebula_app/i18n/es-ES.json b/nebula_app/i18n/es-ES.json index 080b3c6..eae402b 100644 --- a/nebula_app/i18n/es-ES.json +++ b/nebula_app/i18n/es-ES.json @@ -45,6 +45,8 @@ } }, "common": { + "link_command_click": "Command+clic izquierdo para abrir", + "link_ctrl_click": "Ctrl+clic izquierdo para abrir", "settings": "Configuración", "language": "Idioma", "about": "Acerca de", diff --git a/nebula_app/i18n/fr-FR.json b/nebula_app/i18n/fr-FR.json index 1c2afd0..3b18763 100644 --- a/nebula_app/i18n/fr-FR.json +++ b/nebula_app/i18n/fr-FR.json @@ -95,6 +95,8 @@ } }, "common": { + "link_command_click": "Command+clic gauche pour ouvrir", + "link_ctrl_click": "Ctrl+clic gauche pour ouvrir", "settings": "Paramètres", "language": "Langue", "about": "À propos", diff --git a/nebula_app/i18n/it-IT.json b/nebula_app/i18n/it-IT.json index 2c05661..a65ec0e 100644 --- a/nebula_app/i18n/it-IT.json +++ b/nebula_app/i18n/it-IT.json @@ -45,6 +45,8 @@ } }, "common": { + "link_command_click": "Command+clic sinistro per aprire", + "link_ctrl_click": "Ctrl+clic sinistro per aprire", "settings": "Impostazioni", "language": "Lingua", "about": "Informazioni", diff --git a/nebula_app/i18n/ja-JP.json b/nebula_app/i18n/ja-JP.json index 0a56ca2..46e8a24 100644 --- a/nebula_app/i18n/ja-JP.json +++ b/nebula_app/i18n/ja-JP.json @@ -45,6 +45,8 @@ } }, "common": { + "link_command_click": "Command+左クリックで開く", + "link_ctrl_click": "Ctrl+左クリックで開く", "settings": "設定", "language": "言語", "about": "このアプリについて", diff --git a/nebula_app/i18n/ko-KR.json b/nebula_app/i18n/ko-KR.json index 467393f..7e3168d 100644 --- a/nebula_app/i18n/ko-KR.json +++ b/nebula_app/i18n/ko-KR.json @@ -45,6 +45,8 @@ } }, "common": { + "link_command_click": "Command+왼쪽 클릭으로 열기", + "link_ctrl_click": "Ctrl+왼쪽 클릭으로 열기", "settings": "설정", "language": "언어", "about": "정보", diff --git a/nebula_app/i18n/pt-BR.json b/nebula_app/i18n/pt-BR.json index d8a00b9..97714ff 100644 --- a/nebula_app/i18n/pt-BR.json +++ b/nebula_app/i18n/pt-BR.json @@ -45,6 +45,8 @@ } }, "common": { + "link_command_click": "Command+clique esquerdo para abrir", + "link_ctrl_click": "Ctrl+clique esquerdo para abrir", "settings": "Configurações", "language": "Idioma", "about": "Sobre", diff --git a/nebula_app/i18n/ru-RU.json b/nebula_app/i18n/ru-RU.json index 14e5a1e..a12f93f 100644 --- a/nebula_app/i18n/ru-RU.json +++ b/nebula_app/i18n/ru-RU.json @@ -45,6 +45,8 @@ } }, "common": { + "link_command_click": "Command+щелчок левой кнопкой для открытия", + "link_ctrl_click": "Ctrl+щелчок левой кнопкой для открытия", "settings": "Настройки", "language": "Язык", "about": "О программе", diff --git a/nebula_app/i18n/zh-CN.json b/nebula_app/i18n/zh-CN.json index 9bacfd5..a853d52 100644 --- a/nebula_app/i18n/zh-CN.json +++ b/nebula_app/i18n/zh-CN.json @@ -209,6 +209,8 @@ } }, "common": { + "link_command_click": "Command+左键跳转", + "link_ctrl_click": "Ctrl+左键跳转", "settings": "设置", "language": "语言", "about": "关于", diff --git a/nebula_app/i18n/zh-TW.json b/nebula_app/i18n/zh-TW.json index 9f3abf3..b185d84 100644 --- a/nebula_app/i18n/zh-TW.json +++ b/nebula_app/i18n/zh-TW.json @@ -69,6 +69,8 @@ } }, "common": { + "link_command_click": "Command+左鍵開啟", + "link_ctrl_click": "Ctrl+左鍵開啟", "settings": "設定", "language": "語言", "about": "關於", diff --git a/nebula_app/src/display/hint.rs b/nebula_app/src/display/hint.rs index 7e3b6ae..761d539 100644 --- a/nebula_app/src/display/hint.rs +++ b/nebula_app/src/display/hint.rs @@ -469,6 +469,18 @@ pub fn highlighted_at( config: &UiConfig, point: Point, mouse_mods: ModifiersState, +) -> Option { + highlighted_at_with_mouse_override(term, config, point, mouse_mods, false) +} + +/// Allow a shell's explicit link gesture to override application mouse reporting. +/// Configured hint modifiers and the mouse-enabled flag are still respected. +pub fn highlighted_at_with_mouse_override( + term: &Term, + config: &UiConfig, + point: Point, + mouse_mods: ModifiersState, + override_mouse_mode: bool, ) -> Option { let mouse_mode = term.mode().intersects(TermMode::MOUSE_MODE); @@ -477,7 +489,9 @@ pub fn highlighted_at( let highlight = hint.mouse.is_some_and(|mouse| { mouse.enabled && mouse_mods.contains(mouse.mods.0) - && (!mouse_mode || mouse_mods.contains(ModifiersState::SHIFT)) + && (!mouse_mode + || override_mouse_mode + || mouse_mods.contains(ModifiersState::SHIFT)) }); if !highlight { return None; diff --git a/nebula_app/src/gpui_shell/terminal/osc_links.rs b/nebula_app/src/gpui_shell/terminal/osc_links.rs index 94a3912..64a588a 100644 --- a/nebula_app/src/gpui_shell/terminal/osc_links.rs +++ b/nebula_app/src/gpui_shell/terminal/osc_links.rs @@ -1,4 +1,4 @@ -//! GPUI 壳的 OSC 8 / 正则 hint 接线:虚线下划线、悬停预览、Ctrl+点击打开。 +//! GPUI 壳的 OSC 8 / 正则 hint 接线:虚线下划线、悬停预览、平台修饰键+点击打开。 //! //! 匹配与动作全部复用旧壳 `display::hint` + `file_uri` + `daemon`,这里只做 //! 视口坐标、GPUI 修饰键和打开入口。 @@ -16,6 +16,15 @@ use winit::keyboard::ModifiersState; use crate::config::UiConfig; use crate::config::ui_config::{HintAction, HintInternalAction, default_hint_command}; use crate::display::hint::{self, HintMatch}; +use crate::i18n::{Message, UiLanguage}; +use crate::platform::Platform; + +pub(super) fn link_modifier(mods: &gpui::Modifiers) -> bool { + match Platform::current() { + Platform::MacOS => mods.platform, + Platform::Windows | Platform::Linux => mods.control, + } +} /// 悬停目标:旧壳 `highlighted_hint` + 已经解码好的预览文案。 #[derive(Clone)] @@ -78,7 +87,13 @@ pub(super) fn highlighted_at( point: Point, mods: &gpui::Modifiers, ) -> Option { - hint::highlighted_at(term, config, point, winit_mouse_mods(mods)) + hint::highlighted_at_with_mouse_override( + term, + config, + point, + winit_mouse_mods(mods), + link_modifier(mods), + ) } pub(super) fn hover_from_hint( @@ -86,6 +101,7 @@ pub(super) fn hover_from_hint( hint: HintMatch, rows: usize, cols: usize, + language: UiLanguage, ) -> Option { let raw = hint .hyperlink() @@ -98,12 +114,15 @@ pub(super) fn hover_from_hint( point_to_viewport_from(origin, start).filter(|vp| vp.line < rows && vp.column.0 < cols); let (anchor_row, anchor_col) = vp.map(|vp| (vp.line as u16, vp.column.0 as u16)).unwrap_or((0, 0)); - const HINT: &str = " · Ctrl+点击"; + let gesture = language.text(match Platform::current() { + Platform::MacOS => Message::CommonLinkCommandClick, + Platform::Windows | Platform::Linux => Message::CommonLinkCtrlClick, + }); let width = |s: &str| -> usize { s.chars().map(|c| c.width().unwrap_or(0)).sum() }; let target = crate::display::strip_file_scheme(uri); - let budget = cols.saturating_sub(width(HINT) + 1); + let budget = cols.saturating_sub(width(gesture) + width(" · ") + 1); let target = crate::display::fit_tail(&target, budget); - Some(LinkHover { hint, preview: format!("{target}{HINT}"), anchor_row, anchor_col }) + Some(LinkHover { hint, preview: format!("{target} · {gesture}"), anchor_row, anchor_col }) } pub(super) fn open_hint_match( diff --git a/nebula_app/src/gpui_shell/terminal/view.rs b/nebula_app/src/gpui_shell/terminal/view.rs index dea90ae..a3a488d 100644 --- a/nebula_app/src/gpui_shell/terminal/view.rs +++ b/nebula_app/src/gpui_shell/terminal/view.rs @@ -402,7 +402,7 @@ pub struct TerminalView { selection_scroll_epoch: u64, /// 是否已有一条自动回滚定时器链在跑——每次 move 都开一条会叠出 N 倍速。 selection_scroll_active: bool, - /// OSC 8 / 正则 URL:虚线下划线、悬停预览、Ctrl+点击打开。 + /// OSC 8 / 正则 URL:虚线下划线、悬停预览、平台修饰键+点击打开。 pub(super) hint_config: Arc, pub(super) link_hover: Option, pending_link_open: bool, diff --git a/nebula_app/src/gpui_shell/terminal/view/pointer.rs b/nebula_app/src/gpui_shell/terminal/view/pointer.rs index 25367d1..64074d7 100644 --- a/nebula_app/src/gpui_shell/terminal/view/pointer.rs +++ b/nebula_app/src/gpui_shell/terminal/view/pointer.rs @@ -1,3 +1,4 @@ +use super::super::osc_links::link_modifier; use super::*; use gpui_component::WindowExt as _; @@ -143,7 +144,13 @@ impl TerminalView { let term = session.term.lock(); super::super::osc_links::highlighted_at(&term, &self.hint_config, point, mods) .and_then(|hint| { - super::super::osc_links::hover_from_hint(&term, hint, self.rows, self.cols) + super::super::osc_links::hover_from_hint( + &term, + hint, + self.rows, + self.cols, + crate::gpui_shell::config::ui_language(cx), + ) }) }) }; @@ -355,6 +362,7 @@ impl TerminalView { cx: &mut Context, ) { window.focus(&self.focus_handle, cx); + self.pending_link_open = false; cx.emit(TerminalViewEvent::FocusRequested); if self.session.is_none() { return; @@ -400,6 +408,17 @@ impl TerminalView { cx.notify(); return; } + if link_modifier(&event.modifiers) && event.click_count == 1 { + self.update_link_hover(event.position, &event.modifiers, cx); + if self.link_hover.is_some() { + if let Some(session) = &self.session { + session.term.lock().selection = None; + } + self.selecting = false; + self.pending_link_open = true; + return; + } + } if self.mouse_mode_active(&event.modifiers) { self.send_mouse_report( event.position, @@ -409,28 +428,6 @@ impl TerminalView { ); return; } - if event.modifiers.control && event.click_count == 1 { - let (point, _) = self.grid_point(event.position); - let hit = self.session.as_ref().is_some_and(|session| { - let term = session.term.lock(); - super::super::osc_links::highlighted_at( - &term, - &self.hint_config, - point, - &event.modifiers, - ) - .is_some() - }); - if hit { - if let Some(session) = &self.session { - session.term.lock().selection = None; - } - self.selecting = false; - self.pending_link_open = true; - self.update_link_hover(event.position, &event.modifiers, cx); - return; - } - } let (point, side) = self.grid_point(event.position); let ty = match event.click_count { 1 => SelectionType::Simple, @@ -465,6 +462,11 @@ impl TerminalView { window: &mut Window, cx: &mut Context, ) { + // Retain the pressed link until release; dragging must not retarget it + // or leak part of the consumed gesture to the application. + if self.pending_link_open { + return; + } if self.move_completion_popup_scrollbar(event, cx) { cx.stop_propagation(); return; @@ -524,6 +526,12 @@ impl TerminalView { window.focus(&self.focus_handle, cx); cx.emit(TerminalViewEvent::FocusRequested); } + if event.pressed_button.is_none() && link_modifier(&event.modifiers) { + self.update_link_hover(event.position, &event.modifiers, cx); + if self.link_hover.is_some() { + return; + } + } if self.mouse_mode_active(&event.modifiers) { self.clear_link_hover(cx); // 鼠标模式的移动上报:拖动 = 按钮码+32(需 DRAG 或 MOTION 任一), @@ -574,7 +582,8 @@ impl TerminalView { cx.notify(); return; } - if !self.selecting && self.mouse_mode_active(&event.modifiers) { + let pending_link_open = std::mem::take(&mut self.pending_link_open); + if !pending_link_open && !self.selecting && self.mouse_mode_active(&event.modifiers) { self.send_mouse_report( event.position, mouse_protocol::BUTTON_LEFT, @@ -584,15 +593,15 @@ impl TerminalView { return; } self.selecting = false; - let open_link = (self.pending_link_open || event.modifiers.control) + let (point, _) = self.grid_point(event.position); + let open_link = pending_link_open && self.selection_is_empty() - && self.link_hover.is_some(); + && self.link_hover.as_ref().is_some_and(|hover| hover.hint.bounds().contains(&point)); if open_link { self.try_open_hovered_link(window, cx); } else if self.copy_on_select { self.copy_selection(false, window, cx); } - self.pending_link_open = false; cx.notify(); } @@ -612,6 +621,7 @@ impl TerminalView { } self.stop_selection_scroll(); let dragging_scrollbar = self.scrollbar_drag.take().is_some(); + self.pending_link_open = false; if !self.selecting { if dragging_scrollbar { cx.notify(); @@ -619,7 +629,6 @@ impl TerminalView { return; } self.selecting = false; - self.pending_link_open = false; // 指针不在终端上,链接打开不该发生;只补选中即复制这一条收尾。 if self.copy_on_select { self.copy_selection(false, window, cx); diff --git a/nebula_app/src/gpui_shell/terminal/view/pointer/tests.rs b/nebula_app/src/gpui_shell/terminal/view/pointer/tests.rs index afaf667..4baa4a4 100644 --- a/nebula_app/src/gpui_shell/terminal/view/pointer/tests.rs +++ b/nebula_app/src/gpui_shell/terminal/view/pointer/tests.rs @@ -151,3 +151,178 @@ fn hover_does_not_steal_focus_during_drag_overlay_dialog_or_inactive_window( move_over(&mut cx, None); assert_eq!(probe.read_with(&cx, |probe, _| probe.requests.get()), 0); } + +fn link_modifiers() -> Modifiers { + match crate::platform::Platform::current() { + crate::platform::Platform::MacOS => Modifiers { platform: true, ..Modifiers::default() }, + _ => Modifiers { control: true, ..Modifiers::default() }, + } +} + +fn link_fixture( + cx: &mut TestAppContext, + text: &[u8], +) -> (Entity, VisualTestContext, std::sync::mpsc::Receiver) { + cx.update(crate::gpui_shell::math_view::register); + let (probe, mut cx) = open(cx); + let terminal = probe.read_with(&cx, |probe, _| probe.terminal.clone()); + let receiver = terminal.update(&mut cx, |view, cx| { + let (session, receiver) = session::test_session(); + view.session = Some(session); + view.error = None; + view.exited = None; + view.copy_on_select = false; + // Use the production matcher and dispatcher with a deterministic action; + // tests must not launch a user's browser or editor. + let mut config = UiConfig::default(); + Arc::make_mut(&mut config.hints.enabled[0]).action = + crate::config::ui_config::HintAction::Action( + crate::config::ui_config::HintInternalAction::Copy, + ); + view.hint_config = Arc::new(config); + super::super::startup_tests::feed(view, text); + cx.notify(); + receiver + }); + draw(&mut cx); + cx.update(|_, cx| cx.write_to_clipboard(gpui::ClipboardItem::new_string("before".into()))); + (terminal, cx, receiver) +} + +fn cell(view: &Entity, cx: &VisualTestContext, col: usize) -> Point { + view.read_with(cx, |view, _| { + point( + view.origin.x + view.cell_width * (col as f32 + 0.5), + view.origin.y + view.line_height * 0.5, + ) + }) +} + +fn clipboard(cx: &mut VisualTestContext) -> Option { + cx.update(|_, cx| cx.read_from_clipboard().and_then(|item| item.text())) +} + +#[gpui::test] +fn link_gesture_opens_regex_files_and_osc8_with_or_without_mouse_reporting( + cx: &mut TestAppContext, +) { + for (output, target) in [ + ("https://example.com", "https://example.com"), + ("file:///tmp/pebrel-notes.md", "file:///tmp/pebrel-notes.md"), + ("[notes](./notes.md)", "[notes](./notes.md)"), + ("\x1b]8;;https://example.com/osc8\x1b\\site\x1b]8;;\x1b\\", "https://example.com/osc8"), + ] { + for mouse_mode in [false, true] { + let output = if mouse_mode { + format!("\x1b[?1003h\x1b[?1006h{output}") + } else { + output.to_owned() + }; + let (view, mut window, receiver) = link_fixture(cx, output.as_bytes()); + let position = cell(&view, &window, 1); + window.simulate_mouse_move(position, None, link_modifiers()); + assert!(view.read_with(&window, |view, _| view.link_hover.is_some())); + window.simulate_mouse_down(position, MouseButton::Left, link_modifiers()); + assert_eq!(clipboard(&mut window).as_deref(), Some("before")); + assert!(view.read_with(&window, |view, _| view.pending_link_open)); + // Releasing Command/Ctrl first must not send an orphan mouse-up to the TUI. + window.simulate_mouse_up(position, MouseButton::Left, Modifiers::default()); + assert_eq!(clipboard(&mut window).as_deref(), Some(target)); + assert!(!receiver.try_iter().any(|event| matches!(event, Msg::Input(_)))); + } + } +} + +#[gpui::test] +fn plain_click_and_wrong_modifier_do_not_open_links(cx: &mut TestAppContext) { + let (view, mut window, _) = link_fixture(cx, b"https://example.com"); + let position = cell(&view, &window, 1); + let wrong = if link_modifiers().platform { + Modifiers { control: true, ..Modifiers::default() } + } else { + Modifiers { platform: true, ..Modifiers::default() } + }; + for modifiers in [Modifiers::default(), wrong] { + window.simulate_mouse_move(position, None, modifiers); + window.simulate_mouse_down(position, MouseButton::Left, modifiers); + window.simulate_mouse_up(position, MouseButton::Left, modifiers); + assert_eq!(clipboard(&mut window).as_deref(), Some("before")); + } +} + +#[gpui::test] +fn link_drag_cannot_retarget_or_leave_a_pending_open(cx: &mut TestAppContext) { + let (view, mut window, _) = link_fixture(cx, b"https://one.test https://two.test"); + let start = cell(&view, &window, 1); + for end in [cell(&view, &window, 20), point(px(10.0), px(10.0))] { + window.simulate_mouse_down(start, MouseButton::Left, link_modifiers()); + window.simulate_mouse_move(end, Some(MouseButton::Left), link_modifiers()); + window.simulate_mouse_up(end, MouseButton::Left, link_modifiers()); + assert_eq!(clipboard(&mut window).as_deref(), Some("before")); + assert!(!view.read_with(&window, |view, _| view.pending_link_open)); + } +} + +#[gpui::test] +fn mouse_reporting_still_receives_ordinary_and_non_link_clicks(cx: &mut TestAppContext) { + let (view, mut window, receiver) = + link_fixture(cx, b"\x1b[?1000h\x1b[?1006hhttps://example.com plain"); + for (col, modifiers) in [(1, Modifiers::default()), (22, link_modifiers())] { + let position = cell(&view, &window, col); + window.simulate_mouse_down(position, MouseButton::Left, modifiers); + window.simulate_mouse_up(position, MouseButton::Left, modifiers); + let reports: Vec<_> = receiver + .try_iter() + .filter_map(|event| match event { + Msg::Input(bytes) => Some(bytes.into_owned()), + _ => None, + }) + .collect(); + assert_eq!(reports.len(), 2); + assert!(reports[0].ends_with(b"M")); + assert!(reports[1].ends_with(b"m")); + assert_eq!(clipboard(&mut window).as_deref(), Some("before")); + } +} + +#[gpui::test] +fn link_hover_uses_current_language_and_platform(cx: &mut TestAppContext) { + use crate::i18n::UiLanguage; + let (view, mut window, _) = link_fixture(cx, b"https://example.com"); + let position = cell(&view, &window, 1); + let modifier = if link_modifiers().platform { "Command" } else { "Ctrl" }; + for (language, suffix) in [ + (UiLanguage::ZhCn, "+左键跳转"), + (UiLanguage::EnUs, "+left click to open"), + (UiLanguage::KoKr, "+왼쪽 클릭으로 열기"), + ] { + window.update(|_, cx| cx.global_mut::().ui_language = language); + window.simulate_mouse_move(position, None, Modifiers::default()); + let preview = + view.read_with(&window, |view, _| view.link_hover.as_ref().unwrap().preview.clone()); + assert!(preview.ends_with(&format!(" · {modifier}{suffix}")), "{preview}"); + draw(&mut window); + } +} + +#[gpui::test] +fn explicit_link_gesture_respects_disabled_hints_and_required_modifiers(cx: &mut TestAppContext) { + let (view, mut window, _) = link_fixture(cx, b"\x1b[?1000hhttps://example.com"); + let position = cell(&view, &window, 1); + for enabled in [false, true] { + view.update(&mut window, |view, _| { + let config = Arc::make_mut(&mut view.hint_config); + let hint = Arc::make_mut(&mut config.hints.enabled[0]); + let mouse = hint.mouse.as_mut().unwrap(); + mouse.enabled = enabled; + mouse.mods.0 = winit::keyboard::ModifiersState::SHIFT; + }); + window.simulate_mouse_down(position, MouseButton::Left, link_modifiers()); + window.simulate_mouse_up(position, MouseButton::Left, link_modifiers()); + assert_eq!(clipboard(&mut window).as_deref(), Some("before")); + } + let modifiers = Modifiers { shift: true, ..link_modifiers() }; + window.simulate_mouse_down(position, MouseButton::Left, modifiers); + window.simulate_mouse_up(position, MouseButton::Left, modifiers); + assert_eq!(clipboard(&mut window).as_deref(), Some("https://example.com")); +} From 3577d7a0e582fe34d5e9f647df2b0cd7ac63f013 Mon Sep 17 00:00:00 2001 From: ZiChuanShanFeng <270144683+WMU-Demo@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:28:44 +0800 Subject: [PATCH 2/3] feat(launcher): show configured SSH host names in launcher rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- nebula_app/src/gpui_shell/workspace.rs | 10 +++++ .../workspace/launcher_menu/tests.rs | 1 + .../src/gpui_shell/workspace/palette.rs | 13 ++++-- .../src/gpui_shell/workspace/quick_jump.rs | 10 +++-- .../src/gpui_shell/workspace/shell_picker.rs | 24 +++++++++-- nebula_app/src/gpui_shell/workspace/tests.rs | 40 ++++++++++++++++++- 6 files changed, 88 insertions(+), 10 deletions(-) diff --git a/nebula_app/src/gpui_shell/workspace.rs b/nebula_app/src/gpui_shell/workspace.rs index 818accb..567d976 100644 --- a/nebula_app/src/gpui_shell/workspace.rs +++ b/nebula_app/src/gpui_shell/workspace.rs @@ -636,6 +636,15 @@ fn ssh_host_icon_ids(data_dir: &Path) -> std::collections::HashMap std::collections::HashMap { + 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), diff --git a/nebula_app/src/gpui_shell/workspace/launcher_menu/tests.rs b/nebula_app/src/gpui_shell/workspace/launcher_menu/tests.rs index 7fe6cc3..3a4d0d1 100644 --- a/nebula_app/src/gpui_shell/workspace/launcher_menu/tests.rs +++ b/nebula_app/src/gpui_shell/workspace/launcher_menu/tests.rs @@ -38,6 +38,7 @@ fn open(cx: &mut TestAppContext) -> (Entity, VisualTestContext) vec![shell("cmd"), shell("pwsh")], vec![], ["test-host".into()], + &std::collections::HashMap::new(), "cmd", workspace_ui_language(), 1.0, diff --git a/nebula_app/src/gpui_shell/workspace/palette.rs b/nebula_app/src/gpui_shell/workspace/palette.rs index 6fe7e0d..b4ad9e1 100644 --- a/nebula_app/src/gpui_shell/workspace/palette.rs +++ b/nebula_app/src/gpui_shell/workspace/palette.rs @@ -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), diff --git a/nebula_app/src/gpui_shell/workspace/quick_jump.rs b/nebula_app/src/gpui_shell/workspace/quick_jump.rs index 6417753..4ab3d5d 100644 --- a/nebula_app/src/gpui_shell/workspace/quick_jump.rs +++ b/nebula_app/src/gpui_shell/workspace/quick_jump.rs @@ -142,19 +142,23 @@ fn directory_rows(language: crate::display::UiLanguage) -> Vec Vec { 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), diff --git a/nebula_app/src/gpui_shell/workspace/shell_picker.rs b/nebula_app/src/gpui_shell/workspace/shell_picker.rs index d4dde13..c0a9b8a 100644 --- a/nebula_app/src/gpui_shell/workspace/shell_picker.rs +++ b/nebula_app/src/gpui_shell/workspace/shell_picker.rs @@ -15,6 +15,20 @@ fn fallback_shell_glyph(id: &str, has_brand: bool) -> Option { 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, profiles: Vec, ssh_hosts: impl IntoIterator, + ssh_labels: &std::collections::HashMap, 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), diff --git a/nebula_app/src/gpui_shell/workspace/tests.rs b/nebula_app/src/gpui_shell/workspace/tests.rs index a9da3d8..0c0bc03 100644 --- a/nebula_app/src/gpui_shell/workspace/tests.rs +++ b/nebula_app/src/gpui_shell/workspace/tests.rs @@ -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::, "wsl:Ghost", language, 1.0); + let rows = shell_palette_rows(shells, Vec::new(), None::, &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::, + &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::, + &std::collections::HashMap::new(), &id, crate::display::UiLanguage::ZhCn, 1.0, From 98fad00b01da9dd089581e4d4f66a251bfc1404d Mon Sep 17 00:00:00 2001 From: ZiChuanShanFeng <270144683+WMU-Demo@users.noreply.github.com> Date: Thu, 24 Sep 2026 01:00:33 +0800 Subject: [PATCH 3/3] refactor(launcher): keep workspace.rs within its line budget The architecture contracts check enforces max(target, base) lines per exception file, and workspace.rs is already at its base size. Move the SSH label lookup out of workspace.rs instead of growing it: - ssh_hosts.rs: add `merged_with_labels()` which reuses the profiles already loaded by `SshHostLists::load()` (no extra JSON parse) and yields (host, label) pairs in the same authoritative merge order. - shell_picker.rs / quick_jump.rs / palette.rs: consume the label from those pairs; `ssh_host_display()` keeps the alias-or-fallback rule. - workspace.rs: net zero lines (helper and extra argument removed). Also apply the pinned rustfmt (1.97.1) to the touched files. Verified locally: check_architecture.py --base origin/main passes, architecture guardrail tests 53/53, file_line_budget 2/2, workspace:: tests 107/107, gpui-shell --tests check clean. --- nebula_app/src/gpui_shell/ssh_hosts.rs | 15 +++++++++++ nebula_app/src/gpui_shell/workspace.rs | 12 +-------- .../workspace/launcher_menu/tests.rs | 3 +-- .../src/gpui_shell/workspace/palette.rs | 20 +++++++-------- .../src/gpui_shell/workspace/quick_jump.rs | 9 +++---- .../src/gpui_shell/workspace/shell_picker.rs | 15 +++++------ nebula_app/src/gpui_shell/workspace/tests.rs | 25 +++++++++++-------- 7 files changed, 51 insertions(+), 48 deletions(-) diff --git a/nebula_app/src/gpui_shell/ssh_hosts.rs b/nebula_app/src/gpui_shell/ssh_hosts.rs index 6267a37..9b9c0be 100644 --- a/nebula_app/src/gpui_shell/ssh_hosts.rs +++ b/nebula_app/src/gpui_shell/ssh_hosts.rs @@ -64,6 +64,21 @@ impl SshHostLists { ) } + /// 与 [`Self::merged`] 同一顺序权威,同时带出用户在 SSH 设置里起的 + /// 主机名称(地址 → 别名;没起名的是空串,展示端回落地址本身)。 + /// profiles 在 [`Self::load`] 时已经读盘,这里不重复读 JSON;launcher / + /// Quick Jump / 命令面板三处 SSH 行共用,避免各自再解析一次。 + pub fn merged_with_labels(&self) -> Vec<(String, String)> { + let labels = self.profiles.labels(); + self.merged() + .into_iter() + .map(|host| { + let label = labels.get(&host).cloned().unwrap_or_default(); + (host, label) + }) + .collect() + } + /// 隐藏区(设置页"已隐藏"折叠列表的数据源)。 pub fn hidden_hosts(&self) -> Vec { self.hidden.iter().filter(|host| self.configured.contains(host)).cloned().collect() diff --git a/nebula_app/src/gpui_shell/workspace.rs b/nebula_app/src/gpui_shell/workspace.rs index 567d976..f0d7be7 100644 --- a/nebula_app/src/gpui_shell/workspace.rs +++ b/nebula_app/src/gpui_shell/workspace.rs @@ -636,15 +636,6 @@ fn ssh_host_icon_ids(data_dir: &Path) -> std::collections::HashMap std::collections::HashMap { - 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` 同名同义, /// 所以导出/恢复不需要转换层)。 @@ -2237,8 +2228,7 @@ impl NebulaWorkspace { crate::terminal_profiles::TerminalProfiles::load() .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()), + crate::gpui_shell::ssh_hosts::SshHostLists::load().merged_with_labels(), &default_shell_id, language, window.scale_factor().max(0.5), diff --git a/nebula_app/src/gpui_shell/workspace/launcher_menu/tests.rs b/nebula_app/src/gpui_shell/workspace/launcher_menu/tests.rs index 3a4d0d1..d8caafd 100644 --- a/nebula_app/src/gpui_shell/workspace/launcher_menu/tests.rs +++ b/nebula_app/src/gpui_shell/workspace/launcher_menu/tests.rs @@ -37,8 +37,7 @@ fn open(cx: &mut TestAppContext) -> (Entity, VisualTestContext) workspace.palette_override = Some(shell_palette_rows( vec![shell("cmd"), shell("pwsh")], vec![], - ["test-host".into()], - &std::collections::HashMap::new(), + [("test-host".into(), String::new())], "cmd", workspace_ui_language(), 1.0, diff --git a/nebula_app/src/gpui_shell/workspace/palette.rs b/nebula_app/src/gpui_shell/workspace/palette.rs index b4ad9e1..63b49c3 100644 --- a/nebula_app/src/gpui_shell/workspace/palette.rs +++ b/nebula_app/src/gpui_shell/workspace/palette.rs @@ -99,20 +99,19 @@ 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| { + crate::gpui_shell::ssh_hosts::SshHostLists::load() + .merged_with_labels() + .into_iter() + .map(|(host, label)| { let glyph = crate::display::ui::os_icons::resolve( 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(); + let named = (!label.is_empty()).then_some(label.as_str()); + let (label, hint) = shell_picker::ssh_host_display(named, &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(), @@ -125,8 +124,7 @@ impl NebulaWorkspace { icon_glyph: Some(glyph), icon_path: None, } - }, - ), + }), ); rows }); diff --git a/nebula_app/src/gpui_shell/workspace/quick_jump.rs b/nebula_app/src/gpui_shell/workspace/quick_jump.rs index 4ab3d5d..234f40e 100644 --- a/nebula_app/src/gpui_shell/workspace/quick_jump.rs +++ b/nebula_app/src/gpui_shell/workspace/quick_jump.rs @@ -142,15 +142,14 @@ fn directory_rows(language: crate::display::UiLanguage) -> Vec Vec { 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() + .merged_with_labels() .into_iter() - .map(|host| { + .map(|(host, label)| { 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 named = (!label.is_empty()).then_some(label.as_str()); + let (label, hint) = shell_picker::ssh_host_display(named, &host); let search = format!("{label} {host} ssh host remote 远程 连接").to_lowercase(); WorkspacePaletteRow { group_order: 3, diff --git a/nebula_app/src/gpui_shell/workspace/shell_picker.rs b/nebula_app/src/gpui_shell/workspace/shell_picker.rs index c0a9b8a..4702c6d 100644 --- a/nebula_app/src/gpui_shell/workspace/shell_picker.rs +++ b/nebula_app/src/gpui_shell/workspace/shell_picker.rs @@ -19,10 +19,7 @@ fn fallback_shell_glyph(id: &str, has_brand: bool) -> Option { /// /// - 用户在 SSH 设置里起过"主机名称":label 显示别名,hint 给真实连接地址; /// - 没起名:回落地址本身当 label,hint 保持 "SSH" 类型标签,避免重复。 -pub(super) fn ssh_host_display( - label: Option<&str>, - host: &str, -) -> (String, String) { +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()), @@ -35,8 +32,7 @@ pub(super) fn ssh_host_display( pub(super) fn shell_palette_rows( shells: Vec, profiles: Vec, - ssh_hosts: impl IntoIterator, - ssh_labels: &std::collections::HashMap, + ssh_hosts: impl IntoIterator, default_shell_id: &str, language: crate::display::UiLanguage, scale_factor: f32, @@ -100,11 +96,12 @@ pub(super) fn shell_palette_rows( rows.insert(0, default_row); } let ssh_icons = ssh_host_icon_ids(&crate::display::nebula_data_dir()); - rows.extend(ssh_hosts.into_iter().map(|host| { + rows.extend(ssh_hosts.into_iter().map(|(host, label)| { 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); + // 空串 = 没起名:`ssh_host_display` 回落地址本身,hint 保持 "SSH"。 + let named = (!label.is_empty()).then_some(label.as_str()); + let (label, hint) = ssh_host_display(named, &host); let search = format!("{label} {host} ssh host remote lianjie 连接").to_lowercase(); WorkspacePaletteRow { group_order: 2, diff --git a/nebula_app/src/gpui_shell/workspace/tests.rs b/nebula_app/src/gpui_shell/workspace/tests.rs index 0c0bc03..fb76b4b 100644 --- a/nebula_app/src/gpui_shell/workspace/tests.rs +++ b/nebula_app/src/gpui_shell/workspace/tests.rs @@ -556,8 +556,7 @@ fn shell_palette_puts_the_default_shell_first() { let rows = shell_palette_rows( shells.clone(), Vec::new(), - ["box.example".to_owned()], - &std::collections::HashMap::new(), + [("box.example".to_owned(), String::new())], "nu", language, 1.0, @@ -584,7 +583,14 @@ 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::, &std::collections::HashMap::new(), "wsl:Ghost", language, 1.0); + let rows = shell_palette_rows( + shells, + Vec::new(), + None::<(String, String)>, + "wsl:Ghost", + language, + 1.0, + ); assert!(matches!( &rows[0].action, WorkspacePaletteAction::LaunchShell(shell) if shell.id == "pwsh" @@ -596,12 +602,13 @@ fn shell_palette_puts_the_default_shell_first() { /// 回落地址本身、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, + [ + ("root@box.example".to_owned(), "生产机".to_owned()), + ("plain.example".to_owned(), String::new()), + ], "cmd", crate::display::UiLanguage::ZhCn, 1.0, @@ -644,8 +651,7 @@ fn shell_palette_falls_back_to_an_id_glyph_when_brand_art_is_absent() { let rows = shell_palette_rows( shells, Vec::new(), - None::, - &std::collections::HashMap::new(), + None::<(String, String)>, "zsh", crate::display::UiLanguage::ZhCn, 1.0, @@ -686,8 +692,7 @@ fn shell_palette_includes_imported_terminal_profiles() { let rows = shell_palette_rows( Vec::new(), vec![profile], - None::, - &std::collections::HashMap::new(), + None::<(String, String)>, &id, crate::display::UiLanguage::ZhCn, 1.0,