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:
Generated
+2
@@ -669,6 +669,8 @@ dependencies = [
|
||||
name = "glances-tui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"glances-core",
|
||||
"glances-plugins",
|
||||
|
||||
@@ -92,7 +92,7 @@ fn run(mut args: Args) -> Result<()> {
|
||||
return run_quiet(&mut engine, &args);
|
||||
}
|
||||
|
||||
tui_placeholder::run(&mut engine, &args)
|
||||
glances_tui::run(&mut engine)
|
||||
}
|
||||
|
||||
fn display_modules_list(engine: &Engine) {
|
||||
@@ -139,8 +139,11 @@ fn run_stdout(engine: &mut Engine, args: &Args) -> Result<()> {
|
||||
let csv_plugins = args.stdout_csv.as_deref().map(|value| {
|
||||
value
|
||||
.split(',')
|
||||
.map(|p| p.split('.').next().unwrap_or(p).trim().to_string())
|
||||
.filter(|p| !p.is_empty())
|
||||
.map(|p| match p.split_once('.') {
|
||||
Some((plugin, attribute)) => (plugin.trim().to_string(), Some(attribute.to_string())),
|
||||
None => (p.trim().to_string(), None),
|
||||
})
|
||||
.filter(|(p, _)| !p.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Terminal output modes: `--stdout`, `--stdout-csv`, `--stdout-json`.
|
||||
|
||||
use glances_plugins::engine::Engine;
|
||||
use serde_json::{Map, Value, json};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
/// One `<plugin>[.<key>].<attribute>` item of the stdout modes.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -119,9 +119,9 @@ impl CsvRenderer {
|
||||
Self { header: true, ..Default::default() }
|
||||
}
|
||||
|
||||
pub fn render(&mut self, engine: &Engine, plugins: &[String]) -> String {
|
||||
pub fn render(&mut self, engine: &Engine, items: &[(String, Option<String>)]) -> String {
|
||||
let mut line = String::new();
|
||||
for name in plugins {
|
||||
for (name, attribute) in items {
|
||||
let Some(plugin) = engine.get_plugin(name) else {
|
||||
continue;
|
||||
};
|
||||
@@ -130,9 +130,9 @@ impl CsvRenderer {
|
||||
}
|
||||
let stat = plugin.export();
|
||||
if self.header {
|
||||
line.push_str(&self.build_header(name, &stat));
|
||||
line.push_str(&self.build_header(name, attribute.as_deref(), &stat));
|
||||
} else {
|
||||
line.push_str(&self.build_data(name, &stat));
|
||||
line.push_str(&self.build_data(name, attribute.as_deref(), &stat));
|
||||
}
|
||||
}
|
||||
self.header = false;
|
||||
@@ -140,8 +140,12 @@ impl CsvRenderer {
|
||||
line
|
||||
}
|
||||
|
||||
fn build_header(&mut self, plugin: &str, stat: &Value) -> String {
|
||||
fn build_header(&mut self, plugin: &str, attribute: Option<&str>, stat: &Value) -> String {
|
||||
let mut line = String::new();
|
||||
if let Some(attribute) = attribute {
|
||||
line.push_str(&format!("{plugin}.{attribute},"));
|
||||
return line;
|
||||
}
|
||||
match stat {
|
||||
Value::Object(map) => {
|
||||
for key in map.keys() {
|
||||
@@ -178,8 +182,16 @@ impl CsvRenderer {
|
||||
line
|
||||
}
|
||||
|
||||
fn build_data(&self, plugin: &str, stat: &Value) -> String {
|
||||
fn build_data(&self, plugin: &str, attribute: Option<&str>, stat: &Value) -> String {
|
||||
let mut line = String::new();
|
||||
if let Some(attribute) = attribute {
|
||||
let text = match stat {
|
||||
Value::Object(map) => map.get(attribute).map(glances_core::py_str).unwrap_or_else(|| "N/A".to_string()),
|
||||
_ => "N/A".to_string(),
|
||||
};
|
||||
line.push_str(&format!("{text},"));
|
||||
return line;
|
||||
}
|
||||
match stat {
|
||||
Value::Object(map) => {
|
||||
for value in map.values() {
|
||||
|
||||
@@ -5,10 +5,6 @@ use anyhow::Result;
|
||||
use glances_core::Args;
|
||||
use glances_plugins::engine::Engine;
|
||||
|
||||
pub fn run(_engine: &mut Engine, _args: &Args) -> Result<()> {
|
||||
anyhow::bail!("terminal UI is not implemented yet (use --stdout, --stdout-csv or --stdout-json)")
|
||||
}
|
||||
|
||||
pub fn run_fetch(_engine: &mut Engine, _args: &Args) -> Result<()> {
|
||||
anyhow::bail!("fetch mode is not implemented yet")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::history::History;
|
||||
use crate::state::Runtime;
|
||||
use crate::thresholds::thresholds;
|
||||
use crate::timer::Timer;
|
||||
use crate::units::as_f64;
|
||||
use crate::units::{as_f64, auto_unit};
|
||||
use indexmap::IndexMap;
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::collections::VecDeque;
|
||||
@@ -399,6 +399,18 @@ impl PluginModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the list-valued limit (used for nice/status alerts).
|
||||
pub fn get_limit_list(&self, criticality: &str, stat_name: &str) -> Option<Vec<String>> {
|
||||
let stat_name = stat_name.to_lowercase();
|
||||
if let Some(Limit::List(items)) = self.limits.get(&format!("{stat_name}_{criticality}")) {
|
||||
return Some(items.clone());
|
||||
}
|
||||
if let Some(Limit::List(items)) = self.limits.get(&format!("{}_{criticality}", self.name)) {
|
||||
return Some(items.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Return the limit value for the given criticality.
|
||||
pub fn get_limit(&self, criticality: &str, stat_name: &str) -> Option<f64> {
|
||||
let stat_name = stat_name.to_lowercase();
|
||||
@@ -958,6 +970,113 @@ impl PluginModel {
|
||||
self.stats.clone()
|
||||
}
|
||||
|
||||
/// Create a TUI line (port of `curse_add_line`).
|
||||
pub fn curse_add_line(
|
||||
&self,
|
||||
msg: impl Into<String>,
|
||||
decoration: Decoration,
|
||||
optional: bool,
|
||||
additional: bool,
|
||||
splittable: bool,
|
||||
) -> StatLine {
|
||||
StatLine {
|
||||
msg: msg.into(),
|
||||
decoration,
|
||||
optional,
|
||||
additional,
|
||||
splittable,
|
||||
}
|
||||
}
|
||||
|
||||
/// New line marker (port of `curse_new_line`).
|
||||
pub fn curse_new_line(&self) -> StatLine {
|
||||
StatLine::new("\n")
|
||||
}
|
||||
|
||||
/// Add a `key: value` pair to the TUI (port of `curse_add_stat`).
|
||||
pub fn curse_add_stat(&self, key: &str, width: Option<usize>, header: &str, separator: &str, trailer: &str) -> Vec<StatLine> {
|
||||
let Some(value) = self.stats.get(key) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let definition = self.fields.get(key);
|
||||
|
||||
let key_name = definition
|
||||
.and_then(|f| f.short_name.clone())
|
||||
.unwrap_or_else(|| key.to_string());
|
||||
let unit_short = definition.map(|f| f.short_unit()).unwrap_or("");
|
||||
let unit_type = definition.map(|f| f.unit_type()).unwrap_or("float");
|
||||
let value = if definition.map(|f| f.rate).unwrap_or(false) {
|
||||
self.stats.get(&format!("{key}_rate_per_sec")).unwrap_or(&Value::Null)
|
||||
} else {
|
||||
value
|
||||
};
|
||||
let value = as_f64(value);
|
||||
|
||||
let (msg_item, msg_value) = match width {
|
||||
None => {
|
||||
let item = format!("{header}{key_name}{separator}");
|
||||
let value_text = match value {
|
||||
None => "-".to_string(),
|
||||
Some(v) => {
|
||||
if unit_type == "float" {
|
||||
format!("{v:.1}{unit_short}")
|
||||
} else if let Some(min_symbol) = definition.and_then(|f| f.min_symbol.clone()) {
|
||||
format!("{}{unit_short}", auto_unit(v, false, &min_symbol, "-"))
|
||||
} else {
|
||||
format!("{}{unit_short}", v as i64)
|
||||
}
|
||||
}
|
||||
};
|
||||
(item, value_text)
|
||||
}
|
||||
Some(width) => {
|
||||
let item = format!("{header}{key_name:<width$}{separator}", width = width.saturating_sub(7));
|
||||
let value_text = match value {
|
||||
None => format!("{:>5}", "-"),
|
||||
Some(v) => {
|
||||
if unit_type == "float" {
|
||||
format!("{v:>5.1}{unit_short}")
|
||||
} else if let Some(min_symbol) = definition.and_then(|f| f.min_symbol.clone()) {
|
||||
format!("{:>5}{unit_short}", auto_unit(v, false, &min_symbol, "-"))
|
||||
} else {
|
||||
format!("{:>5}{unit_short}", v as i64)
|
||||
}
|
||||
}
|
||||
};
|
||||
(item, value_text)
|
||||
}
|
||||
};
|
||||
let item = format!("{msg_item}{trailer}");
|
||||
|
||||
let decoration = if value.is_some() { self.view_decoration(None, key) } else { Decoration::Default };
|
||||
let optional = self.view_bool(None, key, "optional");
|
||||
|
||||
vec![
|
||||
self.curse_add_line(item, Decoration::Default, optional, false, false),
|
||||
self.curse_add_line(msg_value, decoration, optional, false, false),
|
||||
]
|
||||
}
|
||||
|
||||
/// Return the fields to display for a plugin (port of the display flags).
|
||||
pub fn field_optional(&self, field: &str) -> bool {
|
||||
self.fields.get(field).map(|f| f.optional).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Return the trend message (port of `trend_msg`).
|
||||
pub fn trend_msg(&self, trend: Option<f64>, significant: f64) -> String {
|
||||
match trend {
|
||||
None => " ".to_string(),
|
||||
Some(trend) if trend > significant => "↑".to_string(),
|
||||
Some(trend) if trend < significant => "↓".to_string(),
|
||||
_ => "-".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Human readable auto unit (port of the plugin `auto_unit` method).
|
||||
pub fn auto_unit(&self, number: f64, low_precision: bool, min_symbol: &str) -> String {
|
||||
auto_unit(number, low_precision, min_symbol, "-")
|
||||
}
|
||||
|
||||
/// Update the history with the current stats.
|
||||
pub fn update_stats_history(&mut self) {
|
||||
if !self.history_enabled {
|
||||
@@ -1137,7 +1256,7 @@ pub trait Plugin: Send + Sync {
|
||||
fn exit(&mut self) {}
|
||||
|
||||
/// Render the plugin block in the TUI.
|
||||
fn render(&self, ctx: &RenderContext) -> Vec<StatLine> {
|
||||
fn render(&mut self, ctx: &RenderContext) -> Vec<StatLine> {
|
||||
let _ = ctx;
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
/// Runtime state shared between the engine, the plugins and the TUI.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Runtime {
|
||||
/// Effective set of enabled plugins (kept up to date by the engine/TUI).
|
||||
pub enabled_plugins: std::collections::HashSet<String>,
|
||||
/// Irix mode (task's CPU usage divided by the number of CPUs).
|
||||
pub disable_irix: bool,
|
||||
/// Display Rx and Tx sum instead of per interface values.
|
||||
pub network_sum: bool,
|
||||
/// Display cumulative network values instead of rates.
|
||||
@@ -38,6 +42,8 @@ pub struct Runtime {
|
||||
impl Runtime {
|
||||
pub fn new(args: &crate::args::Args, fs_free_space_config: bool) -> Self {
|
||||
Self {
|
||||
enabled_plugins: std::collections::HashSet::new(),
|
||||
disable_irix: args.disable_irix,
|
||||
network_sum: false,
|
||||
network_cumul: false,
|
||||
percpu: args.percpu,
|
||||
@@ -55,4 +61,9 @@ impl Runtime {
|
||||
reset_minmax_tag: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return True if the given plugin is enabled.
|
||||
pub fn is_plugin_enabled(&self, name: &str) -> bool {
|
||||
self.enabled_plugins.contains(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,35 @@ pub fn auto_unit(number: f64, low_precision: bool, min_symbol: &str, none_symbol
|
||||
format!("{number:.decimal_precision$}")
|
||||
}
|
||||
|
||||
/// Render a progress bar the way the Python `Bar` class does.
|
||||
pub fn bar_percent(percent: f64, size: usize, bar_char: &str, display_value: bool) -> String {
|
||||
let size = if display_value { size.saturating_sub(6) } else { size };
|
||||
let value = percent.clamp(0.0, 100.0);
|
||||
let scaled = size as f64 * value / 100.0;
|
||||
let whole = scaled.floor() as usize;
|
||||
let frac = scaled - whole as f64;
|
||||
let mut bar = bar_char.repeat(whole);
|
||||
let mut whole = whole;
|
||||
if frac > 0.0 {
|
||||
// curses_bars = [empty; 5] + [bar; 5]: index 5..7 draws a partial block.
|
||||
if (frac * 8.0) as usize >= 5 {
|
||||
bar.push_str(bar_char);
|
||||
} else {
|
||||
bar.push(' ');
|
||||
}
|
||||
whole += 1;
|
||||
}
|
||||
bar.push_str(&" ".repeat(size.saturating_sub(whole)));
|
||||
if display_value {
|
||||
if percent >= 100.0 {
|
||||
bar = format!("{bar} {}{:3.0}%", if percent > 100.0 { ">" } else { " " }, 100.0);
|
||||
} else {
|
||||
bar = format!("{bar}{percent:5.1}%");
|
||||
}
|
||||
}
|
||||
bar
|
||||
}
|
||||
|
||||
/// Parse a value like `'12.5 MB'` into a float (port of `string_value_to_float`).
|
||||
pub fn string_value_to_float(s: &str) -> Option<f64> {
|
||||
let cleaned: String = s.chars().filter(|c| *c != ' ').collect();
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
crossterm = "0.29.0"
|
||||
glances-core = { version = "0.1.0", path = "../glances-core" }
|
||||
glances-plugins = { version = "0.1.0", path = "../glances-plugins" }
|
||||
@@ -11,3 +12,6 @@ indexmap.workspace = true
|
||||
ratatui = "0.30.2"
|
||||
serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -1,14 +1,759 @@
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
//! Terminal user interface (port of the Glances curses interface).
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode};
|
||||
use glances_core::plugin::{Align, Decoration, StatLine};
|
||||
use glances_core::processes::processes;
|
||||
use glances_core::{Args, RenderContext};
|
||||
use glances_plugins::engine::Engine;
|
||||
use ratatui::Frame;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Wrap};
|
||||
use std::io::Stdout;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const LEFT_SIDEBAR: [&str; 8] = ["network", "diskio", "fs", "irq", "sensors", "now", "folders", "raid"];
|
||||
const TOP_PLUGINS: [&str; 6] = ["cpu", "percpu", "mem", "memswap", "load", "gpu"];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Popup {
|
||||
None,
|
||||
Help,
|
||||
Filter,
|
||||
Kill,
|
||||
}
|
||||
|
||||
pub struct Tui {
|
||||
popup: Popup,
|
||||
input: String,
|
||||
quit: bool,
|
||||
}
|
||||
|
||||
/// Run the terminal interface until the user quits.
|
||||
pub fn run(engine: &mut Engine) -> Result<()> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = std::io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = ratatui::Terminal::new(backend)?;
|
||||
|
||||
let mut tui = Tui { popup: Popup::None, input: String::new(), quit: false };
|
||||
let result = run_loop(&mut terminal, engine, &mut tui);
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
result
|
||||
}
|
||||
|
||||
fn run_loop(
|
||||
terminal: &mut ratatui::Terminal<CrosstermBackend<Stdout>>,
|
||||
engine: &mut Engine,
|
||||
tui: &mut Tui,
|
||||
) -> Result<()> {
|
||||
let refresh = engine.args.time.unwrap_or(2.0);
|
||||
let mut count: i64 = 0;
|
||||
while !tui.quit {
|
||||
let start = Instant::now();
|
||||
engine.update();
|
||||
tui.draw(terminal, engine)?;
|
||||
|
||||
count += 1;
|
||||
if let Some(max) = engine.args.stop_after {
|
||||
if count >= max {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
let timeout = Duration::from_secs_f64((refresh - elapsed).max(0.05));
|
||||
let deadline = Instant::now() + timeout;
|
||||
while Instant::now() < deadline {
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
if !event::poll(remaining)? {
|
||||
break;
|
||||
}
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => tui.handle_key(engine, key),
|
||||
Event::Resize(_, _) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
fn draw(&self, terminal: &mut ratatui::Terminal<CrosstermBackend<Stdout>>, engine: &mut Engine) -> Result<()> {
|
||||
terminal.draw(|frame| draw_frame(frame, engine, self))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_frame(frame: &mut Frame, engine: &mut Engine, tui: &Tui) {
|
||||
let args = engine.args.clone();
|
||||
let args = &args;
|
||||
let area = frame.area();
|
||||
if area.width < 40 || area.height < 10 {
|
||||
return;
|
||||
}
|
||||
if tui.popup == Popup::Help {
|
||||
tui.draw_help(frame, area);
|
||||
return;
|
||||
}
|
||||
|
||||
// Header.
|
||||
let header_height = 1u16;
|
||||
let top_height = tui.top_height(engine, args, area.width);
|
||||
let footer_height = if tui.popup == Popup::Filter || tui.popup == Popup::Kill { 3 } else { 0 };
|
||||
let main_height = area.height.saturating_sub(header_height + top_height + footer_height);
|
||||
|
||||
let header_area = Rect { x: 0, y: 0, width: area.width, height: header_height };
|
||||
let top_area = Rect { x: 0, y: header_height, width: area.width, height: top_height };
|
||||
let main_area = Rect { x: 0, y: header_height + top_height, width: area.width, height: main_height };
|
||||
let footer_area =
|
||||
Rect { x: 0, y: area.height.saturating_sub(footer_height), width: area.width, height: footer_height };
|
||||
|
||||
tui.draw_header(frame, header_area, engine, args);
|
||||
tui.draw_top(frame, top_area, engine, args);
|
||||
tui.draw_main(frame, main_area, engine, args);
|
||||
if footer_height > 0 {
|
||||
tui.draw_popup(frame, footer_area);
|
||||
}
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
|
||||
fn top_height(&self, engine: &mut Engine, args: &Args, width: u16) -> u16 {
|
||||
if args.disable_top {
|
||||
return 1;
|
||||
}
|
||||
let mut height = 1usize;
|
||||
for name in TOP_PLUGINS {
|
||||
if let Some(plugin) = engine.plugins.get_mut(name) {
|
||||
if !plugin.is_enabled() {
|
||||
continue;
|
||||
}
|
||||
let ctx = render_ctx(args, &engine.runtime, width as usize);
|
||||
let lines = split_lines(plugin.render(&ctx));
|
||||
height = height.max(lines.len());
|
||||
}
|
||||
}
|
||||
// Quicklook.
|
||||
if let Some(plugin) = engine.plugins.get_mut("quicklook") {
|
||||
if plugin.is_enabled() && !args.disable_quicklook {
|
||||
let ql_width = quicklook_width(width);
|
||||
let ctx = render_ctx(args, &engine.runtime, ql_width as usize);
|
||||
let lines = split_lines(plugin.render(&ctx));
|
||||
height = height.max(lines.len());
|
||||
}
|
||||
}
|
||||
(height.min(20) as u16).max(1)
|
||||
}
|
||||
|
||||
fn draw_header(
|
||||
&self,
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
engine: &mut Engine,
|
||||
args: &Args,
|
||||
) {
|
||||
let x = area.x;
|
||||
// System information.
|
||||
if let Some(plugin) = engine.plugins.get_mut("system") {
|
||||
if plugin.is_enabled() {
|
||||
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
|
||||
let lines = split_lines(plugin.render(&ctx));
|
||||
let text: String = lines.first().map(|l| l.iter().map(|s| s.msg.clone()).collect()).unwrap_or_default();
|
||||
let style = lines
|
||||
.first()
|
||||
.and_then(|l| l.first())
|
||||
.map(|s| style_for(s.decoration, args))
|
||||
.unwrap_or_default();
|
||||
let width = text.chars().count() as u16;
|
||||
frame.render_widget(Paragraph::new(text).style(style), Rect { x, y: area.y, width: width.min(area.width), height: 1 });
|
||||
}
|
||||
}
|
||||
// Uptime on the right.
|
||||
if let Some(plugin) = engine.plugins.get_mut("uptime") {
|
||||
if plugin.is_enabled() {
|
||||
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
|
||||
let lines = split_lines(plugin.render(&ctx));
|
||||
let text: String = lines.first().map(|l| l.iter().map(|s| s.msg.clone()).collect()).unwrap_or_default();
|
||||
let width = text.chars().count() as u16;
|
||||
if width + 2 < area.width {
|
||||
frame.render_widget(
|
||||
Paragraph::new(text),
|
||||
Rect { x: area.width.saturating_sub(width), y: area.y, width, height: 1 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_top(&self, frame: &mut Frame, area: Rect, engine: &mut Engine, args: &Args) {
|
||||
let mut x = area.x;
|
||||
// Collect the rendered blocks with their natural widths.
|
||||
struct Block {
|
||||
name: String,
|
||||
lines: Vec<Vec<StatLine>>,
|
||||
natural_width: usize,
|
||||
}
|
||||
let mut blocks: Vec<Block> = Vec::new();
|
||||
|
||||
if !args.disable_quicklook {
|
||||
if let Some(plugin) = engine.plugins.get_mut("quicklook") {
|
||||
if plugin.is_enabled() {
|
||||
let width = quicklook_width(area.width) as usize;
|
||||
let ctx = render_ctx(args, &engine.runtime, width);
|
||||
let lines = split_lines(plugin.render(&ctx));
|
||||
let natural = lines.iter().map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>()).max().unwrap_or(10);
|
||||
blocks.push(Block { name: "quicklook".to_string(), lines, natural_width: natural.max(10) });
|
||||
}
|
||||
}
|
||||
}
|
||||
for name in TOP_PLUGINS {
|
||||
if let Some(plugin) = engine.plugins.get_mut(name) {
|
||||
if !plugin.is_enabled() {
|
||||
continue;
|
||||
}
|
||||
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
|
||||
let lines = split_lines(plugin.render(&ctx));
|
||||
let natural = lines.iter().map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>()).max().unwrap_or(10);
|
||||
blocks.push(Block { name: name.to_string(), lines, natural_width: natural.max(10) });
|
||||
}
|
||||
}
|
||||
if blocks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate the available width proportionally to the natural widths.
|
||||
let total_natural: usize = blocks.iter().map(|b| b.natural_width).sum();
|
||||
let spacing = 2usize * blocks.len().saturating_sub(1);
|
||||
let available = (area.width as usize).saturating_sub(spacing).max(blocks.len() * 8);
|
||||
let mut widths: Vec<u16> = Vec::new();
|
||||
for block in &blocks {
|
||||
let mut width = (available * block.natural_width / total_natural).max(8);
|
||||
width = width.min(block.natural_width + 2);
|
||||
widths.push(width as u16);
|
||||
}
|
||||
// Adjust the last block if the total exceeds the screen.
|
||||
let total: usize = widths.iter().map(|w| *w as usize).sum::<usize>() + spacing;
|
||||
if total > area.width as usize {
|
||||
let excess = total - area.width as usize;
|
||||
if let Some(last) = widths.last_mut() {
|
||||
*last = last.saturating_sub(excess as u16).max(8);
|
||||
}
|
||||
}
|
||||
|
||||
for (block, width) in blocks.into_iter().zip(widths) {
|
||||
if x + width > area.x + area.width {
|
||||
break;
|
||||
}
|
||||
draw_lines(frame, Rect { x, y: area.y, width, height: area.height }, &block.lines, args);
|
||||
x += width + 2;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_main(&self, frame: &mut Frame, area: Rect, engine: &mut Engine, args: &Args) {
|
||||
if area.height == 0 {
|
||||
return;
|
||||
}
|
||||
let left_width = if args.disable_left_sidebar {
|
||||
0
|
||||
} else {
|
||||
(area.width as i32 - 105).clamp(23, 34) as u16
|
||||
};
|
||||
if left_width > 0 && area.width > left_width + 20 {
|
||||
let mut y = area.y;
|
||||
for name in LEFT_SIDEBAR {
|
||||
if y >= area.y + area.height {
|
||||
break;
|
||||
}
|
||||
let Some(plugin) = engine.plugins.get_mut(name) else {
|
||||
continue;
|
||||
};
|
||||
if !plugin.is_enabled() {
|
||||
continue;
|
||||
}
|
||||
let ctx = render_ctx(args, &engine.runtime, left_width as usize);
|
||||
let lines = split_lines(plugin.render(&ctx));
|
||||
if lines.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let height = (lines.len() as u16).min(area.y + area.height - y);
|
||||
y += 1; // separator/blank line
|
||||
if y >= area.y + area.height {
|
||||
break;
|
||||
}
|
||||
let height = height.min(area.y + area.height - y);
|
||||
draw_lines(frame, Rect { x: area.x, y, width: left_width, height }, &lines, args);
|
||||
y += height;
|
||||
}
|
||||
}
|
||||
|
||||
// Right side: process count + processes + alert footer.
|
||||
let right_x = area.x + left_width + if left_width > 0 { 1 } else { 0 };
|
||||
let right_width = area.width.saturating_sub(right_x - area.x);
|
||||
if right_width < 10 {
|
||||
return;
|
||||
}
|
||||
let mut y = area.y;
|
||||
|
||||
// Process count.
|
||||
if let Some(plugin) = engine.plugins.get_mut("processcount") {
|
||||
if plugin.is_enabled() {
|
||||
let ctx = render_ctx(args, &engine.runtime, right_width as usize);
|
||||
let lines = split_lines(plugin.render(&ctx));
|
||||
let height = (lines.len() as u16).min(2);
|
||||
draw_lines(frame, Rect { x: right_x, y, width: right_width, height }, &lines, args);
|
||||
y += height;
|
||||
}
|
||||
}
|
||||
|
||||
// Alerts footer (up to 4 lines).
|
||||
let event_lines = self.event_lines(engine, right_width as usize);
|
||||
let alert_height = event_lines.len().min(5) as u16;
|
||||
let list_height = area.y + area.height - y;
|
||||
let process_height = list_height.saturating_sub(alert_height);
|
||||
|
||||
let list_name = if args.programs { "programlist" } else { "processlist" };
|
||||
if let Some(plugin) = engine.plugins.get_mut(list_name) {
|
||||
if plugin.is_enabled() && process_height > 1 {
|
||||
let ctx = render_ctx(args, &engine.runtime, right_width as usize);
|
||||
let mut lines = split_lines(plugin.render(&ctx));
|
||||
lines.truncate(process_height as usize);
|
||||
draw_lines(frame, Rect { x: right_x, y, width: right_width, height: process_height }, &lines, args);
|
||||
}
|
||||
}
|
||||
y += process_height;
|
||||
if alert_height > 0 {
|
||||
draw_lines(
|
||||
frame,
|
||||
Rect { x: right_x, y, width: right_width, height: alert_height },
|
||||
&event_lines,
|
||||
args,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn event_lines(&self, engine: &Engine, _width: usize) -> Vec<Vec<StatLine>> {
|
||||
let events = glances_core::events::events().lock().unwrap();
|
||||
let mut lines = Vec::new();
|
||||
if events.is_empty() {
|
||||
return lines;
|
||||
}
|
||||
lines.push(vec![StatLine::new("EVENTS HISTORY").decoration(Decoration::Title)]);
|
||||
for event in events.events.iter().rev().take(4) {
|
||||
let state = match event.state.as_str() {
|
||||
"CRITICAL" => Decoration::CriticalLog,
|
||||
"WARNING" => Decoration::WarningLog,
|
||||
_ => Decoration::Default,
|
||||
};
|
||||
let end = if event.is_ongoing() { String::new() } else { " (ended)".to_string() };
|
||||
lines.push(vec![
|
||||
StatLine::new(format!("{} ", event.state)).decoration(state),
|
||||
StatLine::new(format!("{} (avg {:.1}){}", event.r#type, event.avg, end)),
|
||||
]);
|
||||
}
|
||||
let _ = engine;
|
||||
lines
|
||||
}
|
||||
|
||||
fn draw_popup(&self, frame: &mut Frame, area: Rect) {
|
||||
let (title, prompt) = match self.popup {
|
||||
Popup::Filter => ("Filter", format!("Process filter: {}_", self.input)),
|
||||
Popup::Kill => ("Kill process", "Confirm ([y]es/[n]o): ".to_string()),
|
||||
_ => return,
|
||||
};
|
||||
let block = Block::default().borders(Borders::ALL).title(title);
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(Paragraph::new(prompt).block(block), area);
|
||||
}
|
||||
|
||||
fn draw_help(&self, frame: &mut Frame, area: Rect) {
|
||||
let lines = vec![
|
||||
Line::from("Glances-rs help"),
|
||||
Line::from(""),
|
||||
Line::from("q / ESC Quit"),
|
||||
Line::from("h Toggle this help"),
|
||||
Line::from("0 Irix mode (disable-irix)"),
|
||||
Line::from("1 Per CPU mode"),
|
||||
Line::from("2 Disable left sidebar"),
|
||||
Line::from("3 Disable quicklook"),
|
||||
Line::from("4 Full quicklook"),
|
||||
Line::from("5 Disable top menu"),
|
||||
Line::from("6 Mean GPU mode"),
|
||||
Line::from("a c m i o p t u Sort processes (auto/cpu/mem/io/cpu_time/name/time/user)"),
|
||||
Line::from("</> Cycle sort key"),
|
||||
Line::from("UP/DOWN Select process"),
|
||||
Line::from("+ / - Increase/decrease nice level"),
|
||||
Line::from("k Kill selected process"),
|
||||
Line::from("f / E Edit/erase process filter"),
|
||||
Line::from("z Disable/enable processes"),
|
||||
Line::from("e Extended stats for the selected process"),
|
||||
Line::from("w / x Clean warning/critical events"),
|
||||
Line::from("b Byte mode (network)"),
|
||||
Line::from("T / U Network sum / cumulative"),
|
||||
Line::from("B / L Disk IOPS / latency"),
|
||||
Line::from("F FS free space"),
|
||||
Line::from("S Sparkline"),
|
||||
Line::from("M Reset min/max"),
|
||||
Line::from("d n s l D C I V W P K Toggle plugins"),
|
||||
];
|
||||
let block = Block::default().borders(Borders::ALL).title("Help");
|
||||
frame.render_widget(Clear, area);
|
||||
frame.render_widget(Paragraph::new(lines).block(block).wrap(Wrap { trim: false }), area);
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, engine: &mut Engine, key: KeyEvent) {
|
||||
let args = &mut engine.args;
|
||||
// Popup handling first.
|
||||
match self.popup {
|
||||
Popup::Help => {
|
||||
self.popup = Popup::None;
|
||||
return;
|
||||
}
|
||||
Popup::Filter => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let filter = self.input.clone();
|
||||
let mut procs = processes().lock().unwrap();
|
||||
procs.filter.set_filter(if filter.is_empty() { None } else { Some(&filter) });
|
||||
self.popup = Popup::None;
|
||||
}
|
||||
KeyCode::Esc => self.popup = Popup::None,
|
||||
KeyCode::Backspace => {
|
||||
self.input.pop();
|
||||
}
|
||||
KeyCode::Char(c) => self.input.push(c),
|
||||
_ => {}
|
||||
},
|
||||
Popup::Kill => match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('Y') => {
|
||||
let cursor = engine.runtime.cursor_position.max(0) as usize;
|
||||
let pid = processes()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_list(false)
|
||||
.get(cursor)
|
||||
.and_then(|p| p.get("pid"))
|
||||
.and_then(serde_json::Value::as_i64);
|
||||
if let Some(pid) = pid {
|
||||
processes().lock().unwrap().kill(pid);
|
||||
}
|
||||
self.popup = Popup::None;
|
||||
}
|
||||
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => self.popup = Popup::None,
|
||||
_ => {}
|
||||
},
|
||||
Popup::None => {}
|
||||
}
|
||||
if self.popup != Popup::None {
|
||||
return;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit = true,
|
||||
KeyCode::Char('h') => self.popup = Popup::Help,
|
||||
KeyCode::Char('0') => engine.runtime.disable_irix = !engine.runtime.disable_irix,
|
||||
KeyCode::Char('1') => {
|
||||
args.percpu = !args.percpu;
|
||||
engine.runtime.percpu = args.percpu;
|
||||
}
|
||||
KeyCode::Char('2') => args.disable_left_sidebar = !args.disable_left_sidebar,
|
||||
KeyCode::Char('3') => args.disable_quicklook = !args.disable_quicklook,
|
||||
KeyCode::Char('4') => args.full_quicklook = !args.full_quicklook,
|
||||
KeyCode::Char('5') => args.disable_top = !args.disable_top,
|
||||
KeyCode::Char('6') => {
|
||||
args.meangpu = !args.meangpu;
|
||||
engine.runtime.meangpu = args.meangpu;
|
||||
}
|
||||
KeyCode::Char('/') => engine.runtime.process_short_name = !engine.runtime.process_short_name,
|
||||
KeyCode::Char('a') => set_sort(engine, "auto"),
|
||||
KeyCode::Char('c') => set_sort(engine, "cpu_percent"),
|
||||
KeyCode::Char('m') => set_sort(engine, "memory_percent"),
|
||||
KeyCode::Char('i') => set_sort(engine, "io_counters"),
|
||||
KeyCode::Char('o') => set_sort(engine, "cpu_num"),
|
||||
KeyCode::Char('p') => set_sort(engine, "name"),
|
||||
KeyCode::Char('t') => set_sort(engine, "cpu_times"),
|
||||
KeyCode::Char('u') => set_sort(engine, "username"),
|
||||
KeyCode::Char('<') => cycle_sort(engine, false),
|
||||
KeyCode::Char('>') => cycle_sort(engine, true),
|
||||
KeyCode::Up => move_cursor(engine, -1),
|
||||
KeyCode::Down => move_cursor(engine, 1),
|
||||
KeyCode::Char('+') => nice_change(engine, true),
|
||||
KeyCode::Char('-') => nice_change(engine, false),
|
||||
KeyCode::Char('k') => self.popup = Popup::Kill,
|
||||
KeyCode::Char('f') => {
|
||||
self.input = processes().lock().unwrap().filter.filter_input().unwrap_or("").to_string();
|
||||
self.popup = Popup::Filter;
|
||||
}
|
||||
KeyCode::Char('E') => processes().lock().unwrap().filter.set_filter(None),
|
||||
KeyCode::Char('z') => toggle_processes(engine),
|
||||
KeyCode::Char('e') => toggle_extended(engine),
|
||||
KeyCode::Char('w') => {
|
||||
glances_core::events::events().lock().unwrap().clean(false);
|
||||
}
|
||||
KeyCode::Char('x') => {
|
||||
glances_core::events::events().lock().unwrap().clean(true);
|
||||
}
|
||||
KeyCode::Char('b') => args.byte = !args.byte,
|
||||
KeyCode::Char('T') => engine.runtime.network_sum = !engine.runtime.network_sum,
|
||||
KeyCode::Char('U') => engine.runtime.network_cumul = !engine.runtime.network_cumul,
|
||||
KeyCode::Char('B') => args.diskio_iops = !args.diskio_iops,
|
||||
KeyCode::Char('L') => args.diskio_latency = !args.diskio_latency,
|
||||
KeyCode::Char('F') => engine.runtime.fs_free_space = !engine.runtime.fs_free_space,
|
||||
KeyCode::Char('S') => engine.runtime.sparkline = !engine.runtime.sparkline,
|
||||
KeyCode::Char('M') => engine.runtime.reset_minmax_tag = !engine.runtime.reset_minmax_tag,
|
||||
KeyCode::Char('j') => {
|
||||
args.programs = !args.programs;
|
||||
engine.runtime.programs = args.programs;
|
||||
}
|
||||
KeyCode::Char(c @ ('d' | 'n' | 's' | 'l' | 'D' | 'C' | 'I' | 'V' | 'W' | 'P' | 'K' | 'R')) => {
|
||||
toggle_plugin_by_key(engine, c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_ctx<'a>(args: &'a Args, runtime: &'a glances_core::Runtime, max_width: usize) -> RenderContext<'a> {
|
||||
RenderContext { args, runtime, max_width }
|
||||
}
|
||||
|
||||
fn quicklook_width(width: u16) -> u16 {
|
||||
((width as f64 * 0.28) as u16).clamp(24, 48)
|
||||
}
|
||||
|
||||
fn split_lines(lines: Vec<StatLine>) -> Vec<Vec<StatLine>> {
|
||||
let mut result: Vec<Vec<StatLine>> = vec![Vec::new()];
|
||||
for line in lines {
|
||||
if line.msg == "\n" {
|
||||
result.push(Vec::new());
|
||||
} else {
|
||||
result.last_mut().unwrap().push(line);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Draw a list of lines into a rect (a line group is a row).
|
||||
fn draw_lines(frame: &mut Frame, area: Rect, lines: &[Vec<StatLine>], args: &Args) {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
let buffer = frame.buffer_mut();
|
||||
for (row, line) in lines.iter().take(area.height as usize).enumerate() {
|
||||
let y = area.y + row as u16;
|
||||
let mut x = area.x;
|
||||
let mut decoration = Decoration::Default;
|
||||
for segment in line {
|
||||
if segment.decoration != Decoration::Default {
|
||||
decoration = segment.decoration;
|
||||
}
|
||||
let style = style_for(decoration, args);
|
||||
for c in segment.msg.chars() {
|
||||
if x >= area.x + area.width {
|
||||
break;
|
||||
}
|
||||
if let Some(cell) = buffer.cell_mut((x, y)) {
|
||||
cell.set_char(c);
|
||||
cell.set_style(style);
|
||||
}
|
||||
x += 1;
|
||||
}
|
||||
if x >= area.x + area.width {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn style_for(decoration: Decoration, args: &Args) -> Style {
|
||||
let bold = if args.disable_bold { Modifier::empty() } else { Modifier::BOLD };
|
||||
let bg = args.disable_bg;
|
||||
match decoration {
|
||||
Decoration::Default | Decoration::Password => Style::default(),
|
||||
Decoration::Underline => Style::default().add_modifier(Modifier::UNDERLINED),
|
||||
Decoration::Bold | Decoration::Selected | Decoration::Sort | Decoration::Filter => {
|
||||
Style::default().add_modifier(bold | Modifier::UNDERLINED)
|
||||
}
|
||||
Decoration::Title => Style::default().add_modifier(bold),
|
||||
Decoration::Ok | Decoration::Process | Decoration::Status => Style::default().fg(Color::Green),
|
||||
Decoration::Max => Style::default().fg(Color::Green).add_modifier(bold),
|
||||
Decoration::Careful | Decoration::Info => Style::default().fg(Color::Blue),
|
||||
Decoration::CarefulLog => {
|
||||
if bg {
|
||||
Style::default().fg(Color::Blue).add_modifier(bold)
|
||||
} else {
|
||||
Style::default().bg(Color::Blue).add_modifier(bold)
|
||||
}
|
||||
}
|
||||
Decoration::Warning => Style::default().fg(Color::Magenta).add_modifier(bold),
|
||||
Decoration::WarningLog => {
|
||||
if bg {
|
||||
Style::default().fg(Color::Magenta).add_modifier(bold)
|
||||
} else {
|
||||
Style::default().bg(Color::Magenta).add_modifier(bold)
|
||||
}
|
||||
}
|
||||
Decoration::Critical => Style::default().fg(Color::Red).add_modifier(bold),
|
||||
Decoration::CriticalLog => {
|
||||
if bg {
|
||||
Style::default().fg(Color::Red).add_modifier(bold)
|
||||
} else {
|
||||
Style::default().bg(Color::Red).add_modifier(bold)
|
||||
}
|
||||
}
|
||||
Decoration::OkLog => {
|
||||
if bg {
|
||||
Style::default().fg(Color::Green).add_modifier(bold)
|
||||
} else {
|
||||
Style::default().bg(Color::Green).add_modifier(bold)
|
||||
}
|
||||
}
|
||||
Decoration::CpuTime => Style::default().fg(Color::Magenta),
|
||||
Decoration::ProcessSelected => Style::default().fg(Color::Green).add_modifier(bold | Modifier::UNDERLINED),
|
||||
Decoration::Error => Style::default().fg(Color::Red),
|
||||
Decoration::Separator => Style::default().fg(Color::DarkGray),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sort(_engine: &mut Engine, key: &str) {
|
||||
let mut procs = processes().lock().unwrap();
|
||||
procs.set_sort_key(key, key != "auto");
|
||||
procs.auto_sort = key == "auto";
|
||||
}
|
||||
|
||||
fn cycle_sort(engine: &mut Engine, forward: bool) {
|
||||
const KEYS: [&str; 7] = ["cpu_percent", "memory_percent", "username", "cpu_times", "io_counters", "name", "cpu_num"];
|
||||
let current = processes().lock().unwrap().sort_key.clone();
|
||||
let index = KEYS.iter().position(|k| *k == current).unwrap_or(0);
|
||||
let next = if forward {
|
||||
(index + 1) % KEYS.len()
|
||||
} else {
|
||||
(index + KEYS.len() - 1) % KEYS.len()
|
||||
};
|
||||
set_sort(engine, KEYS[next]);
|
||||
}
|
||||
|
||||
fn move_cursor(engine: &mut Engine, delta: i64) {
|
||||
let len = processes().lock().unwrap().get_list(false).len() as i64;
|
||||
let max = (len - 1).max(0);
|
||||
engine.runtime.cursor_position = (engine.runtime.cursor_position + delta).clamp(0, max);
|
||||
}
|
||||
|
||||
fn nice_change(engine: &mut Engine, increase: bool) {
|
||||
let cursor = engine.runtime.cursor_position.max(0) as usize;
|
||||
let pid = processes()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_list(false)
|
||||
.get(cursor)
|
||||
.and_then(|p| p.get("pid"))
|
||||
.and_then(serde_json::Value::as_i64);
|
||||
if let Some(pid) = pid {
|
||||
let procs = processes().lock().unwrap();
|
||||
if increase {
|
||||
procs.nice_increase(pid);
|
||||
} else {
|
||||
procs.nice_decrease(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_processes(engine: &mut Engine) {
|
||||
let enabled = engine.plugins.get("processlist").map(|p| p.is_enabled()).unwrap_or(false);
|
||||
for name in ["processcount", "processlist", "programlist"] {
|
||||
if let Some(plugin) = engine.plugins.get_mut(name) {
|
||||
plugin.set_enabled(!enabled);
|
||||
}
|
||||
}
|
||||
engine.sync_enabled();
|
||||
}
|
||||
|
||||
fn toggle_extended(engine: &mut Engine) {
|
||||
let procs = processes().lock().unwrap();
|
||||
if procs.disable_extended_tag {
|
||||
drop(procs);
|
||||
processes().lock().unwrap().enable_extended();
|
||||
engine.args.enable_process_extended = true;
|
||||
} else {
|
||||
drop(procs);
|
||||
processes().lock().unwrap().disable_extended();
|
||||
engine.args.enable_process_extended = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_plugin_by_key(engine: &mut Engine, key: char) {
|
||||
let name = match key {
|
||||
'd' => "diskio",
|
||||
'n' => "network",
|
||||
's' => "sensors",
|
||||
'l' => "alert",
|
||||
'D' => "containers",
|
||||
'C' => "cloud",
|
||||
'I' => "ip",
|
||||
'V' => "vms",
|
||||
'W' => "wifi",
|
||||
'P' => "ports",
|
||||
'K' => "connections",
|
||||
'R' => "raid",
|
||||
_ => return,
|
||||
};
|
||||
if let Some(plugin) = engine.plugins.get_mut(name) {
|
||||
let enabled = plugin.is_enabled();
|
||||
plugin.set_enabled(!enabled);
|
||||
engine.sync_enabled();
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep the `Align` import alive for future use.
|
||||
#[allow(dead_code)]
|
||||
fn unused(_: Align) {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
fn buffer_to_string(buffer: &ratatui::buffer::Buffer) -> String {
|
||||
let area = buffer.area();
|
||||
let mut out = String::new();
|
||||
for y in 0..area.height {
|
||||
for x in 0..area.width {
|
||||
if let Some(cell) = buffer.cell((x, y)) {
|
||||
out.push_str(cell.symbol());
|
||||
}
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
fn test_tui_layout_renders() {
|
||||
use clap::Parser;
|
||||
use glances_core::Config;
|
||||
let mut args = Args::parse_from(["glances", "--disable-plugin", "all", "--enable-plugin", "cpu,mem,load,processlist,processcount,network,fs,diskio,now,system,uptime,quicklook"]);
|
||||
args.time = Some(1.0);
|
||||
let config = Config::empty(false);
|
||||
let mut engine = Engine::new(args.clone(), config);
|
||||
engine.update();
|
||||
engine.update();
|
||||
let backend = TestBackend::new(140, 40);
|
||||
let mut terminal = ratatui::Terminal::new(backend).unwrap();
|
||||
let tui = Tui { popup: Popup::None, input: String::new(), quit: false };
|
||||
terminal.draw(|frame| draw_frame(frame, &mut engine, &tui)).unwrap();
|
||||
let text = buffer_to_string(terminal.backend().buffer());
|
||||
println!("{text}");
|
||||
assert!(text.contains("CPU"));
|
||||
assert!(text.contains("MEM"));
|
||||
assert!(text.contains("Command") || text.contains("python") || text.contains("COMMAND"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user