feat: add customizable system monitoring position

- Add settings to position the system monitoring panel at the Bottom, Sidebar, or Hidden.
- Support dynamically reallocating UI space when the monitor panel is moved.
- Implement a simplified, compact card-style system monitoring view for the sidebar (similar to v0.3.5 design).
- Add full localization for monitoring setting and sidebar metrics.
This commit is contained in:
TomZz
2026-06-13 03:34:29 +08:00
parent e7e2c67393
commit 4ba3000538
5 changed files with 208 additions and 9 deletions
+4
View File
@@ -68,6 +68,10 @@ system_default: "System Default"
ui_font_family: "UI Font"
terminal_font_family: "Terminal Font"
theme_management_hint: "Theme mode and light/dark theme families are managed from the top-bar theme button."
monitoring_position: "Monitoring Position"
position_bottom: "Bottom"
position_sidebar: "Sidebar"
position_hidden: "Hidden"
local_host: "local host"
local_terminal_opened: "local terminal opened"
connecting: "Connecting"
+5 -1
View File
@@ -69,7 +69,11 @@ terminal_font_size: "终端字体大小"
system_default: "系统默认"
ui_font_family: "界面字体"
terminal_font_family: "终端字体"
theme_management_hint: "主题模式和亮/暗主题系列可通过顶栏的主题按钮管理。"
theme_management_hint: "深色/浅色模式以及主题配色由顶部栏的样式按钮统一管理。"
monitoring_position: "系统监控显示位置"
position_bottom: "底部"
position_sidebar: "侧边栏"
position_hidden: "不显示"
local_host: "本地主机"
local_terminal_opened: "已打开本地终端"
connecting: "正在连接"
+14
View File
@@ -127,6 +127,12 @@ pub struct ConfigFile {
pub transfers: Vec<crate::terminal::Transfer>,
#[serde(default)]
pub show_hidden_files: bool,
#[serde(default = "default_monitoring_position")]
pub monitoring_position: String,
}
fn default_monitoring_position() -> String {
"Sidebar".to_string()
}
fn default_locale() -> String {
@@ -230,6 +236,14 @@ impl ConfigStore {
self.cache.locale = locale.to_string();
}
pub fn monitoring_position(&self) -> &str {
&self.cache.monitoring_position
}
pub fn set_monitoring_position(&mut self, pos: &str) {
self.cache.monitoring_position = pos.to_string();
}
pub fn terminal_font_size(&self) -> f32 {
if self.cache.terminal_font_size <= 0.0 {
default_terminal_font_size()
+66
View File
@@ -1148,6 +1148,72 @@ impl Ashell {
.child(t!("copy_paste_hint", key = if cfg!(target_os = "macos") { "Command" } else { "Ctrl" }).to_string())
)
)
.child(
h_flex()
.items_center()
.gap_3()
.child(div().w(px(240.)).child(t!("monitoring_position").to_string()))
.child(
Button::new("monitoring-position-dropdown")
.small()
.icon(IconName::PanelLeftOpen)
.label({
let pos = view.read(cx).config.monitoring_position().to_string();
if pos == "Sidebar" {
t!("position_sidebar").to_string()
} else if pos == "Hidden" {
t!("position_hidden").to_string()
} else {
t!("position_bottom").to_string()
}
})
.dropdown_menu_with_anchor(Anchor::BottomRight, {
let view = view.clone();
move |mut menu, window, cx| {
let pos = view.read(cx).config.monitoring_position().to_string();
menu = menu
.min_w(160.)
.item(
PopupMenuItem::new(t!("position_bottom").to_string())
.checked(pos == "Bottom")
.on_click(window.listener_for(
&view,
|this, _, _window, cx| {
this.config.set_monitoring_position("Bottom");
let _ = this.config.save();
cx.notify();
},
)),
)
.item(
PopupMenuItem::new(t!("position_sidebar").to_string())
.checked(pos == "Sidebar")
.on_click(window.listener_for(
&view,
|this, _, _window, cx| {
this.config.set_monitoring_position("Sidebar");
let _ = this.config.save();
cx.notify();
},
)),
)
.item(
PopupMenuItem::new(t!("position_hidden").to_string())
.checked(pos == "Hidden")
.on_click(window.listener_for(
&view,
|this, _, _window, cx| {
this.config.set_monitoring_position("Hidden");
let _ = this.config.save();
cx.notify();
},
)),
);
menu
}
}),
)
)
.child(
h_flex()
.items_center()
+119 -8
View File
@@ -692,7 +692,7 @@ impl Ashell {
.text_size(rems(0.833))
.font_weight(FontWeight::SEMIBOLD)
.text_color(cpu_color)
.child("CPU"),
.child(t!("cpu").to_string()),
)
.child(div().flex_1())
.child(
@@ -756,7 +756,7 @@ impl Ashell {
.text_size(rems(0.833))
.font_weight(FontWeight::SEMIBOLD)
.text_color(mem_color)
.child("MEM"),
.child(t!("mem").to_string()),
)
.child(div().flex_1())
.child(
@@ -826,7 +826,7 @@ impl Ashell {
.text_size(rems(0.833))
.font_weight(FontWeight::SEMIBOLD)
.text_color(net_color)
.child("NET"),
.child(t!("net").to_string()),
)
.child(div().flex_1())
.child(
@@ -933,7 +933,7 @@ impl Ashell {
.text_size(rems(0.833))
.font_weight(FontWeight::SEMIBOLD)
.text_color(disk_color)
.child("DISK"),
.child(t!("disk").to_string()),
)
.child(div().flex_1())
.child(
@@ -1030,6 +1030,107 @@ impl Ashell {
panel
}
fn render_sidebar_monitoring_panel(&self, cx: &mut Context<Self>) -> impl IntoElement {
let cpu_pct = self.system.cpu_percent;
let mem_pct = self.system.mem_percent;
let swap_pct = self.system.swap_percent;
let disk_pct = if let Some(root_disk) = self.system.disks.iter().find(|d| d.mount == "/") {
if root_disk.total_bytes > 0 {
(root_disk.total_bytes - root_disk.available_bytes) as f64 / root_disk.total_bytes as f64
} else {
0.0
}
} else {
0.0
} as f32;
let cpu_color = cx.theme().chart_1;
let mem_color = cx.theme().chart_2;
let swap_color = cx.theme().chart_3;
let disk_color = cx.theme().chart_5;
let net_color = cx.theme().chart_4;
let muted_fg = cx.theme().muted_foreground;
v_flex()
.gap_4()
.w_full()
.p_2()
.child(
v_flex()
.gap_1()
.child(
h_flex()
.justify_between()
.child(div().text_size(rems(0.85)).text_color(cpu_color).child(t!("cpu").to_string()))
.child(div().text_size(rems(0.85)).text_color(muted_fg).child(format!("{:.1}%", cpu_pct * 100.0))),
)
.child(Progress::new("sidebar-cpu").value(cpu_pct * 100.0).color(cpu_color).with_size(px(4.)).w_full())
)
.child(
v_flex()
.gap_1()
.child(
h_flex()
.justify_between()
.child(div().text_size(rems(0.85)).text_color(mem_color).child(t!("mem").to_string()))
.child(div().text_size(rems(0.85)).text_color(muted_fg).child(self.system.mem_detail.clone())),
)
.child(Progress::new("sidebar-mem").value(mem_pct * 100.0).color(mem_color).with_size(px(4.)).w_full())
)
.child(
v_flex()
.gap_1()
.child(
h_flex()
.justify_between()
.child(div().text_size(rems(0.85)).text_color(swap_color).child(t!("swap").to_string()))
.child(div().text_size(rems(0.85)).text_color(muted_fg).child(self.system.swap_detail.clone())),
)
.child(Progress::new("sidebar-swap").value(swap_pct * 100.0).color(swap_color).with_size(px(4.)).w_full())
)
.child(
v_flex()
.gap_1()
.child(
h_flex()
.justify_between()
.child(div().text_size(rems(0.85)).text_color(disk_color).child(t!("disk").to_string()))
.child(div().text_size(rems(0.85)).text_color(muted_fg).child(format!("{:.1}%", disk_pct * 100.0))),
)
.child(Progress::new("sidebar-disk").value(disk_pct * 100.0).color(disk_color).with_size(px(4.)).w_full())
)
.child(
v_flex()
.gap_1()
.child(
h_flex()
.justify_between()
.child(div().text_size(rems(0.85)).text_color(net_color).child(t!("net").to_string()))
.child(div().text_size(rems(0.85)).text_color(muted_fg).child(t!("live"))),
)
.child(
h_flex()
.gap_2()
.child(
h_flex()
.flex_1()
.min_w(px(0.))
.gap_1()
.child(div().flex_none().text_size(rems(0.75)).text_color(net_color).child(""))
.child(div().text_size(rems(0.75)).child(self.system.net_rx.clone()))
)
.child(
h_flex()
.flex_1()
.min_w(px(0.))
.gap_1()
.child(div().flex_none().text_size(rems(0.75)).text_color(cx.theme().chart_5).child(""))
.child(div().text_size(rems(0.75)).child(self.system.net_tx.clone()))
)
)
)
}
fn sidebar(&self, cx: &mut Context<Self>) -> impl IntoElement {
let sessions = self.config.sessions().to_vec();
let active_session_id = self.active_session_id().map(ToOwned::to_owned);
@@ -1101,6 +1202,9 @@ impl Ashell {
cx.listener(|this, _, window, cx| this.open_new_ssh_dialog(window, cx)),
),
)
.when(self.config.monitoring_position() == "Sidebar", |this| {
this.child(self.render_sidebar_monitoring_panel(cx))
})
.child(
v_flex()
.flex_1()
@@ -1454,9 +1558,16 @@ impl Render for Ashell {
let monitoring_contents = v_flex()
.size_full()
.child(self.render_monitoring_panel(window.viewport_size().width, cx))
.when(self.config.monitoring_position() == "Bottom", |this| {
this.child(self.render_monitoring_panel(window.viewport_size().width, cx))
})
.child(self.render_sftp_panel(window, cx));
let is_monitor_bottom = self.config.monitoring_position() == "Bottom";
let minimized_height = if is_monitor_bottom { 114. } else { 34. };
let min_panel_height = if is_monitor_bottom { 260. } else { 180. };
let default_panel_height = if is_monitor_bottom { 328. } else { 248. };
let body_panel = if self.sftp_panel_minimized {
v_flex()
.size_full()
@@ -1470,7 +1581,7 @@ impl Render for Ashell {
.child(
div()
.flex_none()
.h(px(114.))
.h(px(minimized_height))
.w_full()
.border_t_1()
.border_color(cx.theme().border)
@@ -1487,8 +1598,8 @@ impl Render for Ashell {
.config
.body_panels()
.and_then(|s| s.get(1).copied())
.unwrap_or(328.)))
.size_range(px(260.)..px(1200.))
.unwrap_or(default_panel_height)))
.size_range(px(min_panel_height)..px(1200.))
.child(monitoring_contents)
)
.into_any_element()