M2: terminal UI (ratatui)

- layout: header, top menu (quicklook, cpu, mem, swap, load), left sidebar,
  process list and alert footer
- decorations/colors, bars, per-plugin render implementations
- hotkeys: quit, help, modes, sorting, cursor, nice, kill, filters, toggles
- TestBackend layout test
This commit is contained in:
glances-rs
2026-10-09 23:35:23 +00:00
parent 975385da5d
commit 8385900ac1
27 changed files with 2053 additions and 31 deletions
+70 -1
View File
@@ -8,6 +8,7 @@ use serde_json::Value;
pub struct PerCpuPlugin {
pub model: PluginModel,
pub max_cpu_display: usize,
}
impl PerCpuPlugin {
@@ -39,7 +40,8 @@ impl PerCpuPlugin {
args.disable_config_exec,
);
model.set_item_key("cpu_number");
Self { model }
let max_cpu_display = config.get_int_value("percpu", "max_cpu_display", 4).max(1) as usize;
Self { model, max_cpu_display }
}
}
@@ -64,4 +66,71 @@ impl Plugin for PerCpuPlugin {
let mut state = cpu_percent().lock().unwrap();
Ok(Value::Array(state.get_percpu().to_vec()))
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
let items: Vec<Value> = self.model.stats.as_array().cloned().unwrap_or_default();
if items.is_empty() || !ctx.runtime.percpu {
return ret;
}
let items = &items;
let quicklook_disabled = !ctx.runtime.is_plugin_enabled("quicklook");
let mut header = vec!["user", "system", "iowait", "idle", "irq", "nice", "steal", "guest"];
if quicklook_disabled {
ret.push(self.model.curse_add_line(format!("{:5}", "CPU"), glances_core::Decoration::Title, false, false, false));
header.insert(0, "total");
}
for stat in &header {
ret.push(self.model.curse_add_line(format!("{stat:>7}"), glances_core::Decoration::Default, false, false, false));
}
// Sort by total and keep the top max_cpu_display cores.
let mut sorted: Vec<Value> = items.clone();
if sorted.len() > self.max_cpu_display {
sorted.sort_by(|a, b| {
b.get("total")
.and_then(Value::as_f64)
.unwrap_or(0.0)
.partial_cmp(&a.get("total").and_then(Value::as_f64).unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal)
});
}
let displayed: Vec<Value> = sorted.iter().take(self.max_cpu_display).cloned().collect();
for cpu in &displayed {
ret.push(self.model.curse_new_line());
if quicklook_disabled {
let cpu_id = cpu.get("cpu_number").and_then(Value::as_i64).unwrap_or(0);
let msg = if cpu_id < 10 { format!("CPU{cpu_id:1} ") } else { format!("{cpu_id:4} ") };
ret.push(self.model.curse_add_line(msg, glances_core::Decoration::Default, false, false, false));
}
for stat in &header {
let value = cpu.get(*stat).and_then(Value::as_f64);
let decoration = match value {
Some(value) => self.model.get_alert(value, 0.0, 100.0, true, false, Some(stat), None, false),
None => glances_core::Decoration::Default,
};
let msg = match value {
Some(value) => format!("{value:6.1}%"),
None => format!("{:>6}%", "?"),
};
ret.push(self.model.curse_add_line(msg, decoration, false, false, false));
}
}
if items.len() > self.max_cpu_display {
ret.push(self.model.curse_new_line());
if quicklook_disabled {
ret.push(self.model.curse_add_line("CPU* ", glances_core::Decoration::Default, false, false, false));
}
for stat in &header {
let values: Vec<f64> = sorted
.iter()
.skip(self.max_cpu_display)
.filter_map(|cpu| cpu.get(*stat).and_then(Value::as_f64))
.collect();
let mean = if values.is_empty() { 0.0 } else { values.iter().sum::<f64>() / values.len() as f64 };
let decoration = self.model.get_alert(mean, 0.0, 100.0, true, false, Some(stat), None, false);
ret.push(self.model.curse_add_line(format!("{mean:6.1}%"), decoration, false, false, false));
}
}
ret
}
}