chore: checkpoint GPUI parity work
This commit is contained in:
1 parent
09f224aa5f
commit
4dee70beb2
9 files changed
+1286
-281
No files matched your search
Generated
+3
-3
@@ -3090,7 +3090,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-component"
|
name = "gpui-component"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "git+https://github.com/Kuddev/gpui-component?rev=0f0ab35233212f8f3277028995caf0c41e13ee6c#0f0ab35233212f8f3277028995caf0c41e13ee6c"
|
source = "git+https://github.com/Kuddev/gpui-component?rev=a59f704c497d6e6e628ec2d401466d825d4a51b2#a59f704c497d6e6e628ec2d401466d825d4a51b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -3129,7 +3129,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-component-assets"
|
name = "gpui-component-assets"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "git+https://github.com/Kuddev/gpui-component?rev=0f0ab35233212f8f3277028995caf0c41e13ee6c#0f0ab35233212f8f3277028995caf0c41e13ee6c"
|
source = "git+https://github.com/Kuddev/gpui-component?rev=a59f704c497d6e6e628ec2d401466d825d4a51b2#a59f704c497d6e6e628ec2d401466d825d4a51b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -3139,7 +3139,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui-component-macros"
|
name = "gpui-component-macros"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "git+https://github.com/Kuddev/gpui-component?rev=0f0ab35233212f8f3277028995caf0c41e13ee6c#0f0ab35233212f8f3277028995caf0c41e13ee6c"
|
source = "git+https://github.com/Kuddev/gpui-component?rev=a59f704c497d6e6e628ec2d401466d825d4a51b2#a59f704c497d6e6e628ec2d401466d825d4a51b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
+7
-7
@@ -65,15 +65,15 @@ winit = { path = "third_party/winit-0.30.13" }
|
|||||||
# "=0.5.1" 精确 pin;[patch] 要求版本匹配,fork 上的版本号保持不动。
|
# "=0.5.1" 精确 pin;[patch] 要求版本匹配,fork 上的版本号保持不动。
|
||||||
#
|
#
|
||||||
# 下面两个 fork 的 `nebula` 分支基于 crates.io 发布基点(读自 registry 包内
|
# 下面两个 fork 的 `nebula` 分支基于 crates.io 发布基点(读自 registry 包内
|
||||||
# 的 .cargo_vcs_info.json,包名/版本已在基点 rev 上核对过),当前补丁栈为空,
|
# 的 .cargo_vcs_info.json,包名/版本已在基点 rev 上核对过)。gpui 保持发布
|
||||||
# 构建产物应与 crates.io 版本一致:
|
# 基点;gpui-component 仅带 popover 紧凑阴影补丁:
|
||||||
# Kuddev/zed nebula @ 69e2130295c2649963eb639fc70b4f2ee8ea1624
|
# Kuddev/zed nebula @ 69e2130295c2649963eb639fc70b4f2ee8ea1624
|
||||||
# (= zed-industries/zed 发布 gpui 0.2.2 的 rev,
|
# (= zed-industries/zed 发布 gpui 0.2.2 的 rev,
|
||||||
# crates/gpui;上游发布时 dirty=true,若首次编译出现
|
# crates/gpui;上游发布时 dirty=true,若首次编译出现
|
||||||
# 与 registry src 的差异以 diff 为准补一个对齐 commit)
|
# 与 registry src 的差异以 diff 为准补一个对齐 commit)
|
||||||
# Kuddev/gpui-component nebula @ 0f0ab35233212f8f3277028995caf0c41e13ee6c
|
# Kuddev/gpui-component nebula @ a59f704c497d6e6e628ec2d401466d825d4a51b2
|
||||||
# (= longbridge/gpui-component v0.5.1,crates/ui;
|
# (= longbridge/gpui-component v0.5.1 + popover
|
||||||
# 上级目录 gpui-component-v0.5.1/ 有源码检出可对照)
|
# 单层紧凑阴影;不修改 gpui)
|
||||||
#
|
#
|
||||||
# 加补丁 = 提交到 fork 的 nebula 分支 → push → 更新下面的 rev(永远 pin
|
# 加补丁 = 提交到 fork 的 nebula 分支 → push → 更新下面的 rev(永远 pin
|
||||||
# exact rev、不 pin branch)。每个补丁注明原因 + upstream PR 链接,上游合并
|
# exact rev、不 pin branch)。每个补丁注明原因 + upstream PR 链接,上游合并
|
||||||
@@ -92,5 +92,5 @@ winit = { path = "third_party/winit-0.30.13" }
|
|||||||
# shaper 遇换行 panic;替换保 byte 长度,run 偏移不失效)
|
# shaper 遇换行 panic;替换保 byte 长度,run 偏移不失效)
|
||||||
# 本地 fork 工作拷贝在 ../gpui-component-fork(已挂 tty7 remote 可 diff)。
|
# 本地 fork 工作拷贝在 ../gpui-component-fork(已挂 tty7 remote 可 diff)。
|
||||||
gpui = { git = "https://github.com/Kuddev/zed", rev = "69e2130295c2649963eb639fc70b4f2ee8ea1624" }
|
gpui = { git = "https://github.com/Kuddev/zed", rev = "69e2130295c2649963eb639fc70b4f2ee8ea1624" }
|
||||||
gpui-component = { git = "https://github.com/Kuddev/gpui-component", rev = "0f0ab35233212f8f3277028995caf0c41e13ee6c" }
|
gpui-component = { git = "https://github.com/Kuddev/gpui-component", rev = "a59f704c497d6e6e628ec2d401466d825d4a51b2" }
|
||||||
gpui-component-assets = { git = "https://github.com/Kuddev/gpui-component", rev = "0f0ab35233212f8f3277028995caf0c41e13ee6c" }
|
gpui-component-assets = { git = "https://github.com/Kuddev/gpui-component", rev = "a59f704c497d6e6e628ec2d401466d825d4a51b2" }
|
||||||
@@ -112,17 +112,22 @@ pub(super) fn pick_font_file(owner: &Window) -> Option<PathBuf> {
|
|||||||
|
|
||||||
pub(super) fn pick_private_key_file(owner: &Window) -> Option<Result<PathBuf, String>> {
|
pub(super) fn pick_private_key_file(owner: &Window) -> Option<Result<PathBuf, String>> {
|
||||||
let path = platform::pick_file(owner, "Choose SSH private key", PRIVATE_KEY_FILTERS)?;
|
let path = platform::pick_file(owner, "Choose SSH private key", PRIVATE_KEY_FILTERS)?;
|
||||||
let contents = match std::fs::read(&path) {
|
Some(validate_private_key_path(&path))
|
||||||
Ok(contents) => contents,
|
}
|
||||||
Err(err) => return Some(Err(format!("无法读取私钥 {}: {err}", path.display()))),
|
|
||||||
};
|
/// Validate a path selected by a non-winit UI shell. The legacy picker and GPUI
|
||||||
Some(match classify_private_key_contents(&contents) {
|
/// both use this exact classifier so a `.pub` file can never silently enter a
|
||||||
PrivateKeyFileKind::PrivateKey => Ok(path),
|
/// profile just because the shell used a different native file-dialog API.
|
||||||
|
pub(crate) fn validate_private_key_path(path: &std::path::Path) -> Result<PathBuf, String> {
|
||||||
|
let contents = std::fs::read(path)
|
||||||
|
.map_err(|err| format!("无法读取私钥 {}: {err}", path.display()))?;
|
||||||
|
match classify_private_key_contents(&contents) {
|
||||||
|
PrivateKeyFileKind::PrivateKey => Ok(path.to_path_buf()),
|
||||||
PrivateKeyFileKind::PublicKey => Err("请选择私钥文件,不要选择 .pub 公钥".to_owned()),
|
PrivateKeyFileKind::PublicKey => Err("请选择私钥文件,不要选择 .pub 公钥".to_owned()),
|
||||||
PrivateKeyFileKind::Unsupported => {
|
PrivateKeyFileKind::Unsupported => {
|
||||||
Err("文件不是受支持的 OpenSSH、PEM 或 PPK 私钥".to_owned())
|
Err("文件不是受支持的 OpenSSH、PEM 或 PPK 私钥".to_owned())
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn pick_upload_files(owner: &Window) -> Vec<PathBuf> {
|
pub(super) fn pick_upload_files(owner: &Window) -> Vec<PathBuf> {
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ pub use state::{
|
|||||||
};
|
};
|
||||||
pub use toast::ToastKind;
|
pub use toast::ToastKind;
|
||||||
|
|
||||||
mod file_dialog;
|
pub(crate) mod file_dialog;
|
||||||
pub(crate) mod keymap;
|
pub(crate) mod keymap;
|
||||||
mod settings;
|
mod settings;
|
||||||
pub(crate) mod ssh_connect;
|
pub(crate) mod ssh_connect;
|
||||||
@@ -122,6 +122,7 @@ mod text_input;
|
|||||||
use ssh_ui::SshDeleteUndo;
|
use ssh_ui::SshDeleteUndo;
|
||||||
pub use ssh_ui::{
|
pub use ssh_ui::{
|
||||||
SSH_DELETE_UNDO_DURATION, SshEditorField, SshEditorHit, SshEditorRects, SshHostEditor,
|
SSH_DELETE_UNDO_DURATION, SshEditorField, SshEditorHit, SshEditorRects, SshHostEditor,
|
||||||
|
auth_sections, join_destination_port, push_private_key, split_destination_port,
|
||||||
};
|
};
|
||||||
pub use ui::theme::NebulaTheme;
|
pub use ui::theme::NebulaTheme;
|
||||||
pub(crate) use ui::theme::write_nebula_prompt_theme;
|
pub(crate) use ui::theme::write_nebula_prompt_theme;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -9,9 +9,9 @@
|
|||||||
//! - 等宽 cell 度量(`SizeInfo`)→ UI 基准字号(同一设置源)
|
//! - 等宽 cell 度量(`SizeInfo`)→ UI 基准字号(同一设置源)
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Bounds, Context, Hsla, InteractiveElement as _,
|
AnyElement, Bounds, Context, Hsla, InteractiveElement as _, StatefulInteractiveElement as _,
|
||||||
IntoElement, MouseButton, ParentElement as _, Pixels, SharedString,
|
IntoElement, MouseButton, ParentElement as _, SharedString,
|
||||||
StatefulInteractiveElement as _, Styled as _, canvas, div, fill, hsla, point,
|
Styled as _, canvas, div, fill, hsla, point,
|
||||||
px, size,
|
px, size,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,11 +93,17 @@ pub(super) fn rail_canvas(
|
|||||||
move |bounds, _, window, _| {
|
move |bounds, _, window, _| {
|
||||||
let w = f32::from(bounds.size.width);
|
let w = f32::from(bounds.size.width);
|
||||||
let node_d = 10.0_f32;
|
let node_d = 10.0_f32;
|
||||||
let x0 = node_d * 0.5;
|
// 当前节点外圈比本体再大 7px;把首尾中心一并内缩,确保
|
||||||
let x1 = (w - node_d * 0.5).max(x0 + 1.0);
|
// canvas 即使启用内容蒙版也不会把两端 halo 裁成半圆。
|
||||||
|
let halo_pad = 7.0_f32;
|
||||||
|
let x0 = node_d * 0.5 + halo_pad;
|
||||||
|
let x1 = (w - node_d * 0.5 - halo_pad).max(x0 + 1.0);
|
||||||
let ox = f32::from(bounds.origin.x);
|
let ox = f32::from(bounds.origin.x);
|
||||||
let oy = f32::from(bounds.origin.y);
|
let oy = f32::from(bounds.origin.y);
|
||||||
let cy = oy + f32::from(bounds.size.height) * 0.5;
|
// x/y/cy 全部保持 canvas 局部坐标;quad 提交时才加一次窗口
|
||||||
|
// origin。此前 cy 已含 oy,quad 又加 oy,整条轨道因此被画到
|
||||||
|
// canvas 下方(截图只剩阶段文字,节点/粒子全部不可见)。
|
||||||
|
let cy = f32::from(bounds.size.height) * 0.5;
|
||||||
|
|
||||||
let mut quad = |x: f32, y: f32, w: f32, h: f32, r: f32, bg: Hsla| {
|
let mut quad = |x: f32, y: f32, w: f32, h: f32, r: f32, bg: Hsla| {
|
||||||
window.paint_quad(
|
window.paint_quad(
|
||||||
@@ -170,9 +176,13 @@ pub(super) fn rail_canvas(
|
|||||||
let mut col = color;
|
let mut col = color;
|
||||||
col.a = alpha * if k == 0 { 1.0 } else { 0.78 };
|
col.a = alpha * if k == 0 { 1.0 } else { 0.78 };
|
||||||
if !light {
|
if !light {
|
||||||
let rad = if k == 0 { 5.4 } else { 4.4 * fade + 1.2 };
|
// GPUI 没有旧壳 `UiQuad::glow` 的软边 primitive;缩小近似盘面,
|
||||||
|
// 避免轨道中段被一串大圆斑连成粗流。
|
||||||
|
let rad = if k == 0 { 3.4 } else { 2.6 * fade + 0.8 };
|
||||||
let mut g = color;
|
let mut g = color;
|
||||||
g.a = alpha * 0.22;
|
// GPUI 用普通 quad 模拟旧壳的软 glow,降低 alpha
|
||||||
|
// 才不会把粒子画成一串高亮斑块。
|
||||||
|
g.a = alpha * 0.14;
|
||||||
quad(x - rad, cy - rad, rad * 2.0, rad * 2.0, rad, g);
|
quad(x - rad, cy - rad, rad * 2.0, rad * 2.0, rad, g);
|
||||||
}
|
}
|
||||||
quad(x - cr, cy - cr, cr * 2.0, cr * 2.0, cr, col);
|
quad(x - cr, cy - cr, cr * 2.0, cr * 2.0, cr, col);
|
||||||
@@ -193,9 +203,11 @@ pub(super) fn rail_canvas(
|
|||||||
quad(x, y, d, d, d * 0.5, brand_r);
|
quad(x, y, d, d, d * 0.5, brand_r);
|
||||||
} else if i == active {
|
} else if i == active {
|
||||||
if !light {
|
if !light {
|
||||||
let mut g = accent;
|
// 与旧壳单层 UiQuad::glow 对齐;额外的大外圈会让
|
||||||
g.a = 0.45;
|
// 当前阶段节点显得像一枚持续发亮的按钮。
|
||||||
quad(x - 3.0, y - 3.0, d + 6.0, d + 6.0, (d + 6.0) * 0.5, g);
|
let mut glow = accent;
|
||||||
|
glow.a = 0.45;
|
||||||
|
quad(x - 3.0, y - 3.0, d + 6.0, d + 6.0, (d + 6.0) * 0.5, glow);
|
||||||
}
|
}
|
||||||
quad(x, y, d, d, d * 0.5, accent);
|
quad(x, y, d, d, d * 0.5, accent);
|
||||||
} else {
|
} else {
|
||||||
@@ -213,7 +225,9 @@ pub(super) fn rail_canvas(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.h(px(10.0))
|
// 节点本体 10px;最外层节点 halo 向两侧各溢出 7px。给足 24px,
|
||||||
|
// 否则 GPUI canvas 会把刚恢复的光晕上下裁平。
|
||||||
|
.h(px(24.0))
|
||||||
.w_full()
|
.w_full()
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -246,7 +260,16 @@ pub(super) fn overlay(
|
|||||||
let panel = theme.popover;
|
let panel = theme.popover;
|
||||||
let card_bg = theme.group_box;
|
let card_bg = theme.group_box;
|
||||||
let hairline = theme.border;
|
let hairline = theme.border;
|
||||||
let accent = theme.link;
|
let dark = theme.is_dark();
|
||||||
|
let elevation = gpui::hsla(
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
if dark { 56.0 / 255.0 } else { 26.0 / 255.0 },
|
||||||
|
);
|
||||||
|
// 遮罩沿用终端 pane 的真实底色;直接铺 popover 会把空网格区域
|
||||||
|
// 换成另一层面板色,尤其在浅色主题下会显得整块发白。
|
||||||
|
let backdrop = crate::gpui_shell::theme::card_content_bg(cx);
|
||||||
|
|
||||||
// ── 身份行:机架图标 + 两行文字 + Logs 按钮 ──
|
// ── 身份行:机架图标 + 两行文字 + Logs 按钮 ──
|
||||||
let rack_ink = ink_dim;
|
let rack_ink = ink_dim;
|
||||||
@@ -298,8 +321,48 @@ pub(super) fn overlay(
|
|||||||
let name: SharedString = ssh_connect::short_name(state.destination()).into();
|
let name: SharedString = ssh_connect::short_name(state.destination()).into();
|
||||||
let meta: SharedString = format!("SSH · {}", state.destination()).into();
|
let meta: SharedString = format!("SSH · {}", state.destination()).into();
|
||||||
let logs_target = cx.entity().downgrade();
|
let logs_target = cx.entity().downgrade();
|
||||||
let logs_label: SharedString =
|
let logs_open = state.is_logs_open();
|
||||||
format!("Logs {}", if state.is_logs_open() { "⌃" } else { "⌄" }).into();
|
let logs_hover = theme.list_hover;
|
||||||
|
let logs_chevron = Icon::new(if logs_open {
|
||||||
|
IconName::ChevronUp
|
||||||
|
} else {
|
||||||
|
IconName::ChevronDown
|
||||||
|
})
|
||||||
|
.xsmall()
|
||||||
|
.text_color(ink_dim);
|
||||||
|
let logs_button = h_flex()
|
||||||
|
.id("ssh-connect-logs")
|
||||||
|
.h(px(30.0))
|
||||||
|
.w(px(68.0))
|
||||||
|
.px(px(12.0))
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.gap(px(4.0))
|
||||||
|
.rounded(px(6.0))
|
||||||
|
.border_1()
|
||||||
|
.border_color(hairline)
|
||||||
|
.bg(card_bg)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(move |button| button.bg(logs_hover))
|
||||||
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
|
.on_click(move |_, _, cx| {
|
||||||
|
if let Some(view) = logs_target.upgrade() {
|
||||||
|
view.update(cx, |this, cx| {
|
||||||
|
if let Some(state) = &mut this.ssh_connect {
|
||||||
|
state.toggle_logs();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.font_family(family.clone())
|
||||||
|
.text_size(px(ui_px * 0.80))
|
||||||
|
.text_color(ink_dim)
|
||||||
|
.child("Logs"),
|
||||||
|
)
|
||||||
|
.child(logs_chevron);
|
||||||
let identity = h_flex()
|
let identity = h_flex()
|
||||||
.gap(px(16.0))
|
.gap(px(16.0))
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -340,20 +403,7 @@ pub(super) fn overlay(
|
|||||||
.child(
|
.child(
|
||||||
// 可交互按钮统一走 gpui-component:命中、Hover、键盘焦点与
|
// 可交互按钮统一走 gpui-component:命中、Hover、键盘焦点与
|
||||||
// 主题 token 都由组件库负责,不在业务卡片里维护第二套按钮。
|
// 主题 token 都由组件库负责,不在业务卡片里维护第二套按钮。
|
||||||
Button::new("ssh-connect-logs")
|
logs_button,
|
||||||
.label(logs_label)
|
|
||||||
.outline()
|
|
||||||
.small()
|
|
||||||
.on_click(move |_, _, cx| {
|
|
||||||
if let Some(view) = logs_target.upgrade() {
|
|
||||||
view.update(cx, |this, cx| {
|
|
||||||
if let Some(state) = &mut this.ssh_connect {
|
|
||||||
state.toggle_logs();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── 轨道 + 阶段标签(首尾贴齐轨道两端,中间以节点为中心)──
|
// ── 轨道 + 阶段标签(首尾贴齐轨道两端,中间以节点为中心)──
|
||||||
@@ -386,21 +436,20 @@ pub(super) fn overlay(
|
|||||||
.text_color(ink_i)
|
.text_color(ink_i)
|
||||||
.child(label.clone());
|
.child(label.clone());
|
||||||
match i {
|
match i {
|
||||||
0 => div().absolute().left(px(0.0)).child(text).into_any_element(),
|
// 节点中心是 12px / width-12px,旧壳首尾标签从
|
||||||
3 => div().absolute().right(px(0.0)).child(text).into_any_element(),
|
// 节点本体边缘(中心 ±5px)起止,因此各留 7px。
|
||||||
// 中间:零宽锚点 + 140px 居中槽(节点近似中心)
|
0 => div().absolute().left(px(7.0)).child(text).into_any_element(),
|
||||||
|
3 => div().absolute().right(px(7.0)).child(text).into_any_element(),
|
||||||
|
// 中间节点精确位于 width/3+4px 与 2*width/3-4px。
|
||||||
|
// 140px 槽以该点为中心,标签与节点共享同一套几何。
|
||||||
_ => div()
|
_ => div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.left(gpui::relative(if i == 1 {
|
.left(gpui::relative(if i == 1 { 1.0 / 3.0 } else { 2.0 / 3.0 }))
|
||||||
0.328
|
|
||||||
} else {
|
|
||||||
0.659
|
|
||||||
}))
|
|
||||||
.w(px(0.0))
|
.w(px(0.0))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.w(px(140.0))
|
.w(px(140.0))
|
||||||
.ml(px(-70.0))
|
.ml(px(if i == 1 { -66.0 } else { -74.0 }))
|
||||||
.flex()
|
.flex()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.child(text),
|
.child(text),
|
||||||
@@ -530,7 +579,7 @@ pub(super) fn overlay(
|
|||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
})
|
})
|
||||||
// 遮罩:pane 底色整块盖住(连接期间 grid 是空的,跟着重绘会闪)
|
// 遮罩:pane 底色整块盖住(连接期间 grid 是空的,跟着重绘会闪)
|
||||||
.child(div().absolute().inset_0().bg(panel))
|
.child(div().absolute().inset_0().bg(backdrop))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
@@ -548,7 +597,16 @@ pub(super) fn overlay(
|
|||||||
.border_1()
|
.border_1()
|
||||||
.border_color(hairline)
|
.border_color(hairline)
|
||||||
.bg(panel)
|
.bg(panel)
|
||||||
.shadow_md()
|
// 旧壳只使用一层黑色阴影:20px 模糊、向下 7px,
|
||||||
|
// 浅色 26/255、深色 56/255。品牌色不参与卡片投影。
|
||||||
|
.shadow(vec![
|
||||||
|
gpui::BoxShadow {
|
||||||
|
color: elevation,
|
||||||
|
offset: point(px(0.0), px(7.0)),
|
||||||
|
blur_radius: px(20.0),
|
||||||
|
spread_radius: px(0.0),
|
||||||
|
},
|
||||||
|
])
|
||||||
.child(identity)
|
.child(identity)
|
||||||
.child(rail_block)
|
.child(rail_block)
|
||||||
.child(status_row)
|
.child(status_row)
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ pub use crate::display::ToastKind;
|
|||||||
/// `DUPLICATE_COOLDOWN` 同值同义;只挡逐字相同的文本)。
|
/// `DUPLICATE_COOLDOWN` 同值同义;只挡逐字相同的文本)。
|
||||||
const DUPLICATE_COOLDOWN: Duration = Duration::from_millis(600);
|
const DUPLICATE_COOLDOWN: Duration = Duration::from_millis(600);
|
||||||
|
|
||||||
|
/// 组件库 Notification 默认固定 `w_112`,短短一句也会铺成近半个窗口。
|
||||||
|
/// Nebula 的 toast 按内容收缩;过长消息到此上限后自然换行。
|
||||||
|
const TOAST_MIN_WIDTH: f32 = 240.0;
|
||||||
|
const TOAST_MAX_WIDTH: f32 = 440.0;
|
||||||
|
|
||||||
static LAST_TOAST: Mutex<Option<(String, Instant)>> = Mutex::new(None);
|
static LAST_TOAST: Mutex<Option<(String, Instant)>> = Mutex::new(None);
|
||||||
|
|
||||||
fn is_duplicate(text: &str) -> bool {
|
fn is_duplicate(text: &str) -> bool {
|
||||||
@@ -42,11 +47,14 @@ fn is_duplicate(text: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn note(kind: ToastKind, text: String) -> Notification {
|
fn note(kind: ToastKind, text: String) -> Notification {
|
||||||
match kind {
|
let note = match kind {
|
||||||
ToastKind::Info => Notification::info(text),
|
ToastKind::Info => Notification::info(text),
|
||||||
ToastKind::Success => Notification::success(text),
|
ToastKind::Success => Notification::success(text),
|
||||||
ToastKind::Warning => Notification::warning(text),
|
ToastKind::Warning => Notification::warning(text),
|
||||||
}
|
};
|
||||||
|
// `refine_style` 在组件自身的 `w_112` 之后应用,所以这里能可靠覆盖
|
||||||
|
// 固定宽度;auto 让短消息收紧,max_w 给长路径/错误信息提供换行约束。
|
||||||
|
note.w_auto().min_w(px(TOAST_MIN_WIDTH)).max_w(px(TOAST_MAX_WIDTH))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 窗口构造闭包里 `NebulaWorkspace::new` 早于外层 `Root::new` 返回;此时
|
/// 窗口构造闭包里 `NebulaWorkspace::new` 早于外层 `Root::new` 返回;此时
|
||||||
|
|||||||
@@ -412,6 +412,15 @@ fn ai_hook_target_pane(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// AI 接续是会话回放的独立闸门:关掉时 pane 的 cwd/launch/layout 仍可
|
||||||
|
/// 恢复,但绝不把 AgentSession 转成会敲进新 shell 的命令。
|
||||||
|
fn restored_agent_command(
|
||||||
|
resume_ai: bool,
|
||||||
|
agent: Option<&crate::session::AgentSession>,
|
||||||
|
) -> Option<String> {
|
||||||
|
if resume_ai { agent.and_then(|agent| agent.resume_command()) } else { None }
|
||||||
|
}
|
||||||
|
|
||||||
fn open_in_file_manager(path: &Path) {
|
fn open_in_file_manager(path: &Path) {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let _ = std::process::Command::new("explorer.exe").arg(path).spawn();
|
let _ = std::process::Command::new("explorer.exe").arg(path).spawn();
|
||||||
@@ -608,7 +617,10 @@ impl NebulaWorkspace {
|
|||||||
ai_events: std::sync::mpsc::Receiver<crate::ai_hook::AiHookEvent>,
|
ai_events: std::sync::mpsc::Receiver<crate::ai_hook::AiHookEvent>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let sidebar_width = nebula_settings::RuntimeSettings::load().sidebar_width;
|
// 启动相关设置只取样一次:本次开窗的恢复决策不能被恢复过程中的
|
||||||
|
// 文件变化拆成互相矛盾的 restore/resume 状态。
|
||||||
|
let runtime = nebula_settings::RuntimeSettings::load();
|
||||||
|
let sidebar_width = runtime.sidebar_width;
|
||||||
let initial_grid = Self::size_window_to_default_grid(window, cx, sidebar_width);
|
let initial_grid = Self::size_window_to_default_grid(window, cx, sidebar_width);
|
||||||
let this = cx.entity().downgrade();
|
let this = cx.entity().downgrade();
|
||||||
let appearance_sub = window.observe_window_appearance(move |_, cx| {
|
let appearance_sub = window.observe_window_appearance(move |_, cx| {
|
||||||
@@ -689,10 +701,11 @@ impl NebulaWorkspace {
|
|||||||
notice,
|
notice,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 会话恢复(共享 v4 schema + 崩溃断路器):恢复不出任何 tab 才落
|
// 会话恢复(共享 v4 schema + 崩溃断路器):restore_session 关闭时
|
||||||
// 到出厂的单终端。1 Hz 自动保存随后启动,首笔成功写盘自然把
|
// 用短路保证不碰 session.json(不加载、隔离、标记或回放),直接落
|
||||||
// `boot_attempts` 清零(快照恒以 0 构造,旧壳同义)。
|
// 到出厂的单终端。恢复开启但 resume_ai 关闭时仍回放布局/cwd,只不
|
||||||
if !this.try_restore_session(window, cx) {
|
// 注入 AI 接续命令。1 Hz 自动保存随后照常启动。
|
||||||
|
if !runtime.restore_session || !this.try_restore_session(runtime.resume_ai, window, cx) {
|
||||||
this.add_terminal(window, cx);
|
this.add_terminal(window, cx);
|
||||||
}
|
}
|
||||||
Self::start_ai_hook_pump(ai_events, cx);
|
Self::start_ai_hook_pump(ai_events, cx);
|
||||||
@@ -1296,7 +1309,12 @@ impl NebulaWorkspace {
|
|||||||
|
|
||||||
/// 启动恢复:断路器跳闸就隔离现场并走干净路径;恢复成功弹一条
|
/// 启动恢复:断路器跳闸就隔离现场并走干净路径;恢复成功弹一条
|
||||||
/// 自动消失的提示(崩溃现场多一句来源说明)。返回是否恢复出了 tab。
|
/// 自动消失的提示(崩溃现场多一句来源说明)。返回是否恢复出了 tab。
|
||||||
fn try_restore_session(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
fn try_restore_session(
|
||||||
|
&mut self,
|
||||||
|
resume_ai: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> bool {
|
||||||
use crate::display::ToastKind;
|
use crate::display::ToastKind;
|
||||||
|
|
||||||
let Some(mut session) = crate::session::load() else { return false };
|
let Some(mut session) = crate::session::load() else { return false };
|
||||||
@@ -1319,7 +1337,7 @@ impl NebulaWorkspace {
|
|||||||
crate::session::mark_boot_attempt(&mut session);
|
crate::session::mark_boot_attempt(&mut session);
|
||||||
let mut restored = 0usize;
|
let mut restored = 0usize;
|
||||||
for tab in &session.tabs {
|
for tab in &session.tabs {
|
||||||
if self.restore_tab(tab, window, cx) {
|
if self.restore_tab(tab, resume_ai, window, cx) {
|
||||||
restored += 1;
|
restored += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1344,6 +1362,7 @@ impl NebulaWorkspace {
|
|||||||
fn restore_tab(
|
fn restore_tab(
|
||||||
&mut self,
|
&mut self,
|
||||||
tab: &crate::session::TabSession,
|
tab: &crate::session::TabSession,
|
||||||
|
resume_ai: bool,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
@@ -1374,7 +1393,7 @@ impl NebulaWorkspace {
|
|||||||
shell_name: None,
|
shell_name: None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let command = agent.as_ref().and_then(|agent| agent.resume_command());
|
let command = restored_agent_command(resume_ai, agent.as_ref());
|
||||||
panes.push(self.new_pane(grid, launch, command, window, cx));
|
panes.push(self.new_pane(grid, launch, command, window, cx));
|
||||||
}
|
}
|
||||||
if panes.is_empty() {
|
if panes.is_empty() {
|
||||||
@@ -3492,6 +3511,7 @@ impl NebulaWorkspace {
|
|||||||
div()
|
div()
|
||||||
.font_family(mono_family.clone())
|
.font_family(mono_family.clone())
|
||||||
.text_size(px(label_px * SIDEBAR_TAG_SCALE))
|
.text_size(px(label_px * SIDEBAR_TAG_SCALE))
|
||||||
|
.font_weight(FontWeight::NORMAL)
|
||||||
.text_color(status_color)
|
.text_color(status_color)
|
||||||
.child(tag)
|
.child(tag)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
@@ -3522,6 +3542,9 @@ impl NebulaWorkspace {
|
|||||||
// 旧壳 pill 圆角 = UI_CORNER_RADIUS_LOGICAL(8),rounded_md(6)
|
// 旧壳 pill 圆角 = UI_CORNER_RADIUS_LOGICAL(8),rounded_md(6)
|
||||||
// 偏小一圈,选中水洗的轮廓形状会不一样。
|
// 偏小一圈,选中水洗的轮廓形状会不一样。
|
||||||
.rounded(px(crate::display::UI_CORNER_RADIUS_LOGICAL))
|
.rounded(px(crate::display::UI_CORNER_RADIUS_LOGICAL))
|
||||||
|
// GPUI 默认文本样式可能把侧栏整行带到中等/粗体;旧壳
|
||||||
|
// tab chrome 使用终端 Regular face,所有子文本从这里继承常规字重。
|
||||||
|
.font_weight(FontWeight::NORMAL)
|
||||||
.cursor_pointer()
|
.cursor_pointer()
|
||||||
.when(active, |item| item.bg(active_bg).text_color(active_fg))
|
.when(active, |item| item.bg(active_bg).text_color(active_fg))
|
||||||
.when(!active && !dragged, |item| {
|
.when(!active && !dragged, |item| {
|
||||||
@@ -3583,6 +3606,7 @@ impl NebulaWorkspace {
|
|||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.font_family(mono_family.clone())
|
.font_family(mono_family.clone())
|
||||||
.text_size(px(label_px))
|
.text_size(px(label_px))
|
||||||
|
.font_weight(FontWeight::NORMAL)
|
||||||
.text_color(if active { active_fg } else { muted })
|
.text_color(if active { active_fg } else { muted })
|
||||||
.child(glyph),
|
.child(glyph),
|
||||||
)
|
)
|
||||||
@@ -3609,6 +3633,9 @@ impl NebulaWorkspace {
|
|||||||
.min_w_0()
|
.min_w_0()
|
||||||
.font_family(mono_family.clone())
|
.font_family(mono_family.clone())
|
||||||
.text_size(px(label_px))
|
.text_size(px(label_px))
|
||||||
|
// 标签标题本身使用 Light;活动态只换前景/背景色,
|
||||||
|
// 不再靠更粗字重强调,避免选中行看起来突然加粗。
|
||||||
|
.font_weight(FontWeight::LIGHT)
|
||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(title)
|
.child(title)
|
||||||
@@ -3728,14 +3755,14 @@ impl NebulaWorkspace {
|
|||||||
.pr_1()
|
.pr_1()
|
||||||
.child(
|
.child(
|
||||||
// 箭头与标题同一条 mono 文本 run(旧壳 codicon
|
// 箭头与标题同一条 mono 文本 run(旧壳 codicon
|
||||||
// 字位 \u{eab4}/\u{eab6} + BOLD),不用细线 SVG——
|
// 字位 \u{eab4}/\u{eab6} + Regular),不用细线 SVG——
|
||||||
// 实心字形才和旧壳一样有存在感。
|
// 保持旧壳的常规字重。
|
||||||
div()
|
div()
|
||||||
.w(px(tabs_disclosure_slot_w))
|
.w(px(tabs_disclosure_slot_w))
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.font_family(mono_family.clone())
|
.font_family(mono_family.clone())
|
||||||
.text_size(px(label_px * SIDEBAR_TITLE_SCALE))
|
.text_size(px(label_px * SIDEBAR_TITLE_SCALE))
|
||||||
.font_weight(FontWeight::BOLD)
|
.font_weight(FontWeight::NORMAL)
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.child(if self.tabs_section_collapsed {
|
.child(if self.tabs_section_collapsed {
|
||||||
"\u{eab6}"
|
"\u{eab6}"
|
||||||
@@ -3746,7 +3773,7 @@ impl NebulaWorkspace {
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_size(px(label_px * SIDEBAR_TITLE_SCALE))
|
.text_size(px(label_px * SIDEBAR_TITLE_SCALE))
|
||||||
.font_weight(FontWeight::BOLD)
|
.font_weight(FontWeight::NORMAL)
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.child("TABS"),
|
.child("TABS"),
|
||||||
)
|
)
|
||||||
@@ -3764,6 +3791,7 @@ impl NebulaWorkspace {
|
|||||||
.rounded_full()
|
.rounded_full()
|
||||||
.bg(theme.muted)
|
.bg(theme.muted)
|
||||||
.text_size(px(label_px * SIDEBAR_TITLE_SCALE))
|
.text_size(px(label_px * SIDEBAR_TITLE_SCALE))
|
||||||
|
.font_weight(FontWeight::NORMAL)
|
||||||
.text_color(faint)
|
.text_color(faint)
|
||||||
.child(count),
|
.child(count),
|
||||||
),
|
),
|
||||||
@@ -4294,7 +4322,13 @@ impl Render for NebulaWorkspace {
|
|||||||
.child(
|
.child(
|
||||||
// 标题栏左上与旧壳同构:侧栏开关 + 齿轮,两枚裸图标,无应用名
|
// 标题栏左上与旧壳同构:侧栏开关 + 齿轮,两枚裸图标,无应用名
|
||||||
// 文案。图标用静态 PanelLeft(旧壳折叠/展开共用同一枚方块标)。
|
// 文案。图标用静态 PanelLeft(旧壳折叠/展开共用同一枚方块标)。
|
||||||
|
//
|
||||||
|
// 组件库默认 TitleBar 只有 34px,而普通 Button 的命中块是
|
||||||
|
// 32px,上下各剩 1px,视觉上就会紧贴窗口顶边。旧壳合同是
|
||||||
|
// 8px 外边距 + 40px 标题带,总高 48px;显式覆写后按钮垂直
|
||||||
|
// 居中,上下各留 8px,且右侧窗口控制仍共享同一标题带。
|
||||||
TitleBar::new()
|
TitleBar::new()
|
||||||
|
.h(px(48.0))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
// 旧壳两枚 32px 命中块之间固定留 8px;默认 Button
|
// 旧壳两枚 32px 命中块之间固定留 8px;默认 Button
|
||||||
@@ -4502,6 +4536,21 @@ mod tests {
|
|||||||
assert_eq!(ai_hook_target_pane(&pane_ids, None, None), None);
|
assert_eq!(ai_hook_target_pane(&pane_ids, None, None), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn restored_agent_command_honors_resume_ai_without_changing_session_data() {
|
||||||
|
let agent = crate::session::AgentSession {
|
||||||
|
source: "claude".to_owned(),
|
||||||
|
session_id: Some("session-42".to_owned()),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
restored_agent_command(true, Some(&agent)),
|
||||||
|
Some("claude --resume session-42".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(restored_agent_command(false, Some(&agent)), None);
|
||||||
|
assert_eq!(restored_agent_command(true, None), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_tab_position_uses_the_shared_runtime_semantics() {
|
fn new_tab_position_uses_the_shared_runtime_semantics() {
|
||||||
use nebula_settings::NewTabPositionName::{AfterCurrent, End};
|
use nebula_settings::NewTabPositionName::{AfterCurrent, End};
|
||||||
|
|||||||
@@ -830,51 +830,84 @@ pub struct SshTestRequest {
|
|||||||
|
|
||||||
const TEST_TIMEOUT: Duration = Duration::from_secs(12);
|
const TEST_TIMEOUT: Duration = Duration::from_secs(12);
|
||||||
|
|
||||||
/// 解析 + 连接 + 无人值守认证一轮,结果连同耗时经事件送回窗口线程。
|
/// 一次草稿测试的完成数据。旧 winit 壳与 GPUI 设置页共用它:业务层只负责
|
||||||
/// 绝不弹 AskPass:交互式方法在测试中一律跳过(见 [`test_authenticate`])。
|
/// 解析、代理和认证,具体 UI 决定怎样把结果投递回自己的事件循环。
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SshTestResult {
|
||||||
|
pub request_id: u64,
|
||||||
|
pub destination: String,
|
||||||
|
pub ok: bool,
|
||||||
|
pub message: String,
|
||||||
|
pub elapsed_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 执行一次无人值守的草稿测试。绝不弹 AskPass:交互式方法在测试中一律跳过
|
||||||
|
/// (见 [`test_authenticate`])。
|
||||||
|
async fn run_test(request: SshTestRequest) -> SshTestResult {
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
let request_id = request.request_id;
|
||||||
|
let raw = request.destination.clone();
|
||||||
|
let outcome = tokio::time::timeout(TEST_TIMEOUT, async {
|
||||||
|
let (resolved, global) = tokio::task::spawn_blocking({
|
||||||
|
let raw = raw.clone();
|
||||||
|
move || {
|
||||||
|
let resolved = SshDestination::resolve(&raw)?;
|
||||||
|
let global = crate::ssh_proxy::SshProxyConfig::load_global();
|
||||||
|
Ok::<_, io::Error>((resolved, global))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| -> SessionError { format!("SSH 地址解析任务失败: {err}").into() })??;
|
||||||
|
// SSH 编辑器不再维护重复的每主机代理;测试与真连都只读取网络页
|
||||||
|
// 的当前配置。`ProxyJump` 属于 OpenSSH 目标解析,仍按其原语义保留。
|
||||||
|
let proxy = resolve_network_proxy(&global, &resolved)
|
||||||
|
.map_err(|err| -> SessionError { format!("SSH 代理配置无效: {err}").into() })?;
|
||||||
|
test_connect(&resolved, &request, proxy.as_ref()).await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let (ok, message) = match outcome {
|
||||||
|
Ok(Ok(())) => (true, String::new()),
|
||||||
|
Ok(Err(err)) => (false, err.to_string()),
|
||||||
|
Err(_) => (false, format!("连接超时({} 秒无响应)", TEST_TIMEOUT.as_secs())),
|
||||||
|
};
|
||||||
|
SshTestResult {
|
||||||
|
request_id,
|
||||||
|
destination: raw,
|
||||||
|
ok,
|
||||||
|
message,
|
||||||
|
elapsed_ms: started.elapsed().as_millis() as u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 启动测试并交给调用方异步等待结果。GPUI 没有旧壳的 winit `EventLoopProxy`,
|
||||||
|
/// 所以它通过这个 receiver 把结果安全地回写到自己的 Entity;网络与认证仍然
|
||||||
|
/// 完全复用同一条 Tokio SSH runtime。
|
||||||
|
pub fn start_test(
|
||||||
|
request: SshTestRequest,
|
||||||
|
) -> io::Result<tokio::sync::oneshot::Receiver<SshTestResult>> {
|
||||||
|
let (sender, receiver) = tokio::sync::oneshot::channel();
|
||||||
|
runtime()?.spawn(async move {
|
||||||
|
let _ = sender.send(run_test(request).await);
|
||||||
|
});
|
||||||
|
Ok(receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 旧 winit 壳的事件投递适配器。测试实现本身在 [`run_test`],避免两个壳
|
||||||
|
/// 各自维护解析、代理和认证的近似副本。
|
||||||
pub fn spawn_test(
|
pub fn spawn_test(
|
||||||
request: SshTestRequest,
|
request: SshTestRequest,
|
||||||
proxy: winit::event_loop::EventLoopProxy<crate::event::Event>,
|
proxy: winit::event_loop::EventLoopProxy<crate::event::Event>,
|
||||||
window_id: winit::window::WindowId,
|
window_id: winit::window::WindowId,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
runtime()?.spawn(async move {
|
runtime()?.spawn(async move {
|
||||||
let started = std::time::Instant::now();
|
let result = run_test(request).await;
|
||||||
let request_id = request.request_id;
|
|
||||||
let raw = request.destination.clone();
|
|
||||||
let outcome = tokio::time::timeout(TEST_TIMEOUT, async {
|
|
||||||
let (resolved, global) = tokio::task::spawn_blocking({
|
|
||||||
let raw = raw.clone();
|
|
||||||
move || {
|
|
||||||
let resolved = SshDestination::resolve(&raw)?;
|
|
||||||
let global = crate::ssh_proxy::SshProxyConfig::load_global();
|
|
||||||
Ok::<_, io::Error>((resolved, global))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|err| -> SessionError {
|
|
||||||
format!("SSH 地址解析任务失败: {err}").into()
|
|
||||||
})??;
|
|
||||||
// SSH 编辑器不再维护重复的每主机代理;测试与真连都只读取网络页
|
|
||||||
// 的当前配置。`ProxyJump` 属于 OpenSSH 目标解析,仍按其原语义保留。
|
|
||||||
let proxy =
|
|
||||||
resolve_network_proxy(&global, &resolved).map_err(|err| -> SessionError {
|
|
||||||
format!("SSH 代理配置无效: {err}").into()
|
|
||||||
})?;
|
|
||||||
test_connect(&resolved, &request, proxy.as_ref()).await
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let (ok, message) = match outcome {
|
|
||||||
Ok(Ok(())) => (true, String::new()),
|
|
||||||
Ok(Err(err)) => (false, err.to_string()),
|
|
||||||
Err(_) => (false, format!("连接超时({} 秒无响应)", TEST_TIMEOUT.as_secs())),
|
|
||||||
};
|
|
||||||
let _ = proxy.send_event(crate::event::Event::new(
|
let _ = proxy.send_event(crate::event::Event::new(
|
||||||
crate::event::EventType::SshTestDone {
|
crate::event::EventType::SshTestDone {
|
||||||
request_id,
|
request_id: result.request_id,
|
||||||
destination: raw,
|
destination: result.destination,
|
||||||
ok,
|
ok: result.ok,
|
||||||
message,
|
message: result.message,
|
||||||
elapsed_ms: started.elapsed().as_millis() as u64,
|
elapsed_ms: result.elapsed_ms,
|
||||||
},
|
},
|
||||||
window_id,
|
window_id,
|
||||||
));
|
));
|
||||||
|
|||||||
Reference in new issue
Block a user