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
+32 -2
View File
@@ -179,6 +179,12 @@ pub fn page_size() -> u64 {
/// Read the load average (1, 5, 15 minutes).
pub fn read_loadavg() -> Option<(f64, f64, f64)> {
let mut loadavg = [0.0f64; 3];
let count = unsafe { libc::getloadavg(loadavg.as_mut_ptr(), 3) };
if count == 3 {
return Some((loadavg[0], loadavg[1], loadavg[2]));
}
// Fallback on /proc/loadavg (less precise).
let content = std::fs::read_to_string("/proc/loadavg").ok()?;
let fields: Vec<&str> = content.split_whitespace().collect();
Some((
@@ -558,7 +564,31 @@ pub fn read_number_file(path: &str) -> Option<f64> {
std::fs::read_to_string(path).ok().and_then(|v| v.trim().parse::<f64>().ok())
}
/// Get the current date formatted with a Python strftime format.
/// Get the current date formatted with a C strftime format (same semantics
/// as Python's `time.strftime`).
pub fn strftime(format: &str) -> String {
chrono::Local::now().format(format).to_string()
let Ok(c_format) = std::ffi::CString::new(format) else {
return String::new();
};
unsafe {
let now = libc::time(std::ptr::null_mut());
let mut tm: libc::tm = std::mem::zeroed();
if libc::localtime_r(&now, &mut tm).is_null() {
return String::new();
}
for size in [256usize, 4096] {
let mut buffer = vec![0u8; size];
let len = libc::strftime(
buffer.as_mut_ptr() as *mut libc::c_char,
buffer.len(),
c_format.as_ptr(),
&tm,
);
if len > 0 {
buffer.truncate(len);
return String::from_utf8_lossy(&buffer).to_string();
}
}
}
String::new()
}
+11
View File
@@ -54,9 +54,20 @@ impl Engine {
let mut engine = Self { args, config, runtime, plugins };
engine.apply_plugin_options();
engine.apply_process_options();
engine.sync_enabled();
engine
}
/// Refresh the runtime set of enabled plugins.
pub fn sync_enabled(&mut self) {
self.runtime.enabled_plugins = self
.plugins
.iter()
.filter(|(_, plugin)| plugin.is_enabled())
.map(|(name, _)| name.clone())
.collect();
}
fn apply_plugin_options(&mut self) {
// Disable plugins from the configuration file.
for section in self.config.sections() {
+74
View File
@@ -138,4 +138,78 @@ impl Plugin for CpuPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
let s = &self.model.stats;
if !s.is_object() || ctx.runtime.percpu {
return ret;
}
let idle_tag = s.get("user").is_none();
// Line 1: total + idle + ctx_switches.
ret.push(self.model.curse_add_line(format!("{:8}", "CPU"), glances_core::Decoration::Title, false, false, false));
if let Some(total) = s.get("total").and_then(Value::as_f64) {
ret.push(self.model.curse_add_line(
format!("{total:>4.1}%"),
self.model.view_decoration(None, "total"),
false,
false,
false,
));
}
if s.get("idle").is_some() && !idle_tag {
let optional = self.model.view_bool(None, "idle", "optional");
ret.push(self.model.curse_add_line(" idle ", glances_core::Decoration::Default, optional, false, false));
if let Some(idle) = s.get("idle").and_then(Value::as_f64) {
ret.push(self.model.curse_add_line(
format!("{idle:>4.1}%"),
self.model.view_decoration(None, "idle"),
optional,
false,
false,
));
}
}
ret.extend(self.model.curse_add_stat("ctx_switches", Some(15), " ", "", ""));
// Line 2.
ret.push(self.model.curse_new_line());
if !idle_tag {
ret.extend(self.model.curse_add_stat("user", Some(15), "", "", ""));
} else {
ret.extend(self.model.curse_add_stat("idle", Some(15), "", "", ""));
}
ret.extend(self.model.curse_add_stat("irq", Some(14), " ", "", ""));
ret.extend(self.model.curse_add_stat("interrupts", Some(15), " ", "", ""));
// Line 3.
ret.push(self.model.curse_new_line());
if !idle_tag {
ret.extend(self.model.curse_add_stat("system", Some(15), "", "", ""));
} else {
ret.extend(self.model.curse_add_stat("core", Some(15), "", "", ""));
}
ret.extend(self.model.curse_add_stat("nice", Some(14), " ", "", ""));
if s.get("soft_interrupts").is_some() {
ret.extend(self.model.curse_add_stat("soft_interrupts", Some(15), " ", "", ""));
} else {
ret.extend(self.model.curse_add_stat("ctx_switches", Some(15), " ", "", ""));
}
// Line 4.
ret.push(self.model.curse_new_line());
if s.get("iowait").is_some() {
ret.extend(self.model.curse_add_stat("iowait", Some(15), "", "", ""));
} else {
ret.extend(self.model.curse_add_stat("dpc", Some(15), "", "", ""));
}
ret.extend(self.model.curse_add_stat("steal", Some(14), " ", "", ""));
if s.get("guest").is_some() {
ret.extend(self.model.curse_add_stat("guest", Some(14), " ", "", ""));
} else {
ret.extend(self.model.curse_add_stat("syscalls", Some(15), " ", "", ""));
}
ret
}
}
@@ -160,4 +160,77 @@ impl Plugin for DiskioPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(13);
ret.push(self.model.curse_add_line(
format!("{:<width$}", "DISK I/O", width = name_max_width),
glances_core::Decoration::Title,
false,
false,
false,
));
if ctx.args.diskio_iops {
ret.push(self.model.curse_add_line(format!("{:>8}", "IOR/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "IOW/s"), glances_core::Decoration::Default, false, false, false));
} else if ctx.args.diskio_latency {
ret.push(self.model.curse_add_line(format!("{:>8}", "ms/opR"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "ms/opW"), glances_core::Decoration::Default, false, false, false));
} else {
ret.push(self.model.curse_add_line(format!("{:>8}", "R/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "W/s"), glances_core::Decoration::Default, false, false, false));
}
let stats = self.model.sorted_stats();
let Some(items) = stats.as_array() else {
return ret;
};
for i in items {
let disk_real_name = i.get("disk_name").and_then(Value::as_str).unwrap_or("").to_string();
if self
.model
.hide_zero_fields
.iter()
.all(|field| self.model.view_bool(Some(&disk_real_name), field, "hidden"))
{
continue;
}
let mut disk_name = i
.get("alias")
.and_then(Value::as_str)
.map(|s| s.to_string())
.unwrap_or_else(|| disk_real_name.clone());
if disk_name.chars().count() > name_max_width {
disk_name = format!("{}_", disk_name.chars().take(name_max_width).collect::<String>());
}
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", disk_name, width = name_max_width + 1),
glances_core::Decoration::Default,
false,
false,
false,
));
if ctx.args.diskio_iops {
let rx = self.model.auto_unit(i.get("read_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K");
let tx = self.model.auto_unit(i.get("write_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K");
ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_count"), false, false, false));
ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_count"), false, false, false));
} else if ctx.args.diskio_latency {
let rx = self.model.auto_unit(i.get("read_latency").and_then(Value::as_f64).unwrap_or(0.0), true, "K");
let tx = self.model.auto_unit(i.get("write_latency").and_then(Value::as_f64).unwrap_or(0.0), true, "K");
ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_latency"), false, false, false));
ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_latency"), false, false, false));
} else {
let rx = self.model.auto_unit(i.get("read_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K");
let tx = self.model.auto_unit(i.get("write_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K");
ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_bytes"), false, false, false));
ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_bytes"), false, false, false));
}
}
ret
}
}
+71
View File
@@ -117,4 +117,75 @@ impl Plugin for FsPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(13);
ret.push(self.model.curse_add_line(
format!("{:<width$}", "FILE SYS", width = name_max_width),
glances_core::Decoration::Title,
false,
false,
false,
));
if ctx.runtime.fs_free_space {
ret.push(self.model.curse_add_line(format!("{:>8}", "Free"), glances_core::Decoration::Default, false, false, false));
} else {
ret.push(self.model.curse_add_line(format!("{:>8}", "Used"), glances_core::Decoration::Default, false, false, false));
}
ret.push(self.model.curse_add_line(format!("{:>7}", "Total"), glances_core::Decoration::Default, false, false, false));
let Some(items) = self.model.stats.as_array() else {
return ret;
};
let mut sorted = items.clone();
sorted.sort_by_key(|i| i.get("mnt_point").and_then(Value::as_str).unwrap_or("").to_string());
for i in &sorted {
let mnt_point_raw = i.get("mnt_point").and_then(Value::as_str).unwrap_or("").to_string();
let device = i.get("device_name").and_then(Value::as_str).unwrap_or("").to_string();
let mut mnt_point = i
.get("alias")
.and_then(Value::as_str)
.map(|s| s.to_string())
.unwrap_or_else(|| mnt_point_raw.clone());
let device_basename = device.rsplit('/').next().unwrap_or("").to_string();
if mnt_point.chars().count() + device_basename.chars().count() <= name_max_width.saturating_sub(3) {
mnt_point = format!("{mnt_point} ({device_basename})");
} else if mnt_point.chars().count() > name_max_width {
mnt_point = format!("{}_", mnt_point.chars().take(name_max_width).collect::<String>());
}
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", mnt_point, width = name_max_width + 1),
glances_core::Decoration::Default,
false,
false,
false,
));
let value = if ctx.runtime.fs_free_space {
i.get("free").and_then(Value::as_f64).unwrap_or(0.0)
} else {
i.get("used").and_then(Value::as_f64).unwrap_or(0.0)
};
ret.push(self.model.curse_add_line(
format!("{:>7}", self.model.auto_unit(value, false, "K")),
self.model.view_decoration(Some(&mnt_point_raw), "used"),
false,
false,
false,
));
let size = i.get("size").and_then(Value::as_f64).unwrap_or(0.0);
ret.push(self.model.curse_add_line(
format!("{:>7}", self.model.auto_unit(size, false, "K")),
glances_core::Decoration::Default,
false,
false,
false,
));
}
ret
}
}
+33
View File
@@ -98,4 +98,37 @@ impl Plugin for IrqPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 {
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(7);
ret.push(self.model.curse_add_line(
format!("{:<width$}", "IRQ", width = name_max_width),
glances_core::Decoration::Title,
false,
false,
false,
));
ret.push(self.model.curse_add_line(format!("{:>9}", "Rate/s"), glances_core::Decoration::Default, false, false, false));
let Some(items) = self.model.stats.as_array() else {
return ret;
};
for i in items {
let line = i.get("irq_line").and_then(Value::as_str).unwrap_or("");
let rate = i.get("irq_rate").and_then(Value::as_i64).unwrap_or(0);
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", line.chars().take(name_max_width).collect::<String>(), width = name_max_width),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(format!("{rate:>9}"), glances_core::Decoration::Default, false, false, false));
}
ret
}
}
@@ -78,4 +78,48 @@ impl Plugin for LoadPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
let s = &self.model.stats;
if !s.is_object() || s.as_object().map(|o| o.is_empty()).unwrap_or(true) {
return ret;
}
ret.push(self.model.curse_add_line(format!("{:4}", "LOAD"), glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(
format!(" {:1}", self.model.trend_msg(self.model.get_trend("min1", 30), 1.0)),
glances_core::Decoration::Default,
false,
false,
false,
));
if let Some(cpucore) = s.get("cpucore").and_then(Value::as_f64) {
if cpucore > 0.0 {
ret.push(self.model.curse_add_line(format!("{:3}core", cpucore as i64), glances_core::Decoration::Default, false, false, false));
}
}
for load_time in ["1", "5", "15"] {
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(format!("{:7}", format!("{load_time} min")), glances_core::Decoration::Default, false, false, false));
let stat = s.get(&format!("min{load_time}")).and_then(Value::as_f64).unwrap_or(0.0);
let value = if ctx.runtime.disable_irix {
let cores = s.get("cpucore").and_then(Value::as_f64).unwrap_or(0.0);
if cores != 0.0 {
format!("{:>5.1}%", stat / cores * 100.0)
} else {
format!("{stat:>6.2}")
}
} else {
format!("{stat:>6.2}")
};
ret.push(self.model.curse_add_line(
value,
self.model.view_decoration(None, &format!("min{load_time}")),
false,
false,
false,
));
}
ret
}
}
+38
View File
@@ -113,4 +113,42 @@ impl Plugin for MemPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
let s = &self.model.stats;
if !s.is_object() {
return ret;
}
// First line: total% + trend + active.
ret.push(self.model.curse_add_line(format!("{}", "MEM"), glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(format!(" {:2}", self.model.trend_msg(self.model.get_trend("percent", 30), 1.0)), glances_core::Decoration::Default, false, false, false));
if let Some(percent) = s.get("percent").and_then(Value::as_f64) {
ret.push(self.model.curse_add_line(
format!("{:>6.1}%", percent),
self.model.view_decoration(None, "percent"),
false,
false,
false,
));
}
ret.extend(self.model.curse_add_stat("active", Some(16), " ", "", ""));
// Second line: total + inactive.
ret.push(self.model.curse_new_line());
ret.extend(self.model.curse_add_stat("total", Some(15), "", "", ""));
ret.extend(self.model.curse_add_stat("inactive", Some(16), " ", "", ""));
// Third line: available + buffers.
ret.push(self.model.curse_new_line());
if self.available {
ret.extend(self.model.curse_add_stat("available", Some(15), "", "", ""));
} else {
ret.extend(self.model.curse_add_stat("used", Some(15), "", "", ""));
}
ret.extend(self.model.curse_add_stat("buffers", Some(16), " ", "", ""));
// Fourth line: free + cached.
ret.push(self.model.curse_new_line());
ret.extend(self.model.curse_add_stat("free", Some(15), "", "", ""));
ret.extend(self.model.curse_add_stat("cached", Some(16), " ", "", ""));
ret
}
}
@@ -82,4 +82,28 @@ impl Plugin for MemSwapPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
let s = &self.model.stats;
if !s.is_object() {
return ret;
}
ret.push(self.model.curse_add_line("SWAP", glances_core::Decoration::Title, false, false, false));
ret.push(self.model.curse_add_line(" ", glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(
format!("{:>6.1}%", s.get("percent").and_then(Value::as_f64).unwrap_or(0.0)),
self.model.view_decoration(None, "percent"),
false,
false,
false,
));
ret.push(self.model.curse_new_line());
ret.extend(self.model.curse_add_stat("total", Some(15), "", "", ""));
ret.push(self.model.curse_new_line());
ret.extend(self.model.curse_add_stat("used", Some(15), "", "", ""));
ret.push(self.model.curse_new_line());
ret.extend(self.model.curse_add_stat("free", Some(15), "", "", ""));
ret
}
}
@@ -38,6 +38,9 @@ impl NetworkPlugin {
args.disable_config_exec,
);
model.set_item_key("interface_name");
model.hide_zero = config.get_bool_value("network", "hide_zero", false);
model.hide_zero_fields =
vec!["bytes_recv_rate_per_sec".to_string(), "bytes_sent_rate_per_sec".to_string()];
let hide_no_up = config.has_section("network") && config.get_bool_value("network", "hide_no_up", false);
let hide_no_ip = config.has_section("network") && config.get_bool_value("network", "hide_no_ip", false);
Self { model, hide_no_up, hide_no_ip }
@@ -136,4 +139,112 @@ impl Plugin for NetworkPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
if ctx.max_width == 0 {
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(12);
// Header
ret.push(self.model.curse_add_line(
format!("{:<width$}", "NETWORK", width = name_max_width),
glances_core::Decoration::Title,
false,
false,
false,
));
if ctx.runtime.network_cumul {
if ctx.runtime.network_sum {
ret.push(self.model.curse_add_line(format!("{:>14}", "Rx+Tx"), glances_core::Decoration::Default, false, false, false));
} else {
ret.push(self.model.curse_add_line(format!("{:>7}", "Rx"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "Tx"), glances_core::Decoration::Default, false, false, false));
}
} else if ctx.runtime.network_sum {
ret.push(self.model.curse_add_line(format!("{:>14}", "Rx+Tx/s"), glances_core::Decoration::Default, false, false, false));
} else {
ret.push(self.model.curse_add_line(format!("{:>7}", "Rx/s"), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>7}", "Tx/s"), glances_core::Decoration::Default, false, false, false));
}
let stats = self.model.sorted_stats();
let Some(items) = stats.as_array() else {
return ret;
};
for i in items {
let item_key = i.get("interface_name").and_then(Value::as_str).unwrap_or("").to_string();
// Hide stats never different from zero.
if self
.model
.hide_zero_fields
.iter()
.all(|field| self.model.view_bool(Some(&item_key), field, "hidden"))
{
continue;
}
let to_bit = if ctx.args.byte { 1.0 } else { 8.0 };
let unit = if ctx.args.byte { "" } else { "b" };
let (rx, tx, ax) = if ctx.runtime.network_cumul {
let recv = i.get("bytes_recv").and_then(Value::as_f64);
let sent = i.get("bytes_sent").and_then(Value::as_f64);
let all = i.get("bytes_all").and_then(Value::as_f64);
match (recv, sent, all) {
(Some(rx), Some(tx), Some(ax)) => (
format!("{}{unit}", self.model.auto_unit(rx * to_bit, false, "K")),
format!("{}{unit}", self.model.auto_unit(tx * to_bit, false, "K")),
format!("{}{unit}", self.model.auto_unit(ax * to_bit, false, "K")),
),
_ => continue,
}
} else {
let recv = i.get("bytes_recv_rate_per_sec").and_then(Value::as_f64);
let sent = i.get("bytes_sent_rate_per_sec").and_then(Value::as_f64);
let all = i.get("bytes_all_rate_per_sec").and_then(Value::as_f64);
match (recv, sent, all) {
(Some(rx), Some(tx), Some(ax)) => (
format!("{}{unit}", self.model.auto_unit(rx * to_bit, false, "K")),
format!("{}{unit}", self.model.auto_unit(tx * to_bit, false, "K")),
format!("{}{unit}", self.model.auto_unit(ax * to_bit, false, "K")),
),
_ => continue,
}
};
let mut if_name = i
.get("alias")
.and_then(Value::as_str)
.map(|s| s.to_string())
.unwrap_or_else(|| item_key.split(':').next().unwrap_or("").to_string());
if if_name.len() > name_max_width {
if_name = format!("_{}", &if_name[if_name.len() - name_max_width + 1..]);
}
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{if_name:<width$}", width = name_max_width),
glances_core::Decoration::Default,
false,
false,
false,
));
if ctx.runtime.network_sum {
ret.push(self.model.curse_add_line(format!("{ax:>14}"), glances_core::Decoration::Default, false, false, false));
} else {
ret.push(self.model.curse_add_line(
format!("{rx:>7}"),
self.model.view_decoration(Some(&item_key), "bytes_recv"),
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{tx:>7}"),
self.model.view_decoration(Some(&item_key), "bytes_sent"),
false,
false,
false,
));
}
}
ret
}
}
+14 -6
View File
@@ -43,22 +43,21 @@ impl Plugin for NowPlugin {
}
fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let now = chrono::Local::now();
let iso = now.format("%Y-%m-%dT%H:%M:%S%:z").to_string();
let iso = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z").to_string();
let strftime = if !ctx.args.strftime_format.is_empty() {
ctx.args.strftime_format.clone()
} else {
ctx.config.get_value("global", "strftime_format", Some("")).unwrap_or_default()
};
let custom = if !strftime.is_empty() {
now.format(&strftime).to_string()
crate::collectors::strftime(&strftime)
} else {
// Choose the tz format depending on the timezone name length.
let tz_name = now.format("%Z").to_string();
let tz_name = crate::collectors::strftime("%Z");
if tz_name.len() > 6 {
now.format("%Y-%m-%d %H:%M:%S %z").to_string()
crate::collectors::strftime("%Y-%m-%d %H:%M:%S %z")
} else {
now.format("%Y-%m-%d %H:%M:%S %Z").to_string()
crate::collectors::strftime("%Y-%m-%d %H:%M:%S %Z")
}
};
Ok(json!({"iso": iso, "custom": custom}))
@@ -67,4 +66,13 @@ impl Plugin for NowPlugin {
fn display_curse(&self) -> bool {
true
}
fn align(&self) -> glances_core::Align {
glances_core::Align::Left
}
fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let custom = self.model.stats.get("custom").and_then(Value::as_str).unwrap_or("");
vec![self.model.curse_add_line(format!("{custom:<23}"), glances_core::Decoration::Default, false, false, false)]
}
}
+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
}
}
@@ -69,4 +69,58 @@ impl Plugin for ProcessCountPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
let s = &self.model.stats;
if !s.is_object() {
return ret;
}
if ctx.args.disable_process {
ret.push(self.model.curse_add_line("PROCESSES DISABLED (press 'z' to display)", glances_core::Decoration::Default, false, false, false));
return ret;
}
// Filter line.
{
let procs = glances_core::processes::processes().lock().unwrap();
if let Some(filter) = procs.filter.filter() {
ret.push(self.model.curse_add_line("Processes filter:", glances_core::Decoration::Title, false, false, false));
let mut msg = format!(" {filter} ");
if let Some(key) = procs.filter.filter_key() {
msg += &format!("on column {key} ");
}
ret.push(self.model.curse_add_line(msg, glances_core::Decoration::Filter, false, false, false));
ret.push(self.model.curse_add_line("('ENTER' to edit, 'E' to reset)", glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_new_line());
}
}
ret.push(self.model.curse_add_line("TASKS", glances_core::Decoration::Title, false, false, false));
let total = s.get("total").and_then(Value::as_i64).unwrap_or(0);
ret.push(self.model.curse_add_line(format!("{total:>4}"), glances_core::Decoration::Default, false, false, false));
if let Some(thread) = s.get("thread").and_then(Value::as_i64) {
ret.push(self.model.curse_add_line(format!(" ({thread} thr),"), glances_core::Decoration::Default, false, false, false));
}
let mut other = total;
if let Some(running) = s.get("running").and_then(Value::as_i64) {
other -= running;
ret.push(self.model.curse_add_line(format!(" {running} run,"), glances_core::Decoration::Default, false, false, false));
}
if let Some(sleeping) = s.get("sleeping").and_then(Value::as_i64) {
other -= sleeping;
ret.push(self.model.curse_add_line(format!(" {sleeping} slp,"), glances_core::Decoration::Default, false, false, false));
}
ret.push(self.model.curse_add_line(format!(" {other} oth "), glances_core::Decoration::Default, false, false, false));
let procs = glances_core::processes::processes().lock().unwrap();
let mut msg = if ctx.args.programs { "Programs" } else { "Threads" }.to_string();
let sort_human = glances_core::processes::sort_for_human(&procs.sort_key);
if procs.auto_sort {
msg += " sorted automatically";
ret.push(self.model.curse_add_line(msg, glances_core::Decoration::Default, false, false, false));
msg = format!(" by {sort_human}");
} else {
msg += &format!(" sorted by {sort_human}");
}
ret.push(self.model.curse_add_line(msg, glances_core::Decoration::Default, false, false, false));
ret
}
}
@@ -8,6 +8,8 @@ use serde_json::Value;
pub struct ProcessListPlugin {
pub model: PluginModel,
pub nb_log_core: usize,
pub disable_virtual_memory: bool,
}
impl ProcessListPlugin {
@@ -65,7 +67,9 @@ impl ProcessListPlugin {
if let Some(sort_key) = args.sort_processes_key {
processes().lock().unwrap().set_sort_key(sort_key.as_str(), false);
}
Self { model }
let nb_log_core = crate::collectors::logical_core_count();
let disable_virtual_memory = config.get_bool_value("processlist", "disable_virtual_memory", false);
Self { model, nb_log_core, disable_virtual_memory }
}
}
@@ -109,6 +113,284 @@ impl Plugin for ProcessListPlugin {
fn align(&self) -> glances_core::Align {
glances_core::Align::Bottom
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
use glances_core::Decoration;
let mut ret = Vec::new();
let items: Vec<Value> = self.model.stats.as_array().cloned().unwrap_or_default();
let Some(_) = items.first() else {
return ret;
};
if ctx.args.disable_process {
return ret;
}
// Values used for the alerts and the layout.
let (max_cpu, max_mem, sort_key) = {
let procs = glances_core::processes::processes().lock().unwrap();
(
procs.max_values.get("cpu_percent").copied().unwrap_or(0.0),
procs.max_values.get("memory_percent").copied().unwrap_or(0.0),
procs.sort_key.clone(),
)
};
let cursor = ctx.runtime.cursor_position;
let disable_cursor = ctx.args.disable_cursor;
let short_name = ctx.runtime.process_short_name;
let wide = ctx.max_width >= 105;
// Header.
if ctx.runtime.disable_irix {
ret.push(self.model.curse_add_line(format!("{:<6} ", "CPUi"), Decoration::Sort, false, false, false));
} else {
ret.push(self.model.curse_add_line(format!("{:<6} ", "CPU%"), Decoration::Sort, false, false, false));
}
ret.push(self.model.curse_add_line(format!("{:<5} ", "MEM%"), Decoration::Default, false, false, false));
if wide {
if !self.disable_virtual_memory {
ret.push(self.model.curse_add_line(format!("{:<5} ", "VIRT"), Decoration::Default, true, false, false));
}
ret.push(self.model.curse_add_line(format!("{:<5} ", "RES"), Decoration::Default, true, false, false));
}
ret.push(self.model.curse_add_line(format!("{:>5} ", "PID"), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:<10} ", "USER"), Decoration::Default, false, false, false));
if wide {
ret.push(self.model.curse_add_line(format!("{:>8} ", "TIME+"), Decoration::Default, true, false, false));
}
ret.push(self.model.curse_add_line(format!("{:<3} ", "THR"), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>3} ", "NI"), Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(format!("{:>1} ", "S"), Decoration::Default, false, false, false));
if wide {
ret.push(self.model.curse_add_line(format!("{:>4} ", "R/s"), Decoration::Default, true, true, false));
ret.push(self.model.curse_add_line(format!("{:<4} ", "W/s"), Decoration::Default, true, true, false));
}
if wide {
ret.push(self.model.curse_add_line(format!("{:>3} ", "CPU"), Decoration::Default, true, false, false));
}
ret.push(self.model.curse_add_line("Command", Decoration::Sort, false, false, false));
let _ = sort_key;
let max_pid_size = {
let procs = glances_core::processes::processes().lock().unwrap();
procs.pid_max().map(|m| m.to_string().len()).unwrap_or(6)
};
for (position, p) in items.iter().enumerate() {
let Some(p) = p.as_object() else {
continue;
};
let selected = position as i64 == cursor;
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
if selected && !disable_cursor { ">" } else { " " },
Decoration::Selected,
false,
false,
false,
));
// CPU%.
let cpu_percent = p.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0);
let cpu_value = if ctx.runtime.disable_irix && self.nb_log_core != 0 {
cpu_percent / self.nb_log_core as f64
} else {
cpu_percent
};
let cpu_decoration = self.model.get_alert(
cpu_percent,
0.0,
100.0,
true,
cpu_percent == max_cpu,
Some("cpu"),
None,
false,
);
ret.push(self.model.curse_add_line(format!("{cpu_value:<6.1}"), cpu_decoration, false, false, false));
// MEM%.
let memory_percent = p.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0);
let mem_decoration = self.model.get_alert(
memory_percent,
0.0,
100.0,
true,
memory_percent == max_mem,
Some("mem"),
None,
false,
);
ret.push(self.model.curse_add_line(format!("{memory_percent:<5.1} "), mem_decoration, false, false, false));
// VIRT and RES.
let memory_info = p.get("memory_info");
if wide {
if !self.disable_virtual_memory {
let vms = memory_info.and_then(|m| m.get("vms")).and_then(Value::as_f64).unwrap_or(0.0);
ret.push(self.model.curse_add_line(
format!("{:<5} ", self.model.auto_unit(vms, false, "K")),
Decoration::Default,
true,
false,
false,
));
}
let rss = memory_info.and_then(|m| m.get("rss")).and_then(Value::as_f64).unwrap_or(0.0);
ret.push(self.model.curse_add_line(
format!("{:<5} ", self.model.auto_unit(rss, false, "K")),
Decoration::Default,
true,
false,
false,
));
}
// PID.
let pid = p.get("pid").and_then(Value::as_i64).unwrap_or(0);
ret.push(self.model.curse_add_line(format!("{pid:>max_pid_size$} "), Decoration::Default, false, false, false));
// USER.
let username = p.get("username").and_then(Value::as_str).unwrap_or("?");
let username: String = username.chars().take(9).collect();
ret.push(self.model.curse_add_line(format!("{username:<10} "), Decoration::Default, false, false, false));
// TIME+.
if wide {
let times = p.get("cpu_times");
let user = times.and_then(|t| t.get("user")).and_then(Value::as_f64).unwrap_or(0.0);
let system = times.and_then(|t| t.get("system")).and_then(Value::as_f64).unwrap_or(0.0);
let total = (user + system) as i64;
let hours = total / 3600;
let minutes = (total % 3600) / 60;
let seconds = total % 60;
let (msg, decoration) = if hours > 99 {
(format!("{hours:<7}h"), Decoration::Default)
} else if hours > 0 {
(format!("{hours}h{minutes}:{seconds:02}"), Decoration::CpuTime)
} else {
(format!("{minutes}:{seconds:02}"), Decoration::Default)
};
ret.push(self.model.curse_add_line(format!("{msg:>8} "), decoration, true, false, false));
}
// THR.
if let Some(threads) = p.get("num_threads").and_then(Value::as_i64) {
ret.push(self.model.curse_add_line(format!("{threads:<3} "), Decoration::Default, false, false, false));
} else {
ret.push(self.model.curse_add_line(format!("{:<3} ", "?"), Decoration::Default, false, false, false));
}
// NI.
let nice_value = p.get("nice").and_then(Value::as_i64).unwrap_or(0);
let nice_decoration = self.nice_alert(nice_value);
ret.push(self.model.curse_add_line(format!("{nice_value:>3} "), nice_decoration, false, false, false));
// Status.
let status = p.get("status").and_then(Value::as_str).unwrap_or("?");
let status_decoration = self.status_alert(status);
ret.push(self.model.curse_add_line(format!("{status:>1} "), status_decoration, false, false, false));
// IO rates.
if wide {
let rate = |slot: usize| -> Option<String> {
let counters = p.get("io_counters")?.as_array()?;
let io_tag = counters.get(4)?.as_i64().unwrap_or(0);
let time_since_update = p.get("time_since_update")?.as_f64()?;
if io_tag != 1 || time_since_update == 0.0 {
return None;
}
let current = counters.get(slot)?.as_f64()?;
let old = counters.get(if slot == 0 { 2 } else { 3 })?.as_f64()?;
let io = (current - old) / time_since_update;
Some(if io == 0.0 { "0".to_string() } else { self.model.auto_unit(io, true, "K") })
};
for (slot, width) in [(0usize, 4usize), (1, 4)] {
match rate(slot) {
Some(value) => ret.push(self.model.curse_add_line(format!("{value:>width$} "), Decoration::Default, true, true, false)),
None => ret.push(self.model.curse_add_line(format!("{:>width$} ", "?"), Decoration::Default, true, true, false)),
}
}
}
// CPU core.
if wide {
let cpu_num = p.get("cpu_num").and_then(Value::as_i64);
let msg = match cpu_num {
Some(value) if value >= 0 => format!("{value:>3} "),
_ => format!("{:>3} ", "-"),
};
ret.push(self.model.curse_add_line(msg, Decoration::Default, true, false, false));
}
// Command.
let name = p.get("name").and_then(Value::as_str).unwrap_or("?");
let cmdline: Vec<String> = p
.get("cmdline")
.and_then(Value::as_array)
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
.unwrap_or_default();
let process_decoration = if selected && !disable_cursor { Decoration::ProcessSelected } else { Decoration::Process };
if !cmdline.is_empty() {
let first = &cmdline[0];
let (path, cmd) = if first.starts_with(name) {
(String::new(), first.clone())
} else {
match first.rsplit_once('/') {
Some((path, cmd)) => (path.to_string(), cmd.to_string()),
None => (String::new(), first.clone()),
}
};
let arguments = cmdline[1..].join(" ");
if !path.is_empty() && std::path::Path::new(&path).is_dir() && !short_name {
ret.push(self.model.curse_add_line(format!("{path}/"), Decoration::Default, false, false, true));
ret.push(self.model.curse_add_line(cmd, process_decoration, false, false, true));
} else {
ret.push(self.model.curse_add_line(cmd, process_decoration, false, false, true));
}
if !arguments.is_empty() {
let msg = format!(" {}", glances_core::units::replace_special_chars(&arguments, ' '));
ret.push(self.model.curse_add_line(msg, Decoration::Default, false, false, true));
}
} else {
ret.push(self.model.curse_add_line(name.to_string(), process_decoration, false, false, true));
}
}
ret
}
}
impl ProcessListPlugin {
fn nice_alert(&self, value: i64) -> glances_core::Decoration {
let value = value.to_string();
for (criticality, decoration) in [
("nice_critical", glances_core::Decoration::Critical),
("nice_warning", glances_core::Decoration::Warning),
("nice_careful", glances_core::Decoration::Careful),
("nice_ok", glances_core::Decoration::Ok),
] {
if let Some(items) = self.model.get_limit_list(criticality, "processlist") {
if items.iter().any(|item| item == &value) {
return decoration;
}
}
}
glances_core::Decoration::Default
}
fn status_alert(&self, value: &str) -> glances_core::Decoration {
for (criticality, decoration) in [
("status_critical", glances_core::Decoration::Critical),
("status_warning", glances_core::Decoration::Warning),
("status_careful", glances_core::Decoration::Careful),
("status_ok", glances_core::Decoration::Ok),
] {
if let Some(items) = self.model.get_limit_list(criticality, "processlist") {
if items.iter().any(|item| item == value) {
return decoration;
}
}
}
if value == "R" { glances_core::Decoration::Ok } else { glances_core::Decoration::Default }
}
}
#[allow(dead_code)]
@@ -163,6 +163,110 @@ impl Plugin for QuicklookPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
let s = &self.model.stats;
if !s.is_object() || ctx.max_width == 0 {
return ret;
}
// System information: CPU name + frequency.
let freq = match (s.get("cpu_hz_current").and_then(Value::as_f64), s.get("cpu_hz").and_then(Value::as_f64)) {
(Some(current), Some(max)) => format!(" {:.2}/{:.2}GHz", current / 1e9, max / 1e9),
(Some(current), None) => format!(" {:.2}GHz", current / 1e9),
_ => String::new(),
};
let cpu_name = s.get("cpu_name").and_then(Value::as_str).unwrap_or("");
let name = if (ctx.max_width as i64 - freq.len() as i64 + 7) > 0 {
format!("{:<width$}", cpu_name, width = ctx.max_width - freq.len() + 7)
} else if freq.is_empty() {
String::new()
} else {
"Frequency".to_string()
};
ret.push(self.model.curse_add_line(name.clone(), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_add_line(freq.clone(), glances_core::Decoration::Default, false, false, false));
ret.push(self.model.curse_new_line());
let bar_size = std::cmp::max(
name.len().saturating_add(freq.len()).saturating_sub(7),
(ctx.max_width as usize).saturating_sub(9),
)
.max(10);
let bar_char = self
.model
.get_conf_value("bar_char", "")
.first()
.cloned()
.unwrap_or_else(|| "|".to_string());
let mut lines: Vec<Vec<glances_core::StatLine>> = Vec::new();
let mut push_line = |msg: String, bar_key: &str, percent: f64, decoration: glances_core::Decoration| {
let bar = glances_core::units::bar_percent(percent, bar_size, &bar_char, true);
lines.push(vec![
self.model.curse_add_line(msg, glances_core::Decoration::Default, false, false, false),
self.model.curse_add_line("[", glances_core::Decoration::Bold, false, false, false),
self.model.curse_add_line(bar, decoration, false, false, false),
self.model.curse_add_line("]", glances_core::Decoration::Bold, false, false, false),
self.model.curse_add_line(" ", glances_core::Decoration::Default, false, false, false),
]);
let _ = bar_key;
};
let stats_list = self.stats_list.clone();
for key in &stats_list {
if key == "cpu" && ctx.runtime.percpu {
if let Some(percpu) = s.get("percpu").and_then(Value::as_array) {
let mut sorted = percpu.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)
});
}
for cpu in sorted.iter().take(self.max_cpu_display) {
let cpu_id = cpu.get("cpu_number").and_then(Value::as_i64).unwrap_or(0);
let total = cpu.get("total").and_then(Value::as_f64).unwrap_or(0.0);
let msg = if cpu_id < 10 { format!("{:<3}{cpu_id} ", key.to_uppercase()) } else { format!("{cpu_id:4} ") };
let decoration = self.decoration_for_percpu(cpu_id);
push_line(msg, key, total, decoration);
}
if percpu.len() > self.max_cpu_display {
let hidden: Vec<f64> = sorted
.iter()
.skip(self.max_cpu_display)
.filter_map(|cpu| cpu.get("total").and_then(Value::as_f64))
.collect();
let mean = if hidden.is_empty() { 0.0 } else { hidden.iter().sum::<f64>() / hidden.len() as f64 };
let decoration = self.decoration_for_percpu(-1);
push_line(format!("{:<3}* ", key.to_uppercase()), key, mean, decoration);
}
}
} else if let Some(value) = s.get(key).and_then(Value::as_f64) {
let decoration = self.model.view_decoration(None, key);
push_line(format!("{:<4} ", key.to_uppercase()), key, value, decoration);
}
}
for line in lines {
ret.extend(line);
ret.push(self.model.curse_new_line());
}
ret.pop();
ret
}
}
impl QuicklookPlugin {
fn decoration_for_percpu(&self, cpu_id: i64) -> glances_core::Decoration {
let key = if cpu_id < 0 { "other".to_string() } else { cpu_id.to_string() };
match self.model.get_view(None, "percpu_decoration", &key) {
Value::String(name) => glances_core::Decoration::from_name(&name),
_ => glances_core::Decoration::Default,
}
}
}
/// Return the style for a CPU percentage without the get_alert side effects.
@@ -210,6 +210,55 @@ impl Plugin for SensorsPlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
let Some(items) = self.model.stats.as_array() else {
return ret;
};
if items.is_empty() || ctx.max_width == 0 {
return ret;
}
let name_max_width = ctx.max_width.saturating_sub(12);
ret.push(self.model.curse_add_line(
format!("{:<width$}", "SENSORS", width = name_max_width),
glances_core::Decoration::Title,
false,
false,
false,
));
for i in items {
let item_key = i.get("label").and_then(Value::as_str).unwrap_or("").to_string();
let label = i.get("label").and_then(Value::as_str).unwrap_or("");
let sensor_type = i.get("type").and_then(Value::as_str).unwrap_or("");
let unit = i.get("unit").and_then(Value::as_str).unwrap_or("");
let value = i.get("value").and_then(Value::as_f64);
ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line(
format!("{:<width$}", label.chars().take(name_max_width).collect::<String>(), width = name_max_width),
glances_core::Decoration::Default,
false,
false,
false,
));
let Some(value) = value else {
continue;
};
let (value, unit) = if ctx.args.fahrenheit && sensor_type != "battery" && sensor_type != "fan_speed" {
(glances_core::to_fahrenheit(value), "F")
} else {
(value, unit)
};
ret.push(self.model.curse_add_line(
format!("{:>14}", format!("{value:.0}{unit}")),
self.model.view_decoration(Some(&item_key), "value"),
false,
false,
false,
));
}
ret
}
}
fn system_thresholds(value: f64, warning: Option<f64>, critical: Option<f64>) -> Decoration {
@@ -77,6 +77,19 @@ impl Plugin for SystemPlugin {
fn align(&self) -> glances_core::Align {
glances_core::Align::Right
}
fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
let s = &self.model.stats;
if !s.is_object() {
return ret;
}
let hostname = s.get("hostname").and_then(Value::as_str).unwrap_or("");
ret.push(self.model.curse_add_line(hostname, glances_core::Decoration::Title, false, false, false));
let hr_name = s.get("hr_name").and_then(Value::as_str).unwrap_or("");
ret.push(self.model.curse_add_line(format!(" {hr_name} "), glances_core::Decoration::Default, true, false, false));
ret
}
}
/// Logical core count helper shared by the load plugin.
@@ -67,4 +67,17 @@ impl Plugin for UptimePlugin {
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
if self.model.stats.is_null() {
return Vec::new();
}
vec![self.model.curse_add_line(
format!("Uptime: {}", glances_core::py_str(&self.model.stats)),
glances_core::Decoration::Default,
false,
false,
false,
)]
}
}