diff --git a/src/app/dialogs.rs b/src/app/dialogs.rs index a972f59..490332b 100644 --- a/src/app/dialogs.rs +++ b/src/app/dialogs.rs @@ -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" diff --git a/src/app/mod.rs b/src/app/mod.rs index c9c7b60..4dd4ffb 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -421,6 +421,7 @@ pub(crate) struct Ashell { pub(crate) search_bar_bounds: Option>, pub(crate) system_tab_id: Option, + pub(crate) last_applied_system_tab_id: Option, pub(crate) sftp_handles: std::collections::HashMap, 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) { + 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(); diff --git a/src/app/ui.rs b/src/app/ui.rs index 9d2242e..d0f1a1b 100644 --- a/src/app/ui.rs +++ b/src/app/ui.rs @@ -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 diff --git a/src/session/mod.rs b/src/session/mod.rs index c65329c..034e4da 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -1267,7 +1267,7 @@ impl Ashell { } pub(crate) fn selector_entries(&self) -> Vec { - 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) { @@ -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(¤t_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();