fix(app): 优化系统监控与会话选择,修复分屏 SFTP 及窗口激活问题 (#119)

This commit is contained in:
TomZz
2026-10-03 17:19:24 +08:00
parent ff4079a60f
commit 0708ac7baa
4 changed files with 409 additions and 153 deletions
+1 -45
View File
@@ -729,50 +729,6 @@ impl Ashell {
),
),
)
.child(
div()
.w_full()
.p_2()
.rounded_md()
.border_1()
.border_color(if selected_index == 1 {
_cx.theme().primary
} else {
_cx.theme().border
})
.bg(if selected_index == 1 {
_cx.theme().tab_active
} else {
_cx.theme().muted
})
.cursor_pointer()
.hover(|this| this.bg(_cx.theme().secondary))
.on_mouse_down(
MouseButton::Left,
window.listener_for(&view, |this, _, window, cx| {
this.active_dialog = None;
window.close_dialog(cx);
this.open_new_ssh_dialog(window, cx);
cx.notify();
}),
)
.child(
v_flex()
.gap_1()
.child(
div()
.text_size(ui_rems(1.0))
.font_weight(FontWeight::SEMIBOLD)
.child(t!("new_connection")),
)
.child(
div()
.text_size(ui_rems(0.917))
.text_color(_cx.theme().muted_foreground)
.child(t!("create_or_edit_ssh_session")),
),
),
)
.child(
div()
.relative()
@@ -790,7 +746,7 @@ impl Ashell {
|(ix, session)| {
let connect_id = session.id.clone();
let is_selected =
selected_index == ix + 2;
selected_index == ix + 1;
let name = session.name.clone();
let detail = if session.protocol
== "serial"
+25 -1
View File
@@ -421,6 +421,7 @@ pub(crate) struct Ashell {
pub(crate) search_bar_bounds: Option<Bounds<Pixels>>,
pub(crate) system_tab_id: Option<String>,
pub(crate) last_applied_system_tab_id: Option<String>,
pub(crate) sftp_handles: std::collections::HashMap<String, crate::sftp::SftpHandle>,
pub(crate) remote_sample_in_flight: bool,
@@ -449,7 +450,6 @@ pub(crate) struct HoveredUrl {
#[derive(Clone)]
pub(crate) enum SelectorEntry {
Local,
NewSsh,
Saved(String),
}
@@ -944,6 +944,7 @@ impl Ashell {
search_bar_bounds: None,
system_tab_id: None,
last_applied_system_tab_id: None,
sftp_handles: std::collections::HashMap::new(),
remote_sample_in_flight: false,
@@ -1283,7 +1284,11 @@ impl Ashell {
}
fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let was_active = self.window_active;
self.sync_window_activation(window);
if !was_active && self.window_active {
window.activate_window();
}
cx.notify();
}
@@ -1390,6 +1395,7 @@ impl Ashell {
tab.disconnected_reason = None;
}
self.sync_sftp_path_from_terminal_title(&tab_id, cx);
self.sync_sftp_to_active_tab();
self.sync_system_tab_to_active_group();
if self.system_tab_id.as_deref() == Some(tab_id.as_str()) {
self.system_status = None;
@@ -1488,6 +1494,12 @@ impl Ashell {
if self.is_connected_system_tab(&tab_id) {
self.remote_sample_in_flight = false;
self.system_status = None;
if self.last_applied_system_tab_id.as_deref() != Some(&tab_id) {
self.last_applied_system_tab_id = Some(tab_id.clone());
self.cpu_history.clear();
self.net_rx_history.clear();
self.net_tx_history.clear();
}
self.apply_system_snapshot(snapshot);
}
}
@@ -1496,6 +1508,11 @@ impl Ashell {
self.remote_sample_in_flight = false;
self.system_status = Some(reason.clone().into());
self.status = reason.into();
self.last_applied_system_tab_id = Some(tab_id);
self.system = SystemSnapshot::default();
self.cpu_history.clear();
self.net_rx_history.clear();
self.net_tx_history.clear();
}
}
BackendEvent::RemoteProcesses { tab_id, processes } => {
@@ -1734,6 +1751,12 @@ impl Ashell {
}
return false;
}
if self.last_applied_system_tab_id.is_some() {
self.last_applied_system_tab_id = None;
self.cpu_history.clear();
self.net_rx_history.clear();
self.net_tx_history.clear();
}
let snapshot = self.system_sampler.sample();
self.apply_system_snapshot(snapshot);
return true;
@@ -1760,6 +1783,7 @@ impl Ashell {
self.cpu_history.clear();
self.net_rx_history.clear();
self.net_tx_history.clear();
self.last_applied_system_tab_id = None;
self.remote_sample_in_flight = false;
self.remote_processes_in_flight = false;
self.remote_processes.clear();
+262 -81
View File
@@ -1884,10 +1884,26 @@ impl Ashell {
};
let mem_pct = self.system.mem_percent;
let swap_pct = self.system.swap_percent;
let mem_detail = self.system.mem_detail.clone();
let swap_detail = self.system.swap_detail.clone();
let net_rx = self.system.net_rx.clone();
let net_tx = self.system.net_tx.clone();
let mem_detail = if self.system.mem_detail.is_empty() {
"-- / --".to_string()
} else {
self.system.mem_detail.clone()
};
let swap_detail = if self.system.swap_detail.is_empty() {
"-- / --".to_string()
} else {
self.system.swap_detail.clone()
};
let net_rx = if self.system.net_rx.is_empty() {
"--".to_string()
} else {
self.system.net_rx.clone()
};
let net_tx = if self.system.net_tx.is_empty() {
"--".to_string()
} else {
self.system.net_tx.clone()
};
let (disk_used, disk_total) = self.system.disks.iter().fold((0u64, 0u64), |(u, t), d| {
(u + (d.total_bytes - d.available_bytes), t + d.total_bytes)
@@ -2208,7 +2224,11 @@ impl Ashell {
div()
.text_size(ui_rems(0.833))
.text_color(muted_fg)
.child(format!("{:.0}%", disk_pct)),
.child(if disk_total > 0 {
format!("{:.0}%", disk_pct)
} else {
"--".to_string()
}),
),
)
.child(
@@ -2222,40 +2242,72 @@ impl Ashell {
.track_scroll(&self.disk_scroll_handle)
.overflow_y_scroll()
.size_full()
.children(disks.iter().map(|disk| {
let pct = if disk.total_bytes > 0 {
(disk.total_bytes - disk.available_bytes) as f64
/ disk.total_bytes as f64
* 100.0
} else {
0.0
};
let mount_short = disk.mount.clone();
let mount_id = format!("disk-{}", mount_short);
h_flex()
.w_full()
.items_center()
.gap_1()
.child(
div()
.text_size(ui_rems(0.667))
.text_color(muted_fg)
.child(mount_short),
)
.child(
Progress::new(mount_id)
.value(pct as f32)
.color(disk_color)
.with_size(px(4.))
.flex_1(),
)
.child(
div()
.text_size(ui_rems(0.667))
.text_color(muted_fg)
.child(format!("{:.0}%", pct)),
)
})),
.children(if disks.is_empty() {
vec![
h_flex()
.w_full()
.items_center()
.gap_1()
.child(
div()
.w(px(20.))
.h(px(8.))
.rounded_sm()
.bg(cx.theme().skeleton),
)
.child(
div()
.flex_1()
.h(px(4.))
.rounded_full()
.bg(cx.theme().skeleton.opacity(0.4)),
)
.child(
div()
.w(px(22.))
.h(px(8.))
.rounded_sm()
.bg(cx.theme().skeleton.opacity(0.7)),
)
.into_any_element(),
]
} else {
disks.iter().map(|disk| {
let pct = if disk.total_bytes > 0 {
(disk.total_bytes - disk.available_bytes) as f64
/ disk.total_bytes as f64
* 100.0
} else {
0.0
};
let mount_short = disk.mount.clone();
let mount_id = format!("disk-{}", mount_short);
h_flex()
.w_full()
.items_center()
.gap_1()
.child(
div()
.text_size(ui_rems(0.667))
.text_color(muted_fg)
.child(mount_short),
)
.child(
Progress::new(mount_id)
.value(pct as f32)
.color(disk_color)
.with_size(px(4.))
.flex_1(),
)
.child(
div()
.text_size(ui_rems(0.667))
.text_color(muted_fg)
.child(format!("{:.0}%", pct)),
)
.into_any_element()
}).collect()
}),
)
.child(
div()
@@ -2313,7 +2365,32 @@ impl Ashell {
let disk_color = cx.theme().chart_5;
let net_color = cx.theme().chart_4;
let muted_fg = cx.theme().muted_foreground;
let mem_detail = if self.system.mem_detail.is_empty() {
"-- / --".to_string()
} else {
self.system.mem_detail.clone()
};
let swap_detail = if self.system.swap_detail.is_empty() {
"-- / --".to_string()
} else {
self.system.swap_detail.clone()
};
let net_rx = if self.system.net_rx.is_empty() {
"--".to_string()
} else {
self.system.net_rx.clone()
};
let net_tx = if self.system.net_tx.is_empty() {
"--".to_string()
} else {
self.system.net_tx.clone()
};
let active_is_ssh = matches!(self.active_kind(), Some(TabKind::Ssh));
let is_connecting_ssh = self
.active_tab
.as_ref()
.and_then(|active_id| self.tabs.iter().find(|tab| tab.id == *active_id))
.is_some_and(|tab| tab.kind == TabKind::Ssh && !tab.connected && tab.disconnected_reason.is_none());
let (monitor_title, monitor_detail) = self
.active_tab
.as_ref()
@@ -2323,6 +2400,8 @@ impl Ashell {
.unwrap_or_else(|| (t!("system_info").to_string(), t!("live").to_string()));
let status_color = if interactive {
cx.theme().success
} else if is_connecting_ssh {
cx.theme().warning
} else if active_is_ssh {
cx.theme().danger
} else {
@@ -2448,7 +2527,7 @@ impl Ashell {
div()
.text_size(ui_rems(0.85))
.text_color(muted_fg)
.child(self.system.mem_detail.clone()),
.child(mem_detail),
),
)
.child(
@@ -2475,7 +2554,7 @@ impl Ashell {
div()
.text_size(ui_rems(0.85))
.text_color(muted_fg)
.child(self.system.swap_detail.clone()),
.child(swap_detail),
),
)
.child(
@@ -2514,49 +2593,146 @@ impl Ashell {
div()
.relative()
.w_full()
.h(px(90.))
.child(
v_flex()
.id("sidebar-disk-scroll")
.track_scroll(&self.disk_scroll_handle)
.overflow_y_scroll()
.max_h(px(90.))
.size_full()
.gap_2()
.children(self.system.disks.iter().map(|disk| {
let pct = if disk.total_bytes > 0 {
(disk.total_bytes - disk.available_bytes) as f64
/ disk.total_bytes as f64
* 100.0
} else {
0.0
};
let mount_short = disk.mount.clone();
let mount_id = format!("sidebar-disk-{}", mount_short);
v_flex()
.gap_0p5()
.child(
h_flex()
.justify_between()
.child(
div()
.text_size(ui_rems(0.75))
.text_color(muted_fg)
.child(mount_short),
)
.child(
div()
.text_size(ui_rems(0.75))
.text_color(muted_fg)
.child(format!("{:.1}%", pct)),
),
)
.child(
Progress::new(mount_id)
.value(pct as f32)
.color(disk_color)
.with_size(px(4.))
.w_full(),
)
})),
.children(if self.system.disks.is_empty() {
vec![
v_flex()
.gap_0p5()
.child(
h_flex()
.justify_between()
.items_center()
.h(px(14.))
.child(
div()
.w(px(44.))
.h(px(10.))
.rounded_sm()
.bg(cx.theme().skeleton),
)
.child(
div()
.w(px(28.))
.h(px(10.))
.rounded_sm()
.bg(cx.theme().skeleton.opacity(0.7)),
),
)
.child(
div()
.w_full()
.h(px(4.))
.rounded_full()
.bg(cx.theme().skeleton.opacity(0.4)),
)
.into_any_element(),
v_flex()
.gap_0p5()
.child(
h_flex()
.justify_between()
.items_center()
.h(px(14.))
.child(
div()
.w(px(64.))
.h(px(10.))
.rounded_sm()
.bg(cx.theme().skeleton),
)
.child(
div()
.w(px(32.))
.h(px(10.))
.rounded_sm()
.bg(cx.theme().skeleton.opacity(0.7)),
),
)
.child(
div()
.w_full()
.h(px(4.))
.rounded_full()
.bg(cx.theme().skeleton.opacity(0.4)),
)
.into_any_element(),
v_flex()
.gap_0p5()
.child(
h_flex()
.justify_between()
.items_center()
.h(px(14.))
.child(
div()
.w(px(36.))
.h(px(10.))
.rounded_sm()
.bg(cx.theme().skeleton),
)
.child(
div()
.w(px(26.))
.h(px(10.))
.rounded_sm()
.bg(cx.theme().skeleton.opacity(0.7)),
),
)
.child(
div()
.w_full()
.h(px(4.))
.rounded_full()
.bg(cx.theme().skeleton.opacity(0.4)),
)
.into_any_element(),
]
} else {
self.system.disks.iter().map(|disk| {
let pct = if disk.total_bytes > 0 {
(disk.total_bytes - disk.available_bytes) as f64
/ disk.total_bytes as f64
* 100.0
} else {
0.0
};
let mount_short = disk.mount.clone();
let mount_id = format!("sidebar-disk-{}", mount_short);
v_flex()
.gap_0p5()
.child(
h_flex()
.justify_between()
.child(
div()
.text_size(ui_rems(0.75))
.text_color(muted_fg)
.child(mount_short),
)
.child(
div()
.text_size(ui_rems(0.75))
.text_color(muted_fg)
.child(format!("{:.1}%", pct)),
),
)
.child(
Progress::new(mount_id)
.value(pct as f32)
.color(disk_color)
.with_size(px(4.))
.w_full(),
)
.into_any_element()
}).collect()
}),
)
.child(
div()
@@ -2621,7 +2797,7 @@ impl Ashell {
.child(
div()
.text_size(ui_rems(0.75))
.child(self.system.net_rx.clone()),
.child(net_rx),
),
)
.child(
@@ -2639,7 +2815,7 @@ impl Ashell {
.child(
div()
.text_size(ui_rems(0.75))
.child(self.system.net_tx.clone()),
.child(net_tx),
),
),
),
@@ -5311,7 +5487,12 @@ impl Render for Ashell {
}
}
let has_ssh_session = self.active_ssh_session().is_some();
let has_ssh_session = self.active_ssh_session().is_some()
|| self
.active_group
.as_ref()
.and_then(|id| self.tab_groups.iter().find(|g| &g.id == id))
.is_some_and(|g| g.sftp.is_some());
let is_monitor_bottom = self.config.monitoring_position() == "Bottom";
let is_active_ssh_connected = self
.active_tab
+121 -26
View File
@@ -1267,7 +1267,7 @@ impl Ashell {
}
pub(crate) fn selector_entries(&self) -> Vec<SelectorEntry> {
let mut entries = vec![SelectorEntry::Local, SelectorEntry::NewSsh];
let mut entries = vec![SelectorEntry::Local];
entries.extend(
self.config
.sessions()
@@ -1278,11 +1278,7 @@ impl Ashell {
}
pub(crate) fn default_selector_index(&self) -> usize {
if self.config.sessions().is_empty() {
0
} else {
2
}
0
}
pub(crate) fn move_selector_selection(&mut self, delta: i32, cx: &mut Context<Self>) {
@@ -1294,8 +1290,8 @@ impl Ashell {
let next = (current + delta).clamp(0, entries.len() as i32 - 1) as usize;
if next != self.selector_selection {
self.selector_selection = next;
if next >= 2 {
self.selector_scroll_handle.scroll_to_item(next - 2);
if next >= 1 {
self.selector_scroll_handle.scroll_to_item(next - 1);
}
cx.notify();
}
@@ -1317,10 +1313,6 @@ impl Ashell {
self.open_local(cx);
window.close_dialog(cx);
}
SelectorEntry::NewSsh => {
window.close_dialog(cx);
self.open_new_ssh_dialog(window, cx);
}
SelectorEntry::Saved(session_id) => {
self.connect_saved_session(session_id, window, cx);
window.close_dialog(cx);
@@ -2111,12 +2103,28 @@ impl Ashell {
}
pub(crate) fn active_ssh_session(&self) -> Option<(String, Session)> {
let active_id = self.active_tab.as_ref()?;
let tab = self.tabs.iter().find(|tab| &tab.id == active_id)?;
if !tab.connected {
return None;
if let Some(active_id) = self.active_tab.as_ref() {
if let Some(tab) = self.tabs.iter().find(|tab| &tab.id == active_id) {
if tab.kind == TabKind::Ssh {
if let Some(session) = tab.session.clone() {
return Some((tab.id.clone(), session));
}
}
}
}
Some((tab.id.clone(), tab.session.clone()?))
if let Some(group_id) = self.active_group.as_ref() {
if let Some(group) = self.tab_groups.iter().find(|g| &g.id == group_id) {
let pane_tab_ids = group.pane_root.tab_ids();
for tab in &self.tabs {
if pane_tab_ids.contains(&tab.id.as_str()) && tab.kind == TabKind::Ssh {
if let Some(session) = tab.session.clone() {
return Some((tab.id.clone(), session));
}
}
}
}
}
None
}
pub(crate) fn active_session_id(&self) -> Option<&str> {
@@ -2536,7 +2544,7 @@ impl Ashell {
let Some(group_id) = self.active_group.clone() else {
return;
};
let target = self.active_tab.as_ref().and_then(|active_id| {
let mut target = self.active_tab.as_ref().and_then(|active_id| {
self.tabs
.iter()
.find(|tab| {
@@ -2544,12 +2552,49 @@ impl Ashell {
})
.and_then(|tab| tab.session.clone().map(|session| (tab.id.clone(), session)))
});
let target_tab_id = target.as_ref().map(|(tab_id, _)| tab_id.as_str());
if target.is_none() {
if let Some(group) = self.tab_groups.iter().find(|g| g.id == group_id) {
let pane_tab_ids = group.pane_root.tab_ids();
target = self
.tabs
.iter()
.find(|tab| {
pane_tab_ids.contains(&tab.id.as_str())
&& tab.kind == TabKind::Ssh
&& (tab.connected || force)
})
.and_then(|tab| tab.session.clone().map(|session| (tab.id.clone(), session)));
}
}
let current_tab_id = self
.tab_groups
.iter()
.find(|group| group.id == group_id)
.and_then(|group| group.sftp_tab_id.clone());
let group_has_ssh = self
.tab_groups
.iter()
.find(|g| g.id == group_id)
.map(|g| {
let pane_tab_ids = g.pane_root.tab_ids();
self.tabs
.iter()
.any(|t| pane_tab_ids.contains(&t.id.as_str()) && t.kind == TabKind::Ssh)
})
.unwrap_or(false);
if target.is_none()
&& !force
&& group_has_ssh
&& (self.sftp_handles.contains_key(&group_id) || current_tab_id.is_some())
{
return;
}
let target_tab_id = target.as_ref().map(|(tab_id, _)| tab_id.as_str());
let current_session_id = current_tab_id.as_ref().and_then(|tab_id| {
self.tabs
.iter()
@@ -2560,7 +2605,7 @@ impl Ashell {
let target_session_id = target.as_ref().map(|(_, session)| session.id.as_str());
if !force {
if current_tab_id.as_deref() == target_tab_id {
if current_tab_id.as_deref() == target_tab_id && target_tab_id.is_some() {
return;
}
if current_session_id.as_deref() == target_session_id
@@ -2605,12 +2650,51 @@ impl Ashell {
}
pub(crate) fn sync_system_tab_to_active_group(&mut self) {
let active_ssh_tab = self.active_tab.as_ref().and_then(|id| {
self.tabs
.iter()
.find(|tab| tab.id == *id && tab.kind == TabKind::Ssh)
let active_group = self.active_group.as_ref().and_then(|group_id| {
self.tab_groups.iter().find(|group| &group.id == group_id)
});
let new_id = active_ssh_tab.map(|tab| tab.id.clone());
if let Some(current_sys_id) = &self.system_tab_id {
if let Some(group) = active_group {
let pane_tab_ids = group.pane_root.tab_ids();
if pane_tab_ids.contains(&current_sys_id.as_str()) {
if self
.tabs
.iter()
.any(|t| &t.id == current_sys_id && t.kind == TabKind::Ssh && t.connected)
{
return;
}
}
}
}
let new_id = self
.active_tab
.as_ref()
.and_then(|id| {
self.tabs
.iter()
.find(|tab| tab.id == *id && tab.kind == TabKind::Ssh)
.map(|tab| tab.id.clone())
})
.or_else(|| {
active_group.and_then(|group| {
let pane_tab_ids = group.pane_root.tab_ids();
self.tabs
.iter()
.find(|tab| {
pane_tab_ids.contains(&tab.id.as_str())
&& tab.kind == TabKind::Ssh
&& tab.connected
})
.map(|tab| tab.id.clone())
})
});
let active_ssh_tab = new_id
.as_ref()
.and_then(|id| self.tabs.iter().find(|tab| &tab.id == id && tab.kind == TabKind::Ssh));
let active_ssh_status = active_ssh_tab.and_then(|tab| {
(!tab.connected).then(|| {
tab.disconnected_reason
@@ -2621,13 +2705,24 @@ impl Ashell {
if self.system_tab_id != new_id {
self.system_tab_id = new_id;
self.reset_system_monitor_state();
self.remote_sample_in_flight = false;
self.remote_processes_in_flight = false;
self.remote_ports_in_flight = false;
if let Some(status) = active_ssh_status {
self.system_status = Some(status.clone().into());
self.remote_process_status = Some(status.into());
self.system = crate::system::SystemSnapshot::default();
self.cpu_history.clear();
self.net_rx_history.clear();
self.net_tx_history.clear();
self.remote_processes.clear();
self.remote_ports.clear();
self.last_applied_system_tab_id = self.system_tab_id.clone();
} else {
self.system_status = None;
}
self.request_active_system_snapshot();
if self.active_dialog == Some(crate::app::DialogKind::Processes) {
self.request_active_process_snapshot();