diff --git a/Cargo.lock b/Cargo.lock index 4a42063..e1cb995 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -722,6 +722,7 @@ dependencies = [ "glances-core", "glances-plugins", "glances-tui", + "indexmap", "serde_json", "tracing", "tracing-subscriber", diff --git a/README.md b/README.md index 1792ac6..850fe8e 100644 --- a/README.md +++ b/README.md @@ -7,24 +7,60 @@ This project is a drop-in replacement for the **standalone** Glances modes: - curses-like terminal UI (`glances`) - stdout modes: `--stdout`, `--stdout-csv`, `--stdout-json` -- `--fetch`, `--issue`, `--modules-list` -- all plugins, including extended ones (containers, GPUs, sensors, ...) -- the same `glances.conf` configuration file +- `--fetch` (with the shipped templates), `--issue`, `--module-list` +- the same `glances.conf` configuration file (limits, thresholds, aliases, + show/hide lists, per-plugin refresh, history, actions) +- compatible stats schemas and CLI flags for the implemented features The following Python Glances features are intentionally **not** provided: -REST API, WebUI, MCP, client/server mode, discovery/browser, SNMP and exporters. +REST API, WebUI, MCP, client/server mode, discovery/browser, SNMP and +exporters. ## Status -Work in progress. See the milestones in the project plan; the workspace is -organized as: +| Area | Status | +| --- | --- | +| CLI (clap, drop-in flags) | done | +| Configuration (`glances.conf`) | done | +| Plugin model (limits, views, rates, MMM, history, events, actions) | done | +| Terminal UI (ratatui) | done | +| stdout / CSV / JSON / fetch / issue modes | done | +| Core plugins | cpu, percpu, mem, memswap, load, fs, network, diskio, sensors, irq, system, uptime, now, core, version, psutilversion, quicklook, processcount, processlist, programlist | +| Extended plugins | folders, amps, ports, raid, wifi, ip | +| Not ported yet | containers, vms, gpu/npu/mpp, connections, smart, cloud | +| Python-specific flags | `--trace-malloc`, `--api-doc`, `--api-restful-doc` rejected with a clear error | + +## Build and run + +```console +$ cargo build --release +$ ./target/release/glances # terminal UI +$ ./target/release/glances --stdout-json cpu,mem,load +$ ./target/release/glances --fetch +``` + +## Differential testing + +The `tools/parity.py` script compares the Rust implementation with the Python +reference implementation (the oracle) plugin by plugin: it runs both for a few +refreshes and diffs the flattened key sets and value types. + +```console +$ python3 -m venv /tmp/opencode/glances-py +$ /tmp/opencode/glances-py/bin/pip install /path/to/python-glances +$ cargo build +$ python3 tools/parity.py # all implemented plugins +$ python3 tools/parity.py cpu mem # selected plugins +``` + +## Workspace layout | Crate | Purpose | | --- | --- | -| `glances-core` | plugin model, config, limits, history, events, filters, output | -| `glances-plugins` | collectors and plugin registry/engine | +| `glances-core` | plugin model, config, limits, history, events, filters, processes | +| `glances-plugins` | collectors, plugin registry and update engine | | `glances-tui` | terminal UI (ratatui) | -| `glances-cli` | the `glances` binary | +| `glances-cli` | the `glances` binary, stdout modes and fetch renderer | ## License diff --git a/crates/glances-cli/Cargo.toml b/crates/glances-cli/Cargo.toml index 86d6b81..93cf1e7 100644 --- a/crates/glances-cli/Cargo.toml +++ b/crates/glances-cli/Cargo.toml @@ -15,6 +15,7 @@ ctrlc = "3.5.2" glances-core = { version = "0.1.0", path = "../glances-core" } glances-plugins = { version = "0.1.0", path = "../glances-plugins" } glances-tui = { version = "0.1.0", path = "../glances-tui" } +indexmap.workspace = true serde_json.workspace = true tracing.workspace = true tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } diff --git a/crates/glances-cli/src/fetch.rs b/crates/glances-cli/src/fetch.rs new file mode 100644 index 0000000..53d2e31 --- /dev/null +++ b/crates/glances-cli/src/fetch.rs @@ -0,0 +1,969 @@ +//! Fetch mode (`--fetch`): render a template against the Glances API. +//! +//! The Python implementation uses Jinja2. This module implements the small +//! template subset used by the shipped templates (expressions, filters, +//! `for`/`if` blocks, dotted access, calls). + +use anyhow::{Result, bail}; +use glances_core::plugin::Plugin; +use glances_core::{Args, py_str}; +use glances_plugins::engine::Engine; +use indexmap::IndexMap; +use serde_json::Value; + +/// Default template (same as the Python `DEFAULT_FETCH_TEMPLATE`). +const DEFAULT_TEMPLATE: &str = r#" +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +✨ {{ gl.system['hostname'] }}{{ ' | ' + gl.ip['address'] if gl.ip['address'] else '' }} | Uptime: {{ gl.uptime }} +⚙️ {{ gl.system['hr_name'] }} + +💡 LOAD {{ '%0.2f'| format(gl.load['min1']) }}/min1 |\ + {{ '%0.2f'| format(gl.load['min5']) }}/min5 |\ + {{ '%0.2f'| format(gl.load['min15']) }}/min15 +⚡ CPU {{ gl.bar(gl.cpu['total']) }} {{ gl.cpu['total'] }}% of {{ gl.core['log'] }} cores +🧠 MEM {{ gl.bar(gl.mem['percent']) }} {{ gl.mem['percent'] }}% ({{ gl.auto_unit(gl.mem['used']) }} /\ + {{ gl.auto_unit(gl.mem['total']) }}) +{% for fs in gl.fs.keys() %}\ +💾 {% if loop.index == 1 %}DISK{% else %} {% endif %}\ + {{ gl.bar(gl.fs[fs]['percent']) }} {{ gl.fs[fs]['percent'] }}% ({{ gl.auto_unit(gl.fs[fs]['used']) }} /\ + {{ gl.auto_unit(gl.fs[fs]['size']) }}) for {{ fs }} +{% endfor %}\ +{% for net in gl.network.keys() %}\ +📡 {% if loop.index == 1 %}NET{% else %} {% endif %}\ + ↓ {{ gl.auto_unit(gl.network[net]['bytes_recv_rate_per_sec']) }}b/s\ + ↑ {{ gl.auto_unit(gl.network[net]['bytes_sent_rate_per_sec']) }}b/s for {{ net }} +{% endfor %}\ + +🔥 TOP PROCESS by CPU +{% for process in gl.top_process() %}\ +{{ loop.index }}️⃣ {{ process['name'][:20] }}{{ ' ' * (20 - process['name'][:20] | length) }}\ + ⚡ {{ process['cpu_percent'] }}% CPU\ +{{ ' ' * (8 - (gl.auto_unit(process['cpu_percent']) | length)) }}\ + 🧠 {{ gl.auto_unit(process['memory_info']['rss']) }}B MEM +{% endfor %}\ +🔥 TOP PROCESS by MEM +{% for process in gl.top_process(sorted_by='memory_percent', sorted_by_secondary='cpu_percent') %}\ +{{ loop.index }}️⃣ {{ process['name'][:20] }}{{ ' ' * (20 - process['name'][:20] | length) }}\ + 🧠 {{ gl.auto_unit(process['memory_info']['rss']) }}B MEM\ +{{ ' ' * (7 - (gl.auto_unit(process['memory_info']['rss']) | length)) }}\ + ⚡ {{ process['cpu_percent'] }}% CPU +{% endfor %}\ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +"#; + +#[derive(Debug, Clone, PartialEq)] +enum JVal { + Null, + Bool(bool), + Num(f64), + Str(String), + List(Vec), + Map(IndexMap), + /// A builtin function of the API object. + Builtin(&'static str), +} + +impl JVal { + fn truthy(&self) -> bool { + match self { + JVal::Null => false, + JVal::Bool(b) => *b, + JVal::Num(n) => *n != 0.0, + JVal::Str(s) => !s.is_empty(), + JVal::List(l) => !l.is_empty(), + JVal::Map(m) => !m.is_empty(), + JVal::Builtin(_) => true, + } + } + + fn as_num(&self) -> f64 { + match self { + JVal::Num(n) => *n, + JVal::Bool(b) => f64::from(*b as u8), + JVal::Str(s) => s.parse().unwrap_or(0.0), + _ => 0.0, + } + } + + fn to_text(&self) -> String { + match self { + JVal::Null => "".to_string(), + JVal::Bool(b) => if *b { "True" } else { "False" }.to_string(), + JVal::Num(n) => { + if n.fract() == 0.0 { + format!("{}", *n as i64) + } else { + format!("{n}") + } + } + JVal::Str(s) => s.clone(), + JVal::List(_) | JVal::Map(_) => String::new(), + JVal::Builtin(_) => String::new(), + } + } +} + +fn json_to_jval(value: &Value) -> JVal { + match value { + Value::Null => JVal::Null, + Value::Bool(b) => JVal::Bool(*b), + Value::Number(n) => JVal::Num(n.as_f64().unwrap_or(0.0)), + Value::String(s) => JVal::Str(s.clone()), + Value::Array(items) => JVal::List(items.iter().map(json_to_jval).collect()), + Value::Object(map) => JVal::Map( + map.iter() + .map(|(k, v)| (k.clone(), json_to_jval(v))) + .collect(), + ), + } +} + +/// API object exposed to the template as `gl`. +struct Api<'a> { + engine: &'a Engine, + #[allow(dead_code)] + args: &'a Args, +} + +impl<'a> Api<'a> { + fn new(engine: &'a Engine, args: &'a Args) -> Self { + Self { engine, args } + } + + fn plugin_stats(&self, name: &str) -> JVal { + self.engine + .get_plugin(name) + .map(|p| json_to_jval(p.stats())) + .unwrap_or(JVal::Null) + } + + /// Build the `gl` object. + fn object(&self) -> JVal { + let mut map: IndexMap = IndexMap::new(); + map.insert("system".to_string(), self.plugin_stats("system")); + map.insert("ip".to_string(), self.plugin_stats("ip")); + map.insert("uptime".to_string(), self.plugin_stats("uptime")); + map.insert("load".to_string(), self.plugin_stats("load")); + map.insert("cpu".to_string(), self.plugin_stats("cpu")); + map.insert("core".to_string(), self.plugin_stats("core")); + map.insert("mem".to_string(), self.plugin_stats("mem")); + // fs and network are exposed as dicts keyed by mount point/interface. + map.insert( + "fs".to_string(), + self.plugin_stats_as_dict("fs", "mnt_point"), + ); + map.insert( + "network".to_string(), + self.plugin_stats_as_dict("network", "interface_name"), + ); + map.insert("bar".to_string(), JVal::Builtin("bar")); + map.insert("auto_unit".to_string(), JVal::Builtin("auto_unit")); + map.insert("top_process".to_string(), JVal::Builtin("top_process")); + JVal::Map(map) + } + + fn plugin_stats_as_dict(&self, plugin: &str, key: &str) -> JVal { + let mut map = IndexMap::new(); + if let Some(plugin) = self.engine.get_plugin(plugin) + && let Some(items) = plugin.stats().as_array() + { + for item in items { + if let Some(identity) = item.get(key).and_then(Value::as_str) { + map.insert(identity.to_string(), json_to_jval(item)); + } + } + } + JVal::Map(map) + } + + fn top_process(&self, sorted_by: &str, sorted_by_secondary: &str, limit: usize) -> JVal { + let procs = glances_core::processes::processes().lock().unwrap(); + let mut list: Vec = procs + .get_list(false) + .iter() + .filter(|p| { + let cmdline = p + .get("cmdline") + .and_then(Value::as_array) + .map(|a| { + a.iter() + .filter_map(|v| v.as_str()) + .collect::>() + .join(" ") + }) + .unwrap_or_default(); + !cmdline.contains("glances") + }) + .cloned() + .collect(); + glances_core::processes::sort_stats(&mut list, sorted_by, sorted_by_secondary, true); + JVal::List(list.iter().take(limit).map(json_to_jval).collect()) + } +} + +// --------------------------------------------------------------------------- +// Template parser +// --------------------------------------------------------------------------- + +#[derive(Debug)] +enum Node { + Text(String), + Expr(String), + For { + var: String, + iterable: String, + body: Vec, + }, + If { + cond: String, + then_body: Vec, + else_body: Vec, + }, +} + +#[derive(Debug)] +enum Token { + Text(String), + Expr(String), + Stmt(String), +} + +fn tokenize(template: &str) -> Result> { + let mut tokens = Vec::new(); + let mut chars = template.chars().peekable(); + let mut text = String::new(); + while let Some(c) = chars.next() { + // Jinja line continuation. + if c == '\\' && chars.peek() == Some(&'\n') { + chars.next(); + continue; + } + if c == '{' { + match chars.peek() { + Some('{') => { + chars.next(); + if !text.is_empty() { + tokens.push(Token::Text(std::mem::take(&mut text))); + } + let mut expr = String::new(); + while let Some(c2) = chars.next() { + if c2 == '}' && chars.peek() == Some(&'}') { + chars.next(); + break; + } + expr.push(c2); + } + tokens.push(Token::Expr(expr.trim().to_string())); + continue; + } + Some('%') => { + chars.next(); + if !text.is_empty() { + tokens.push(Token::Text(std::mem::take(&mut text))); + } + let mut stmt = String::new(); + while let Some(c2) = chars.next() { + if c2 == '%' && chars.peek() == Some(&'}') { + chars.next(); + break; + } + stmt.push(c2); + } + tokens.push(Token::Stmt(stmt.trim().to_string())); + continue; + } + _ => {} + } + } + text.push(c); + } + if !text.is_empty() { + tokens.push(Token::Text(text)); + } + Ok(tokens) +} + +fn parse_block(tokens: &[Token], index: &mut usize, stop: &[&str]) -> Result> { + let mut nodes = Vec::new(); + while *index < tokens.len() { + match &tokens[*index] { + Token::Text(text) => { + nodes.push(Node::Text(text.clone())); + *index += 1; + } + Token::Expr(expr) => { + nodes.push(Node::Expr(expr.clone())); + *index += 1; + } + Token::Stmt(stmt) => { + if stop + .iter() + .any(|s| stmt == s || stmt.starts_with(&format!("{s} "))) + { + return Ok(nodes); + } + if let Some(rest) = stmt.strip_prefix("for ") { + let (var, iterable) = rest.split_once(" in ").unwrap_or((rest, "")); + *index += 1; + let body = parse_block(tokens, index, &["endfor"])?; + if *index < tokens.len() { + *index += 1; // consume endfor + } + nodes.push(Node::For { + var: var.trim().to_string(), + iterable: iterable.trim().to_string(), + body, + }); + continue; + } + if let Some(cond) = stmt.strip_prefix("if ") { + *index += 1; + let then_body = parse_block(tokens, index, &["else", "endif"])?; + let mut else_body = Vec::new(); + if *index < tokens.len() { + let current = &tokens[*index]; + let is_else = matches!(current, Token::Stmt(s) if s == "else"); + *index += 1; + if is_else { + else_body = parse_block(tokens, index, &["endif"])?; + if *index < tokens.len() { + *index += 1; + } + } + } + nodes.push(Node::If { + cond: cond.trim().to_string(), + then_body, + else_body, + }); + continue; + } + // Unknown statement: ignore it. + *index += 1; + } + } + } + Ok(nodes) +} + +fn parse_template(template: &str) -> Result> { + let tokens = tokenize(template)?; + let mut index = 0; + let nodes = parse_block(&tokens, &mut index, &[])?; + if index < tokens.len() { + bail!("unexpected template statement"); + } + Ok(nodes) +} + +// --------------------------------------------------------------------------- +// Expression evaluator +// --------------------------------------------------------------------------- + +struct ExprParser<'a> { + input: &'a [u8], + pos: usize, +} + +impl<'a> ExprParser<'a> { + fn new(input: &'a str) -> Self { + Self { + input: input.as_bytes(), + pos: 0, + } + } + + fn skip_ws(&mut self) { + while self.pos < self.input.len() && (self.input[self.pos] as char).is_whitespace() { + self.pos += 1; + } + } + + fn peek_word(&mut self) -> String { + self.skip_ws(); + let start = self.pos; + while self.pos < self.input.len() { + let c = self.input[self.pos] as char; + if c.is_alphanumeric() || c == '_' { + self.pos += 1; + } else { + break; + } + } + String::from_utf8_lossy(&self.input[start..self.pos]).to_string() + } + + fn parse(&mut self, ctx: &EvalContext) -> Result { + let value = self.parse_ternary(ctx)?; + Ok(value) + } + + fn parse_ternary(&mut self, ctx: &EvalContext) -> Result { + let value = self.parse_or(ctx)?; + let save = self.pos; + let word = self.peek_word(); + if word == "if" { + let cond = self.parse_or(ctx)?; + let word = self.peek_word(); + if word == "else" { + let other = self.parse_ternary(ctx)?; + return Ok(if cond.truthy() { value } else { other }); + } + self.pos = save; + } else { + self.pos = save; + } + Ok(value) + } + + fn parse_or(&mut self, ctx: &EvalContext) -> Result { + let mut value = self.parse_and(ctx)?; + loop { + let save = self.pos; + if self.peek_word() == "or" { + let right = self.parse_and(ctx)?; + value = JVal::Bool(value.truthy() || right.truthy()); + } else { + self.pos = save; + return Ok(value); + } + } + } + + fn parse_and(&mut self, ctx: &EvalContext) -> Result { + let mut value = self.parse_not(ctx)?; + loop { + let save = self.pos; + if self.peek_word() == "and" { + let right = self.parse_not(ctx)?; + value = JVal::Bool(value.truthy() && right.truthy()); + } else { + self.pos = save; + return Ok(value); + } + } + } + + fn parse_not(&mut self, ctx: &EvalContext) -> Result { + let save = self.pos; + if self.peek_word() == "not" { + let value = self.parse_not(ctx)?; + return Ok(JVal::Bool(!value.truthy())); + } + self.pos = save; + self.parse_comparison(ctx) + } + + fn parse_comparison(&mut self, ctx: &EvalContext) -> Result { + let left = self.parse_additive(ctx)?; + self.skip_ws(); + for op in ["==", "!=", ">=", "<=", ">", "<"] { + if self.input[self.pos..].starts_with(op.as_bytes()) { + self.pos += op.len(); + let right = self.parse_additive(ctx)?; + let result = match op { + "==" => { + left == right + || (left.to_text() == right.to_text() + && !matches!(left, JVal::Map(_) | JVal::List(_))) + } + "!=" => left != right, + ">" => left.as_num() > right.as_num(), + "<" => left.as_num() < right.as_num(), + ">=" => left.as_num() >= right.as_num(), + "<=" => left.as_num() <= right.as_num(), + _ => false, + }; + return Ok(JVal::Bool(result)); + } + } + Ok(left) + } + + fn parse_additive(&mut self, ctx: &EvalContext) -> Result { + let mut value = self.parse_multiplicative(ctx)?; + loop { + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b'+' { + self.pos += 1; + let right = self.parse_multiplicative(ctx)?; + value = match (&value, &right) { + (JVal::Num(a), JVal::Num(b)) => JVal::Num(a + b), + _ => JVal::Str(value.to_text() + &right.to_text()), + }; + } else if self.pos < self.input.len() && self.input[self.pos] == b'-' { + self.pos += 1; + let right = self.parse_multiplicative(ctx)?; + value = JVal::Num(value.as_num() - right.as_num()); + } else { + return Ok(value); + } + } + } + + fn parse_multiplicative(&mut self, ctx: &EvalContext) -> Result { + let mut value = self.parse_unary(ctx)?; + loop { + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b'*' { + self.pos += 1; + let right = self.parse_unary(ctx)?; + value = match (&value, &right) { + (JVal::Str(s), JVal::Num(n)) | (JVal::Num(n), JVal::Str(s)) => { + JVal::Str(s.repeat(n.max(0.0) as usize)) + } + _ => JVal::Num(value.as_num() * right.as_num()), + }; + } else if self.pos < self.input.len() && self.input[self.pos] == b'/' { + self.pos += 1; + let right = self.parse_unary(ctx)?; + let divisor = right.as_num(); + value = JVal::Num(if divisor == 0.0 { + 0.0 + } else { + value.as_num() / divisor + }); + } else { + return Ok(value); + } + } + } + + fn parse_unary(&mut self, ctx: &EvalContext) -> Result { + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b'-' { + self.pos += 1; + let value = self.parse_unary(ctx)?; + return Ok(JVal::Num(-value.as_num())); + } + self.parse_postfix(ctx) + } + + fn parse_postfix(&mut self, ctx: &EvalContext) -> Result { + let mut value = self.parse_primary(ctx)?; + loop { + self.skip_ws(); + if self.pos >= self.input.len() { + return Ok(value); + } + match self.input[self.pos] { + b'.' => { + self.pos += 1; + let name = self.peek_word(); + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b'(' { + // Method call on a dict/list or builtin function call. + self.pos += 1; + let (args, kwargs) = self.parse_args(ctx)?; + let member = match &value { + JVal::Map(map) => map.get(&name).cloned().unwrap_or(JVal::Null), + _ => JVal::Null, + }; + if let JVal::Builtin(builtin) = &member { + value = ctx.call_builtin(builtin, &args, &kwargs); + continue; + } + value = match (&value, name.as_str()) { + (JVal::Map(map), "keys") => { + JVal::List(map.keys().map(|k| JVal::Str(k.clone())).collect()) + } + (JVal::Map(map), "values") => { + JVal::List(map.values().cloned().collect()) + } + (JVal::Map(map), "items") => JVal::List( + map.iter() + .map(|(k, v)| JVal::List(vec![JVal::Str(k.clone()), v.clone()])) + .collect(), + ), + (JVal::Map(map), "get") => { + let key = args.first().map(|v| v.to_text()).unwrap_or_default(); + map.get(&key) + .cloned() + .unwrap_or_else(|| args.get(1).cloned().unwrap_or(JVal::Null)) + } + _ => JVal::Null, + }; + continue; + } + value = match &value { + JVal::Map(map) => map.get(&name).cloned().unwrap_or(JVal::Null), + _ => JVal::Null, + }; + } + b'[' => { + self.pos += 1; + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b':' { + // Slice [:n] + self.pos += 1; + let end = self.parse_ternary(ctx)?.as_num() as usize; + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b']' { + self.pos += 1; + } + if let JVal::Str(s) = &value { + let chars: Vec = s.chars().collect(); + value = JVal::Str(chars[..end.min(chars.len())].iter().collect()); + } + } else { + let key = self.parse_ternary(ctx)?; + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b']' { + self.pos += 1; + } + value = match &value { + JVal::Map(map) => { + map.get(&key.to_text()).cloned().unwrap_or(JVal::Null) + } + JVal::List(items) => { + let index = key.as_num() as usize; + items.get(index).cloned().unwrap_or(JVal::Null) + } + JVal::Str(s) => { + let chars: Vec = s.chars().collect(); + let index = key.as_num() as usize; + chars + .get(index) + .map(|c| JVal::Str(c.to_string())) + .unwrap_or(JVal::Null) + } + _ => JVal::Null, + }; + } + } + b'(' => { + self.pos += 1; + let (args, kwargs) = self.parse_args(ctx)?; + value = self.call(value, &args, &kwargs, ctx); + } + b'|' => { + self.pos += 1; + let filter = self.peek_word(); + let mut args: Vec = Vec::new(); + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b'(' { + self.pos += 1; + loop { + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b')' { + self.pos += 1; + break; + } + let arg = self.parse_ternary(ctx)?; + args.push(arg); + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b',' { + self.pos += 1; + } + } + } + value = self.apply_filter(&filter, value, &args); + } + _ => return Ok(value), + } + } + } + + fn call( + &self, + callee: JVal, + args: &[JVal], + kwargs: &IndexMap, + ctx: &EvalContext, + ) -> JVal { + match callee { + JVal::Builtin(name) => ctx.call_builtin(name, args, kwargs), + _ => JVal::Null, + } + } + + fn apply_filter(&self, name: &str, value: JVal, args: &[JVal]) -> JVal { + match name { + "length" => JVal::Num(match &value { + JVal::Str(s) => s.chars().count() as f64, + JVal::List(l) => l.len() as f64, + JVal::Map(m) => m.len() as f64, + _ => 0.0, + }), + "format" => { + let mut result = value.to_text(); + for arg in args { + // Only the first (already embedded) value is formatted. + let replacement = format_c(arg, &result); + result = replacement; + } + JVal::Str(result) + } + "upper" => JVal::Str(value.to_text().to_uppercase()), + "lower" => JVal::Str(value.to_text().to_lowercase()), + _ => value, + } + } + + fn parse_args(&mut self, ctx: &EvalContext) -> Result<(Vec, IndexMap)> { + let mut args: Vec = Vec::new(); + let mut kwargs: IndexMap = IndexMap::new(); + loop { + self.skip_ws(); + if self.pos >= self.input.len() { + break; + } + if self.input[self.pos] == b')' { + self.pos += 1; + break; + } + let save = self.pos; + let word = self.peek_word(); + let mut is_kwarg = false; + if !word.is_empty() { + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b'=' { + is_kwarg = true; + self.pos += 1; + } + } + if is_kwarg { + let value = self.parse_ternary(ctx)?; + kwargs.insert(word, value); + } else { + self.pos = save; + let before = self.pos; + let value = self.parse_ternary(ctx)?; + args.push(value); + if self.pos == before { + // Defensive: never loop forever on an unexpected token. + self.pos += 1; + } + } + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b',' { + self.pos += 1; + } + } + Ok((args, kwargs)) + } + + fn parse_primary(&mut self, ctx: &EvalContext) -> Result { + self.skip_ws(); + if self.pos >= self.input.len() { + return Ok(JVal::Null); + } + let c = self.input[self.pos] as char; + if c == '(' { + self.pos += 1; + let value = self.parse_ternary(ctx)?; + self.skip_ws(); + if self.pos < self.input.len() && self.input[self.pos] == b')' { + self.pos += 1; + } + return Ok(value); + } + if c == '\'' || c == '"' { + let quote = self.input[self.pos]; + self.pos += 1; + let start = self.pos; + while self.pos < self.input.len() && self.input[self.pos] != quote { + self.pos += 1; + } + let text = String::from_utf8_lossy(&self.input[start..self.pos]).to_string(); + self.pos += 1; + return Ok(JVal::Str(text)); + } + if c.is_ascii_digit() { + let start = self.pos; + while self.pos < self.input.len() + && ((self.input[self.pos] as char).is_ascii_digit() || self.input[self.pos] == b'.') + { + self.pos += 1; + } + let text = String::from_utf8_lossy(&self.input[start..self.pos]).to_string(); + return Ok(JVal::Num(text.parse().unwrap_or(0.0))); + } + let word = self.peek_word(); + match word.as_str() { + "" => Ok(JVal::Null), + "true" | "True" => Ok(JVal::Bool(true)), + "false" | "False" => Ok(JVal::Bool(false)), + "none" | "None" | "null" => Ok(JVal::Null), + _ => Ok(ctx.lookup(&word)), + } + } +} + +/// Poor man's `%`-format: only `%0.2f`, `%d`, `%s` and `%x.yf` are needed. +fn format_c(value: &JVal, format: &str) -> String { + if let Some(spec) = format.strip_prefix('%') { + let spec = spec + .trim_end_matches('f') + .trim_end_matches('d') + .trim_end_matches('s'); + let precision = spec + .split('.') + .nth(1) + .and_then(|p| p.parse::().ok()) + .unwrap_or(6); + let width = spec + .split('.') + .next() + .and_then(|w| w.trim_start_matches('0').parse::().ok()) + .unwrap_or(0); + if format.ends_with('d') { + return format!("{:>width$}", value.as_num() as i64); + } + if format.ends_with('s') { + return value.to_text(); + } + return format!("{:>width$.precision$}", value.as_num()); + } + value.to_text() +} + +struct EvalContext<'a> { + api: &'a Api<'a>, + variables: Vec>, + loop_var: Option>, +} + +impl<'a> EvalContext<'a> { + fn lookup(&self, name: &str) -> JVal { + if name == "loop" + && let Some(loop_var) = &self.loop_var + { + return JVal::Map(loop_var.clone()); + } + if name == "gl" { + return self.api.object(); + } + for scope in self.variables.iter().rev() { + if let Some(value) = scope.get(name) { + return value.clone(); + } + } + JVal::Null + } + + fn call_builtin(&self, name: &str, args: &[JVal], kwargs: &IndexMap) -> JVal { + match name { + "bar" => { + let value = args.first().map(|v| v.as_num()).unwrap_or(0.0); + JVal::Str(glances_core::units::bar_custom(value, 18, "■", "□", false)) + } + "auto_unit" => { + let value = args.first().map(|v| v.as_num()).unwrap_or(0.0); + let low_precision = kwargs + .get("low_precision") + .map(|v| v.truthy()) + .unwrap_or(false); + let min_symbol = kwargs + .get("min_symbol") + .map(|v| v.to_text()) + .unwrap_or_else(|| "K".to_string()); + JVal::Str(glances_core::auto_unit( + value, + low_precision, + &min_symbol, + "-", + )) + } + "top_process" => { + let sorted_by = kwargs + .get("sorted_by") + .map(|v| v.to_text()) + .unwrap_or_else(|| "cpu_percent".to_string()); + let sorted_by_secondary = kwargs + .get("sorted_by_secondary") + .map(|v| v.to_text()) + .unwrap_or_else(|| "memory_percent".to_string()); + let limit = args.first().map(|v| v.as_num() as usize).unwrap_or(3); + self.api + .top_process(&sorted_by, &sorted_by_secondary, limit) + } + _ => JVal::Null, + } + } +} + +fn render_nodes(nodes: &[Node], ctx: &mut EvalContext) -> Result { + let mut out = String::new(); + for node in nodes { + match node { + Node::Text(text) => out.push_str(text), + Node::Expr(expr) => { + let mut parser = ExprParser::new(expr); + let value = parser.parse(ctx)?; + out.push_str(&value.to_text()); + } + Node::For { + var, + iterable, + body, + } => { + let mut parser = ExprParser::new(iterable); + let collection = parser.parse(ctx)?; + let items: Vec = match collection { + JVal::List(items) => items, + JVal::Map(map) => map.keys().map(|k| JVal::Str(k.clone())).collect(), + _ => Vec::new(), + }; + let length = items.len(); + for (index, item) in items.into_iter().enumerate() { + let mut scope = IndexMap::new(); + scope.insert(var.clone(), item); + let mut loop_var = IndexMap::new(); + loop_var.insert("index".to_string(), JVal::Num((index + 1) as f64)); + loop_var.insert("index0".to_string(), JVal::Num(index as f64)); + loop_var.insert("first".to_string(), JVal::Bool(index == 0)); + loop_var.insert("last".to_string(), JVal::Bool(index + 1 == length)); + loop_var.insert("length".to_string(), JVal::Num(length as f64)); + ctx.variables.push(scope); + ctx.loop_var = Some(loop_var); + out.push_str(&render_nodes(body, ctx)?); + ctx.variables.pop(); + ctx.loop_var = None; + } + } + Node::If { + cond, + then_body, + else_body, + } => { + let mut parser = ExprParser::new(cond); + let value = parser.parse(ctx)?; + if value.truthy() { + out.push_str(&render_nodes(then_body, ctx)?); + } else { + out.push_str(&render_nodes(else_body, ctx)?); + } + } + } + } + Ok(out) +} + +/// Run the fetch mode: render the template and exit. +pub fn run_fetch(engine: &mut Engine, args: &Args) -> Result<()> { + engine.update(); + let template = if args.fetch_template.is_empty() { + DEFAULT_TEMPLATE.to_string() + } else { + std::fs::read_to_string(&args.fetch_template).map_err(|e| { + anyhow::anyhow!("cannot read fetch template '{}': {e}", args.fetch_template) + })? + }; + let nodes = parse_template(&template)?; + let api = Api::new(engine, args); + let mut ctx = EvalContext { + api: &api, + variables: Vec::new(), + loop_var: None, + }; + let output = render_nodes(&nodes, &mut ctx)?; + print!("{output}"); + Ok(()) +} + +#[allow(dead_code)] +fn unused(_: &dyn Plugin, _: &str) -> String { + py_str(&Value::Null) +} diff --git a/crates/glances-cli/src/logging.rs b/crates/glances-cli/src/logging.rs index d79bd8a..fa25071 100644 --- a/crates/glances-cli/src/logging.rs +++ b/crates/glances-cli/src/logging.rs @@ -9,15 +9,22 @@ pub fn log_filename() -> PathBuf { .ok() .filter(|v| !v.is_empty()) .map(PathBuf::from) - .or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join(".local").join("share"))); - if let Some(base) = base { - if base.is_dir() { - let dir = base.join("glances"); - let _ = std::fs::create_dir_all(&dir); - return dir.join("glances.log"); - } + .or_else(|| { + std::env::var("HOME") + .ok() + .map(|h| PathBuf::from(h).join(".local").join("share")) + }); + if let Some(base) = base + && base.is_dir() + { + let dir = base.join("glances"); + let _ = std::fs::create_dir_all(&dir); + return dir.join("glances.log"); } - std::env::temp_dir().join(format!("glances-{}.log", std::env::var("USER").unwrap_or_else(|_| "user".into()))) + std::env::temp_dir().join(format!( + "glances-{}.log", + std::env::var("USER").unwrap_or_else(|_| "user".into()) + )) } /// Initialise the logger: everything goes to the log file, warnings and diff --git a/crates/glances-cli/src/main.rs b/crates/glances-cli/src/main.rs index a57fb98..7986c72 100644 --- a/crates/glances-cli/src/main.rs +++ b/crates/glances-cli/src/main.rs @@ -1,13 +1,13 @@ //! The `glances` binary. +mod fetch; mod logging; mod outputs; -mod tui_placeholder; use anyhow::Result; use clap::{CommandFactory, Parser}; -use glances_core::config::Config; use glances_core::Args; +use glances_core::config::Config; use glances_plugins::engine::Engine; use std::io::Write; use std::sync::atomic::{AtomicBool, Ordering}; @@ -21,7 +21,10 @@ fn main() { let args = Args::parse(); if args.version { - println!("{}", args.version_message(&logging::log_filename().display().to_string())); + println!( + "{}", + args.version_message(&logging::log_filename().display().to_string()) + ); return; } if let Some(shell) = &args.print_completion { @@ -85,7 +88,7 @@ fn run(mut args: Args) -> Result<()> { } if args.stdout_fetch { - return tui_placeholder::run_fetch(&mut engine, &args); + return fetch::run_fetch(&mut engine, &args); } if args.quiet { @@ -105,11 +108,28 @@ fn display_modules_list(engine: &Engine) { fn print_completion(shell: &str) { let mut command = Args::command(); match shell { - "bash" => clap_complete::generate(clap_complete::Shell::Bash, &mut command, "glances", &mut std::io::stdout()), - "zsh" => clap_complete::generate(clap_complete::Shell::Zsh, &mut command, "glances", &mut std::io::stdout()), - "fish" => clap_complete::generate(clap_complete::Shell::Fish, &mut command, "glances", &mut std::io::stdout()), + "bash" => clap_complete::generate( + clap_complete::Shell::Bash, + &mut command, + "glances", + &mut std::io::stdout(), + ), + "zsh" => clap_complete::generate( + clap_complete::Shell::Zsh, + &mut command, + "glances", + &mut std::io::stdout(), + ), + "fish" => clap_complete::generate( + clap_complete::Shell::Fish, + &mut command, + "glances", + &mut std::io::stdout(), + ), other => { - eprintln!("glances: error: unsupported shell for completion: {other} (use bash, zsh or fish)"); + eprintln!( + "glances: error: unsupported shell for completion: {other} (use bash, zsh or fish)" + ); std::process::exit(2); } } @@ -134,13 +154,19 @@ fn run_stdout(engine: &mut Engine, args: &Args) -> Result<()> { let mut csv = outputs::CsvRenderer::new(); let stdout_items = args.stdout.as_deref().map(outputs::build_stdout_list); let json_plugins = args.stdout_json.as_deref().map(|value| { - value.split(',').map(|p| p.trim().to_string()).filter(|p| !p.is_empty()).collect::>() + value + .split(',') + .map(|p| p.trim().to_string()) + .filter(|p| !p.is_empty()) + .collect::>() }); let csv_plugins = args.stdout_csv.as_deref().map(|value| { value .split(',') .map(|p| match p.split_once('.') { - Some((plugin, attribute)) => (plugin.trim().to_string(), Some(attribute.to_string())), + Some((plugin, attribute)) => { + (plugin.trim().to_string(), Some(attribute.to_string())) + } None => (p.trim().to_string(), None), }) .filter(|(p, _)| !p.is_empty()) @@ -164,10 +190,10 @@ fn run_stdout(engine: &mut Engine, args: &Args) -> Result<()> { std::io::stdout().flush().ok(); count += 1; - if let Some(max) = iterations { - if count >= max { - break; - } + if let Some(max) = iterations + && count >= max + { + break; } let elapsed = start.elapsed().as_secs_f64(); sleep_with_signal(refresh - elapsed); @@ -182,10 +208,10 @@ fn run_quiet(engine: &mut Engine, args: &Args) -> Result<()> { let start = Instant::now(); engine.update(); count += 1; - if let Some(max) = args.stop_after { - if count >= max { - break; - } + if let Some(max) = args.stop_after + && count >= max + { + break; } let elapsed = start.elapsed().as_secs_f64(); sleep_with_signal(refresh - elapsed); diff --git a/crates/glances-cli/src/outputs.rs b/crates/glances-cli/src/outputs.rs index 6adbb21..869c137 100644 --- a/crates/glances-cli/src/outputs.rs +++ b/crates/glances-cli/src/outputs.rs @@ -17,7 +17,11 @@ pub fn build_stdout_list(value: &str) -> Vec { .map(|item| { let parts: Vec<&str> = item.split('.').collect(); match parts.len() { - 1 => StdoutItem { plugin: parts[0].to_string(), key: None, attribute: None }, + 1 => StdoutItem { + plugin: parts[0].to_string(), + key: None, + attribute: None, + }, 2 => StdoutItem { plugin: parts[0].to_string(), key: None, @@ -48,9 +52,18 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String { Some(attribute) => match &stat { Value::Object(map) => { if let Some(value) = map.get(attribute) { - out.push_str(&format!("{}.{}: {}\n", item.plugin, attribute, glances_core::py_str(value))); + out.push_str(&format!( + "{}.{}: {}\n", + item.plugin, + attribute, + glances_core::py_str(value) + )); } else { - tracing::error!("Can not display stat {}.{} (KeyError)", item.plugin, attribute); + tracing::error!( + "Can not display stat {}.{} (KeyError)", + item.plugin, + attribute + ); } } Value::Array(list) => { @@ -66,10 +79,10 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String { let Some(entry_key) = entry_key else { continue; }; - if let Some(wanted) = &item.key { - if wanted != &entry_key { - continue; - } + if let Some(wanted) = &item.key + && wanted != &entry_key + { + continue; } if let Some(value) = entry.get(attribute) { out.push_str(&format!( @@ -85,7 +98,11 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String { _ => {} }, None => { - out.push_str(&format!("{}: {}\n", item.plugin, glances_core::py_repr(&stat))); + out.push_str(&format!( + "{}: {}\n", + item.plugin, + glances_core::py_repr(&stat) + )); } } } @@ -116,7 +133,10 @@ pub struct CsvRenderer { impl CsvRenderer { pub fn new() -> Self { - Self { header: true, ..Default::default() } + Self { + header: true, + ..Default::default() + } } pub fn render(&mut self, engine: &Engine, items: &[(String, Option)]) -> String { @@ -172,7 +192,8 @@ impl CsvRenderer { } self.list_keys.insert(plugin.to_string(), keys_order); if let Some(first) = list.iter().find_map(|entry| entry.as_object()) { - self.header_field_names.insert(plugin.to_string(), first.keys().cloned().collect()); + self.header_field_names + .insert(plugin.to_string(), first.keys().cloned().collect()); } } _ => { @@ -186,7 +207,10 @@ impl CsvRenderer { 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()), + 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},")); @@ -213,9 +237,19 @@ impl CsvRenderer { current.insert(glances_core::py_str(identity), entry.clone()); } let empty = Map::new(); - for identity in self.list_keys.get(plugin).map(|k| k.as_slice()).unwrap_or(&[]) { + for identity in self + .list_keys + .get(plugin) + .map(|k| k.as_slice()) + .unwrap_or(&[]) + { let entry = current.get(identity).unwrap_or(&empty); - for field in self.header_field_names.get(plugin).map(|f| f.as_slice()).unwrap_or(&[]) { + for field in self + .header_field_names + .get(plugin) + .map(|f| f.as_slice()) + .unwrap_or(&[]) + { let value = entry.get(field.as_str()); let text = match value { Some(value) => glances_core::py_str(value), @@ -241,8 +275,15 @@ pub fn render_issue(engine: &Engine) -> String { let Some(plugin) = engine.get_plugin(&name) else { continue; }; - let enabled = if plugin.is_enabled() { "enabled" } else { "disabled" }; - out.push_str(&format!("{name} ({enabled}): {}\n", glances_core::py_repr(plugin.stats()))); + let enabled = if plugin.is_enabled() { + "enabled" + } else { + "disabled" + }; + out.push_str(&format!( + "{name} ({enabled}): {}\n", + glances_core::py_repr(plugin.stats()) + )); } out } diff --git a/crates/glances-cli/src/tui_placeholder.rs b/crates/glances-cli/src/tui_placeholder.rs deleted file mode 100644 index bcd6258..0000000 --- a/crates/glances-cli/src/tui_placeholder.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Temporary placeholder for the terminal UI and the fetch mode. -//! The real TUI lives in the `glances-tui` crate (next milestone). - -use anyhow::Result; -use glances_core::Args; -use glances_plugins::engine::Engine; - -pub fn run_fetch(_engine: &mut Engine, _args: &Args) -> Result<()> { - anyhow::bail!("fetch mode is not implemented yet") -} diff --git a/crates/glances-core/src/actions.rs b/crates/glances-core/src/actions.rs index 7c4a6f4..72f6481 100644 --- a/crates/glances-core/src/actions.rs +++ b/crates/glances-core/src/actions.rs @@ -35,12 +35,20 @@ fn render_range(template: &str, data: &Value, pos: &mut usize) -> Result Option<&str> { @@ -254,8 +263,14 @@ mod tests { #[test] fn test_render_simple() { let data = json!({"name": "nginx", "pid": 42}); - assert_eq!(render_template("kill {{pid}} {{name}}", &data).unwrap(), "kill 42 nginx"); - assert_eq!(render_template("raw {{{name}}}", &data).unwrap(), "raw nginx"); + assert_eq!( + render_template("kill {{pid}} {{name}}", &data).unwrap(), + "kill 42 nginx" + ); + assert_eq!( + render_template("raw {{{name}}}", &data).unwrap(), + "raw nginx" + ); } #[test] @@ -268,13 +283,25 @@ mod tests { #[test] fn test_render_section() { let data = json!({"items": [1, 2]}); - assert_eq!(render_template("{{#items}}x{{/items}}", &data).unwrap(), "xx"); - assert_eq!(render_template("{{^missing}}y{{/missing}}", &data).unwrap(), "y"); + assert_eq!( + render_template("{{#items}}x{{/items}}", &data).unwrap(), + "xx" + ); + assert_eq!( + render_template("{{^missing}}y{{/missing}}", &data).unwrap(), + "y" + ); } #[test] fn test_actions_no_trigger_before_start_timer() { let mut actions = Actions::new(false, 100.0); - assert!(!actions.run("cpu_user", "critical", &["/bin/true".to_string()], false, None)); + assert!(!actions.run( + "cpu_user", + "critical", + &["/bin/true".to_string()], + false, + None + )); } } diff --git a/crates/glances-core/src/args.rs b/crates/glances-core/src/args.rs index 3d50fd4..ef5be2b 100644 --- a/crates/glances-core/src/args.rs +++ b/crates/glances-core/src/args.rs @@ -379,8 +379,14 @@ impl Args { (self.server, "server mode (-s/--server)"), (self.browser, "the server browser (--browser)"), (self.export.is_some(), "export modules (--export)"), - (self.stdout_api_doc, "the Python API documentation (--api-doc)"), - (self.stdout_api_restful_doc, "the RESTful API documentation (--api-restful-doc)"), + ( + self.stdout_api_doc, + "the Python API documentation (--api-doc)", + ), + ( + self.stdout_api_restful_doc, + "the RESTful API documentation (--api-restful-doc)", + ), (self.trace_malloc, "Python memory tracing (--trace-malloc)"), ]; checks.iter().find(|(used, _)| *used).map(|(_, name)| *name) @@ -398,10 +404,7 @@ impl Args { pub fn version_message(&self, log_file: &str) -> String { format!( "Glances version:\t{}\nGlances API version:\t{}\nGlances-rs version:\t{}\nLog file:\t\t{}", - "4.5.7", - GLANCES_API_VERSION, - GLANCES_VERSION, - log_file + "4.5.7", GLANCES_API_VERSION, GLANCES_VERSION, log_file ) } } @@ -492,7 +495,11 @@ mod tests { assert!(parse(&["glances", "-w"]).unsupported().is_some()); assert!(parse(&["glances", "-s"]).unsupported().is_some()); assert!(parse(&["glances", "-c", "host"]).unsupported().is_some()); - assert!(parse(&["glances", "--export", "csv"]).unsupported().is_some()); + assert!( + parse(&["glances", "--export", "csv"]) + .unsupported() + .is_some() + ); assert!(parse(&["glances"]).unsupported().is_none()); } diff --git a/crates/glances-core/src/config.rs b/crates/glances-core/src/config.rs index 22c1ae2..8128db6 100644 --- a/crates/glances-core/src/config.rs +++ b/crates/glances-core/src/config.rs @@ -34,8 +34,11 @@ impl Config { /// Build an empty configuration (used by tests and by callers that /// already read a file). pub fn empty(disable_config_exec: bool) -> Self { - let mut config = Config::default(); - config.disable_config_exec = disable_config_exec; + let mut config = Config { + sections: IndexMap::new(), + loaded_file: None, + disable_config_exec, + }; config.set_defaults(); config } @@ -64,7 +67,11 @@ impl Config { .ok() .filter(|s| !s.is_empty()) .map(PathBuf::from) - .or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join(".config"))); + .or_else(|| { + std::env::var("HOME") + .ok() + .map(|h| PathBuf::from(h).join(".config")) + }); if let Some(base) = user_base { paths.push(base.join("glances").join(CONFIG_FILENAME)); } @@ -74,12 +81,12 @@ impl Config { // Default location: /share/doc/glances (and the checkout // layout next to the executable, useful while developing). - if let Ok(exe) = std::env::current_exe() { - if let Some(bin_dir) = exe.parent() { - for prefix in [bin_dir.join(".."), bin_dir.join("../..")] { - paths.push(prefix.join("share/doc/glances").join(CONFIG_FILENAME)); - paths.push(prefix.join("conf").join(CONFIG_FILENAME)); - } + if let Ok(exe) = std::env::current_exe() + && let Some(bin_dir) = exe.parent() + { + for prefix in [bin_dir.join(".."), bin_dir.join("../..")] { + paths.push(prefix.join("share/doc/glances").join(CONFIG_FILENAME)); + paths.push(prefix.join("conf").join(CONFIG_FILENAME)); } } @@ -96,7 +103,10 @@ impl Config { tracing::info!("Read configuration file '{}'", config_file.display()); } Err(err) => { - tracing::error!("Can not read configuration file '{}': {err}", config_file.display()); + tracing::error!( + "Can not read configuration file '{}': {err}", + config_file.display() + ); std::process::exit(1); } } @@ -123,10 +133,19 @@ impl Config { self.set_default_cwc("cpu", Some("steal"), ["50", "70", "90"]); self.set_default_cwc("cpu", Some("iowait"), ["50", "70", "90"]); // Context switches bottleneck identification #1212 - let ctx_switches_bottleneck = - 500000.0 * 0.10 * std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as f64; - let careful = format!("{}", ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20)); - let warning = format!("{}", ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10)); + let ctx_switches_bottleneck = 500000.0 + * 0.10 + * std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1) as f64; + let careful = format!( + "{}", + ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20) + ); + let warning = format!( + "{}", + ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10) + ); let critical = format!("{ctx_switches_bottleneck}"); self.set_default_cwc("cpu", Some("ctx_switches"), [&careful, &warning, &critical]); @@ -159,7 +178,12 @@ impl Config { self.set_default_cwc("processlist", Some("mem"), ["50", "70", "90"]); } - fn set_default_cwc(&mut self, section: &str, option_header: Option<&str>, cwc: [&str; N]) { + fn set_default_cwc( + &mut self, + section: &str, + option_header: Option<&str>, + cwc: [&str; N], + ) { let header = match option_header { Some(header) => format!("{header}_"), None => String::new(), @@ -171,7 +195,9 @@ impl Config { fn set_default(&mut self, section: &str, option: &str, default: &str) { let entry = self.sections.entry(section.to_string()).or_default(); - entry.entry(option.to_string()).or_insert_with(|| default.to_string()); + entry + .entry(option.to_string()) + .or_insert_with(|| default.to_string()); } pub fn loaded_config_file(&self) -> Option<&Path> { @@ -215,7 +241,9 @@ impl Config { if let Some(value) = ret.as_mut() { for command in find_backticks(value) { if self.disable_config_exec { - tracing::warn!("Config exec disabled: skipping command `{command}` in [{section}] {option}"); + tracing::warn!( + "Config exec disabled: skipping command `{command}` in [{section}] {option}" + ); } else { tracing::warn!("Executing config command `{command}` for [{section}] {option}"); let output = system_exec(&command); @@ -294,11 +322,11 @@ fn parse_ini(content: &str) -> IndexMap> { } // Continuation line (indented). if line.starts_with(' ') || line.starts_with('\t') { - if let (Some(section), Some(key)) = (current_section.as_ref(), last_key.as_ref()) { - if let Some(value) = sections.get_mut(section).and_then(|s| s.get_mut(key)) { - value.push('\n'); - value.push_str(trimmed); - } + if let (Some(section), Some(key)) = (current_section.as_ref(), last_key.as_ref()) + && let Some(value) = sections.get_mut(section).and_then(|s| s.get_mut(key)) + { + value.push('\n'); + value.push_str(trimmed); } continue; } @@ -341,7 +369,9 @@ fn system_exec(command: &str) -> String { return String::new(); }; match Command::new(program).args(args).output() { - Ok(output) => String::from_utf8_lossy(&output.stdout).trim_end().to_string(), + Ok(output) => String::from_utf8_lossy(&output.stdout) + .trim_end() + .to_string(), Err(e) => format!("ERROR: {e}"), } } @@ -387,6 +417,9 @@ mod tests { let config = Config::from_str("[x]\nprefix=`echo hello`\n", false); assert_eq!(config.get_value("x", "prefix", None).unwrap(), "hello"); let config = Config::from_str("[x]\nprefix=`echo hello`\n", true); - assert_eq!(config.get_value("x", "prefix", None).unwrap(), "`echo hello`"); + assert_eq!( + config.get_value("x", "prefix", None).unwrap(), + "`echo hello`" + ); } } diff --git a/crates/glances-core/src/events.rs b/crates/glances-core/src/events.rs index aedf8f4..ba6b054 100644 --- a/crates/glances-core/src/events.rs +++ b/crates/glances-core/src/events.rs @@ -13,7 +13,11 @@ const TREE: [(&str, &[&str], u32); 9] = [ ("High CPU user mode", &["cpu_user"], 2), ("High CPU kernel usage", &["cpu_system"], 2), ("High CPU I/O waiting", &["cpu_iowait"], 2), - ("Large CPU stolen time. System running the hypervisor is too busy.", &["cpu_steal"], 2), + ( + "Large CPU stolen time. System running the hypervisor is too busy.", + &["cpu_steal"], + 2, + ), ("High CPU niced value", &["cpu_niced"], 2), ("System overloaded in the last 5 minutes", &["load"], 2), ("High swap (paging) usage", &["memswap"], 2), @@ -28,14 +32,22 @@ pub fn build_global_message() -> String { let mut best_min = 0u32; let mut best_msg = TREE[0].0; for (msg, names, min) in TREE { - let weight: u32 = names.iter().filter_map(|n| current.get(*n)).map(|t| t.value() as u32).sum(); + let weight: u32 = names + .iter() + .filter_map(|n| current.get(*n)) + .map(|t| t.value() as u32) + .sum(); if weight > best_weight { best_weight = weight; best_min = min; best_msg = msg; } } - if best_weight >= best_min { best_msg.to_string() } else { TREE[0].0.to_string() } + if best_weight >= best_min { + best_msg.to_string() + } else { + TREE[0].0.to_string() + } } #[derive(Debug, Clone)] @@ -106,11 +118,16 @@ impl Event { let mut sorted: Vec = proc_list.to_vec(); sort_stats(&mut sorted, sort_key, "memory_percent", true); for process in sorted.iter().take(6) { - let name = process.get("name").and_then(Value::as_str).unwrap_or("?").to_string(); + let name = process + .get("name") + .and_then(Value::as_str) + .unwrap_or("?") + .to_string(); *self.top_dict.entry(name).or_insert(0) += 1; } - let mut counts: Vec<(String, i64)> = self.top_dict.iter().map(|(k, v)| (k.clone(), *v)).collect(); - counts.sort_by(|a, b| b.1.cmp(&a.1)); + let mut counts: Vec<(String, i64)> = + self.top_dict.iter().map(|(k, v)| (k.clone(), *v)).collect(); + counts.sort_by_key(|entry| std::cmp::Reverse(entry.1)); self.top = counts.into_iter().take(3).map(|(name, _)| name).collect(); } else { self.top_dict.clear(); @@ -136,7 +153,12 @@ impl Default for EventsList { impl EventsList { pub fn new(max_events: usize, min_duration: f64, min_interval: f64) -> Self { - Self { events: Vec::new(), max_events, min_duration, min_interval } + Self { + events: Vec::new(), + max_events, + min_duration, + min_interval, + } } pub fn get(&self) -> Vec { @@ -184,14 +206,27 @@ impl EventsList { } /// Add a new item to the events list. - pub fn add(&mut self, event_state: &str, event_type: &str, event_value: f64, proc_desc: &str) -> usize { + pub fn add( + &mut self, + event_state: &str, + event_type: &str, + event_value: f64, + proc_desc: &str, + ) -> usize { let event_time = chrono::Utc::now().timestamp() as f64; let global_message = build_global_message(); let proc_list: Vec = processes().lock().unwrap().get_list(false).to_vec(); match self.event_exist(event_time, event_type) { None => { - self.create_event(event_time, event_state, event_type, event_value, proc_desc, &global_message); + self.create_event( + event_time, + event_state, + event_type, + event_value, + proc_desc, + &global_message, + ); } Some(index) => { self.update_event( @@ -266,7 +301,14 @@ impl EventsList { Self::set_process_sort(event_type); let sort_key = Self::get_event_sort_key(event_type).to_string(); let event = &mut self.events[event_index]; - event.update(event_state, event_value, &sort_key, proc_list, proc_desc, global_message); + event.update( + event_state, + event_value, + &sort_key, + proc_list, + proc_desc, + global_message, + ); } } diff --git a/crates/glances-core/src/filter.rs b/crates/glances-core/src/filter.rs index 64ba171..dc82ecd 100644 --- a/crates/glances-core/src/filter.rs +++ b/crates/glances-core/src/filter.rs @@ -74,9 +74,14 @@ impl Filter { return false; } if self.filter_key.is_none() { - return self.filter_matches(process, "name", filter_re) || self.filter_matches(process, "cmdline", filter_re); + return self.filter_matches(process, "name", filter_re) + || self.filter_matches(process, "cmdline", filter_re); } - self.filter_matches(process, self.filter_key.as_deref().unwrap_or("name"), filter_re) + self.filter_matches( + process, + self.filter_key.as_deref().unwrap_or("name"), + filter_re, + ) } fn filter_matches(&self, process: &Value, key: &str, filter_re: &Regex) -> bool { @@ -89,13 +94,21 @@ impl Filter { if key == "cmdline" && !items.is_empty() { items[0].as_str().unwrap_or_default().to_string() } else { - items.iter().map(|i| i.as_str().unwrap_or_default()).collect::>().join(" ") + items + .iter() + .map(|i| i.as_str().unwrap_or_default()) + .collect::>() + .join(" ") } } Value::Null => return false, other => crate::units::py_str(other), }; - filter_re.is_match(&text) && filter_re.find(&text).map(|m| m.len() == text.len()).unwrap_or(false) + filter_re.is_match(&text) + && filter_re + .find(&text) + .map(|m| m.len() == text.len()) + .unwrap_or(false) } } diff --git a/crates/glances-core/src/history.rs b/crates/glances-core/src/history.rs index b8cf309..400151f 100644 --- a/crates/glances-core/src/history.rs +++ b/crates/glances-core/src/history.rs @@ -15,7 +15,11 @@ pub struct Attribute { } impl Attribute { - pub fn new(name: impl Into, description: impl Into, history_max_size: Option) -> Self { + pub fn new( + name: impl Into, + description: impl Into, + history_max_size: Option, + ) -> Self { Self { name: name.into(), description: description.into(), @@ -44,7 +48,15 @@ impl Attribute { if nb == 0 { self.history.iter().cloned().collect() } else { - self.history.iter().rev().take(nb).collect::>().into_iter().rev().cloned().collect() + self.history + .iter() + .rev() + .take(nb) + .collect::>() + .into_iter() + .rev() + .cloned() + .collect() } } @@ -63,7 +75,13 @@ pub struct History { } impl History { - pub fn add(&mut self, key: &str, value: f64, description: &str, history_max_size: Option) { + pub fn add( + &mut self, + key: &str, + value: f64, + description: &str, + history_max_size: Option, + ) { let attribute = self .attributes .entry(key.to_string()) @@ -78,16 +96,24 @@ impl History { } pub fn get(&self, nb: usize) -> IndexMap, f64)>> { - self.attributes.iter().map(|(key, attr)| (key.clone(), attr.history_raw(nb))).collect() + self.attributes + .iter() + .map(|(key, attr)| (key.clone(), attr.history_raw(nb))) + .collect() } pub fn get_json(&self, nb: usize) -> IndexMap> { - self.attributes.iter().map(|(key, attr)| (key.clone(), attr.history_json(nb))).collect() + self.attributes + .iter() + .map(|(key, attr)| (key.clone(), attr.history_json(nb))) + .collect() } /// Return the values of one item (used for TUI trends). pub fn item_values(&self, item: &str) -> Option> { - self.attributes.get(item).map(|a| a.history.iter().map(|(_, v)| *v).collect()) + self.attributes + .get(item) + .map(|a| a.history.iter().map(|(_, v)| *v).collect()) } } diff --git a/crates/glances-core/src/lib.rs b/crates/glances-core/src/lib.rs index ede763a..5ec8249 100644 --- a/crates/glances-core/src/lib.rs +++ b/crates/glances-core/src/lib.rs @@ -1,5 +1,7 @@ //! glances-core: shared foundations of the glances-rs rewrite. //! +#![allow(clippy::type_complexity)] + //! This crate contains the pieces that are common to every plugin and output: //! argument parsing, configuration file handling, the plugin model (limits, //! views, history, rates...), formatting helpers, the event/threshold/action diff --git a/crates/glances-core/src/plugin.rs b/crates/glances-core/src/plugin.rs index a5f80be..9f6fa22 100644 --- a/crates/glances-core/src/plugin.rs +++ b/crates/glances-core/src/plugin.rs @@ -161,7 +161,10 @@ pub struct Field { impl Field { pub fn new(description: &str) -> Self { - Self { description: description.to_string(), ..Default::default() } + Self { + description: description.to_string(), + ..Default::default() + } } pub fn unit(mut self, unit: &str) -> Self { @@ -208,7 +211,9 @@ impl Field { pub fn unit_type(&self) -> &'static str { match self.unit.as_deref() { Some("percent" | "percents") => "float", - Some("number" | "numbers" | "int" | "ints" | "second" | "seconds" | "byte" | "bytes") => "int", + Some( + "number" | "numbers" | "int" | "ints" | "second" | "seconds" | "byte" | "bytes", + ) => "int", _ => "float", } } @@ -233,7 +238,10 @@ pub struct HistoryItem { impl HistoryItem { pub fn new(name: &str, description: &str) -> Self { - Self { name: name.to_string(), description: description.to_string() } + Self { + name: name.to_string(), + description: description.to_string(), + } } } @@ -357,11 +365,16 @@ impl PluginModel { /// Load limits from the configuration file. pub fn load_limits(&mut self, config: &Config) { self.limits.clear(); - self.limits.insert("history_size".to_string(), Limit::Num(HISTORY_MAX_SIZE as f64)); + self.limits.insert( + "history_size".to_string(), + Limit::Num(HISTORY_MAX_SIZE as f64), + ); if config.has_section("global") { - let history_size = config.get_float_value("global", "history_size", HISTORY_MAX_SIZE as f64); - self.limits.insert("history_size".to_string(), Limit::Num(history_size)); + let history_size = + config.get_float_value("global", "history_size", HISTORY_MAX_SIZE as f64); + self.limits + .insert("history_size".to_string(), Limit::Num(history_size)); } if config.has_section(&self.name) { @@ -372,7 +385,8 @@ impl PluginModel { self.limits.insert(limit.clone(), Limit::Num(number)); } Err(_) => { - let items: Vec = value.split(',').map(|s| s.trim().to_string()).collect(); + let items: Vec = + value.split(',').map(|s| s.trim().to_string()).collect(); self.limits.insert(limit.clone(), Limit::List(items)); } } @@ -389,7 +403,8 @@ impl PluginModel { } pub fn set_refresh(&mut self, value: f64) { - self.limits.insert(format!("{}_refresh", self.name), Limit::Num(value)); + self.limits + .insert(format!("{}_refresh", self.name), Limit::Num(value)); } pub fn get_refresh(&self) -> f64 { @@ -424,12 +439,19 @@ impl PluginModel { } pub fn is_limit(&self, criticality: &str, stat_name: &str) -> bool { - let key = format!("{}_{criticality}", self.get_stat_name(Some(stat_name), None).to_lowercase()); + let key = format!( + "{}_{criticality}", + self.get_stat_name(Some(stat_name), None).to_lowercase() + ); self.limits.contains_key(&key) } /// Return the (commands, repeat) action pair for the alert. - pub fn get_limit_action(&self, criticality: &str, stat_name: &str) -> (Option>, bool) { + pub fn get_limit_action( + &self, + criticality: &str, + stat_name: &str, + ) -> (Option>, bool) { let candidates = [ (format!("{stat_name}_{criticality}_action"), false), (format!("{stat_name}_{criticality}_action_repeat"), true), @@ -446,10 +468,18 @@ impl PluginModel { pub fn get_limit_log(&self, stat_name: &str, default_action: bool) -> bool { if let Some(limit) = self.limits.get(&format!("{stat_name}_log")) { - return limit.as_list().and_then(|l| l.first()).map(|v| v.to_lowercase() == "true").unwrap_or(false); + return limit + .as_list() + .and_then(|l| l.first()) + .map(|v| v.to_lowercase() == "true") + .unwrap_or(false); } if let Some(limit) = self.limits.get(&format!("{}_log", self.name)) { - return limit.as_list().and_then(|l| l.first()).map(|v| v.to_lowercase() == "true").unwrap_or(false); + return limit + .as_list() + .and_then(|l| l.first()) + .map(|v| v.to_lowercase() == "true") + .unwrap_or(false); } default_action } @@ -477,7 +507,10 @@ impl PluginModel { key = format!("{key}_{header}"); } match self.limits.get(&format!("{key}_{value}")) { - Some(Limit::List(items)) => items.first().map(|v| v.to_lowercase() == "true").unwrap_or(false), + Some(Limit::List(items)) => items + .first() + .map(|v| v.to_lowercase() == "true") + .unwrap_or(false), Some(Limit::Num(n)) => *n != 0.0, None => false, } @@ -487,7 +520,11 @@ impl PluginModel { pub fn is_show(&self, value: &str, header: &str) -> bool { let alias = self.has_alias(value); self.get_conf_value("show", header).iter().any(|pattern| { - fullmatch(pattern, value) || alias.as_deref().map(|a| fullmatch(pattern, a)).unwrap_or(false) + fullmatch(pattern, value) + || alias + .as_deref() + .map(|a| fullmatch(pattern, a)) + .unwrap_or(false) }) } @@ -495,7 +532,11 @@ impl PluginModel { pub fn is_hide(&self, value: &str, header: &str) -> bool { let alias = self.has_alias(value); self.get_conf_value("hide", header).iter().any(|pattern| { - fullmatch(pattern, value) || alias.as_deref().map(|a| fullmatch(pattern, a)).unwrap_or(false) + fullmatch(pattern, value) + || alias + .as_deref() + .map(|a| fullmatch(pattern, a)) + .unwrap_or(false) }) } @@ -537,17 +578,17 @@ impl PluginModel { pub fn get_stat_name(&self, header: Option<&str>, action_key: Option<&str>) -> String { let mut ret = self.name.clone(); - if let Some(key) = action_key { - if !key.is_empty() { - ret.push('_'); - ret.push_str(key); - } + if let Some(key) = action_key + && !key.is_empty() + { + ret.push('_'); + ret.push_str(key); } - if let Some(header) = header { - if !header.is_empty() { - ret.push('_'); - ret.push_str(header); - } + if let Some(header) = header + && !header.is_empty() + { + ret.push('_'); + ret.push_str(header); } ret } @@ -560,15 +601,32 @@ impl PluginModel { .map(|(name, _)| name.clone()) .collect(); for field_name in mmm_names { - let unit = self.fields.get(&field_name).and_then(|f| f.unit.clone()).unwrap_or_default(); + let unit = self + .fields + .get(&field_name) + .and_then(|f| f.unit.clone()) + .unwrap_or_default(); self.mmm.insert( field_name.clone(), - MmmState { values: VecDeque::new(), min: None, max: None }, + MmmState { + values: VecDeque::new(), + min: None, + max: None, + }, ); for (suffix, description) in [ - ("_min", format!("Minimum {field_name} observed since Glances startup.")), - ("_max", format!("Maximum {field_name} observed since Glances startup.")), - ("_mean", format!("Mean (average) {field_name} computed from the history.")), + ( + "_min", + format!("Minimum {field_name} observed since Glances startup."), + ), + ( + "_max", + format!("Maximum {field_name} observed since Glances startup."), + ), + ( + "_mean", + format!("Mean (average) {field_name} computed from the history."), + ), ] { let generated = format!("{field_name}{suffix}"); let unit = unit.clone(); @@ -604,7 +662,10 @@ impl PluginModel { let mean = state.values.iter().sum::() / state.values.len() as f64; map.insert(format!("{field_name}_min"), json!(min)); map.insert(format!("{field_name}_max"), json!(max)); - map.insert(format!("{field_name}_mean"), json!((mean * 100.0).round() / 100.0)); + map.insert( + format!("{field_name}_mean"), + json!((mean * 100.0).round() / 100.0), + ); } } @@ -623,7 +684,8 @@ impl PluginModel { .map(|list| { list.iter() .filter_map(|item| { - item.get(key).map(|v| (crate::units::py_str(v), item.clone())) + item.get(key) + .map(|v| (crate::units::py_str(v), item.clone())) }) .collect() }) @@ -633,7 +695,7 @@ impl PluginModel { for item in items.iter_mut() { let previous_item = key .and_then(|k| item.get(k)) - .map(|v| crate::units::py_str(v)) + .map(crate::units::py_str) .and_then(|k| previous_by_key.get(&k).cloned()) .unwrap_or(Value::Object(Map::new())); self.compute_rate_on_stat(item, Some(&previous_item)); @@ -660,13 +722,20 @@ impl PluginModel { let Value::Object(map) = stat else { return; }; - let rate_fields: Vec = - self.fields.iter().filter(|(_, f)| f.rate).map(|(name, _)| name.clone()).collect(); + let rate_fields: Vec = self + .fields + .iter() + .filter(|(_, f)| f.rate) + .map(|(name, _)| name.clone()) + .collect(); for field in rate_fields { if !map.contains_key(&field) { continue; } - map.insert("time_since_update".to_string(), json!(self.time_since_last_update)); + map.insert( + "time_since_update".to_string(), + json!(self.time_since_last_update), + ); let current_value = map.get(&field).cloned().unwrap_or(Value::Null); map.insert(format!("{field}_gauge"), current_value.clone()); let current = as_f64(¤t_value); @@ -703,7 +772,10 @@ impl PluginModel { None => self.views.get(field), Some(item) => self.views.get(item).and_then(|v| v.get(field)), }; - item_views.and_then(|v| v.get(option)).cloned().unwrap_or_else(|| json!("DEFAULT")) + item_views + .and_then(|v| v.get(option)) + .cloned() + .unwrap_or_else(|| json!("DEFAULT")) } pub fn view_decoration(&self, item: Option<&str>, field: &str) -> Decoration { @@ -714,7 +786,9 @@ impl PluginModel { } pub fn view_bool(&self, item: Option<&str>, field: &str, option: &str) -> bool { - self.get_view(item, field, option).as_bool().unwrap_or(false) + self.get_view(item, field, option) + .as_bool() + .unwrap_or(false) } /// Override the decoration of a view (used by plugins which add specific @@ -733,18 +807,37 @@ impl PluginModel { let Some(definition) = self.fields.get(field).cloned() else { return Decoration::Default; }; - if definition.rate && self.stats.is_object() { - if self.stats.get("time_since_update").and_then(as_f64) == Some(0.0) { - return Decoration::Default; - } + if definition.rate + && self.stats.is_object() + && self.stats.get("time_since_update").and_then(as_f64) == Some(0.0) + { + return Decoration::Default; } if definition.log { let (current, minimum, maximum) = numeric_alert_args(value); - return self.get_alert(current, minimum, maximum, true, false, Some(field), None, true); + return self.get_alert( + current, + minimum, + maximum, + true, + false, + Some(field), + None, + true, + ); } if definition.alert { let (current, minimum, maximum) = numeric_alert_args(value); - return self.get_alert(current, minimum, maximum, true, false, Some(field), None, false); + return self.get_alert( + current, + minimum, + maximum, + true, + false, + Some(field), + None, + false, + ); } Decoration::Default } @@ -756,14 +849,18 @@ impl PluginModel { let hidden = if !self.hide_zero { false } else if let Some(key) = key { - if let Some(existing) = self.views.get(key).and_then(|v| v.get(field)).and_then(|v| v.get("hidden")) { + if let Some(existing) = self + .views + .get(key) + .and_then(|v| v.get(field)) + .and_then(|v| v.get("hidden")) + { let mut hidden = existing.as_bool().unwrap_or(false); - if self.hide_zero_fields.iter().any(|f| f == field) { - if let Some(raw) = self.get_raw_stats_key(field, key).and_then(|v| as_f64(&v)) { - if raw > self.hide_threshold_bytes { - hidden = false; - } - } + if self.hide_zero_fields.iter().any(|f| f == field) + && let Some(raw) = self.get_raw_stats_key(field, key).and_then(|v| as_f64(&v)) + && raw > self.hide_threshold_bytes + { + hidden = false; } hidden } else { @@ -860,29 +957,31 @@ impl PluginModel { let value = current * 100.0 / maximum; let stat_name = self.get_stat_name(header, action_key).to_lowercase(); - let mut ret = if is_max { Decoration::Max } else { Decoration::Ok }; + let mut ret = if is_max { + Decoration::Max + } else { + Decoration::Ok + }; let critical = self.get_limit("critical", &stat_name); let warning = self.get_limit("warning", &stat_name); let careful = self.get_limit("careful", &stat_name); - if let Some(critical) = critical { - if value >= critical { - ret = Decoration::Critical; - } + if let Some(critical) = critical + && value >= critical + { + ret = Decoration::Critical; } - if ret == Decoration::Ok { - if let Some(warning) = warning { - if value >= warning { - ret = Decoration::Warning; - } - } + if ret == Decoration::Ok + && let Some(warning) = warning + && value >= warning + { + ret = Decoration::Warning; } - if ret == Decoration::Ok { - if let Some(careful) = careful { - if value >= careful { - ret = Decoration::Careful; - } - } + if ret == Decoration::Ok + && let Some(careful) = careful + && value >= careful + { + ret = Decoration::Careful; } if ret == Decoration::Ok && careful.is_none() && warning.is_none() && critical.is_none() { ret = Decoration::Default; @@ -903,12 +1002,20 @@ impl PluginModel { Decoration::Max => "MAX", _ => "OK", }; - events().lock().unwrap().add(log_ret, &stat_name.to_uppercase(), value, ""); + events() + .lock() + .unwrap() + .add(log_ret, &stat_name.to_uppercase(), value, ""); } thresholds().lock().unwrap().add(&stat_name, ret.name()); - self.manage_action(&stat_name, ret.name().to_lowercase().as_str(), header, action_key); + self.manage_action( + &stat_name, + ret.name().to_lowercase().as_str(), + header, + action_key, + ); match (ret, log_str) { (Decoration::Ok, true) => Decoration::OkLog, @@ -919,11 +1026,22 @@ impl PluginModel { } } - pub fn get_alert_log(&mut self, current: f64, maximum: f64, header: Option<&str>) -> Decoration { + pub fn get_alert_log( + &mut self, + current: f64, + maximum: f64, + header: Option<&str>, + ) -> Decoration { self.get_alert(current, 0.0, maximum, false, false, header, None, true) } - fn manage_action(&mut self, stat_name: &str, trigger: &str, header: Option<&str>, action_key: Option<&str>) { + fn manage_action( + &mut self, + stat_name: &str, + trigger: &str, + header: Option<&str>, + action_key: Option<&str>, + ) { let (command, repeat) = self.get_limit_action(trigger, stat_name); match command { None => { @@ -935,12 +1053,11 @@ impl PluginModel { let mut mustache_dict = match &stats_action { Value::Array(items) if action_key.is_some() => { let action_key = action_key.unwrap(); - let key_field = self.item_key.clone(); + let key_field = self.item_key; items .iter() .find(|item| { key_field - .as_deref() .and_then(|k| item.get(k)) .map(crate::units::py_str) .as_deref() @@ -953,15 +1070,29 @@ impl PluginModel { _ => Value::Object(Map::new()), }; if let Value::Object(map) = &mut mustache_dict { - map.insert("critical".to_string(), json!(self.get_limit("critical", stat_name))); - map.insert("warning".to_string(), json!(self.get_limit("warning", stat_name))); - map.insert("careful".to_string(), json!(self.get_limit("careful", stat_name))); + map.insert( + "critical".to_string(), + json!(self.get_limit("critical", stat_name)), + ); + map.insert( + "warning".to_string(), + json!(self.get_limit("warning", stat_name)), + ); + map.insert( + "careful".to_string(), + json!(self.get_limit("careful", stat_name)), + ); map.insert( "time".to_string(), - json!(chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.6f").to_string()), + json!( + chrono::Local::now() + .format("%Y-%m-%dT%H:%M:%S%.6f") + .to_string() + ), ); } - self.actions.run(stat_name, trigger, &commands, repeat, Some(&mustache_dict)); + self.actions + .run(stat_name, trigger, &commands, repeat, Some(&mustache_dict)); } } } @@ -994,7 +1125,14 @@ impl PluginModel { } /// Add a `key: value` pair to the TUI (port of `curse_add_stat`). - pub fn curse_add_stat(&self, key: &str, width: Option, header: &str, separator: &str, trailer: &str) -> Vec { + pub fn curse_add_stat( + &self, + key: &str, + width: Option, + header: &str, + separator: &str, + trailer: &str, + ) -> Vec { let Some(value) = self.stats.get(key) else { return Vec::new(); }; @@ -1006,7 +1144,9 @@ impl PluginModel { 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) + self.stats + .get(format!("{key}_rate_per_sec")) + .unwrap_or(&Value::Null) } else { value }; @@ -1020,7 +1160,9 @@ impl PluginModel { 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()) { + } 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) @@ -1030,13 +1172,18 @@ impl PluginModel { (item, value_text) } Some(width) => { - let item = format!("{header}{key_name: 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()) { + } 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) @@ -1048,7 +1195,11 @@ impl PluginModel { }; let item = format!("{msg_item}{trailer}"); - let decoration = if value.is_some() { self.view_decoration(None, key) } else { Decoration::Default }; + let decoration = if value.is_some() { + self.view_decoration(None, key) + } else { + Decoration::Default + }; let optional = self.view_bool(None, key, "optional"); vec![ @@ -1088,12 +1239,14 @@ impl PluginModel { } let history_max_size = self.history_size(); let items = self.items_history_list.clone(); - let key = self.item_key.clone(); + let key = self.item_key; for item in items { match &export { Value::Array(list) => { for entry in list { - let Some(item_key) = key.as_deref().and_then(|k| entry.get(k)).map(crate::units::py_str) else { + let Some(item_key) = + key.and_then(|k| entry.get(k)).map(crate::units::py_str) + else { continue; }; if let Some(value) = entry.get(&item.name).and_then(as_f64) { @@ -1108,7 +1261,12 @@ impl PluginModel { } Value::Object(_) => { if let Some(value) = export.get(&item.name).and_then(as_f64) { - self.history.add(&item.name, value, &item.description, Some(history_max_size)); + self.history.add( + &item.name, + value, + &item.description, + Some(history_max_size), + ); } } _ => {} diff --git a/crates/glances-core/src/processes.rs b/crates/glances-core/src/processes.rs index 95c5b76..97094b3 100644 --- a/crates/glances-core/src/processes.rs +++ b/crates/glances-core/src/processes.rs @@ -15,8 +15,15 @@ use sysinfo::{ProcessesToUpdate, System, Users}; pub const MANDATORY_PROCESS_STATS: [&str; 2] = ["pid", "name"]; /// Available process sort keys. -pub const SORT_PROCESSES_STATS: [&str; 7] = - ["cpu_percent", "memory_percent", "username", "cpu_times", "io_counters", "name", "cpu_num"]; +pub const SORT_PROCESSES_STATS: [&str; 7] = [ + "cpu_percent", + "memory_percent", + "username", + "cpu_times", + "io_counters", + "name", + "cpu_num", +]; /// Sort keys as displayed to humans. pub fn sort_for_human(key: &str) -> &str { @@ -155,7 +162,9 @@ impl Processes { } pub fn pid_max(&self) -> Option { - std::fs::read_to_string("/proc/sys/kernel/pid_max").ok().and_then(|v| v.trim().parse::().ok()) + std::fs::read_to_string("/proc/sys/kernel/pid_max") + .ok() + .and_then(|v| v.trim().parse::().ok()) } pub fn processes_count(&self, max_processes: Option) -> i64 { @@ -199,20 +208,28 @@ impl Processes { let stat = read_proc_stat(pid); let name = process.name().to_string_lossy().to_string(); - let cmdline: Vec = - process.cmd().iter().map(|c| c.to_string_lossy().to_string()).collect(); + let cmdline: Vec = process + .cmd() + .iter() + .map(|c| c.to_string_lossy().to_string()) + .collect(); let username = process .user_id() .and_then(|uid| self.users.get_user_by_id(uid)) .map(|u| u.name().to_string()) .unwrap_or_default(); - let status = stat.as_ref().map(|s| s.state.clone()).unwrap_or_else(|| "?".to_string()); + let status = stat + .as_ref() + .map(|s| s.state.clone()) + .unwrap_or_else(|| "?".to_string()); let nice = stat.as_ref().map(|s| s.nice).unwrap_or(0); let num_threads = stat.as_ref().map(|s| s.num_threads).unwrap_or(0); let cpu_num = stat.as_ref().map(|s| s.processor).unwrap_or(0); - let (user_time, system_time) = - stat.as_ref().map(|s| (s.utime, s.stime)).unwrap_or((0.0, 0.0)); + let (user_time, system_time) = stat + .as_ref() + .map(|s| (s.utime, s.stime)) + .unwrap_or((0.0, 0.0)); let rss = process.memory() as f64; let vms = process.virtual_memory() as f64; @@ -269,7 +286,12 @@ impl Processes { } // Sort processes by the current sort key. - sort_stats(&mut processlist, &self.sort_key, "memory_percent", self.sort_reverse); + sort_stats( + &mut processlist, + &self.sort_key, + "memory_percent", + self.sort_reverse, + ); self.update_processcount(&processlist); @@ -284,23 +306,25 @@ impl Processes { { self.extended_process_pid = Some(pid); } - if let Some(target) = self.extended_process_pid { - if target == pid { - let extended = self.set_extended_stats(pid, proc); - if let Value::Object(map) = extended { - if let Some(obj) = proc.as_object_mut() { - for (k, v) in map { - obj.insert(k, v); - } - } + if let Some(target) = self.extended_process_pid + && target == pid + { + let extended = self.set_extended_stats(pid, proc); + if let Value::Object(map) = extended + && let Some(obj) = proc.as_object_mut() + { + for (k, v) in map { + obj.insert(k, v); } } } } // Remove non running processes from the IO cache. - let running: std::collections::HashSet = - processlist.iter().filter_map(|p| p.get("pid").and_then(Value::as_i64)).collect(); + let running: std::collections::HashSet = processlist + .iter() + .filter_map(|p| p.get("pid").and_then(Value::as_i64)) + .collect(); self.io_old.retain(|pid, _| running.contains(pid)); // Export list. @@ -343,8 +367,12 @@ impl Processes { let mean_counter_key = format!("{stat_prefix}_mean_counter"); let prev_min = prev.and_then(|p| p.get(&min_key)).and_then(Value::as_f64); let prev_max = prev.and_then(|p| p.get(&max_key)).and_then(Value::as_f64); - let prev_sum = prev.and_then(|p| p.get(&mean_sum_key)).and_then(Value::as_f64); - let prev_count = prev.and_then(|p| p.get(&mean_counter_key)).and_then(Value::as_i64); + let prev_sum = prev + .and_then(|p| p.get(&mean_sum_key)) + .and_then(Value::as_f64); + let prev_count = prev + .and_then(|p| p.get(&mean_counter_key)) + .and_then(Value::as_i64); let min = prev_min.map(|m| m.min(current)).unwrap_or(current); let max = prev_max.map(|m| m.max(current)).unwrap_or(current); let sum = prev_sum.map(|s| s + current).unwrap_or(current); @@ -356,8 +384,14 @@ impl Processes { ret.insert(format!("{stat_prefix}_mean"), json!(sum / count as f64)); }; - let cpu_percent = proc.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0); - let memory_percent = proc.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0); + let cpu_percent = proc + .get("cpu_percent") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let memory_percent = proc + .get("memory_percent") + .and_then(Value::as_f64) + .unwrap_or(0.0); prefix(&mut ret, "cpu", cpu_percent, previous.as_ref()); prefix(&mut ret, "memory", memory_percent, previous.as_ref()); ret.insert("extended_stats".to_string(), json!(true)); @@ -387,19 +421,31 @@ impl Processes { fn update_list(&self, processlist: &[Value]) -> Vec { if !self.filter_focus.is_empty() { - return processlist.iter().filter(|p| self.filter_focus.is_filtered(p)).cloned().collect(); + return processlist + .iter() + .filter(|p| self.filter_focus.is_filtered(p)) + .cloned() + .collect(); } if self.filter.filter().is_none() { return processlist.to_vec(); } - processlist.iter().filter(|p| self.filter.is_filtered(p)).cloned().collect() + processlist + .iter() + .filter(|p| self.filter.is_filtered(p)) + .cloned() + .collect() } fn update_export_list(&self, processlist: &[Value]) -> Vec { if self.filter_export.is_empty() { return Vec::new(); } - processlist.iter().filter(|p| self.filter_export.is_filtered(p)).cloned().collect() + processlist + .iter() + .filter(|p| self.filter_export.is_filtered(p)) + .cloned() + .collect() } /// Get the process list (sorted or not). @@ -422,16 +468,23 @@ impl Processes { } pub fn get_stats(&self, pid: i64) -> Option<&Value> { - self.processlist.iter().find(|p| p.get("pid").and_then(Value::as_i64) == Some(pid)) + self.processlist + .iter() + .find(|p| p.get("pid").and_then(Value::as_i64) == Some(pid)) } /// Decrease the nice level of a process (higher priority). pub fn nice_decrease(&self, pid: i64) { let current = read_nice(pid).unwrap_or(0) as i32; if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, current - 1) } == 0 { - tracing::info!("Set nice level of process {pid} to {} (higher the priority)", current - 1); + tracing::info!( + "Set nice level of process {pid} to {} (higher the priority)", + current - 1 + ); } else { - tracing::warn!("Can not decrease (higher the priority) the nice level of process {pid} (access denied)"); + tracing::warn!( + "Can not decrease (higher the priority) the nice level of process {pid} (access denied)" + ); } } @@ -439,15 +492,23 @@ impl Processes { pub fn nice_increase(&self, pid: i64) { let current = read_nice(pid).unwrap_or(0) as i32; if unsafe { libc::setpriority(libc::PRIO_PROCESS, pid as libc::id_t, current + 1) } == 0 { - tracing::info!("Set nice level of process {pid} to {} (lower the priority)", current + 1); + tracing::info!( + "Set nice level of process {pid} to {} (lower the priority)", + current + 1 + ); } else { - tracing::warn!("Can not increase (lower the priority) the nice level of process {pid} (access denied)"); + tracing::warn!( + "Can not increase (lower the priority) the nice level of process {pid} (access denied)" + ); } } /// Kill a process. pub fn kill(&self, pid: i64) { - assert!(pid != std::process::id() as i64, "Glances can kill itself..."); + assert!( + pid != std::process::id() as i64, + "Glances can kill itself..." + ); tracing::debug!("Send kill signal to process: {pid}"); unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL); @@ -472,23 +533,48 @@ pub fn sort_stats(stats: &mut [Value], sorted_by: &str, sorted_by_secondary: &st "io_counters" => { // [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag] let counters = process.get("io_counters").and_then(Value::as_array); - let read = counters.and_then(|c| c.first()).and_then(Value::as_f64).unwrap_or(0.0); - let write = counters.and_then(|c| c.get(1)).and_then(Value::as_f64).unwrap_or(0.0); - let read_old = counters.and_then(|c| c.get(2)).and_then(Value::as_f64).unwrap_or(0.0); - let write_old = counters.and_then(|c| c.get(3)).and_then(Value::as_f64).unwrap_or(0.0); + let read = counters + .and_then(|c| c.first()) + .and_then(Value::as_f64) + .unwrap_or(0.0); + let write = counters + .and_then(|c| c.get(1)) + .and_then(Value::as_f64) + .unwrap_or(0.0); + let read_old = counters + .and_then(|c| c.get(2)) + .and_then(Value::as_f64) + .unwrap_or(0.0); + let write_old = counters + .and_then(|c| c.get(3)) + .and_then(Value::as_f64) + .unwrap_or(0.0); read - read_old + write - write_old } "cpu_times" => { let times = process.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 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); user + system } - _ => process.get(key).and_then(Value::as_f64).unwrap_or(f64::NEG_INFINITY), + _ => process + .get(key) + .and_then(Value::as_f64) + .unwrap_or(f64::NEG_INFINITY), } }; let string_key = |process: &Value, key: &str| -> String { - process.get(key).and_then(Value::as_str).unwrap_or("~").to_string() + process + .get(key) + .and_then(Value::as_str) + .unwrap_or("~") + .to_string() }; stats.sort_by(|a, b| { @@ -523,22 +609,53 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec { let no_io = [0u64; 5]; let mut programs: indexmap::IndexMap = indexmap::IndexMap::new(); for process in processlist { - let name = process.get("name").and_then(Value::as_str).unwrap_or("?").to_string(); - let num_threads = process.get("num_threads").and_then(Value::as_f64).unwrap_or(0.0).max(0.0); - let cpu_percent = process.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0); - let memory_percent = process.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0); - let cpu_times = process.get("cpu_times").cloned().unwrap_or_else(|| json!({})); - let memory_info = process.get("memory_info").cloned().unwrap_or_else(|| json!({})); + let name = process + .get("name") + .and_then(Value::as_str) + .unwrap_or("?") + .to_string(); + let num_threads = process + .get("num_threads") + .and_then(Value::as_f64) + .unwrap_or(0.0) + .max(0.0); + let cpu_percent = process + .get("cpu_percent") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let memory_percent = process + .get("memory_percent") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let cpu_times = process + .get("cpu_times") + .cloned() + .unwrap_or_else(|| json!({})); + let memory_info = process + .get("memory_info") + .cloned() + .unwrap_or_else(|| json!({})); let io_counters: Vec = process .get("io_counters") .and_then(Value::as_array) .map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect()) .unwrap_or_else(|| no_io.to_vec()); let pid = process.get("pid").and_then(Value::as_i64).unwrap_or(0); - let username = process.get("username").and_then(Value::as_str).unwrap_or("_").to_string(); + let username = process + .get("username") + .and_then(Value::as_str) + .unwrap_or("_") + .to_string(); let nice = process.get("nice").and_then(Value::as_i64).unwrap_or(0); - let status = process.get("status").and_then(Value::as_str).unwrap_or("?").to_string(); - let time_since_update = process.get("time_since_update").cloned().unwrap_or(Value::Null); + let status = process + .get("status") + .and_then(Value::as_str) + .unwrap_or("?") + .to_string(); + let time_since_update = process + .get("time_since_update") + .cloned() + .unwrap_or(Value::Null); match programs.get_mut(&name) { None => { @@ -575,20 +692,26 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec { add(program, "memory_percent", memory_percent); for key in ["user", "system", "iowait"] { let value = cpu_times.get(key).and_then(Value::as_f64).unwrap_or(0.0); - if value != 0.0 || cpu_times.get(key).is_some() { - if let Some(times) = program.get_mut("cpu_times").and_then(Value::as_object_mut) { - let current = times.get(key).and_then(Value::as_f64).unwrap_or(0.0); - times.insert(key.to_string(), json!(current + value)); - } + if (value != 0.0 || cpu_times.get(key).is_some()) + && let Some(times) = + program.get_mut("cpu_times").and_then(Value::as_object_mut) + { + let current = times.get(key).and_then(Value::as_f64).unwrap_or(0.0); + times.insert(key.to_string(), json!(current + value)); } } - if let Some(program_memory) = program.get_mut("memory_info").and_then(Value::as_object_mut) { - if let Some(addition) = memory_info.as_object() { - for (key, value) in addition { - let current = program_memory.get(key).and_then(Value::as_f64).unwrap_or(0.0); - let value = value.as_f64().unwrap_or(0.0); - program_memory.insert(key.clone(), json!(current + value)); - } + if let Some(program_memory) = program + .get_mut("memory_info") + .and_then(Value::as_object_mut) + && let Some(addition) = memory_info.as_object() + { + for (key, value) in addition { + let current = program_memory + .get(key) + .and_then(Value::as_f64) + .unwrap_or(0.0); + let value = value.as_f64().unwrap_or(0.0); + program_memory.insert(key.clone(), json!(current + value)); } } // io_counters: sum slots 0..4, OR the tag. @@ -598,10 +721,15 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec { .and_then(Value::as_array) .map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect()) .unwrap_or_else(|| no_io.to_vec()); - for index in 0..4 { - summed[index] = existing.get(index).copied().unwrap_or(0) + io_counters.get(index).copied().unwrap_or(0); + for (index, value) in summed.iter_mut().take(4).enumerate() { + *value = existing.get(index).copied().unwrap_or(0) + + io_counters.get(index).copied().unwrap_or(0); } - summed[4] = existing.get(4).copied().unwrap_or(0).max(io_counters.get(4).copied().unwrap_or(0)); + summed[4] = existing + .get(4) + .copied() + .unwrap_or(0) + .max(io_counters.get(4).copied().unwrap_or(0)); program.insert("io_counters".to_string(), json!(summed)); if let Some(children) = program.get_mut("childrens").and_then(Value::as_array_mut) { children.push(json!(pid)); @@ -625,7 +753,11 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec { .into_values() .map(|mut program| { if let Some(program) = program.as_object_mut() { - let nprocs = program.get("childrens").and_then(Value::as_array).map(|a| a.len()).unwrap_or(0); + let nprocs = program + .get("childrens") + .and_then(Value::as_array) + .map(|a| a.len()) + .unwrap_or(0); program.insert("nprocs".to_string(), json!(nprocs)); } program @@ -657,7 +789,12 @@ fn read_proc_stat(pid: i64) -> Option { let fields: Vec<&str> = content[close + 1..].split_whitespace().collect(); // fields[0] = state (stat field 3), so stat field N (1-based) is fields[N-3]. let field = |n: usize| fields.get(n).copied().unwrap_or(""); - let state = field(0).chars().next().unwrap_or('?').to_ascii_uppercase().to_string(); + let state = field(0) + .chars() + .next() + .unwrap_or('?') + .to_ascii_uppercase() + .to_string(); let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64; let ticks = if ticks <= 0.0 { 100.0 } else { ticks }; let mut stat = ProcStat { @@ -680,13 +817,16 @@ fn read_proc_stat(pid: i64) -> Option { } } } - if let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status")) { - if let Some(line) = status.lines().find(|l| l.starts_with("Gid:")) { - let gids: Vec = line[4..].split_whitespace().filter_map(|v| v.parse().ok()).collect(); - stat.gid_real = gids.first().copied().unwrap_or(0); - stat.gid_effective = gids.get(1).copied().unwrap_or(0); - stat.gid_saved = gids.get(2).copied().unwrap_or(0); - } + if let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status")) + && let Some(line) = status.lines().find(|l| l.starts_with("Gid:")) + { + let gids: Vec = line[4..] + .split_whitespace() + .filter_map(|v| v.parse().ok()) + .collect(); + stat.gid_real = gids.first().copied().unwrap_or(0); + stat.gid_effective = gids.get(1).copied().unwrap_or(0); + stat.gid_saved = gids.get(2).copied().unwrap_or(0); } Some(stat) } @@ -712,7 +852,10 @@ mod tests { #[test] fn test_sort_stats() { - let mut stats = vec![json!({"name": "a", "cpu_percent": 1.0}), json!({"name": "b", "cpu_percent": 3.0})]; + let mut stats = vec![ + json!({"name": "a", "cpu_percent": 1.0}), + json!({"name": "b", "cpu_percent": 3.0}), + ]; sort_stats(&mut stats, "cpu_percent", "memory_percent", true); assert_eq!(stats[0]["name"], "b"); } diff --git a/crates/glances-core/src/secure.rs b/crates/glances-core/src/secure.rs index af34946..8fdac4a 100644 --- a/crates/glances-core/src/secure.rs +++ b/crates/glances-core/src/secure.rs @@ -24,16 +24,21 @@ pub fn sanitize_value(value: &serde_json::Value) -> serde_json::Value { serde_json::Value::Array(items) => { serde_json::Value::Array(items.iter().map(sanitize_value).collect()) } - serde_json::Value::Object(map) => { - serde_json::Value::Object(map.iter().map(|(k, v)| (k.clone(), sanitize_value(v))).collect()) - } + serde_json::Value::Object(map) => serde_json::Value::Object( + map.iter() + .map(|(k, v)| (k.clone(), sanitize_value(v))) + .collect(), + ), other => other.clone(), } } /// Split a command string into an argument list. Spaces are separators except /// within single or double quotes (the surrounding quotes are removed). -pub fn split_args(cmd: &str, render: Option<&dyn Fn(&str) -> Result>) -> Result, String> { +pub fn split_args( + cmd: &str, + render: Option<&dyn Fn(&str) -> Result>, +) -> Result, String> { let mut args: Vec = Vec::new(); let mut current = String::new(); let mut in_single = false; @@ -182,16 +187,20 @@ fn secure_popen_one(cmd: &str, render: Option<&dyn Fn(&str) -> Result Result>) -> String { +pub fn secure_popen( + cmd: &str, + allow_operators: bool, + render: Option<&dyn Fn(&str) -> Result>, +) -> String { if !allow_operators { return run_argv(cmd, render); } diff --git a/crates/glances-core/src/thresholds.rs b/crates/glances-core/src/thresholds.rs index b9fcfa1..4325924 100644 --- a/crates/glances-core/src/thresholds.rs +++ b/crates/glances-core/src/thresholds.rs @@ -13,6 +13,7 @@ pub enum Threshold { } impl Threshold { + #[allow(clippy::should_implement_trait)] pub fn from_str(name: &str) -> Option { match name { "OK" => Some(Threshold::Ok), diff --git a/crates/glances-core/src/timer.rs b/crates/glances-core/src/timer.rs index 684cd6f..e17490d 100644 --- a/crates/glances-core/src/timer.rs +++ b/crates/glances-core/src/timer.rs @@ -14,7 +14,10 @@ pub struct Timer { impl Timer { pub fn new(duration: f64) -> Self { - let mut timer = Self { duration, target: Instant::now() }; + let mut timer = Self { + duration, + target: Instant::now(), + }; timer.start(); timer } @@ -32,7 +35,11 @@ impl Timer { /// Elapsed seconds since the timer was started. pub fn get(&self) -> f64 { - self.duration - self.target.saturating_duration_since(Instant::now()).as_secs_f64() + self.duration + - self + .target + .saturating_duration_since(Instant::now()) + .as_secs_f64() } pub fn set(&mut self, duration: f64) { @@ -62,7 +69,9 @@ impl Default for Counter { impl Counter { pub fn new() -> Self { - Self { start: Instant::now() } + Self { + start: Instant::now(), + } } pub fn reset(&mut self) { diff --git a/crates/glances-core/src/units.rs b/crates/glances-core/src/units.rs index 2499aad..0e7fea9 100644 --- a/crates/glances-core/src/units.rs +++ b/crates/glances-core/src/units.rs @@ -69,7 +69,22 @@ pub fn auto_unit(number: f64, low_precision: bool, min_symbol: &str, none_symbol /// 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 }; + bar_custom(percent, size, bar_char, " ", display_value) +} + +/// Render a progress bar with custom characters. +pub fn bar_custom( + percent: f64, + size: usize, + bar_char: &str, + empty_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; @@ -81,14 +96,18 @@ pub fn bar_percent(percent: f64, size: usize, bar_char: &str, display_value: boo if (frac * 8.0) as usize >= 5 { bar.push_str(bar_char); } else { - bar.push(' '); + bar.push_str(empty_char); } whole += 1; } - bar.push_str(&" ".repeat(size.saturating_sub(whole))); + bar.push_str(&empty_char.repeat(size.saturating_sub(whole))); if display_value { if percent >= 100.0 { - bar = format!("{bar} {}{:3.0}%", if percent > 100.0 { ">" } else { " " }, 100.0); + bar = format!( + "{bar} {}{:3.0}%", + if percent > 100.0 { ">" } else { " " }, + 100.0 + ); } else { bar = format!("{bar}{percent:5.1}%"); } @@ -104,11 +123,9 @@ pub fn string_value_to_float(s: &str) -> Option { let mut digits = String::new(); let mut rest = String::new(); for c in cleaned.chars() { - if c.is_ascii_digit() || c == '.' { - if rest.is_empty() { - digits.push(c); - continue; - } + if (c.is_ascii_digit() || c == '.') && rest.is_empty() { + digits.push(c); + continue; } rest.push(c); } @@ -195,7 +212,9 @@ pub fn py_repr(value: &Value) -> String { pub fn as_f64(value: &Value) -> Option { match value { Value::Number(n) => n.as_f64(), - Value::Bool(_) | Value::Null | Value::String(_) | Value::Array(_) | Value::Object(_) => None, + Value::Bool(_) | Value::Null | Value::String(_) | Value::Array(_) | Value::Object(_) => { + None + } } } @@ -242,7 +261,10 @@ pub fn split_esc(input: &str, sep: char, esc: char) -> Vec { /// shell by adding a `$` prefix in front of them. (Only the replacement /// flavour is needed by the plugins.) pub fn replace_special_chars(input: &str, by: char) -> String { - input.replace("\r\n", &by.to_string()).replace('\n', &by.to_string()).replace('\t', &by.to_string()) + input + .replace("\r\n", &by.to_string()) + .replace('\n', &by.to_string()) + .replace('\t', &by.to_string()) } #[cfg(test)] diff --git a/crates/glances-plugins/src/collectors.rs b/crates/glances-plugins/src/collectors.rs index 0719995..0b976a8 100644 --- a/crates/glances-plugins/src/collectors.rs +++ b/crates/glances-plugins/src/collectors.rs @@ -94,7 +94,13 @@ fn clock_ticks() -> f64 { fn parse_cpu_line(fields: &[&str]) -> CpuTimes { let ticks = clock_ticks(); - let value = |index: usize| -> f64 { fields.get(index).and_then(|v| v.parse::().ok()).unwrap_or(0.0) / ticks }; + let value = |index: usize| -> f64 { + fields + .get(index) + .and_then(|v| v.parse::().ok()) + .unwrap_or(0.0) + / ticks + }; CpuTimes { user: value(0), nice: value(1), @@ -150,7 +156,11 @@ pub fn read_meminfo() -> HashMap { continue; }; let unit = parts.next().unwrap_or(""); - let bytes = if unit.starts_with("kB") { value * 1024 } else { value }; + let bytes = if unit.starts_with("kB") { + value * 1024 + } else { + value + }; values.insert(key.trim().to_string(), bytes); } values @@ -163,10 +173,10 @@ pub fn read_vmstat() -> HashMap { return values; }; for line in content.lines() { - if let Some((key, value)) = line.split_once(' ') { - if let Ok(value) = value.trim().parse::() { - values.insert(key.to_string(), value); - } + if let Some((key, value)) = line.split_once(' ') + && let Ok(value) = value.trim().parse::() + { + values.insert(key.to_string(), value); } } values @@ -212,7 +222,9 @@ pub fn uptime_seconds() -> Option { /// Number of logical CPUs. pub fn logical_core_count() -> usize { - std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1) } /// Number of physical cores, parsed from `/proc/cpuinfo`. @@ -262,20 +274,21 @@ pub fn cpu_name() -> String { let mut implementer: Option = None; let mut ret = fallback; for line in content.lines() { - if line.starts_with("model name") || line.starts_with("Model") || line.starts_with("cpu model") { - if let Some((_, value)) = line.split_once(':') { - return value.trim().to_string(); - } + if (line.starts_with("model name") + || line.starts_with("Model") + || line.starts_with("cpu model")) + && let Some((_, value)) = line.split_once(':') + { + return value.trim().to_string(); } if line.starts_with("CPU implementer") { implementer = line.split_once(':').map(|(_, v)| v.trim().to_string()); } - if line.starts_with("CPU part") { - if let Some(part) = line.split_once(':').map(|(_, v)| v.trim().to_string()) { - if let Some(implementer) = &implementer { - ret = format!("{implementer} part {part}"); - } - } + if line.starts_with("CPU part") + && let Some(part) = line.split_once(':').map(|(_, v)| v.trim().to_string()) + && let Some(implementer) = &implementer + { + ret = format!("{implementer} part {part}"); } } ret @@ -316,7 +329,10 @@ pub fn read_net_dev() -> IndexMap { let Some((interface, values)) = line.split_once(':') else { continue; }; - let fields: Vec = values.split_whitespace().filter_map(|v| v.parse().ok()).collect(); + let fields: Vec = values + .split_whitespace() + .filter_map(|v| v.parse().ok()) + .collect(); if fields.len() < 16 { continue; } @@ -429,7 +445,8 @@ pub fn read_diskstats() -> IndexMap { continue; } let name = fields[2].to_string(); - let value = |index: usize| -> u64 { fields.get(index).and_then(|v| v.parse().ok()).unwrap_or(0) }; + let value = + |index: usize| -> u64 { fields.get(index).and_then(|v| v.parse().ok()).unwrap_or(0) }; result.insert( name, DiskStat { @@ -520,12 +537,25 @@ pub fn disk_usage(path: &str) -> Option { if unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) } != 0 { return None; } - let block_size = if stat.f_frsize > 0 { stat.f_frsize } else { stat.f_bsize } as u64; + let block_size = if stat.f_frsize > 0 { + stat.f_frsize + } else { + stat.f_bsize + } as u64; let total = stat.f_blocks as u64 * block_size; let free = stat.f_bavail as u64 * block_size; let used = (stat.f_blocks as u64).saturating_sub(stat.f_bfree as u64) * block_size; - let percent = if used + free > 0 { (used as f64 * 100.0) / (used + free) as f64 } else { 0.0 }; - Some(FsUsage { total, used, free, percent }) + let percent = if used + free > 0 { + (used as f64 * 100.0) / (used + free) as f64 + } else { + 0.0 + }; + Some(FsUsage { + total, + used, + free, + percent, + }) } /// Linux distribution name/version (`/etc/os-release`). @@ -546,7 +576,8 @@ pub fn read_os_release() -> (String, String) { pub fn hostname() -> String { let mut buffer = [0u8; 256]; - let result = unsafe { libc::gethostname(buffer.as_mut_ptr() as *mut libc::c_char, buffer.len()) }; + let result = + unsafe { libc::gethostname(buffer.as_mut_ptr() as *mut libc::c_char, buffer.len()) }; if result == 0 { let end = buffer.iter().position(|c| *c == 0).unwrap_or(buffer.len()); String::from_utf8_lossy(&buffer[..end]).to_string() @@ -571,10 +602,17 @@ pub fn get_ip_address() -> (Option, Option) { addr.sin_family = libc::AF_INET as u16; addr.sin_port = 53u16.to_be(); addr.sin_addr.s_addr = u32::from_be_bytes([192, 0, 2, 1]).to_be(); - if libc::connect(sock, &addr as *const _ as *const libc::sockaddr, std::mem::size_of::() as u32) == 0 { + if libc::connect( + sock, + &addr as *const _ as *const libc::sockaddr, + std::mem::size_of::() as u32, + ) == 0 + { let mut local: libc::sockaddr_in = std::mem::zeroed(); let mut len = std::mem::size_of::() as u32; - if libc::getsockname(sock, &mut local as *mut _ as *mut libc::sockaddr, &mut len) == 0 { + if libc::getsockname(sock, &mut local as *mut _ as *mut libc::sockaddr, &mut len) + == 0 + { let bytes = local.sin_addr.s_addr.to_ne_bytes(); let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]); if !candidate.starts_with("127.") && candidate != "0.0.0.0" { @@ -599,11 +637,15 @@ pub fn get_ip_address() -> (Option, Option) { if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET { let sin = addr as *const libc::sockaddr_in; let bytes = (*sin).sin_addr.s_addr.to_ne_bytes(); - let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]); + let candidate = + format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]); if &candidate == ip && !mask.is_null() { let sin_mask = mask as *const libc::sockaddr_in; let mbytes = (*sin_mask).sin_addr.s_addr.to_ne_bytes(); - netmask = Some(format!("{}.{}.{}.{}", mbytes[0], mbytes[1], mbytes[2], mbytes[3])); + netmask = Some(format!( + "{}.{}.{}.{}", + mbytes[0], mbytes[1], mbytes[2], mbytes[3] + )); break; } } @@ -624,18 +666,24 @@ pub fn get_ip_address() -> (Option, Option) { while !current.is_null() { let addr = (*current).ifa_addr; if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET { - let name = std::ffi::CStr::from_ptr((*current).ifa_name).to_string_lossy().to_string(); + let name = std::ffi::CStr::from_ptr((*current).ifa_name) + .to_string_lossy() + .to_string(); let flags = (*current).ifa_flags as u32; if name != "lo" && flags & libc::IFF_UP as u32 != 0 { let sin = addr as *const libc::sockaddr_in; let bytes = (*sin).sin_addr.s_addr.to_ne_bytes(); - let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]); + let candidate = + format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]); let mask = (*current).ifa_netmask; let mut netmask = None; if !mask.is_null() { let sin_mask = mask as *const libc::sockaddr_in; let mbytes = (*sin_mask).sin_addr.s_addr.to_ne_bytes(); - netmask = Some(format!("{}.{}.{}.{}", mbytes[0], mbytes[1], mbytes[2], mbytes[3])); + netmask = Some(format!( + "{}.{}.{}.{}", + mbytes[0], mbytes[1], mbytes[2], mbytes[3] + )); } found = (Some(candidate), netmask); break; @@ -661,7 +709,11 @@ pub fn get_default_gateway() -> Option { let gateway = fields[2]; if gateway.len() == 8 { let bytes: Vec = (0..4) - .map(|i| u8::from_str_radix(&gateway[i * 2..i * 2 + 2], 16).unwrap_or(0).to_string()) + .map(|i| { + u8::from_str_radix(&gateway[i * 2..i * 2 + 2], 16) + .unwrap_or(0) + .to_string() + }) .collect(); return Some(bytes.join(".")); } @@ -684,7 +736,9 @@ pub fn ip_to_cidr(ip: Option<&str>) -> u32 { /// Read a sysfs hwmon input value. pub fn read_number_file(path: &str) -> Option { - std::fs::read_to_string(path).ok().and_then(|v| v.trim().parse::().ok()) + std::fs::read_to_string(path) + .ok() + .and_then(|v| v.trim().parse::().ok()) } /// Get the current date formatted with a C strftime format (same semantics diff --git a/crates/glances-plugins/src/cpu_percent.rs b/crates/glances-plugins/src/cpu_percent.rs index de74b0c..ebe6b36 100644 --- a/crates/glances-plugins/src/cpu_percent.rs +++ b/crates/glances-plugins/src/cpu_percent.rs @@ -20,6 +20,7 @@ pub struct CpuPercentState { prev_cpu: Option, prev_percpu: Vec, cpu_percent: f64, + cpu_timer: std::time::Instant, percpu_percent: Vec, } @@ -30,6 +31,7 @@ impl CpuPercentState { prev_cpu: None, prev_percpu: Vec::new(), cpu_percent: 0.0, + cpu_timer: std::time::Instant::now(), percpu_percent: Vec::new(), }; state.cpu_percent = state.compute_cpu(); @@ -39,12 +41,12 @@ impl CpuPercentState { /// Update and/or return the CPU usage (psutil `cpu_percent`). pub fn get_cpu(&mut self) -> f64 { - // The Python implementation caches this value for `cached_timer_cpu` - // seconds. Since the engine refreshes at least every `args.time` - // seconds, the value is recomputed on every call there; keep the cache - // for direct calls. - let _ = self.cached_timer_cpu; - self.cpu_percent = self.compute_cpu(); + // The value is cached for `cached_timer_cpu` seconds like in the + // Python implementation. + if self.cpu_timer.elapsed().as_secs_f64() >= self.cached_timer_cpu { + self.cpu_percent = self.compute_cpu(); + self.cpu_timer = std::time::Instant::now(); + } self.cpu_percent } diff --git a/crates/glances-plugins/src/engine.rs b/crates/glances-plugins/src/engine.rs index 720dc7d..b963ccd 100644 --- a/crates/glances-plugins/src/engine.rs +++ b/crates/glances-plugins/src/engine.rs @@ -27,37 +27,102 @@ impl Engine { // Registration order mirrors the alphabetical plugin loading of the // Python implementation (in particular processcount runs before // processlist and programlist). - plugins.insert("amps".to_string(), Box::new(AmpsPlugin::new(&config, &args))); - plugins.insert("core".to_string(), Box::new(CorePlugin::new(&config, &args))); + plugins.insert( + "amps".to_string(), + Box::new(AmpsPlugin::new(&config, &args)), + ); + plugins.insert( + "core".to_string(), + Box::new(CorePlugin::new(&config, &args)), + ); plugins.insert("cpu".to_string(), Box::new(CpuPlugin::new(&config, &args))); - plugins.insert("diskio".to_string(), Box::new(DiskioPlugin::new(&config, &args))); - plugins.insert("folders".to_string(), Box::new(FoldersPlugin::new(&config, &args))); + plugins.insert( + "diskio".to_string(), + Box::new(DiskioPlugin::new(&config, &args)), + ); + plugins.insert( + "folders".to_string(), + Box::new(FoldersPlugin::new(&config, &args)), + ); plugins.insert("fs".to_string(), Box::new(FsPlugin::new(&config, &args))); plugins.insert("ip".to_string(), Box::new(IpPlugin::new(&config, &args))); plugins.insert("irq".to_string(), Box::new(IrqPlugin::new(&config, &args))); - plugins.insert("load".to_string(), Box::new(LoadPlugin::new(&config, &args))); + plugins.insert( + "load".to_string(), + Box::new(LoadPlugin::new(&config, &args)), + ); plugins.insert("mem".to_string(), Box::new(MemPlugin::new(&config, &args))); - plugins.insert("memswap".to_string(), Box::new(MemSwapPlugin::new(&config, &args))); - plugins.insert("network".to_string(), Box::new(NetworkPlugin::new(&config, &args))); + plugins.insert( + "memswap".to_string(), + Box::new(MemSwapPlugin::new(&config, &args)), + ); + plugins.insert( + "network".to_string(), + Box::new(NetworkPlugin::new(&config, &args)), + ); plugins.insert("now".to_string(), Box::new(NowPlugin::new(&config, &args))); - plugins.insert("percpu".to_string(), Box::new(PerCpuPlugin::new(&config, &args))); - plugins.insert("ports".to_string(), Box::new(PortsPlugin::new(&config, &args))); - plugins.insert("processcount".to_string(), Box::new(ProcessCountPlugin::new(&config, &args))); - plugins.insert("processlist".to_string(), Box::new(ProcessListPlugin::new(&config, &args))); - plugins.insert("programlist".to_string(), Box::new(ProgramListPlugin::new(&config, &args))); - plugins.insert("psutilversion".to_string(), Box::new(PsutilVersionPlugin::new(&config, &args))); - plugins.insert("quicklook".to_string(), Box::new(QuicklookPlugin::new(&config, &args))); - plugins.insert("raid".to_string(), Box::new(RaidPlugin::new(&config, &args))); - plugins.insert("sensors".to_string(), Box::new(SensorsPlugin::new(&config, &args))); - plugins.insert("system".to_string(), Box::new(SystemPlugin::new(&config, &args))); - plugins.insert("uptime".to_string(), Box::new(UptimePlugin::new(&config, &args))); - plugins.insert("version".to_string(), Box::new(VersionPlugin::new(&config, &args))); - plugins.insert("wifi".to_string(), Box::new(WifiPlugin::new(&config, &args))); + plugins.insert( + "percpu".to_string(), + Box::new(PerCpuPlugin::new(&config, &args)), + ); + plugins.insert( + "ports".to_string(), + Box::new(PortsPlugin::new(&config, &args)), + ); + plugins.insert( + "processcount".to_string(), + Box::new(ProcessCountPlugin::new(&config, &args)), + ); + plugins.insert( + "processlist".to_string(), + Box::new(ProcessListPlugin::new(&config, &args)), + ); + plugins.insert( + "programlist".to_string(), + Box::new(ProgramListPlugin::new(&config, &args)), + ); + plugins.insert( + "psutilversion".to_string(), + Box::new(PsutilVersionPlugin::new(&config, &args)), + ); + plugins.insert( + "quicklook".to_string(), + Box::new(QuicklookPlugin::new(&config, &args)), + ); + plugins.insert( + "raid".to_string(), + Box::new(RaidPlugin::new(&config, &args)), + ); + plugins.insert( + "sensors".to_string(), + Box::new(SensorsPlugin::new(&config, &args)), + ); + plugins.insert( + "system".to_string(), + Box::new(SystemPlugin::new(&config, &args)), + ); + plugins.insert( + "uptime".to_string(), + Box::new(UptimePlugin::new(&config, &args)), + ); + plugins.insert( + "version".to_string(), + Box::new(VersionPlugin::new(&config, &args)), + ); + plugins.insert( + "wifi".to_string(), + Box::new(WifiPlugin::new(&config, &args)), + ); // The refresh time is set on the model, which was built with the // command line value already. let _ = refresh_time; - let mut engine = Self { args, config, runtime, plugins }; + let mut engine = Self { + args, + config, + runtime, + plugins, + }; engine.apply_plugin_options(); engine.apply_process_options(); engine.sync_enabled(); @@ -77,11 +142,11 @@ impl Engine { fn apply_plugin_options(&mut self) { // Disable plugins from the configuration file. for section in self.config.sections() { - if self.config.get_bool_value(section, "disable", false) { - if let Some(plugin) = self.plugins.get_mut(section) { - debug!("{} disabled by the configuration file", section); - plugin.set_enabled(false); - } + if self.config.get_bool_value(section, "disable", false) + && let Some(plugin) = self.plugins.get_mut(section) + { + debug!("{} disabled by the configuration file", section); + plugin.set_enabled(false); } } @@ -102,10 +167,18 @@ impl Engine { // --disable-plugin / --enable-plugin. let mut explicit_enable: Vec = Vec::new(); if let Some(enable) = &self.args.enable_plugin { - explicit_enable = enable.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); + explicit_enable = enable + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); } if let Some(disable) = &self.args.disable_plugin { - let tokens: Vec = disable.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); + let tokens: Vec = disable + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); if tokens.iter().any(|t| t == "all") { for name in self.plugins.keys().cloned().collect::>() { self.plugins.get_mut(&name).unwrap().set_enabled(false); @@ -134,12 +207,18 @@ impl Engine { } // Processlist needs processcount to update the shared process list. - let processcount_enabled = self.plugins.get("processcount").map(|p| p.is_enabled()).unwrap_or(false); + let processcount_enabled = self + .plugins + .get("processcount") + .map(|p| p.is_enabled()) + .unwrap_or(false); if !processcount_enabled { for name in ["processlist", "programlist"] { if let Some(plugin) = self.plugins.get_mut(name) { if plugin.is_enabled() { - tracing::warn!("Processcount is disabled, so processlist (updated by processcount) is also disabled"); + tracing::warn!( + "Processcount is disabled, so processlist (updated by processcount) is also disabled" + ); } plugin.set_enabled(false); } @@ -210,7 +289,10 @@ impl Engine { /// Update every enabled plugin. pub fn update(&mut self) { let names: Vec = self.plugins.keys().cloned().collect(); - let enabled: Vec = names.into_iter().filter(|n| self.plugins[n].is_enabled()).collect(); + let enabled: Vec = names + .into_iter() + .filter(|n| self.plugins[n].is_enabled()) + .collect(); self.update_plugins(&enabled); } @@ -222,7 +304,13 @@ impl Engine { } fn update_plugin(&mut self, name: &str) { - let Engine { plugins, args, config, runtime, .. } = self; + let Engine { + plugins, + args, + config, + runtime, + .. + } = self; let Some(plugin) = plugins.get_mut(name) else { return; }; @@ -289,10 +377,10 @@ impl Engine { }; let mut result = serde_json::Map::new(); for name in names { - if let Some(plugin) = self.plugins.get(&name) { - if plugin.is_enabled() { - result.insert(name, plugin.stats().clone()); - } + if let Some(plugin) = self.plugins.get(&name) + && plugin.is_enabled() + { + result.insert(name, plugin.stats().clone()); } } result diff --git a/crates/glances-plugins/src/lib.rs b/crates/glances-plugins/src/lib.rs index 5340821..ed3f19c 100644 --- a/crates/glances-plugins/src/lib.rs +++ b/crates/glances-plugins/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::type_complexity)] + //! glances-plugins: collectors, plugin registry and update engine. pub mod collectors; diff --git a/crates/glances-plugins/src/plugins/amps.rs b/crates/glances-plugins/src/plugins/amps.rs index 1a3113b..4fa205d 100644 --- a/crates/glances-plugins/src/plugins/amps.rs +++ b/crates/glances-plugins/src/plugins/amps.rs @@ -62,8 +62,12 @@ impl AmpsPlugin { .get_value(section, "regex", None) .and_then(|pattern| regex::Regex::new(&pattern).ok()); let refresh = config.get_float_value(section, "refresh", 3.0); - let countmin = config.get_value(section, "countmin", None).and_then(|v| v.parse::().ok()); - let countmax = config.get_value(section, "countmax", None).and_then(|v| v.parse::().ok()); + let countmin = config + .get_value(section, "countmin", None) + .and_then(|v| v.parse::().ok()); + let countmax = config + .get_value(section, "countmax", None) + .and_then(|v| v.parse::().ok()); let one_line = config.get_bool_value(section, "one_line", false); amps.push(AmpConfig { name: amp_display_name(section), @@ -142,7 +146,12 @@ impl Plugin for AmpsPlugin { let cmdline = process .get("cmdline") .and_then(Value::as_array) - .map(|a| a.iter().filter_map(|v| v.as_str()).collect::>().join(" ")) + .map(|a| { + a.iter() + .filter_map(|v| v.as_str()) + .collect::>() + .join(" ") + }) .unwrap_or_default(); regex.is_match(name) || regex.is_match(&cmdline) }) @@ -191,7 +200,8 @@ impl Plugin for AmpsPlugin { } else { glances_core::Decoration::Ok }; - self.model.set_view_decoration(Some(&name), "count", decoration); + self.model + .set_view_decoration(Some(&name), "count", decoration); } } @@ -201,7 +211,14 @@ impl Plugin for AmpsPlugin { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { let mut ret = Vec::new(); - if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + 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(12); @@ -212,7 +229,13 @@ impl Plugin for AmpsPlugin { false, false, )); - ret.push(self.model.curse_add_line(format!("{:>9}", "Count"), glances_core::Decoration::Default, false, false, false)); + ret.push(self.model.curse_add_line( + format!("{:>9}", "Count"), + glances_core::Decoration::Default, + false, + false, + false, + )); let items = self.model.sorted_stats(); let Some(items) = items.as_array() else { return ret; @@ -221,15 +244,28 @@ impl Plugin for AmpsPlugin { let name = i.get("name").and_then(Value::as_str).unwrap_or(""); let count = i.get("count").and_then(Value::as_i64).unwrap_or(0); let result = i.get("result").and_then(Value::as_str).unwrap_or(""); - let display_name = if result.is_empty() { name.to_string() } else { format!("{name}: {result}") }; + let display_name = if result.is_empty() { + name.to_string() + } else { + format!("{name}: {result}") + }; ret.push(self.model.curse_new_line()); - ret.push(self.model.curse_add_line( - format!("{:(), width = name_max_width), - glances_core::Decoration::Default, - false, - false, - false, - )); + ret.push( + self.model.curse_add_line( + format!( + "{:(), + width = name_max_width + ), + glances_core::Decoration::Default, + false, + false, + false, + ), + ); ret.push(self.model.curse_add_line( format!("{count:>9}"), self.model.view_decoration(Some(name), "count"), diff --git a/crates/glances-plugins/src/plugins/cpu.rs b/crates/glances-plugins/src/plugins/cpu.rs index 73a9495..6a29e7f 100644 --- a/crates/glances-plugins/src/plugins/cpu.rs +++ b/crates/glances-plugins/src/plugins/cpu.rs @@ -48,7 +48,11 @@ impl CpuPlugin { args.disable_config_exec, ); let nb_log_core = crate::collectors::logical_core_count(); - Self { model, previous_times: None, nb_log_core } + Self { + model, + previous_times: None, + nb_log_core, + } } fn times_percent(&mut self) -> CpuTimes { @@ -105,11 +109,23 @@ impl Plugin for CpuPlugin { let mut soft_interrupts = 0u64; for line in content.lines() { if let Some(rest) = line.strip_prefix("ctxt ") { - ctx_switches = rest.split_whitespace().next().and_then(|v| v.parse().ok()).unwrap_or(0); + ctx_switches = rest + .split_whitespace() + .next() + .and_then(|v| v.parse().ok()) + .unwrap_or(0); } else if let Some(rest) = line.strip_prefix("intr ") { - interrupts = rest.split_whitespace().next().and_then(|v| v.parse().ok()).unwrap_or(0); + interrupts = rest + .split_whitespace() + .next() + .and_then(|v| v.parse().ok()) + .unwrap_or(0); } else if let Some(rest) = line.strip_prefix("softirq ") { - soft_interrupts = rest.split_whitespace().next().and_then(|v| v.parse().ok()).unwrap_or(0); + soft_interrupts = rest + .split_whitespace() + .next() + .and_then(|v| v.parse().ok()) + .unwrap_or(0); } } stats.insert("ctx_switches".to_string(), json!(ctx_switches)); @@ -125,13 +141,27 @@ impl Plugin for CpuPlugin { fn update_views(&mut self) { let key = self.get_key(); self.model.update_views(key); - if self.model.stats.get("time_since_update").and_then(Value::as_f64) != Some(0.0) { - if let Some(ctx_switches) = self.model.stats.get("ctx_switches").and_then(Value::as_f64) { - let maximum = 100.0 * self.nb_log_core as f64; - let decoration = - self.model.get_alert(ctx_switches, 0.0, maximum, true, false, Some("ctx_switches"), None, false); - self.model.set_view_decoration(None, "ctx_switches", decoration); - } + if self + .model + .stats + .get("time_since_update") + .and_then(Value::as_f64) + != Some(0.0) + && let Some(ctx_switches) = self.model.stats.get("ctx_switches").and_then(Value::as_f64) + { + let maximum = 100.0 * self.nb_log_core as f64; + let decoration = self.model.get_alert( + ctx_switches, + 0.0, + maximum, + true, + false, + Some("ctx_switches"), + None, + false, + ); + self.model + .set_view_decoration(None, "ctx_switches", decoration); } } @@ -148,7 +178,13 @@ impl Plugin for CpuPlugin { 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)); + 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}%"), @@ -160,7 +196,13 @@ impl Plugin for CpuPlugin { } 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)); + 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}%"), @@ -171,7 +213,10 @@ impl Plugin for CpuPlugin { )); } } - ret.extend(self.model.curse_add_stat("ctx_switches", Some(15), " ", "", "")); + ret.extend( + self.model + .curse_add_stat("ctx_switches", Some(15), " ", "", ""), + ); // Line 2. ret.push(self.model.curse_new_line()); @@ -181,7 +226,10 @@ impl Plugin for CpuPlugin { 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), " ", "", "")); + ret.extend( + self.model + .curse_add_stat("interrupts", Some(15), " ", "", ""), + ); // Line 3. ret.push(self.model.curse_new_line()); @@ -192,9 +240,15 @@ impl Plugin for CpuPlugin { } 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), " ", "", "")); + ret.extend( + self.model + .curse_add_stat("soft_interrupts", Some(15), " ", "", ""), + ); } else { - ret.extend(self.model.curse_add_stat("ctx_switches", Some(15), " ", "", "")); + ret.extend( + self.model + .curse_add_stat("ctx_switches", Some(15), " ", "", ""), + ); } // Line 4. @@ -208,7 +262,10 @@ impl Plugin for CpuPlugin { 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.extend( + self.model + .curse_add_stat("syscalls", Some(15), " ", "", ""), + ); } ret } diff --git a/crates/glances-plugins/src/plugins/diskio.rs b/crates/glances-plugins/src/plugins/diskio.rs index caf8f71..1e6c0e7 100644 --- a/crates/glances-plugins/src/plugins/diskio.rs +++ b/crates/glances-plugins/src/plugins/diskio.rs @@ -39,9 +39,12 @@ impl DiskioPlugin { ); model.set_item_key("disk_name"); model.hide_zero = config.get_bool_value("diskio", "hide_zero", false); - model.hide_threshold_bytes = config.get_int_value("diskio", "hide_threshold_bytes", 0) as f64; - model.hide_zero_fields = - vec!["read_bytes_rate_per_sec".to_string(), "write_bytes_rate_per_sec".to_string()]; + model.hide_threshold_bytes = + config.get_int_value("diskio", "hide_threshold_bytes", 0) as f64; + model.hide_zero_fields = vec![ + "read_bytes_rate_per_sec".to_string(), + "write_bytes_rate_per_sec".to_string(), + ]; Self { model } } } @@ -75,8 +78,14 @@ impl Plugin for DiskioPlugin { let mut item = serde_json::Map::new(); item.insert("read_count".to_string(), json!(disk_stat.read_count)); item.insert("write_count".to_string(), json!(disk_stat.write_count)); - item.insert("read_bytes".to_string(), json!(disk_stat.sectors_read * SECTOR_SIZE)); - item.insert("write_bytes".to_string(), json!(disk_stat.sectors_written * SECTOR_SIZE)); + item.insert( + "read_bytes".to_string(), + json!(disk_stat.sectors_read * SECTOR_SIZE), + ); + item.insert( + "write_bytes".to_string(), + json!(disk_stat.sectors_written * SECTOR_SIZE), + ); item.insert("read_time".to_string(), json!(disk_stat.read_time)); item.insert("write_time".to_string(), json!(disk_stat.write_time)); item.insert("key".to_string(), json!("disk_name")); @@ -95,20 +104,34 @@ impl Plugin for DiskioPlugin { let Some(item) = item.as_object_mut() else { continue; }; - let read_time = item.get("read_time_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); - let read_count = item.get("read_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); - let write_time = item.get("write_time_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); - let write_count = item.get("write_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); - let read_latency = if item.contains_key("read_time_rate_per_sec") && read_count > 0.0 { - (read_time / read_count).floor() as i64 - } else { - 0 - }; - let write_latency = if item.contains_key("write_time_rate_per_sec") && write_count > 0.0 { - (write_time / write_count).floor() as i64 - } else { - 0 - }; + let read_time = item + .get("read_time_rate_per_sec") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let read_count = item + .get("read_count_rate_per_sec") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let write_time = item + .get("write_time_rate_per_sec") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let write_count = item + .get("write_count_rate_per_sec") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let read_latency = + if item.contains_key("read_time_rate_per_sec") && read_count > 0.0 { + (read_time / read_count).floor() as i64 + } else { + 0 + }; + let write_latency = + if item.contains_key("write_time_rate_per_sec") && write_count > 0.0 { + (write_time / write_count).floor() as i64 + } else { + 0 + }; item.insert("read_latency".to_string(), json!(read_latency)); item.insert("write_latency".to_string(), json!(write_latency)); } @@ -124,36 +147,104 @@ impl Plugin for DiskioPlugin { .iter() .filter_map(|item| { let name = item.get("disk_name")?.as_str()?.to_string(); - let read_rate = item.get("read_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); - let write_rate = item.get("write_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); - let read_latency = item.get("read_latency").and_then(Value::as_f64).unwrap_or(0.0); - let write_latency = item.get("write_latency").and_then(Value::as_f64).unwrap_or(0.0); + let read_rate = item + .get("read_bytes_rate_per_sec") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let write_rate = item + .get("write_bytes_rate_per_sec") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let read_latency = item + .get("read_latency") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let write_latency = item + .get("write_latency") + .and_then(Value::as_f64) + .unwrap_or(0.0); Some((name, read_rate, write_rate, read_latency, write_latency)) }) .collect(), _ => Vec::new(), }; for (disk_name, read_rate, write_rate, read_latency, write_latency) in items { - let alert_rx = self.model.get_alert(read_rate, 0.0, 100.0, true, false, Some("rx"), Some(&disk_name), false); - let alert_tx = self.model.get_alert(write_rate, 0.0, 100.0, true, false, Some("tx"), Some(&disk_name), false); + let alert_rx = self.model.get_alert( + read_rate, + 0.0, + 100.0, + true, + false, + Some("rx"), + Some(&disk_name), + false, + ); + let alert_tx = self.model.get_alert( + write_rate, + 0.0, + 100.0, + true, + false, + Some("tx"), + Some(&disk_name), + false, + ); for field in ["read_bytes", "read_bytes_rate_per_sec"] { - self.model.set_view_decoration(Some(&disk_name), field, alert_rx); + self.model + .set_view_decoration(Some(&disk_name), field, alert_rx); } for field in ["write_bytes", "write_bytes_rate_per_sec"] { - self.model.set_view_decoration(Some(&disk_name), field, alert_tx); + self.model + .set_view_decoration(Some(&disk_name), field, alert_tx); } - let mut alert_latency_rx = - self.model.get_alert(read_latency, 0.0, 100.0, true, false, Some("rx_latency"), Some(&disk_name), false); - let mut alert_latency_tx = - self.model.get_alert(write_latency, 0.0, 100.0, true, false, Some("tx_latency"), Some(&disk_name), false); + let mut alert_latency_rx = self.model.get_alert( + read_latency, + 0.0, + 100.0, + true, + false, + Some("rx_latency"), + Some(&disk_name), + false, + ); + let mut alert_latency_tx = self.model.get_alert( + write_latency, + 0.0, + 100.0, + true, + false, + Some("tx_latency"), + Some(&disk_name), + false, + ); if alert_latency_rx == Decoration::Default { - alert_latency_rx = self.model.get_alert(read_latency, 0.0, 100.0, true, false, Some("rx_latency"), None, false); + alert_latency_rx = self.model.get_alert( + read_latency, + 0.0, + 100.0, + true, + false, + Some("rx_latency"), + None, + false, + ); } if alert_latency_tx == Decoration::Default { - alert_latency_tx = self.model.get_alert(write_latency, 0.0, 100.0, true, false, Some("tx_latency"), None, false); + alert_latency_tx = self.model.get_alert( + write_latency, + 0.0, + 100.0, + true, + false, + Some("tx_latency"), + None, + false, + ); } - self.model.set_view_decoration(Some(&disk_name), "read_latency", alert_latency_rx); - self.model.set_view_decoration(Some(&disk_name), "write_latency", alert_latency_tx); + self.model + .set_view_decoration(Some(&disk_name), "read_latency", alert_latency_rx); + self.model + .set_view_decoration(Some(&disk_name), "write_latency", alert_latency_tx); } } @@ -163,7 +254,14 @@ impl Plugin for DiskioPlugin { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { let mut ret = Vec::new(); - if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + 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); @@ -175,21 +273,61 @@ impl Plugin for DiskioPlugin { 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)); + 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)); + 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)); + 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(); + let disk_real_name = i + .get("disk_name") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); if self .model .hide_zero_fields @@ -204,7 +342,10 @@ impl Plugin for DiskioPlugin { .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::()); + disk_name = format!( + "{}_", + disk_name.chars().take(name_max_width).collect::() + ); } ret.push(self.model.curse_new_line()); ret.push(self.model.curse_add_line( @@ -215,20 +356,108 @@ impl Plugin for DiskioPlugin { 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)); + 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)); + 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)); + 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 diff --git a/crates/glances-plugins/src/plugins/folders.rs b/crates/glances-plugins/src/plugins/folders.rs index 0f8f696..2ced9c7 100644 --- a/crates/glances-plugins/src/plugins/folders.rs +++ b/crates/glances-plugins/src/plugins/folders.rs @@ -14,7 +14,6 @@ pub struct FoldersPlugin { #[derive(Debug)] struct FolderConfig { - indice: String, path: String, refresh: f64, careful: Option, @@ -59,12 +58,14 @@ impl FoldersPlugin { if path.is_empty() { continue; } - let refresh = config.get_float_value("folders", &format!("{key}refresh"), default_refresh); + let refresh = + config.get_float_value("folders", &format!("{key}refresh"), default_refresh); let threshold = |name: &str| -> Option { - config.get_value("folders", &format!("{key}{name}"), None).and_then(|v| v.parse().ok()) + config + .get_value("folders", &format!("{key}{name}"), None) + .and_then(|v| v.parse().ok()) }; folders.push(FolderConfig { - indice: index.to_string(), path, refresh, careful: threshold("careful"), @@ -123,21 +124,22 @@ impl Plugin for FoldersPlugin { fn update_views(&mut self) { let key = self.get_key(); self.model.update_views(key); - let items: Vec<(String, i64, u64, Option, Option, Option)> = match &self.model.stats { - Value::Array(list) => list - .iter() - .filter_map(|item| { - let path = item.get("path")?.as_str()?.to_string(); - let errno = item.get("errno")?.as_i64()?; - let size = item.get("size")?.as_u64()?; - let careful = item.get("careful").and_then(Value::as_f64); - let warning = item.get("warning").and_then(Value::as_f64); - let critical = item.get("critical").and_then(Value::as_f64); - Some((path, errno, size, careful, warning, critical)) - }) - .collect(), - _ => Vec::new(), - }; + let items: Vec<(String, i64, u64, Option, Option, Option)> = + match &self.model.stats { + Value::Array(list) => list + .iter() + .filter_map(|item| { + let path = item.get("path")?.as_str()?.to_string(); + let errno = item.get("errno")?.as_i64()?; + let size = item.get("size")?.as_u64()?; + let careful = item.get("careful").and_then(Value::as_f64); + let warning = item.get("warning").and_then(Value::as_f64); + let critical = item.get("critical").and_then(Value::as_f64); + Some((path, errno, size, careful, warning, critical)) + }) + .collect(), + _ => Vec::new(), + }; for (path, errno, size, careful, warning, critical) in items { if errno != 0 { continue; @@ -151,7 +153,8 @@ impl Plugin for FoldersPlugin { } else { glances_core::Decoration::Default }; - self.model.set_view_decoration(Some(&path), "size", decoration); + self.model + .set_view_decoration(Some(&path), "size", decoration); } } @@ -161,7 +164,14 @@ impl Plugin for FoldersPlugin { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { let mut ret = Vec::new(); - if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + 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(12); @@ -172,7 +182,13 @@ impl Plugin for FoldersPlugin { false, false, )); - ret.push(self.model.curse_add_line(format!("{:>9}", "Size"), glances_core::Decoration::Default, false, false, false)); + ret.push(self.model.curse_add_line( + format!("{:>9}", "Size"), + glances_core::Decoration::Default, + false, + false, + false, + )); let items = self.model.sorted_stats(); let Some(items) = items.as_array() else { return ret; @@ -183,7 +199,11 @@ impl Plugin for FoldersPlugin { let errno = i.get("errno").and_then(Value::as_i64).unwrap_or(0); ret.push(self.model.curse_new_line()); ret.push(self.model.curse_add_line( - format!("{:(), width = name_max_width), + format!( + "{:(), + width = name_max_width + ), glances_core::Decoration::Default, false, false, diff --git a/crates/glances-plugins/src/plugins/fs.rs b/crates/glances-plugins/src/plugins/fs.rs index 02cb96b..0df7b8c 100644 --- a/crates/glances-plugins/src/plugins/fs.rs +++ b/crates/glances-plugins/src/plugins/fs.rs @@ -59,11 +59,16 @@ impl Plugin for FsPlugin { let mut stats = Vec::new(); for mount in crate::collectors::read_mounts() { let is_physical = mount.device.starts_with("/dev/"); - let is_allowed = allowed_fs_types.iter().any(|fs_type| mount.fs_type.contains(fs_type.as_str())); + let is_allowed = allowed_fs_types + .iter() + .any(|fs_type| mount.fs_type.contains(fs_type.as_str())); if !is_physical && !is_allowed { continue; } - if !self.model.is_display_any(&[&mount.mount_point, &mount.device], "") { + if !self + .model + .is_display_any(&[&mount.mount_point, &mount.device], "") + { continue; } let Some(usage) = crate::collectors::disk_usage(&mount.mount_point) else { @@ -78,7 +83,10 @@ impl Plugin for FsPlugin { item.insert("size".to_string(), json!(usage.total)); item.insert("used".to_string(), json!(usage.used)); item.insert("free".to_string(), json!(usage.free)); - item.insert("percent".to_string(), json!((usage.percent * 10.0).round() / 10.0)); + item.insert( + "percent".to_string(), + json!((usage.percent * 10.0).round() / 10.0), + ); item.insert("key".to_string(), json!("mnt_point")); if let Some(alias) = self.model.has_alias(&mnt_point) { item.insert("alias".to_string(), json!(alias)); @@ -96,7 +104,11 @@ impl Plugin for FsPlugin { .iter() .filter_map(|item| { let mnt = item.get("mnt_point")?.as_str()?.to_string(); - let options = item.get("options").and_then(Value::as_str).unwrap_or("").to_string(); + let options = item + .get("options") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); let size = item.get("size")?.as_f64()?; let free = item.get("free")?.as_f64()?; Some((mnt, size, free, options)) @@ -109,8 +121,18 @@ impl Plugin for FsPlugin { if options.split(',').any(|o| o == "ro") { continue; } - let decoration = self.model.get_alert(size - free, 0.0, size, true, false, Some(&mnt_point), None, false); - self.model.set_view_decoration(Some(&mnt_point), "used", decoration); + let decoration = self.model.get_alert( + size - free, + 0.0, + size, + true, + false, + Some(&mnt_point), + None, + false, + ); + self.model + .set_view_decoration(Some(&mnt_point), "used", decoration); } } @@ -120,7 +142,14 @@ impl Plugin for FsPlugin { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { let mut ret = Vec::new(); - if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + 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); @@ -132,30 +161,66 @@ impl Plugin for FsPlugin { false, )); if ctx.runtime.fs_free_space { - ret.push(self.model.curse_add_line(format!("{:>8}", "Free"), glances_core::Decoration::Default, false, false, false)); + 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!("{:>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)); + 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()); + 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 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) { + 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::()); + mnt_point = format!( + "{}_", + mnt_point.chars().take(name_max_width).collect::() + ); } ret.push(self.model.curse_new_line()); ret.push(self.model.curse_add_line( diff --git a/crates/glances-plugins/src/plugins/ip.rs b/crates/glances-plugins/src/plugins/ip.rs index b5d0755..9d2af91 100644 --- a/crates/glances-plugins/src/plugins/ip.rs +++ b/crates/glances-plugins/src/plugins/ip.rs @@ -80,12 +80,15 @@ impl IpPlugin { if let (Some(username), Some(password)) = (&self.public_username, &self.public_password) { request = request.header("Authorization", &format!("Basic {}:{}", username, password)); } - if let Ok(mut response) = request.call() { - if let Ok(value) = response.body_mut().read_json::() { - self.public_address = - value.get(&self.public_field).and_then(Value::as_str).unwrap_or("").to_string(); - self.public_info = Some(value); - } + if let Ok(mut response) = request.call() + && let Ok(value) = response.body_mut().read_json::() + { + self.public_address = value + .get(&self.public_field) + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + self.public_info = Some(value); } } @@ -130,7 +133,10 @@ impl Plugin for IpPlugin { if let Some(mask) = &mask { stats.insert("mask".to_string(), json!(mask)); } - stats.insert("mask_cidr".to_string(), json!(crate::collectors::ip_to_cidr(mask.as_deref()))); + stats.insert( + "mask_cidr".to_string(), + json!(crate::collectors::ip_to_cidr(mask.as_deref())), + ); self.refresh_public(); if !self.public_address.is_empty() { let public = if ctx.args.hide_public_info { @@ -158,14 +164,26 @@ impl Plugin for IpPlugin { return ret; }; let mut text = address.to_string(); - if let Some(mask_cidr) = s.get("mask_cidr").and_then(Value::as_u64) { - if mask_cidr > 0 { - text += &format!("/{mask_cidr}"); - } + if let Some(mask_cidr) = s.get("mask_cidr").and_then(Value::as_u64) + && mask_cidr > 0 + { + text += &format!("/{mask_cidr}"); } - ret.push(self.model.curse_add_line(text, glances_core::Decoration::Default, false, false, false)); + ret.push(self.model.curse_add_line( + text, + glances_core::Decoration::Default, + false, + false, + false, + )); if let Some(gateway) = s.get("gateway").and_then(Value::as_str) { - ret.push(self.model.curse_add_line(format!(" gw {gateway}"), glances_core::Decoration::Default, true, false, false)); + ret.push(self.model.curse_add_line( + format!(" gw {gateway}"), + glances_core::Decoration::Default, + true, + false, + false, + )); } ret } diff --git a/crates/glances-plugins/src/plugins/irq.rs b/crates/glances-plugins/src/plugins/irq.rs index 7a0f0b6..cf39e57 100644 --- a/crates/glances-plugins/src/plugins/irq.rs +++ b/crates/glances-plugins/src/plugins/irq.rs @@ -28,7 +28,10 @@ impl IrqPlugin { args.disable_config_exec, ); model.set_item_key("irq_line"); - Self { model, lasts: IndexMap::new() } + Self { + model, + lasts: IndexMap::new(), + } } } @@ -57,7 +60,10 @@ impl Plugin for IrqPlugin { return Ok(Value::Array(stats)); }; let mut lines = content.lines(); - let cpu_number = lines.next().map(|header| header.split_whitespace().count()).unwrap_or(0); + let cpu_number = lines + .next() + .map(|header| header.split_whitespace().count()) + .unwrap_or(0); for line in lines { let fields: Vec<&str> = line.split_whitespace().collect(); if fields.len() < 2 { @@ -69,7 +75,10 @@ impl Plugin for IrqPlugin { } else { first.to_string() }; - let current: u64 = fields[1..1 + cpu_number].iter().filter_map(|v| v.parse::().ok()).sum(); + let current: u64 = fields[1..1 + cpu_number] + .iter() + .filter_map(|v| v.parse::().ok()) + .sum(); let irq_rate = match self.lasts.get(&irq_line) { Some(last) if *last > 0 && time_since_update > 0.0 => { ((current.saturating_sub(*last)) as f64 / time_since_update) as i64 @@ -101,7 +110,14 @@ impl Plugin for IrqPlugin { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { let mut ret = Vec::new(); - if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + 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); @@ -112,7 +128,13 @@ impl Plugin for IrqPlugin { false, false, )); - ret.push(self.model.curse_add_line(format!("{:>9}", "Rate/s"), glances_core::Decoration::Default, 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; }; @@ -121,13 +143,23 @@ impl Plugin for IrqPlugin { 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 = name_max_width), + format!( + "{:(), + 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.push(self.model.curse_add_line(format!("{rate:>9}"), glances_core::Decoration::Default, false, false, false)); } ret } diff --git a/crates/glances-plugins/src/plugins/load.rs b/crates/glances-plugins/src/plugins/load.rs index 6e118a7..355f50e 100644 --- a/crates/glances-plugins/src/plugins/load.rs +++ b/crates/glances-plugins/src/plugins/load.rs @@ -64,13 +64,21 @@ impl Plugin for LoadPlugin { fn update_views(&mut self) { let key = self.get_key(); self.model.update_views(key); - let max = self.model.stats.get("cpucore").and_then(Value::as_f64).unwrap_or(0.0) * 100.0; + let max = self + .model + .stats + .get("cpucore") + .and_then(Value::as_f64) + .unwrap_or(0.0) + * 100.0; if let Some(min15) = self.model.stats.get("min15").and_then(Value::as_f64) { let decoration = self.model.get_alert_log(min15, max, None); self.model.set_view_decoration(None, "min15", decoration); } if let Some(min5) = self.model.stats.get("min5").and_then(Value::as_f64) { - let decoration = self.model.get_alert(min5, 0.0, max, true, false, None, None, false); + let decoration = self + .model + .get_alert(min5, 0.0, max, true, false, None, None, false); self.model.set_view_decoration(None, "min5", decoration); } } @@ -85,23 +93,47 @@ impl Plugin for LoadPlugin { 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)), + 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)); - } + if let Some(cpucore) = s.get("cpucore").and_then(Value::as_f64) + && 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); + 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 { diff --git a/crates/glances-plugins/src/plugins/mem.rs b/crates/glances-plugins/src/plugins/mem.rs index 7ecc655..c6b99b1 100644 --- a/crates/glances-plugins/src/plugins/mem.rs +++ b/crates/glances-plugins/src/plugins/mem.rs @@ -69,7 +69,11 @@ impl Plugin for MemPlugin { let buffers = get("Buffers"); // procps-ng "free" sums reclaimable slab to cached. let cached = get("Cached") + get("SReclaimable"); - let shared = if meminfo.contains_key("Shmem") { get("Shmem") } else { get("MemShared") }; + let shared = if meminfo.contains_key("Shmem") { + get("Shmem") + } else { + get("MemShared") + }; let active = get("Active"); let inactive = get("Inactive"); let mut available = get("MemAvailable"); @@ -104,8 +108,18 @@ impl Plugin for MemPlugin { fn update_views(&mut self) { let key = self.get_key(); self.model.update_views(key); - let total = self.model.stats.get("total").and_then(Value::as_f64).unwrap_or(0.0); - let used = self.model.stats.get("used").and_then(Value::as_f64).unwrap_or(0.0); + let total = self + .model + .stats + .get("total") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let used = self + .model + .stats + .get("used") + .and_then(Value::as_f64) + .unwrap_or(0.0); let decoration = self.model.get_alert_log(used, total, None); self.model.set_view_decoration(None, "percent", decoration); } @@ -121,8 +135,20 @@ impl Plugin for MemPlugin { 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)); + ret.push(self.model.curse_add_line( + "MEM".to_string(), + 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), @@ -136,7 +162,10 @@ impl Plugin for MemPlugin { // 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), " ", "", "")); + ret.extend( + self.model + .curse_add_stat("inactive", Some(16), " ", "", ""), + ); // Third line: available + buffers. ret.push(self.model.curse_new_line()); if self.available { diff --git a/crates/glances-plugins/src/plugins/memswap.rs b/crates/glances-plugins/src/plugins/memswap.rs index 43cd706..487ce56 100644 --- a/crates/glances-plugins/src/plugins/memswap.rs +++ b/crates/glances-plugins/src/plugins/memswap.rs @@ -53,7 +53,11 @@ impl Plugin for MemSwapPlugin { let total = meminfo.get("SwapTotal").copied().unwrap_or(0); let free = meminfo.get("SwapFree").copied().unwrap_or(0); let used = total.saturating_sub(free); - let percent = if total > 0 { (used as f64 * 100.0 / total as f64 * 10.0).round() / 10.0 } else { 0.0 }; + let percent = if total > 0 { + (used as f64 * 100.0 / total as f64 * 10.0).round() / 10.0 + } else { + 0.0 + }; let vmstat = crate::collectors::read_vmstat(); // pswpin/pswpout are expressed in 4 KiB pages. let sin = vmstat.get("pswpin").copied().unwrap_or(0) * 4 * 1024; @@ -73,8 +77,18 @@ impl Plugin for MemSwapPlugin { fn update_views(&mut self) { let key = self.get_key(); self.model.update_views(key); - let total = self.model.stats.get("total").and_then(Value::as_f64).unwrap_or(0.0); - let used = self.model.stats.get("used").and_then(Value::as_f64).unwrap_or(0.0); + let total = self + .model + .stats + .get("total") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let used = self + .model + .stats + .get("used") + .and_then(Value::as_f64) + .unwrap_or(0.0); let decoration = self.model.get_alert_log(used, total, None); self.model.set_view_decoration(None, "percent", decoration); } @@ -89,10 +103,25 @@ impl Plugin for MemSwapPlugin { 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)), + "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, diff --git a/crates/glances-plugins/src/plugins/network.rs b/crates/glances-plugins/src/plugins/network.rs index 3db9986..b183582 100644 --- a/crates/glances-plugins/src/plugins/network.rs +++ b/crates/glances-plugins/src/plugins/network.rs @@ -39,11 +39,19 @@ impl NetworkPlugin { ); 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 } + 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, + } } } @@ -90,7 +98,10 @@ impl Plugin for NetworkPlugin { item.insert("key".to_string(), json!("interface_name")); item.insert("interface_name".to_string(), json!(interface_name)); item.insert("alias".to_string(), json!(alias)); - item.insert("bytes_all".to_string(), json!(counters.bytes_sent + counters.bytes_recv)); + item.insert( + "bytes_all".to_string(), + json!(counters.bytes_sent + counters.bytes_recv), + ); stats.push(Value::Object(item)); } Ok(Value::Array(stats)) @@ -104,8 +115,14 @@ impl Plugin for NetworkPlugin { .iter() .filter_map(|item| { let name = item.get("interface_name")?.as_str()?.to_string(); - let rx = item.get("bytes_recv_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); - let tx = item.get("bytes_sent_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); + let rx = item + .get("bytes_recv_rate_per_sec") + .and_then(Value::as_f64) + .unwrap_or(0.0); + let tx = item + .get("bytes_sent_rate_per_sec") + .and_then(Value::as_f64) + .unwrap_or(0.0); let speed = item.get("speed").and_then(Value::as_f64).unwrap_or(0.0); let real_name = name.split(':').next().unwrap_or("").to_string(); Some((name, rx, tx, speed, Some(real_name))) @@ -117,21 +134,57 @@ impl Plugin for NetworkPlugin { let bps_rx = rx as i64 * 8; let bps_tx = tx as i64 * 8; let action_key = action_key.as_deref(); - let mut alert_rx = - self.model.get_alert(bps_rx as f64, 0.0, 100.0, true, false, Some("rx"), action_key, false); - let mut alert_tx = - self.model.get_alert(bps_tx as f64, 0.0, 100.0, true, false, Some("tx"), action_key, false); + let mut alert_rx = self.model.get_alert( + bps_rx as f64, + 0.0, + 100.0, + true, + false, + Some("rx"), + action_key, + false, + ); + let mut alert_tx = self.model.get_alert( + bps_tx as f64, + 0.0, + 100.0, + true, + false, + Some("tx"), + action_key, + false, + ); if alert_rx == glances_core::Decoration::Default && speed != 0.0 { - alert_rx = self.model.get_alert(bps_rx as f64, 0.0, speed, true, false, Some("rx"), None, false); + alert_rx = self.model.get_alert( + bps_rx as f64, + 0.0, + speed, + true, + false, + Some("rx"), + None, + false, + ); } if alert_tx == glances_core::Decoration::Default && speed != 0.0 { - alert_tx = self.model.get_alert(bps_tx as f64, 0.0, speed, true, false, Some("tx"), None, false); + alert_tx = self.model.get_alert( + bps_tx as f64, + 0.0, + speed, + true, + false, + Some("tx"), + None, + false, + ); } for field in ["bytes_recv", "bytes_recv_rate_per_sec"] { - self.model.set_view_decoration(Some(&interface_name), field, alert_rx); + self.model + .set_view_decoration(Some(&interface_name), field, alert_rx); } for field in ["bytes_sent", "bytes_sent_rate_per_sec"] { - self.model.set_view_decoration(Some(&interface_name), field, alert_tx); + self.model + .set_view_decoration(Some(&interface_name), field, alert_tx); } } } @@ -156,16 +209,52 @@ impl Plugin for NetworkPlugin { )); 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)); + 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)); + 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)); + 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)); + 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(); @@ -173,7 +262,11 @@ impl Plugin for NetworkPlugin { return ret; }; for i in items { - let item_key = i.get("interface_name").and_then(Value::as_str).unwrap_or("").to_string(); + 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 @@ -227,7 +320,13 @@ impl Plugin for NetworkPlugin { false, )); if ctx.runtime.network_sum { - ret.push(self.model.curse_add_line(format!("{ax:>14}"), glances_core::Decoration::Default, false, false, false)); + 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}"), diff --git a/crates/glances-plugins/src/plugins/now.rs b/crates/glances-plugins/src/plugins/now.rs index 4bd983f..ea0452b 100644 --- a/crates/glances-plugins/src/plugins/now.rs +++ b/crates/glances-plugins/src/plugins/now.rs @@ -43,11 +43,15 @@ impl Plugin for NowPlugin { } fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result { - let iso = chrono::Local::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() + ctx.config + .get_value("global", "strftime_format", Some("")) + .unwrap_or_default() }; let custom = if !strftime.is_empty() { crate::collectors::strftime(&strftime) @@ -72,7 +76,18 @@ impl Plugin for NowPlugin { } fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec { - 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)] + 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, + )] } } diff --git a/crates/glances-plugins/src/plugins/percpu.rs b/crates/glances-plugins/src/plugins/percpu.rs index a119454..3908d1a 100644 --- a/crates/glances-plugins/src/plugins/percpu.rs +++ b/crates/glances-plugins/src/plugins/percpu.rs @@ -41,7 +41,10 @@ impl PerCpuPlugin { ); model.set_item_key("cpu_number"); let max_cpu_display = config.get_int_value("percpu", "max_cpu_display", 4).max(1) as usize; - Self { model, max_cpu_display } + Self { + model, + max_cpu_display, + } } } @@ -75,13 +78,27 @@ impl Plugin for PerCpuPlugin { } let items = &items; let quicklook_disabled = !ctx.runtime.is_plugin_enabled("quicklook"); - let mut header = vec!["user", "system", "iowait", "idle", "irq", "nice", "steal", "guest"]; + 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)); + 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)); + 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 = items.clone(); @@ -99,26 +116,54 @@ impl Plugin for PerCpuPlugin { 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)); + 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), + 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)); + 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)); + ret.push(self.model.curse_add_line( + "CPU* ", + glances_core::Decoration::Default, + false, + false, + false, + )); } for stat in &header { let values: Vec = sorted @@ -126,9 +171,21 @@ impl Plugin for PerCpuPlugin { .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::() / 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)); + let mean = if values.is_empty() { + 0.0 + } else { + values.iter().sum::() / 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 diff --git a/crates/glances-plugins/src/plugins/ports.rs b/crates/glances-plugins/src/plugins/ports.rs index 712d5f0..df016da 100644 --- a/crates/glances-plugins/src/plugins/ports.rs +++ b/crates/glances-plugins/src/plugins/ports.rs @@ -57,21 +57,21 @@ impl PortsPlugin { let mut entries = Vec::new(); if config.has_section("ports") { // Default gateway entry. - if config.get_bool_value("ports", "port_default_gateway", false) { - if let Some(gateway) = crate::collectors::get_default_gateway() { - entries.push(PortEntry { - indice: "port_0".to_string(), - host: gateway, - port: None, - url: None, - description: "Default gateway".to_string(), - refresh, - timeout: default_timeout, - rtt_warning: None, - timer: Instant::now() - Duration::from_secs(3600), - status: None, - }); - } + if config.get_bool_value("ports", "port_default_gateway", false) + && let Some(gateway) = crate::collectors::get_default_gateway() + { + entries.push(PortEntry { + indice: "port_0".to_string(), + host: gateway, + port: None, + url: None, + description: "Default gateway".to_string(), + refresh, + timeout: default_timeout, + rtt_warning: None, + timer: Instant::now() - Duration::from_secs(3600), + status: None, + }); } for index in 1..=255 { let key = format!("port_{index}_"); @@ -81,14 +81,17 @@ impl PortsPlugin { if host.is_empty() { continue; } - let port = config.get_value("ports", &format!("{key}port"), None).and_then(|v| v.parse().ok()); + let port = config + .get_value("ports", &format!("{key}port"), None) + .and_then(|v| v.parse().ok()); let description = config .get_value("ports", &format!("{key}description"), None) .unwrap_or_else(|| match port { Some(port) => format!("{host}:{port}"), None => host.clone(), }); - let timeout = config.get_float_value("ports", &format!("{key}timeout"), default_timeout); + let timeout = + config.get_float_value("ports", &format!("{key}timeout"), default_timeout); let rtt_warning = config .get_value("ports", &format!("{key}rtt_warning"), None) .and_then(|v| v.parse::().ok()) @@ -114,8 +117,11 @@ impl PortsPlugin { if url.is_empty() { continue; } - let description = config.get_value("ports", &format!("{key}description"), None).unwrap_or_else(|| url.clone()); - let timeout = config.get_float_value("ports", &format!("{key}timeout"), default_timeout); + let description = config + .get_value("ports", &format!("{key}description"), None) + .unwrap_or_else(|| url.clone()); + let timeout = + config.get_float_value("ports", &format!("{key}timeout"), default_timeout); let rtt_warning = config .get_value("ports", &format!("{key}rtt_warning"), None) .and_then(|v| v.parse::().ok()) @@ -134,14 +140,21 @@ impl PortsPlugin { }); } } - Self { model, entries, default_timeout } + Self { + model, + entries, + default_timeout, + } } fn check(entry: &PortEntry) -> Option { let start = Instant::now(); if let Some(url) = &entry.url { let timeout = Duration::from_secs_f64(entry.timeout.max(0.1)); - let request = ureq::get(url).config().timeout_global(Some(timeout)).build(); + let request = ureq::get(url) + .config() + .timeout_global(Some(timeout)) + .build(); match request.call() { Ok(_) => Some(start.elapsed().as_secs_f64()), Err(_) => None, @@ -158,7 +171,13 @@ impl PortsPlugin { } else { // ICMP check with the ping command. let output = std::process::Command::new("ping") - .args(["-c", "1", "-W", &format!("{}", entry.timeout.max(1.0) as i64), &entry.host]) + .args([ + "-c", + "1", + "-W", + &format!("{}", entry.timeout.max(1.0) as i64), + &entry.host, + ]) .output() .ok()?; let text = String::from_utf8_lossy(&output.stdout); @@ -237,10 +256,13 @@ impl Plugin for PortsPlugin { for (indice, status, warning) in items { let decoration = match status { None => glances_core::Decoration::Critical, - Some(status) if warning.is_some_and(|w| status > w) => glances_core::Decoration::Warning, + Some(status) if warning.is_some_and(|w| status > w) => { + glances_core::Decoration::Warning + } Some(_) => glances_core::Decoration::Ok, }; - self.model.set_view_decoration(Some(&indice), "status", decoration); + self.model + .set_view_decoration(Some(&indice), "status", decoration); } } @@ -250,7 +272,14 @@ impl Plugin for PortsPlugin { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { let mut ret = Vec::new(); - if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + 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(12); @@ -261,7 +290,13 @@ impl Plugin for PortsPlugin { false, false, )); - ret.push(self.model.curse_add_line(format!("{:>9}", "RTT"), glances_core::Decoration::Default, false, false, false)); + ret.push(self.model.curse_add_line( + format!("{:>9}", "RTT"), + glances_core::Decoration::Default, + false, + false, + false, + )); let items = self.model.sorted_stats(); let Some(items) = items.as_array() else { return ret; @@ -272,7 +307,11 @@ impl Plugin for PortsPlugin { let status = i.get("status").and_then(Value::as_f64); ret.push(self.model.curse_new_line()); ret.push(self.model.curse_add_line( - format!("{:(), width = name_max_width), + format!( + "{:(), + width = name_max_width + ), glances_core::Decoration::Default, false, false, diff --git a/crates/glances-plugins/src/plugins/processcount.rs b/crates/glances-plugins/src/plugins/processcount.rs index fc22801..3ce8fa1 100644 --- a/crates/glances-plugins/src/plugins/processcount.rs +++ b/crates/glances-plugins/src/plugins/processcount.rs @@ -77,50 +77,127 @@ impl Plugin for ProcessCountPlugin { 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)); + 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)); + 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_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)); + 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)); + 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)); + 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)); + 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!(" {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)); + 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 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)); + 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.push(self.model.curse_add_line( + msg, + glances_core::Decoration::Default, + false, + false, + false, + )); ret } } diff --git a/crates/glances-plugins/src/plugins/processlist.rs b/crates/glances-plugins/src/plugins/processlist.rs index 9bc7b97..7b9cb1f 100644 --- a/crates/glances-plugins/src/plugins/processlist.rs +++ b/crates/glances-plugins/src/plugins/processlist.rs @@ -43,33 +43,48 @@ impl ProcessListPlugin { model.set_item_key("pid"); // Load the configuration. - if config.has_option("processlist", "sort_key") { - if let Some(sort_key) = config.get_value("processlist", "sort_key", None) { - let mut procs = processes().lock().unwrap(); - procs.set_sort_key(&sort_key, false); - } + if config.has_option("processlist", "sort_key") + && let Some(sort_key) = config.get_value("processlist", "sort_key", None) + { + let mut procs = processes().lock().unwrap(); + procs.set_sort_key(&sort_key, false); } - if config.has_option("processlist", "focus") { - if let Some(focus) = config.get_value("processlist", "focus", None) { - processes().lock().unwrap().filter_focus.set_filter(Some(&focus)); - } + if config.has_option("processlist", "focus") + && let Some(focus) = config.get_value("processlist", "focus", None) + { + processes() + .lock() + .unwrap() + .filter_focus + .set_filter(Some(&focus)); } - if config.has_option("processlist", "disable_stats") { - if let Some(disabled) = config.get_value("processlist", "disable_stats", None) { - let stats: Vec = disabled - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty() && !glances_core::processes::MANDATORY_PROCESS_STATS.contains(&s.as_str())) - .collect(); - processes().lock().unwrap().disable_stats = stats; - } + if config.has_option("processlist", "disable_stats") + && let Some(disabled) = config.get_value("processlist", "disable_stats", None) + { + let stats: Vec = disabled + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| { + !s.is_empty() + && !glances_core::processes::MANDATORY_PROCESS_STATS.contains(&s.as_str()) + }) + .collect(); + processes().lock().unwrap().disable_stats = stats; } if let Some(sort_key) = args.sort_processes_key { - processes().lock().unwrap().set_sort_key(sort_key.as_str(), false); + processes() + .lock() + .unwrap() + .set_sort_key(sort_key.as_str(), false); } 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 } + let disable_virtual_memory = + config.get_bool_value("processlist", "disable_virtual_memory", false); + Self { + model, + nb_log_core, + disable_virtual_memory, + } } } @@ -129,7 +144,11 @@ impl Plugin for ProcessListPlugin { 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 + .max_values + .get("memory_percent") + .copied() + .unwrap_or(0.0), procs.sort_key.clone(), ) }; @@ -140,33 +159,120 @@ impl Plugin for ProcessListPlugin { // Header. if ctx.runtime.disable_irix { - ret.push(self.model.curse_add_line(format!("{:<6} ", "CPUi"), Decoration::Sort, false, false, false)); + 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!("{:<6} ", "CPU%"), + Decoration::Sort, + false, + false, + false, + )); } - ret.push(self.model.curse_add_line(format!("{:<5} ", "MEM%"), Decoration::Default, 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} ", "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} ", "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)); + 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!("{:>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)); + 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)); + 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( + format!("{:>3} ", "CPU"), + Decoration::Default, + true, + false, + false, + )); } - ret.push(self.model.curse_add_line("Command", Decoration::Sort, false, false, false)); + ret.push( + self.model + .curse_add_line("Command", Decoration::Sort, false, false, false), + ); let _ = sort_key; let max_pid_size = { @@ -181,7 +287,11 @@ impl Plugin for ProcessListPlugin { 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 { " " }, + if selected && !disable_cursor { + ">" + } else { + " " + }, Decoration::Selected, false, false, @@ -205,10 +315,19 @@ impl Plugin for ProcessListPlugin { None, false, ); - ret.push(self.model.curse_add_line(format!("{cpu_value:<6.1}"), cpu_decoration, false, false, 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 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, @@ -219,13 +338,22 @@ impl Plugin for ProcessListPlugin { None, false, ); - ret.push(self.model.curse_add_line(format!("{memory_percent:<5.1} "), mem_decoration, false, false, 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); + 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, @@ -234,7 +362,10 @@ impl Plugin for ProcessListPlugin { false, )); } - let rss = memory_info.and_then(|m| m.get("rss")).and_then(Value::as_f64).unwrap_or(0.0); + 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, @@ -246,18 +377,36 @@ impl Plugin for ProcessListPlugin { // 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)); + 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)); + 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 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; @@ -265,29 +414,62 @@ impl Plugin for ProcessListPlugin { let (msg, decoration) = if hours > 99 { (format!("{hours:<7}h"), Decoration::Default) } else if hours > 0 { - (format!("{hours}h{minutes}:{seconds:02}"), Decoration::CpuTime) + ( + 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)); + 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)); + 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)); + 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)); + 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)); + ret.push(self.model.curse_add_line( + format!("{status:>1} "), + status_decoration, + false, + false, + false, + )); // IO rates. if wide { @@ -301,12 +483,28 @@ impl Plugin for ProcessListPlugin { 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") }) + 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)), + 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, + )), } } } @@ -318,7 +516,10 @@ impl Plugin for ProcessListPlugin { Some(value) if value >= 0 => format!("{value:>3} "), _ => format!("{:>3} ", "-"), }; - ret.push(self.model.curse_add_line(msg, Decoration::Default, true, false, false)); + ret.push( + self.model + .curse_add_line(msg, Decoration::Default, true, false, false), + ); } // Command. @@ -326,9 +527,17 @@ impl Plugin for ProcessListPlugin { let cmdline: Vec = p .get("cmdline") .and_then(Value::as_array) - .map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect()) + .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 }; + 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) { @@ -341,17 +550,50 @@ impl Plugin for ProcessListPlugin { }; 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)); + 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)); + 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)); + 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.push(self.model.curse_add_line( + name.to_string(), + process_decoration, + false, + false, + true, + )); } } ret @@ -367,10 +609,10 @@ impl ProcessListPlugin { ("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; - } + if let Some(items) = self.model.get_limit_list(criticality, "processlist") + && items.iter().any(|item| item == &value) + { + return decoration; } } glances_core::Decoration::Default @@ -383,13 +625,17 @@ impl ProcessListPlugin { ("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 let Some(items) = self.model.get_limit_list(criticality, "processlist") + && items.iter().any(|item| item == value) + { + return decoration; } } - if value == "R" { glances_core::Decoration::Ok } else { glances_core::Decoration::Default } + if value == "R" { + glances_core::Decoration::Ok + } else { + glances_core::Decoration::Default + } } } diff --git a/crates/glances-plugins/src/plugins/quicklook.rs b/crates/glances-plugins/src/plugins/quicklook.rs index a008870..1d41936 100644 --- a/crates/glances-plugins/src/plugins/quicklook.rs +++ b/crates/glances-plugins/src/plugins/quicklook.rs @@ -51,13 +51,23 @@ impl QuicklookPlugin { args.disable_config_exec, ); let stats_list = model.get_conf_value("list", ""); - let mut stats_list: Vec = - if stats_list.is_empty() { DEFAULT_STATS_LIST.iter().map(|s| s.to_string()).collect() } else { stats_list }; - if stats_list.iter().any(|s| !AVAILABLE_STATS_LIST.contains(&s.as_str())) { + let mut stats_list: Vec = if stats_list.is_empty() { + DEFAULT_STATS_LIST.iter().map(|s| s.to_string()).collect() + } else { + stats_list + }; + if stats_list + .iter() + .any(|s| !AVAILABLE_STATS_LIST.contains(&s.as_str())) + { stats_list = DEFAULT_STATS_LIST.iter().map(|s| s.to_string()).collect(); } let max_cpu_display = config.get_int_value("percpu", "max_cpu_display", 4).max(0) as usize; - Self { model, stats_list, max_cpu_display } + Self { + model, + stats_list, + max_cpu_display, + } } fn mhz_to_hz(value: Option) -> Option { @@ -88,7 +98,10 @@ impl Plugin for QuicklookPlugin { (info, cpu, percpu) }; stats.insert("cpu_name".to_string(), json!(info.cpu_name)); - stats.insert("cpu_hz_current".to_string(), json!(Self::mhz_to_hz(info.cpu_hz_current))); + stats.insert( + "cpu_hz_current".to_string(), + json!(Self::mhz_to_hz(info.cpu_hz_current)), + ); stats.insert("cpu_hz".to_string(), json!(Self::mhz_to_hz(info.cpu_hz))); stats.insert("cpu".to_string(), json!(cpu_value)); stats.insert("percpu".to_string(), Value::Array(percpu_values)); @@ -98,14 +111,19 @@ impl Plugin for QuicklookPlugin { let available = meminfo.get("MemAvailable").copied().unwrap_or(0) as f64; stats.insert( "mem".to_string(), - json!(if total > 0.0 { ((total - available) / total * 100.0 * 10.0).round() / 10.0 } else { 0.0 }), + json!(if total > 0.0 { + ((total - available) / total * 100.0 * 10.0).round() / 10.0 + } else { + 0.0 + }), ); let swap_total = meminfo.get("SwapTotal").copied().unwrap_or(0); let swap_free = meminfo.get("SwapFree").copied().unwrap_or(0); stats.insert( "swap".to_string(), json!(if swap_total > 0 { - (((swap_total - swap_free) as f64 * 100.0 / swap_total as f64) * 10.0).round() / 10.0 + (((swap_total - swap_free) as f64 * 100.0 / swap_total as f64) * 10.0).round() + / 10.0 } else { 0.0 }), @@ -113,9 +131,16 @@ impl Plugin for QuicklookPlugin { let log_core = crate::collectors::logical_core_count(); stats.insert("cpu_log_core".to_string(), json!(log_core)); - stats.insert("cpu_phys_core".to_string(), json!(crate::collectors::physical_core_count())); + stats.insert( + "cpu_phys_core".to_string(), + json!(crate::collectors::physical_core_count()), + ); let load = crate::collectors::read_loadavg().map(|(_, _, min15)| { - if log_core > 0 { min15 / log_core as f64 * 100.0 } else { 0.0 } + if log_core > 0 { + min15 / log_core as f64 * 100.0 + } else { + 0.0 + } }); stats.insert("load".to_string(), json!(load)); stats.insert("gpu_mem".to_string(), Value::Null); @@ -130,8 +155,16 @@ impl Plugin for QuicklookPlugin { let stats_list = self.stats_list.clone(); for stat_key in &stats_list { if let Some(value) = self.model.stats.get(stat_key).and_then(Value::as_f64) { - let decoration = - self.model.get_alert(value, 0.0, 100.0, true, false, Some(stat_key), None, false); + let decoration = self.model.get_alert( + value, + 0.0, + 100.0, + true, + false, + Some(stat_key), + None, + false, + ); self.model.set_view_decoration(None, stat_key, decoration); } } @@ -142,22 +175,36 @@ impl Plugin for QuicklookPlugin { let mut percpu_decoration = serde_json::Map::new(); if let Some(percpu) = self.model.stats.get("percpu").and_then(Value::as_array) { for cpu in percpu { - let number = cpu.get("cpu_number").map(glances_core::py_str).unwrap_or_default(); + let number = cpu + .get("cpu_number") + .map(glances_core::py_str) + .unwrap_or_default(); let total = cpu.get("total").and_then(Value::as_f64).unwrap_or(0.0); - percpu_decoration.insert(number, json!(decoration_for_percent(&self.model, total).name())); + percpu_decoration.insert( + number, + json!(decoration_for_percent(&self.model, total).name()), + ); } if percpu.len() > self.max_cpu_display && self.max_cpu_display > 0 { - let mut hidden: Vec = - percpu.iter().filter_map(|cpu| cpu.get("total").and_then(Value::as_f64)).collect(); + let mut hidden: Vec = percpu + .iter() + .filter_map(|cpu| cpu.get("total").and_then(Value::as_f64)) + .collect(); hidden.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); let hidden = &hidden[self.max_cpu_display.min(hidden.len())..]; if !hidden.is_empty() { let mean = hidden.iter().sum::() / hidden.len() as f64; - percpu_decoration.insert("other".to_string(), json!(decoration_for_percent(&self.model, mean).name())); + percpu_decoration.insert( + "other".to_string(), + json!(decoration_for_percent(&self.model, mean).name()), + ); } } } - self.model.views.insert("percpu_decoration".to_string(), Value::Object(percpu_decoration)); + self.model.views.insert( + "percpu_decoration".to_string(), + Value::Object(percpu_decoration), + ); } fn display_curse(&self) -> bool { @@ -171,26 +218,45 @@ impl Plugin for QuicklookPlugin { 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)) { + 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!("{:> = Vec::new(); - let mut push_line = |msg: String, bar_key: &str, percent: f64, decoration: glances_core::Decoration| { + 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), + 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; }; @@ -230,7 +314,11 @@ impl Plugin for QuicklookPlugin { 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 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); } @@ -240,14 +328,28 @@ impl Plugin for QuicklookPlugin { .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::() / hidden.len() as f64 }; + let mean = if hidden.is_empty() { + 0.0 + } else { + hidden.iter().sum::() / hidden.len() as f64 + }; let decoration = self.decoration_for_percpu(-1); - push_line(format!("{:<3}* ", key.to_uppercase()), key, mean, decoration); + 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); + push_line( + format!("{:<4} ", key.to_uppercase()), + key, + value, + decoration, + ); } } for line in lines { @@ -261,7 +363,11 @@ impl Plugin for QuicklookPlugin { 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() }; + 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, diff --git a/crates/glances-plugins/src/plugins/raid.rs b/crates/glances-plugins/src/plugins/raid.rs index dbf5938..f680e66 100644 --- a/crates/glances-plugins/src/plugins/raid.rs +++ b/crates/glances-plugins/src/plugins/raid.rs @@ -64,16 +64,18 @@ impl Plugin for RaidPlugin { let status = fields[2].to_string(); let raid_type = fields .iter() - .find(|f| f.starts_with("raid") || f.starts_with("linear") || f.starts_with("multipath")) + .find(|f| { + f.starts_with("raid") || f.starts_with("linear") || f.starts_with("multipath") + }) .map(|f| f.to_string()) .unwrap_or_else(|| "unknown".to_string()); // Devices (name[index]). let mut components = serde_json::Map::new(); for field in fields.iter().skip(3) { - if let Some(device) = field.split('[').next() { - if !device.is_empty() { - components.insert(device.to_string(), json!("active sync")); - } + if let Some(device) = field.split('[').next() + && !device.is_empty() + { + components.insert(device.to_string(), json!("active sync")); } } // The following line contains blocks and [used/available] [UU]. @@ -81,30 +83,30 @@ impl Plugin for RaidPlugin { let mut available = components.len(); if index + 1 < lines.len() { let detail = lines[index + 1]; - if let Some(start) = detail.rfind('[') { - if let Some(end) = detail[start..].find(']') { - let counts = &detail[start + 1..start + end]; - if let Some((u, a)) = counts.split_once('/') { - used = u.parse().unwrap_or(used as i64) as usize; - available = a.parse().unwrap_or(available as i64) as usize; - } + if let Some(start) = detail.rfind('[') + && let Some(end) = detail[start..].find(']') + { + let counts = &detail[start + 1..start + end]; + if let Some((u, a)) = counts.split_once('/') { + used = u.parse().unwrap_or(used as i64) as usize; + available = a.parse().unwrap_or(available as i64) as usize; } } - if let Some(start) = detail.rfind('[') { - if let Some(end) = detail[start..].find(']') { - let flags = &detail[start + 1..start + end]; - if flags.len() == components.len() { - let names: Vec = components.keys().cloned().collect(); - for (position, flag) in flags.chars().enumerate() { - if let Some(device) = names.get(position) { - let status = match flag { - 'U' => "active sync", - '_' => "faulty", - 'S' => "spare", - _ => "unknown", - }; - components.insert(device.clone(), json!(status)); - } + if let Some(start) = detail.rfind('[') + && let Some(end) = detail[start..].find(']') + { + let flags = &detail[start + 1..start + end]; + if flags.len() == components.len() { + let names: Vec = components.keys().cloned().collect(); + for (position, flag) in flags.chars().enumerate() { + if let Some(device) = names.get(position) { + let status = match flag { + 'U' => "active sync", + '_' => "faulty", + 'S' => "spare", + _ => "unknown", + }; + components.insert(device.clone(), json!(status)); } } } @@ -146,8 +148,20 @@ impl Plugin for RaidPlugin { false, false, )); - ret.push(self.model.curse_add_line(format!("{:>7}", "Used"), glances_core::Decoration::Default, false, false, false)); - ret.push(self.model.curse_add_line(format!("{:>7}", "Avail"), glances_core::Decoration::Default, false, false, false)); + ret.push(self.model.curse_add_line( + format!("{:>7}", "Used"), + glances_core::Decoration::Default, + false, + false, + false, + )); + ret.push(self.model.curse_add_line( + format!("{:>7}", "Avail"), + glances_core::Decoration::Default, + false, + false, + false, + )); let mut names: Vec<&String> = arrays.keys().collect(); names.sort(); for name in names { @@ -157,25 +171,48 @@ impl Plugin for RaidPlugin { let status = array.get("status").and_then(Value::as_str).unwrap_or(""); let used = array.get("used").and_then(Value::as_i64).unwrap_or(0); let available = array.get("available").and_then(Value::as_i64).unwrap_or(0); - let array_type = array.get("type").and_then(Value::as_str).unwrap_or("unknown"); + let array_type = array + .get("type") + .and_then(Value::as_str) + .unwrap_or("unknown"); let decoration = raid_alert(status, used, available, array_type); ret.push(self.model.curse_new_line()); ret.push(self.model.curse_add_line( - format!("{:(), width = name_max_width), + format!( + "{:(), + width = name_max_width + ), decoration, false, false, false, )); - ret.push(self.model.curse_add_line(format!("{used:>7}"), glances_core::Decoration::Default, false, false, false)); - ret.push(self.model.curse_add_line(format!("{available:>7}"), glances_core::Decoration::Default, false, false, false)); + ret.push(self.model.curse_add_line( + format!("{used:>7}"), + glances_core::Decoration::Default, + false, + false, + false, + )); + ret.push(self.model.curse_add_line( + format!("{available:>7}"), + glances_core::Decoration::Default, + false, + false, + false, + )); } ret } } - -fn raid_alert(status: &str, used: i64, available: i64, array_type: &str) -> glances_core::Decoration { +fn raid_alert( + status: &str, + used: i64, + available: i64, + array_type: &str, +) -> glances_core::Decoration { if status != "active" || used < available { return glances_core::Decoration::Critical; } diff --git a/crates/glances-plugins/src/plugins/sensors.rs b/crates/glances-plugins/src/plugins/sensors.rs index 2175ba9..a4a6260 100644 --- a/crates/glances-plugins/src/plugins/sensors.rs +++ b/crates/glances-plugins/src/plugins/sensors.rs @@ -49,27 +49,36 @@ fn read_hwmon() -> Vec { let name = file.file_name().to_string_lossy().to_string(); let path = file.path(); // Temperatures (millidegrees). - if let Some(rest) = name.strip_prefix("temp") { - if let Some(rest) = rest.strip_suffix("_input") { - let Some(raw) = read_value(&path) else { - continue; - }; - let value = raw / 1000.0; - let label = read_label(&dir, &format!("temp{rest}_label"), &format!("temp{rest}")); - let warning = read_label_value(&dir, &format!("temp{rest}_max")).map(|v| v / 1000.0); - let critical = read_label_value(&dir, &format!("temp{rest}_crit")).map(|v| v / 1000.0); - stats.push(sensor_item(&label, "C", value, warning, critical, "temperature_core")); - } + if let Some(rest) = name.strip_prefix("temp") + && let Some(rest) = rest.strip_suffix("_input") + { + let Some(raw) = read_value(&path) else { + continue; + }; + let value = raw / 1000.0; + let label = read_label(&dir, &format!("temp{rest}_label"), &format!("temp{rest}")); + let warning = + read_label_value(&dir, &format!("temp{rest}_max")).map(|v| v / 1000.0); + let critical = + read_label_value(&dir, &format!("temp{rest}_crit")).map(|v| v / 1000.0); + stats.push(sensor_item( + &label, + "C", + value, + warning, + critical, + "temperature_core", + )); } // Fans (RPM). - if let Some(rest) = name.strip_prefix("fan") { - if let Some(rest) = rest.strip_suffix("_input") { - let Some(value) = read_value(&path) else { - continue; - }; - let label = read_label(&dir, &format!("fan{rest}_label"), &format!("fan{rest}")); - stats.push(sensor_item(&label, "R", value, None, None, "fan_speed")); - } + if let Some(rest) = name.strip_prefix("fan") + && let Some(rest) = rest.strip_suffix("_input") + { + let Some(value) = read_value(&path) else { + continue; + }; + let label = read_label(&dir, &format!("fan{rest}_label"), &format!("fan{rest}")); + stats.push(sensor_item(&label, "R", value, None, None, "fan_speed")); } } } @@ -88,7 +97,9 @@ fn read_batteries() -> Vec { let dir = entry.path(); let name = entry.file_name().to_string_lossy().to_string(); let is_battery = name.starts_with("BAT") - || read_string(&dir.join("type")).map(|t| t == "Battery").unwrap_or(false); + || read_string(&dir.join("type")) + .map(|t| t == "Battery") + .unwrap_or(false); if !is_battery { continue; } @@ -97,14 +108,23 @@ fn read_batteries() -> Vec { }; let mut item = sensor_item("Battery", "%", value, None, None, "battery"); if let Some(status) = read_string(&dir.join("status")) { - item.as_object_mut().unwrap().insert("status".to_string(), json!(status)); + item.as_object_mut() + .unwrap() + .insert("status".to_string(), json!(status)); } stats.push(item); } stats } -fn sensor_item(label: &str, unit: &str, value: f64, warning: Option, critical: Option, r#type: &str) -> Value { +fn sensor_item( + label: &str, + unit: &str, + value: f64, + warning: Option, + critical: Option, + r#type: &str, +) -> Value { json!({ "label": label, "unit": unit, @@ -117,7 +137,9 @@ fn sensor_item(label: &str, unit: &str, value: f64, warning: Option, critic } fn read_string(path: &std::path::Path) -> Option { - std::fs::read_to_string(path).ok().map(|v| v.trim().to_string()) + std::fs::read_to_string(path) + .ok() + .map(|v| v.trim().to_string()) } fn read_value(path: &std::path::Path) -> Option { @@ -154,23 +176,40 @@ impl Plugin for SensorsPlugin { stats.extend(read_batteries()); // Apply show/hide filters and aliases. stats.retain(|stat| { - let label = stat.get("label").and_then(Value::as_str).unwrap_or("").to_lowercase(); + let label = stat + .get("label") + .and_then(Value::as_str) + .unwrap_or("") + .to_lowercase(); self.model.is_display(&label, "") }); for stat in stats.iter_mut() { if let (Some(label), Some(r#type)) = ( - stat.get("label").and_then(Value::as_str).map(|s| s.to_lowercase()), - stat.get("type").and_then(Value::as_str).map(|s| s.to_lowercase()), + stat.get("label") + .and_then(Value::as_str) + .map(|s| s.to_lowercase()), + stat.get("type") + .and_then(Value::as_str) + .map(|s| s.to_lowercase()), ) { if let Some(alias) = self.model.has_alias(&format!("{}_{}", label, r#type)) { - stat.as_object_mut().unwrap().insert("label".to_string(), json!(alias)); + stat.as_object_mut() + .unwrap() + .insert("label".to_string(), json!(alias)); } else if let Some(alias) = self.model.has_alias(&label) { - stat.as_object_mut().unwrap().insert("label".to_string(), json!(alias)); + stat.as_object_mut() + .unwrap() + .insert("label".to_string(), json!(alias)); } } } // Sort by label. - stats.sort_by_key(|stat| stat.get("label").and_then(Value::as_str).unwrap_or("").to_lowercase()); + stats.sort_by_key(|stat| { + stat.get("label") + .and_then(Value::as_str) + .unwrap_or("") + .to_lowercase() + }); Ok(Value::Array(stats)) } @@ -196,14 +235,34 @@ impl Plugin for SensorsPlugin { if warning.is_some() || critical.is_some() { system_thresholds(value, warning, critical) } else { - self.model.get_alert(value, 0.0, 100.0, true, false, Some(&label), Some(&r#type), false) + self.model.get_alert( + value, + 0.0, + 100.0, + true, + false, + Some(&label), + Some(&r#type), + false, + ) } } else if r#type == "battery" { - self.model.get_alert(100.0 - value, 0.0, 100.0, true, false, Some(&r#type), None, false) + self.model.get_alert( + 100.0 - value, + 0.0, + 100.0, + true, + false, + Some(&r#type), + None, + false, + ) } else { - self.model.get_alert(value, 0.0, 100.0, true, false, Some(&r#type), None, false) + self.model + .get_alert(value, 0.0, 100.0, true, false, Some(&r#type), None, false) }; - self.model.set_view_decoration(Some(&label), "value", decoration); + self.model + .set_view_decoration(Some(&label), "value", decoration); } } @@ -228,14 +287,22 @@ impl Plugin for SensorsPlugin { false, )); for i in items { - let item_key = i.get("label").and_then(Value::as_str).unwrap_or("").to_string(); + 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 = name_max_width), + format!( + "{:(), + width = name_max_width + ), glances_core::Decoration::Default, false, false, @@ -244,11 +311,12 @@ impl Plugin for SensorsPlugin { 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) - }; + 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"), diff --git a/crates/glances-plugins/src/plugins/system.rs b/crates/glances-plugins/src/plugins/system.rs index f0cb405..b8d060d 100644 --- a/crates/glances-plugins/src/plugins/system.rs +++ b/crates/glances-plugins/src/plugins/system.rs @@ -85,9 +85,21 @@ impl Plugin for SystemPlugin { 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)); + 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.push(self.model.curse_add_line( + format!(" {hr_name} "), + glances_core::Decoration::Default, + true, + false, + false, + )); ret } } diff --git a/crates/glances-plugins/src/plugins/wifi.rs b/crates/glances-plugins/src/plugins/wifi.rs index c362ff2..2b5c266 100644 --- a/crates/glances-plugins/src/plugins/wifi.rs +++ b/crates/glances-plugins/src/plugins/wifi.rs @@ -63,8 +63,14 @@ impl Plugin for WifiPlugin { continue; } let ssid = fields[0].trim_end_matches(':').to_string(); - let quality_link = fields[2].trim_end_matches('.').parse::().unwrap_or(0.0); - let quality_level = fields[3].trim_end_matches('.').parse::().unwrap_or(0.0); + let quality_link = fields[2] + .trim_end_matches('.') + .parse::() + .unwrap_or(0.0); + let quality_level = fields[3] + .trim_end_matches('.') + .parse::() + .unwrap_or(0.0); stats.push(json!({ "key": "ssid", "ssid": ssid, @@ -105,7 +111,8 @@ impl Plugin for WifiPlugin { } else { Decoration::Ok }; - self.model.set_view_decoration(Some(&ssid), "quality_level", decoration); + self.model + .set_view_decoration(Some(&ssid), "quality_level", decoration); } } @@ -115,7 +122,14 @@ impl Plugin for WifiPlugin { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { let mut ret = Vec::new(); - if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + if self + .model + .stats + .as_array() + .map(|a| a.is_empty()) + .unwrap_or(true) + || ctx.max_width == 0 + { return ret; } let if_name_max_width = ctx.max_width.saturating_sub(5); @@ -126,16 +140,29 @@ impl Plugin for WifiPlugin { false, false, )); - ret.push(self.model.curse_add_line(format!("{:>7}", "dBm"), glances_core::Decoration::Default, false, false, false)); + ret.push(self.model.curse_add_line( + format!("{:>7}", "dBm"), + glances_core::Decoration::Default, + false, + false, + false, + )); let Some(items) = self.model.stats.as_array() else { return ret; }; for i in items { let ssid = i.get("ssid").and_then(Value::as_str).unwrap_or(""); - let level = i.get("quality_level").and_then(Value::as_f64).unwrap_or(0.0); + let level = i + .get("quality_level") + .and_then(Value::as_f64) + .unwrap_or(0.0); ret.push(self.model.curse_new_line()); ret.push(self.model.curse_add_line( - format!("{:(), width = if_name_max_width), + format!( + "{:(), + width = if_name_max_width + ), glances_core::Decoration::Default, false, false, diff --git a/crates/glances-tui/src/lib.rs b/crates/glances-tui/src/lib.rs index ec612e6..ebe834f 100644 --- a/crates/glances-tui/src/lib.rs +++ b/crates/glances-tui/src/lib.rs @@ -3,7 +3,9 @@ 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 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}; @@ -18,17 +20,7 @@ use std::io::Stdout; use std::time::{Duration, Instant}; const LEFT_SIDEBAR: [&str; 11] = [ - "network", - "ports", - "wifi", - "diskio", - "fs", - "irq", - "folders", - "raid", - "sensors", - "ip", - "now", + "network", "ports", "wifi", "diskio", "fs", "irq", "folders", "raid", "sensors", "ip", "now", ]; const TOP_PLUGINS: [&str; 6] = ["cpu", "percpu", "mem", "memswap", "load", "gpu"]; @@ -54,7 +46,11 @@ pub fn run(engine: &mut Engine) -> Result<()> { 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 mut tui = Tui { + popup: Popup::None, + input: String::new(), + quit: false, + }; let result = run_loop(&mut terminal, engine, &mut tui); disable_raw_mode()?; @@ -76,10 +72,10 @@ fn run_loop( tui.draw(terminal, engine)?; count += 1; - if let Some(max) = engine.args.stop_after { - if count >= max { - break; - } + if let Some(max) = engine.args.stop_after + && count >= max + { + break; } let elapsed = start.elapsed().as_secs_f64(); @@ -101,7 +97,11 @@ fn run_loop( } impl Tui { - fn draw(&self, terminal: &mut ratatui::Terminal>, engine: &mut Engine) -> Result<()> { + fn draw( + &self, + terminal: &mut ratatui::Terminal>, + engine: &mut Engine, + ) -> Result<()> { terminal.draw(|frame| draw_frame(frame, engine, self))?; Ok(()) } @@ -122,14 +122,39 @@ fn draw_frame(frame: &mut Frame, engine: &mut Engine, tui: &Tui) { // 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 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 }; + 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); @@ -140,7 +165,6 @@ fn draw_frame(frame: &mut Frame, engine: &mut Engine, tui: &Tui) { } impl Tui { - fn top_height(&self, engine: &mut Engine, args: &Args, width: u16) -> u16 { if args.disable_top { return 1; @@ -157,53 +181,67 @@ impl Tui { } } // 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()); - } + if let Some(plugin) = engine.plugins.get_mut("quicklook") + && 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, - ) { + 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 }); - } + if let Some(plugin) = engine.plugins.get_mut("system") + && 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 }, - ); - } + if let Some(plugin) = engine.plugins.get_mut("uptime") + && 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, + }, + ); } } } @@ -212,22 +250,27 @@ impl Tui { let mut x = area.x; // Collect the rendered blocks with their natural widths. struct Block { - name: String, lines: Vec>, natural_width: usize, } let mut blocks: Vec = 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::()).max().unwrap_or(10); - blocks.push(Block { name: "quicklook".to_string(), lines, natural_width: natural.max(10) }); - } - } + if !args.disable_quicklook + && let Some(plugin) = engine.plugins.get_mut("quicklook") + && 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::()) + .max() + .unwrap_or(10); + blocks.push(Block { + lines, + natural_width: natural.max(10), + }); } for name in TOP_PLUGINS { if let Some(plugin) = engine.plugins.get_mut(name) { @@ -236,8 +279,15 @@ impl Tui { } 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::()).max().unwrap_or(10); - blocks.push(Block { name: name.to_string(), lines, natural_width: natural.max(10) }); + let natural = lines + .iter() + .map(|l| l.iter().map(|s| s.msg.chars().count()).sum::()) + .max() + .unwrap_or(10); + blocks.push(Block { + lines, + natural_width: natural.max(10), + }); } } if blocks.is_empty() { @@ -247,7 +297,9 @@ impl Tui { // 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 available = (area.width as usize) + .saturating_sub(spacing) + .max(blocks.len() * 8); let mut widths: Vec = Vec::new(); for block in &blocks { let mut width = (available * block.natural_width / total_natural).max(8); @@ -267,7 +319,17 @@ impl Tui { if x + width > area.x + area.width { break; } - draw_lines(frame, Rect { x, y: area.y, width, height: area.height }, &block.lines, args); + draw_lines( + frame, + Rect { + x, + y: area.y, + width, + height: area.height, + }, + &block.lines, + args, + ); x += width + 2; } } @@ -304,7 +366,17 @@ impl Tui { break; } let height = height.min(area.y + area.height - y); - draw_lines(frame, Rect { x: area.x, y, width: left_width, height }, &lines, args); + draw_lines( + frame, + Rect { + x: area.x, + y, + width: left_width, + height, + }, + &lines, + args, + ); y += height; } } @@ -318,14 +390,24 @@ impl Tui { 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; - } + if let Some(plugin) = engine.plugins.get_mut("processcount") + && 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). @@ -334,20 +416,40 @@ impl Tui { 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); - } + let list_name = if args.programs { + "programlist" + } else { + "processlist" + }; + if let Some(plugin) = engine.plugins.get_mut(list_name) + && 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 }, + Rect { + x: right_x, + y, + width: right_width, + height: alert_height, + }, &event_lines, args, ); @@ -360,14 +462,20 @@ impl Tui { if events.is_empty() { return lines; } - lines.push(vec![StatLine::new("EVENTS HISTORY").decoration(Decoration::Title)]); + 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() }; + 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)), @@ -401,7 +509,9 @@ impl Tui { 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( + "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"), @@ -420,7 +530,12 @@ impl Tui { ]; 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); + frame.render_widget( + Paragraph::new(lines) + .block(block) + .wrap(Wrap { trim: false }), + area, + ); } fn handle_key(&mut self, engine: &mut Engine, key: KeyEvent) { @@ -435,7 +550,11 @@ impl Tui { 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) }); + procs.filter.set_filter(if filter.is_empty() { + None + } else { + Some(&filter) + }); self.popup = Popup::None; } KeyCode::Esc => self.popup = Popup::None, @@ -485,7 +604,9 @@ impl Tui { args.meangpu = !args.meangpu; engine.runtime.meangpu = args.meangpu; } - KeyCode::Char('/') => engine.runtime.process_short_name = !engine.runtime.process_short_name, + 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"), @@ -502,7 +623,13 @@ impl Tui { 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.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), @@ -521,12 +648,16 @@ impl Tui { 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('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')) => { + KeyCode::Char( + c @ ('d' | 'n' | 's' | 'l' | 'D' | 'C' | 'I' | 'V' | 'W' | 'P' | 'K' | 'R'), + ) => { toggle_plugin_by_key(engine, c); } _ => {} @@ -534,8 +665,16 @@ impl Tui { } } -fn render_ctx<'a>(args: &'a Args, runtime: &'a glances_core::Runtime, max_width: usize) -> RenderContext<'a> { - RenderContext { args, runtime, max_width } +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 { @@ -587,7 +726,11 @@ fn draw_lines(frame: &mut Frame, area: Rect, lines: &[Vec], args: &Arg } fn style_for(decoration: Decoration, args: &Args) -> Style { - let bold = if args.disable_bold { Modifier::empty() } else { Modifier::BOLD }; + let bold = if args.disable_bold { + Modifier::empty() + } else { + Modifier::BOLD + }; let bg = args.disable_bg; match decoration { Decoration::Default | Decoration::Password => Style::default(), @@ -596,7 +739,9 @@ fn style_for(decoration: Decoration, args: &Args) -> Style { 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::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 => { @@ -630,7 +775,9 @@ fn style_for(decoration: Decoration, args: &Args) -> Style { } } Decoration::CpuTime => Style::default().fg(Color::Magenta), - Decoration::ProcessSelected => Style::default().fg(Color::Green).add_modifier(bold | Modifier::UNDERLINED), + 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), } @@ -643,7 +790,15 @@ fn set_sort(_engine: &mut Engine, key: &str) { } fn cycle_sort(engine: &mut Engine, forward: bool) { - const KEYS: [&str; 7] = ["cpu_percent", "memory_percent", "username", "cpu_times", "io_counters", "name", "cpu_num"]; + 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 { @@ -680,7 +835,11 @@ fn nice_change(engine: &mut Engine, increase: bool) { } fn toggle_processes(engine: &mut Engine) { - let enabled = engine.plugins.get("processlist").map(|p| p.is_enabled()).unwrap_or(false); + 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); @@ -752,7 +911,13 @@ mod tests { 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"]); + 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); @@ -760,8 +925,14 @@ mod tests { 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 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"));