M4: fetch mode, cleanup and documentation

- --fetch with a Jinja-subset renderer and the shipped templates
- clippy clean, cargo fmt, all tests green
- README status and differential testing instructions
This commit is contained in:
glances-rs
2026-10-09 23:59:26 +00:00
parent cb22e53aa6
commit 6819ce4e18
47 changed files with 4145 additions and 935 deletions
Generated
+1
View File
@@ -722,6 +722,7 @@ dependencies = [
"glances-core", "glances-core",
"glances-plugins", "glances-plugins",
"glances-tui", "glances-tui",
"indexmap",
"serde_json", "serde_json",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
+45 -9
View File
@@ -7,24 +7,60 @@ This project is a drop-in replacement for the **standalone** Glances modes:
- curses-like terminal UI (`glances`) - curses-like terminal UI (`glances`)
- stdout modes: `--stdout`, `--stdout-csv`, `--stdout-json` - stdout modes: `--stdout`, `--stdout-csv`, `--stdout-json`
- `--fetch`, `--issue`, `--modules-list` - `--fetch` (with the shipped templates), `--issue`, `--module-list`
- all plugins, including extended ones (containers, GPUs, sensors, ...) - the same `glances.conf` configuration file (limits, thresholds, aliases,
- the same `glances.conf` configuration file 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: 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 ## Status
Work in progress. See the milestones in the project plan; the workspace is | Area | Status |
organized as: | --- | --- |
| 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 | | Crate | Purpose |
| --- | --- | | --- | --- |
| `glances-core` | plugin model, config, limits, history, events, filters, output | | `glances-core` | plugin model, config, limits, history, events, filters, processes |
| `glances-plugins` | collectors and plugin registry/engine | | `glances-plugins` | collectors, plugin registry and update engine |
| `glances-tui` | terminal UI (ratatui) | | `glances-tui` | terminal UI (ratatui) |
| `glances-cli` | the `glances` binary | | `glances-cli` | the `glances` binary, stdout modes and fetch renderer |
## License ## License
+1
View File
@@ -15,6 +15,7 @@ ctrlc = "3.5.2"
glances-core = { version = "0.1.0", path = "../glances-core" } glances-core = { version = "0.1.0", path = "../glances-core" }
glances-plugins = { version = "0.1.0", path = "../glances-plugins" } glances-plugins = { version = "0.1.0", path = "../glances-plugins" }
glances-tui = { version = "0.1.0", path = "../glances-tui" } glances-tui = { version = "0.1.0", path = "../glances-tui" }
indexmap.workspace = true
serde_json.workspace = true serde_json.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
+969
View File
@@ -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<JVal>),
Map(IndexMap<String, JVal>),
/// 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<String, JVal> = 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<Value> = 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::<Vec<_>>()
.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<Node>,
},
If {
cond: String,
then_body: Vec<Node>,
else_body: Vec<Node>,
},
}
#[derive(Debug)]
enum Token {
Text(String),
Expr(String),
Stmt(String),
}
fn tokenize(template: &str) -> Result<Vec<Token>> {
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<Vec<Node>> {
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<Vec<Node>> {
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<JVal> {
let value = self.parse_ternary(ctx)?;
Ok(value)
}
fn parse_ternary(&mut self, ctx: &EvalContext) -> Result<JVal> {
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<JVal> {
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<JVal> {
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<JVal> {
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<JVal> {
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<JVal> {
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<JVal> {
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<JVal> {
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<JVal> {
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<char> = 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<char> = 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<JVal> = 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<String, JVal>,
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<JVal>, IndexMap<String, JVal>)> {
let mut args: Vec<JVal> = Vec::new();
let mut kwargs: IndexMap<String, JVal> = 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<JVal> {
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::<usize>().ok())
.unwrap_or(6);
let width = spec
.split('.')
.next()
.and_then(|w| w.trim_start_matches('0').parse::<usize>().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<IndexMap<String, JVal>>,
loop_var: Option<IndexMap<String, JVal>>,
}
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<String, JVal>) -> 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<String> {
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<JVal> = 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)
}
+15 -8
View File
@@ -9,15 +9,22 @@ pub fn log_filename() -> PathBuf {
.ok() .ok()
.filter(|v| !v.is_empty()) .filter(|v| !v.is_empty())
.map(PathBuf::from) .map(PathBuf::from)
.or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join(".local").join("share"))); .or_else(|| {
if let Some(base) = base { std::env::var("HOME")
if base.is_dir() { .ok()
let dir = base.join("glances"); .map(|h| PathBuf::from(h).join(".local").join("share"))
let _ = std::fs::create_dir_all(&dir); });
return dir.join("glances.log"); 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 /// Initialise the logger: everything goes to the log file, warnings and
+44 -18
View File
@@ -1,13 +1,13 @@
//! The `glances` binary. //! The `glances` binary.
mod fetch;
mod logging; mod logging;
mod outputs; mod outputs;
mod tui_placeholder;
use anyhow::Result; use anyhow::Result;
use clap::{CommandFactory, Parser}; use clap::{CommandFactory, Parser};
use glances_core::config::Config;
use glances_core::Args; use glances_core::Args;
use glances_core::config::Config;
use glances_plugins::engine::Engine; use glances_plugins::engine::Engine;
use std::io::Write; use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@@ -21,7 +21,10 @@ fn main() {
let args = Args::parse(); let args = Args::parse();
if args.version { if args.version {
println!("{}", args.version_message(&logging::log_filename().display().to_string())); println!(
"{}",
args.version_message(&logging::log_filename().display().to_string())
);
return; return;
} }
if let Some(shell) = &args.print_completion { if let Some(shell) = &args.print_completion {
@@ -85,7 +88,7 @@ fn run(mut args: Args) -> Result<()> {
} }
if args.stdout_fetch { if args.stdout_fetch {
return tui_placeholder::run_fetch(&mut engine, &args); return fetch::run_fetch(&mut engine, &args);
} }
if args.quiet { if args.quiet {
@@ -105,11 +108,28 @@ fn display_modules_list(engine: &Engine) {
fn print_completion(shell: &str) { fn print_completion(shell: &str) {
let mut command = Args::command(); let mut command = Args::command();
match shell { match shell {
"bash" => clap_complete::generate(clap_complete::Shell::Bash, &mut command, "glances", &mut std::io::stdout()), "bash" => clap_complete::generate(
"zsh" => clap_complete::generate(clap_complete::Shell::Zsh, &mut command, "glances", &mut std::io::stdout()), clap_complete::Shell::Bash,
"fish" => clap_complete::generate(clap_complete::Shell::Fish, &mut command, "glances", &mut std::io::stdout()), &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 => { 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); std::process::exit(2);
} }
} }
@@ -134,13 +154,19 @@ fn run_stdout(engine: &mut Engine, args: &Args) -> Result<()> {
let mut csv = outputs::CsvRenderer::new(); let mut csv = outputs::CsvRenderer::new();
let stdout_items = args.stdout.as_deref().map(outputs::build_stdout_list); let stdout_items = args.stdout.as_deref().map(outputs::build_stdout_list);
let json_plugins = args.stdout_json.as_deref().map(|value| { let json_plugins = args.stdout_json.as_deref().map(|value| {
value.split(',').map(|p| p.trim().to_string()).filter(|p| !p.is_empty()).collect::<Vec<_>>() value
.split(',')
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
}); });
let csv_plugins = args.stdout_csv.as_deref().map(|value| { let csv_plugins = args.stdout_csv.as_deref().map(|value| {
value value
.split(',') .split(',')
.map(|p| match p.split_once('.') { .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), None => (p.trim().to_string(), None),
}) })
.filter(|(p, _)| !p.is_empty()) .filter(|(p, _)| !p.is_empty())
@@ -164,10 +190,10 @@ fn run_stdout(engine: &mut Engine, args: &Args) -> Result<()> {
std::io::stdout().flush().ok(); std::io::stdout().flush().ok();
count += 1; count += 1;
if let Some(max) = iterations { if let Some(max) = iterations
if count >= max { && count >= max
break; {
} break;
} }
let elapsed = start.elapsed().as_secs_f64(); let elapsed = start.elapsed().as_secs_f64();
sleep_with_signal(refresh - elapsed); sleep_with_signal(refresh - elapsed);
@@ -182,10 +208,10 @@ fn run_quiet(engine: &mut Engine, args: &Args) -> Result<()> {
let start = Instant::now(); let start = Instant::now();
engine.update(); engine.update();
count += 1; count += 1;
if let Some(max) = args.stop_after { if let Some(max) = args.stop_after
if count >= max { && count >= max
break; {
} break;
} }
let elapsed = start.elapsed().as_secs_f64(); let elapsed = start.elapsed().as_secs_f64();
sleep_with_signal(refresh - elapsed); sleep_with_signal(refresh - elapsed);
+56 -15
View File
@@ -17,7 +17,11 @@ pub fn build_stdout_list(value: &str) -> Vec<StdoutItem> {
.map(|item| { .map(|item| {
let parts: Vec<&str> = item.split('.').collect(); let parts: Vec<&str> = item.split('.').collect();
match parts.len() { 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 { 2 => StdoutItem {
plugin: parts[0].to_string(), plugin: parts[0].to_string(),
key: None, key: None,
@@ -48,9 +52,18 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String {
Some(attribute) => match &stat { Some(attribute) => match &stat {
Value::Object(map) => { Value::Object(map) => {
if let Some(value) = map.get(attribute) { 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 { } else {
tracing::error!("Can not display stat {}.{} (KeyError)", item.plugin, attribute); tracing::error!(
"Can not display stat {}.{} (KeyError)",
item.plugin,
attribute
);
} }
} }
Value::Array(list) => { Value::Array(list) => {
@@ -66,10 +79,10 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String {
let Some(entry_key) = entry_key else { let Some(entry_key) = entry_key else {
continue; continue;
}; };
if let Some(wanted) = &item.key { if let Some(wanted) = &item.key
if wanted != &entry_key { && wanted != &entry_key
continue; {
} continue;
} }
if let Some(value) = entry.get(attribute) { if let Some(value) = entry.get(attribute) {
out.push_str(&format!( out.push_str(&format!(
@@ -85,7 +98,11 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String {
_ => {} _ => {}
}, },
None => { 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 { impl CsvRenderer {
pub fn new() -> Self { 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>)]) -> String { pub fn render(&mut self, engine: &Engine, items: &[(String, Option<String>)]) -> String {
@@ -172,7 +192,8 @@ impl CsvRenderer {
} }
self.list_keys.insert(plugin.to_string(), keys_order); self.list_keys.insert(plugin.to_string(), keys_order);
if let Some(first) = list.iter().find_map(|entry| entry.as_object()) { 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(); let mut line = String::new();
if let Some(attribute) = attribute { if let Some(attribute) = attribute {
let text = match stat { 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(), _ => "N/A".to_string(),
}; };
line.push_str(&format!("{text},")); line.push_str(&format!("{text},"));
@@ -213,9 +237,19 @@ impl CsvRenderer {
current.insert(glances_core::py_str(identity), entry.clone()); current.insert(glances_core::py_str(identity), entry.clone());
} }
let empty = Map::new(); 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); 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 value = entry.get(field.as_str());
let text = match value { let text = match value {
Some(value) => glances_core::py_str(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 { let Some(plugin) = engine.get_plugin(&name) else {
continue; continue;
}; };
let enabled = if plugin.is_enabled() { "enabled" } else { "disabled" }; let enabled = if plugin.is_enabled() {
out.push_str(&format!("{name} ({enabled}): {}\n", glances_core::py_repr(plugin.stats()))); "enabled"
} else {
"disabled"
};
out.push_str(&format!(
"{name} ({enabled}): {}\n",
glances_core::py_repr(plugin.stats())
));
} }
out out
} }
-10
View File
@@ -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")
}
+35 -8
View File
@@ -35,12 +35,20 @@ fn render_range(template: &str, data: &Value, pos: &mut usize) -> Result<String,
let Some(close) = stripped.find("}}}") else { let Some(close) = stripped.find("}}}") else {
return Err("unclosed mustache tag".to_string()); return Err("unclosed mustache tag".to_string());
}; };
(stripped[..close].trim().to_string(), true, tag_start + 1 + close + 3) (
stripped[..close].trim().to_string(),
true,
tag_start + 1 + close + 3,
)
} else { } else {
let Some(close) = after.find("}}") else { let Some(close) = after.find("}}") else {
return Err("unclosed mustache tag".to_string()); return Err("unclosed mustache tag".to_string());
}; };
(after[..close].trim().to_string(), false, tag_start + close + 2) (
after[..close].trim().to_string(),
false,
tag_start + close + 2,
)
}; };
if let Some(name) = tag.strip_prefix('#') { if let Some(name) = tag.strip_prefix('#') {
@@ -199,7 +207,8 @@ impl Actions {
} }
pub fn set(&mut self, stat_name: &str, criticality: &str) { pub fn set(&mut self, stat_name: &str, criticality: &str) {
self.status.insert(stat_name.to_string(), criticality.to_string()); self.status
.insert(stat_name.to_string(), criticality.to_string());
} }
pub fn get(&self, stat_name: &str) -> Option<&str> { pub fn get(&self, stat_name: &str) -> Option<&str> {
@@ -254,8 +263,14 @@ mod tests {
#[test] #[test]
fn test_render_simple() { fn test_render_simple() {
let data = json!({"name": "nginx", "pid": 42}); let data = json!({"name": "nginx", "pid": 42});
assert_eq!(render_template("kill {{pid}} {{name}}", &data).unwrap(), "kill 42 nginx"); assert_eq!(
assert_eq!(render_template("raw {{{name}}}", &data).unwrap(), "raw nginx"); render_template("kill {{pid}} {{name}}", &data).unwrap(),
"kill 42 nginx"
);
assert_eq!(
render_template("raw {{{name}}}", &data).unwrap(),
"raw nginx"
);
} }
#[test] #[test]
@@ -268,13 +283,25 @@ mod tests {
#[test] #[test]
fn test_render_section() { fn test_render_section() {
let data = json!({"items": [1, 2]}); let data = json!({"items": [1, 2]});
assert_eq!(render_template("{{#items}}x{{/items}}", &data).unwrap(), "xx"); assert_eq!(
assert_eq!(render_template("{{^missing}}y{{/missing}}", &data).unwrap(), "y"); render_template("{{#items}}x{{/items}}", &data).unwrap(),
"xx"
);
assert_eq!(
render_template("{{^missing}}y{{/missing}}", &data).unwrap(),
"y"
);
} }
#[test] #[test]
fn test_actions_no_trigger_before_start_timer() { fn test_actions_no_trigger_before_start_timer() {
let mut actions = Actions::new(false, 100.0); 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
));
} }
} }
+14 -7
View File
@@ -379,8 +379,14 @@ impl Args {
(self.server, "server mode (-s/--server)"), (self.server, "server mode (-s/--server)"),
(self.browser, "the server browser (--browser)"), (self.browser, "the server browser (--browser)"),
(self.export.is_some(), "export modules (--export)"), (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)"), (self.trace_malloc, "Python memory tracing (--trace-malloc)"),
]; ];
checks.iter().find(|(used, _)| *used).map(|(_, name)| *name) checks.iter().find(|(used, _)| *used).map(|(_, name)| *name)
@@ -398,10 +404,7 @@ impl Args {
pub fn version_message(&self, log_file: &str) -> String { pub fn version_message(&self, log_file: &str) -> String {
format!( format!(
"Glances version:\t{}\nGlances API version:\t{}\nGlances-rs version:\t{}\nLog file:\t\t{}", "Glances version:\t{}\nGlances API version:\t{}\nGlances-rs version:\t{}\nLog file:\t\t{}",
"4.5.7", "4.5.7", GLANCES_API_VERSION, GLANCES_VERSION, log_file
GLANCES_API_VERSION,
GLANCES_VERSION,
log_file
) )
} }
} }
@@ -492,7 +495,11 @@ mod tests {
assert!(parse(&["glances", "-w"]).unsupported().is_some()); assert!(parse(&["glances", "-w"]).unsupported().is_some());
assert!(parse(&["glances", "-s"]).unsupported().is_some()); assert!(parse(&["glances", "-s"]).unsupported().is_some());
assert!(parse(&["glances", "-c", "host"]).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()); assert!(parse(&["glances"]).unsupported().is_none());
} }
+57 -24
View File
@@ -34,8 +34,11 @@ impl Config {
/// Build an empty configuration (used by tests and by callers that /// Build an empty configuration (used by tests and by callers that
/// already read a file). /// already read a file).
pub fn empty(disable_config_exec: bool) -> Self { pub fn empty(disable_config_exec: bool) -> Self {
let mut config = Config::default(); let mut config = Config {
config.disable_config_exec = disable_config_exec; sections: IndexMap::new(),
loaded_file: None,
disable_config_exec,
};
config.set_defaults(); config.set_defaults();
config config
} }
@@ -64,7 +67,11 @@ impl Config {
.ok() .ok()
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.map(PathBuf::from) .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 { if let Some(base) = user_base {
paths.push(base.join("glances").join(CONFIG_FILENAME)); paths.push(base.join("glances").join(CONFIG_FILENAME));
} }
@@ -74,12 +81,12 @@ impl Config {
// Default location: <prefix>/share/doc/glances (and the checkout // Default location: <prefix>/share/doc/glances (and the checkout
// layout next to the executable, useful while developing). // layout next to the executable, useful while developing).
if let Ok(exe) = std::env::current_exe() { if let Ok(exe) = std::env::current_exe()
if let Some(bin_dir) = exe.parent() { && 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)); for prefix in [bin_dir.join(".."), bin_dir.join("../..")] {
paths.push(prefix.join("conf").join(CONFIG_FILENAME)); 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()); tracing::info!("Read configuration file '{}'", config_file.display());
} }
Err(err) => { 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); 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("steal"), ["50", "70", "90"]);
self.set_default_cwc("cpu", Some("iowait"), ["50", "70", "90"]); self.set_default_cwc("cpu", Some("iowait"), ["50", "70", "90"]);
// Context switches bottleneck identification #1212 // Context switches bottleneck identification #1212
let ctx_switches_bottleneck = let ctx_switches_bottleneck = 500000.0
500000.0 * 0.10 * std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) as f64; * 0.10
let careful = format!("{}", ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20)); * std::thread::available_parallelism()
let warning = format!("{}", ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10)); .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}"); let critical = format!("{ctx_switches_bottleneck}");
self.set_default_cwc("cpu", Some("ctx_switches"), [&careful, &warning, &critical]); 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"]); self.set_default_cwc("processlist", Some("mem"), ["50", "70", "90"]);
} }
fn set_default_cwc<const N: usize>(&mut self, section: &str, option_header: Option<&str>, cwc: [&str; N]) { fn set_default_cwc<const N: usize>(
&mut self,
section: &str,
option_header: Option<&str>,
cwc: [&str; N],
) {
let header = match option_header { let header = match option_header {
Some(header) => format!("{header}_"), Some(header) => format!("{header}_"),
None => String::new(), None => String::new(),
@@ -171,7 +195,9 @@ impl Config {
fn set_default(&mut self, section: &str, option: &str, default: &str) { fn set_default(&mut self, section: &str, option: &str, default: &str) {
let entry = self.sections.entry(section.to_string()).or_default(); 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> { pub fn loaded_config_file(&self) -> Option<&Path> {
@@ -215,7 +241,9 @@ impl Config {
if let Some(value) = ret.as_mut() { if let Some(value) = ret.as_mut() {
for command in find_backticks(value) { for command in find_backticks(value) {
if self.disable_config_exec { 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 { } else {
tracing::warn!("Executing config command `{command}` for [{section}] {option}"); tracing::warn!("Executing config command `{command}` for [{section}] {option}");
let output = system_exec(&command); let output = system_exec(&command);
@@ -294,11 +322,11 @@ fn parse_ini(content: &str) -> IndexMap<String, IndexMap<String, String>> {
} }
// Continuation line (indented). // Continuation line (indented).
if line.starts_with(' ') || line.starts_with('\t') { 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(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)) { && let Some(value) = sections.get_mut(section).and_then(|s| s.get_mut(key))
value.push('\n'); {
value.push_str(trimmed); value.push('\n');
} value.push_str(trimmed);
} }
continue; continue;
} }
@@ -341,7 +369,9 @@ fn system_exec(command: &str) -> String {
return String::new(); return String::new();
}; };
match Command::new(program).args(args).output() { 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}"), Err(e) => format!("ERROR: {e}"),
} }
} }
@@ -387,6 +417,9 @@ mod tests {
let config = Config::from_str("[x]\nprefix=`echo hello`\n", false); let config = Config::from_str("[x]\nprefix=`echo hello`\n", false);
assert_eq!(config.get_value("x", "prefix", None).unwrap(), "hello"); assert_eq!(config.get_value("x", "prefix", None).unwrap(), "hello");
let config = Config::from_str("[x]\nprefix=`echo hello`\n", true); 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`"
);
} }
} }
+52 -10
View File
@@ -13,7 +13,11 @@ const TREE: [(&str, &[&str], u32); 9] = [
("High CPU user mode", &["cpu_user"], 2), ("High CPU user mode", &["cpu_user"], 2),
("High CPU kernel usage", &["cpu_system"], 2), ("High CPU kernel usage", &["cpu_system"], 2),
("High CPU I/O waiting", &["cpu_iowait"], 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), ("High CPU niced value", &["cpu_niced"], 2),
("System overloaded in the last 5 minutes", &["load"], 2), ("System overloaded in the last 5 minutes", &["load"], 2),
("High swap (paging) usage", &["memswap"], 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_min = 0u32;
let mut best_msg = TREE[0].0; let mut best_msg = TREE[0].0;
for (msg, names, min) in TREE { 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 { if weight > best_weight {
best_weight = weight; best_weight = weight;
best_min = min; best_min = min;
best_msg = msg; 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)] #[derive(Debug, Clone)]
@@ -106,11 +118,16 @@ impl Event {
let mut sorted: Vec<Value> = proc_list.to_vec(); let mut sorted: Vec<Value> = proc_list.to_vec();
sort_stats(&mut sorted, sort_key, "memory_percent", true); sort_stats(&mut sorted, sort_key, "memory_percent", true);
for process in sorted.iter().take(6) { 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; *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(); let mut counts: Vec<(String, i64)> =
counts.sort_by(|a, b| b.1.cmp(&a.1)); 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(); self.top = counts.into_iter().take(3).map(|(name, _)| name).collect();
} else { } else {
self.top_dict.clear(); self.top_dict.clear();
@@ -136,7 +153,12 @@ impl Default for EventsList {
impl EventsList { impl EventsList {
pub fn new(max_events: usize, min_duration: f64, min_interval: f64) -> Self { 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<Value> { pub fn get(&self) -> Vec<Value> {
@@ -184,14 +206,27 @@ impl EventsList {
} }
/// Add a new item to the events list. /// 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 event_time = chrono::Utc::now().timestamp() as f64;
let global_message = build_global_message(); let global_message = build_global_message();
let proc_list: Vec<Value> = processes().lock().unwrap().get_list(false).to_vec(); let proc_list: Vec<Value> = processes().lock().unwrap().get_list(false).to_vec();
match self.event_exist(event_time, event_type) { match self.event_exist(event_time, event_type) {
None => { 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) => { Some(index) => {
self.update_event( self.update_event(
@@ -266,7 +301,14 @@ impl EventsList {
Self::set_process_sort(event_type); Self::set_process_sort(event_type);
let sort_key = Self::get_event_sort_key(event_type).to_string(); let sort_key = Self::get_event_sort_key(event_type).to_string();
let event = &mut self.events[event_index]; 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,
);
} }
} }
+17 -4
View File
@@ -74,9 +74,14 @@ impl Filter {
return false; return false;
} }
if self.filter_key.is_none() { 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 { fn filter_matches(&self, process: &Value, key: &str, filter_re: &Regex) -> bool {
@@ -89,13 +94,21 @@ impl Filter {
if key == "cmdline" && !items.is_empty() { if key == "cmdline" && !items.is_empty() {
items[0].as_str().unwrap_or_default().to_string() items[0].as_str().unwrap_or_default().to_string()
} else { } else {
items.iter().map(|i| i.as_str().unwrap_or_default()).collect::<Vec<_>>().join(" ") items
.iter()
.map(|i| i.as_str().unwrap_or_default())
.collect::<Vec<_>>()
.join(" ")
} }
} }
Value::Null => return false, Value::Null => return false,
other => crate::units::py_str(other), 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)
} }
} }
+32 -6
View File
@@ -15,7 +15,11 @@ pub struct Attribute {
} }
impl Attribute { impl Attribute {
pub fn new(name: impl Into<String>, description: impl Into<String>, history_max_size: Option<usize>) -> Self { pub fn new(
name: impl Into<String>,
description: impl Into<String>,
history_max_size: Option<usize>,
) -> Self {
Self { Self {
name: name.into(), name: name.into(),
description: description.into(), description: description.into(),
@@ -44,7 +48,15 @@ impl Attribute {
if nb == 0 { if nb == 0 {
self.history.iter().cloned().collect() self.history.iter().cloned().collect()
} else { } else {
self.history.iter().rev().take(nb).collect::<Vec<_>>().into_iter().rev().cloned().collect() self.history
.iter()
.rev()
.take(nb)
.collect::<Vec<_>>()
.into_iter()
.rev()
.cloned()
.collect()
} }
} }
@@ -63,7 +75,13 @@ pub struct History {
} }
impl History { impl History {
pub fn add(&mut self, key: &str, value: f64, description: &str, history_max_size: Option<usize>) { pub fn add(
&mut self,
key: &str,
value: f64,
description: &str,
history_max_size: Option<usize>,
) {
let attribute = self let attribute = self
.attributes .attributes
.entry(key.to_string()) .entry(key.to_string())
@@ -78,16 +96,24 @@ impl History {
} }
pub fn get(&self, nb: usize) -> IndexMap<String, Vec<(DateTime<Utc>, f64)>> { pub fn get(&self, nb: usize) -> IndexMap<String, Vec<(DateTime<Utc>, 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<String, Vec<(String, f64)>> { pub fn get_json(&self, nb: usize) -> IndexMap<String, Vec<(String, f64)>> {
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). /// Return the values of one item (used for TUI trends).
pub fn item_values(&self, item: &str) -> Option<Vec<f64>> { pub fn item_values(&self, item: &str) -> Option<Vec<f64>> {
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())
} }
} }
+2
View File
@@ -1,5 +1,7 @@
//! glances-core: shared foundations of the glances-rs rewrite. //! 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: //! This crate contains the pieces that are common to every plugin and output:
//! argument parsing, configuration file handling, the plugin model (limits, //! argument parsing, configuration file handling, the plugin model (limits,
//! views, history, rates...), formatting helpers, the event/threshold/action //! views, history, rates...), formatting helpers, the event/threshold/action
+246 -88
View File
@@ -161,7 +161,10 @@ pub struct Field {
impl Field { impl Field {
pub fn new(description: &str) -> Self { 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 { pub fn unit(mut self, unit: &str) -> Self {
@@ -208,7 +211,9 @@ impl Field {
pub fn unit_type(&self) -> &'static str { pub fn unit_type(&self) -> &'static str {
match self.unit.as_deref() { match self.unit.as_deref() {
Some("percent" | "percents") => "float", 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", _ => "float",
} }
} }
@@ -233,7 +238,10 @@ pub struct HistoryItem {
impl HistoryItem { impl HistoryItem {
pub fn new(name: &str, description: &str) -> Self { 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. /// Load limits from the configuration file.
pub fn load_limits(&mut self, config: &Config) { pub fn load_limits(&mut self, config: &Config) {
self.limits.clear(); 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") { if config.has_section("global") {
let history_size = config.get_float_value("global", "history_size", HISTORY_MAX_SIZE as f64); let history_size =
self.limits.insert("history_size".to_string(), Limit::Num(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) { if config.has_section(&self.name) {
@@ -372,7 +385,8 @@ impl PluginModel {
self.limits.insert(limit.clone(), Limit::Num(number)); self.limits.insert(limit.clone(), Limit::Num(number));
} }
Err(_) => { Err(_) => {
let items: Vec<String> = value.split(',').map(|s| s.trim().to_string()).collect(); let items: Vec<String> =
value.split(',').map(|s| s.trim().to_string()).collect();
self.limits.insert(limit.clone(), Limit::List(items)); self.limits.insert(limit.clone(), Limit::List(items));
} }
} }
@@ -389,7 +403,8 @@ impl PluginModel {
} }
pub fn set_refresh(&mut self, value: f64) { 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 { pub fn get_refresh(&self) -> f64 {
@@ -424,12 +439,19 @@ impl PluginModel {
} }
pub fn is_limit(&self, criticality: &str, stat_name: &str) -> bool { 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) self.limits.contains_key(&key)
} }
/// Return the (commands, repeat) action pair for the alert. /// Return the (commands, repeat) action pair for the alert.
pub fn get_limit_action(&self, criticality: &str, stat_name: &str) -> (Option<Vec<String>>, bool) { pub fn get_limit_action(
&self,
criticality: &str,
stat_name: &str,
) -> (Option<Vec<String>>, bool) {
let candidates = [ let candidates = [
(format!("{stat_name}_{criticality}_action"), false), (format!("{stat_name}_{criticality}_action"), false),
(format!("{stat_name}_{criticality}_action_repeat"), true), (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 { pub fn get_limit_log(&self, stat_name: &str, default_action: bool) -> bool {
if let Some(limit) = self.limits.get(&format!("{stat_name}_log")) { 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)) { 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 default_action
} }
@@ -477,7 +507,10 @@ impl PluginModel {
key = format!("{key}_{header}"); key = format!("{key}_{header}");
} }
match self.limits.get(&format!("{key}_{value}")) { 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, Some(Limit::Num(n)) => *n != 0.0,
None => false, None => false,
} }
@@ -487,7 +520,11 @@ impl PluginModel {
pub fn is_show(&self, value: &str, header: &str) -> bool { pub fn is_show(&self, value: &str, header: &str) -> bool {
let alias = self.has_alias(value); let alias = self.has_alias(value);
self.get_conf_value("show", header).iter().any(|pattern| { 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 { pub fn is_hide(&self, value: &str, header: &str) -> bool {
let alias = self.has_alias(value); let alias = self.has_alias(value);
self.get_conf_value("hide", header).iter().any(|pattern| { 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 { pub fn get_stat_name(&self, header: Option<&str>, action_key: Option<&str>) -> String {
let mut ret = self.name.clone(); let mut ret = self.name.clone();
if let Some(key) = action_key { if let Some(key) = action_key
if !key.is_empty() { && !key.is_empty()
ret.push('_'); {
ret.push_str(key); ret.push('_');
} ret.push_str(key);
} }
if let Some(header) = header { if let Some(header) = header
if !header.is_empty() { && !header.is_empty()
ret.push('_'); {
ret.push_str(header); ret.push('_');
} ret.push_str(header);
} }
ret ret
} }
@@ -560,15 +601,32 @@ impl PluginModel {
.map(|(name, _)| name.clone()) .map(|(name, _)| name.clone())
.collect(); .collect();
for field_name in mmm_names { 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( self.mmm.insert(
field_name.clone(), field_name.clone(),
MmmState { values: VecDeque::new(), min: None, max: None }, MmmState {
values: VecDeque::new(),
min: None,
max: None,
},
); );
for (suffix, description) in [ for (suffix, description) in [
("_min", format!("Minimum {field_name} observed since Glances startup.")), (
("_max", format!("Maximum {field_name} observed since Glances startup.")), "_min",
("_mean", format!("Mean (average) {field_name} computed from the history.")), 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 generated = format!("{field_name}{suffix}");
let unit = unit.clone(); let unit = unit.clone();
@@ -604,7 +662,10 @@ impl PluginModel {
let mean = state.values.iter().sum::<f64>() / state.values.len() as f64; let mean = state.values.iter().sum::<f64>() / state.values.len() as f64;
map.insert(format!("{field_name}_min"), json!(min)); map.insert(format!("{field_name}_min"), json!(min));
map.insert(format!("{field_name}_max"), json!(max)); 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| { .map(|list| {
list.iter() list.iter()
.filter_map(|item| { .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() .collect()
}) })
@@ -633,7 +695,7 @@ impl PluginModel {
for item in items.iter_mut() { for item in items.iter_mut() {
let previous_item = key let previous_item = key
.and_then(|k| item.get(k)) .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()) .and_then(|k| previous_by_key.get(&k).cloned())
.unwrap_or(Value::Object(Map::new())); .unwrap_or(Value::Object(Map::new()));
self.compute_rate_on_stat(item, Some(&previous_item)); self.compute_rate_on_stat(item, Some(&previous_item));
@@ -660,13 +722,20 @@ impl PluginModel {
let Value::Object(map) = stat else { let Value::Object(map) = stat else {
return; return;
}; };
let rate_fields: Vec<String> = let rate_fields: Vec<String> = self
self.fields.iter().filter(|(_, f)| f.rate).map(|(name, _)| name.clone()).collect(); .fields
.iter()
.filter(|(_, f)| f.rate)
.map(|(name, _)| name.clone())
.collect();
for field in rate_fields { for field in rate_fields {
if !map.contains_key(&field) { if !map.contains_key(&field) {
continue; 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); let current_value = map.get(&field).cloned().unwrap_or(Value::Null);
map.insert(format!("{field}_gauge"), current_value.clone()); map.insert(format!("{field}_gauge"), current_value.clone());
let current = as_f64(&current_value); let current = as_f64(&current_value);
@@ -703,7 +772,10 @@ impl PluginModel {
None => self.views.get(field), None => self.views.get(field),
Some(item) => self.views.get(item).and_then(|v| v.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 { 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 { 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 /// 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 { let Some(definition) = self.fields.get(field).cloned() else {
return Decoration::Default; return Decoration::Default;
}; };
if definition.rate && self.stats.is_object() { if definition.rate
if self.stats.get("time_since_update").and_then(as_f64) == Some(0.0) { && self.stats.is_object()
return Decoration::Default; && self.stats.get("time_since_update").and_then(as_f64) == Some(0.0)
} {
return Decoration::Default;
} }
if definition.log { if definition.log {
let (current, minimum, maximum) = numeric_alert_args(value); 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 { if definition.alert {
let (current, minimum, maximum) = numeric_alert_args(value); 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 Decoration::Default
} }
@@ -756,14 +849,18 @@ impl PluginModel {
let hidden = if !self.hide_zero { let hidden = if !self.hide_zero {
false false
} else if let Some(key) = key { } 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); let mut hidden = existing.as_bool().unwrap_or(false);
if self.hide_zero_fields.iter().any(|f| f == field) { 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)) { && let Some(raw) = self.get_raw_stats_key(field, key).and_then(|v| as_f64(&v))
if raw > self.hide_threshold_bytes { && raw > self.hide_threshold_bytes
hidden = false; {
} hidden = false;
}
} }
hidden hidden
} else { } else {
@@ -860,29 +957,31 @@ impl PluginModel {
let value = current * 100.0 / maximum; let value = current * 100.0 / maximum;
let stat_name = self.get_stat_name(header, action_key).to_lowercase(); 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 critical = self.get_limit("critical", &stat_name);
let warning = self.get_limit("warning", &stat_name); let warning = self.get_limit("warning", &stat_name);
let careful = self.get_limit("careful", &stat_name); let careful = self.get_limit("careful", &stat_name);
if let Some(critical) = critical { if let Some(critical) = critical
if value >= critical { && value >= critical
ret = Decoration::Critical; {
} ret = Decoration::Critical;
} }
if ret == Decoration::Ok { if ret == Decoration::Ok
if let Some(warning) = warning { && let Some(warning) = warning
if value >= warning { && value >= warning
ret = Decoration::Warning; {
} ret = Decoration::Warning;
}
} }
if ret == Decoration::Ok { if ret == Decoration::Ok
if let Some(careful) = careful { && let Some(careful) = careful
if value >= careful { && value >= careful
ret = Decoration::Careful; {
} ret = Decoration::Careful;
}
} }
if ret == Decoration::Ok && careful.is_none() && warning.is_none() && critical.is_none() { if ret == Decoration::Ok && careful.is_none() && warning.is_none() && critical.is_none() {
ret = Decoration::Default; ret = Decoration::Default;
@@ -903,12 +1002,20 @@ impl PluginModel {
Decoration::Max => "MAX", Decoration::Max => "MAX",
_ => "OK", _ => "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()); 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) { match (ret, log_str) {
(Decoration::Ok, true) => Decoration::OkLog, (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) 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); let (command, repeat) = self.get_limit_action(trigger, stat_name);
match command { match command {
None => { None => {
@@ -935,12 +1053,11 @@ impl PluginModel {
let mut mustache_dict = match &stats_action { let mut mustache_dict = match &stats_action {
Value::Array(items) if action_key.is_some() => { Value::Array(items) if action_key.is_some() => {
let action_key = action_key.unwrap(); let action_key = action_key.unwrap();
let key_field = self.item_key.clone(); let key_field = self.item_key;
items items
.iter() .iter()
.find(|item| { .find(|item| {
key_field key_field
.as_deref()
.and_then(|k| item.get(k)) .and_then(|k| item.get(k))
.map(crate::units::py_str) .map(crate::units::py_str)
.as_deref() .as_deref()
@@ -953,15 +1070,29 @@ impl PluginModel {
_ => Value::Object(Map::new()), _ => Value::Object(Map::new()),
}; };
if let Value::Object(map) = &mut mustache_dict { if let Value::Object(map) = &mut mustache_dict {
map.insert("critical".to_string(), json!(self.get_limit("critical", stat_name))); map.insert(
map.insert("warning".to_string(), json!(self.get_limit("warning", stat_name))); "critical".to_string(),
map.insert("careful".to_string(), json!(self.get_limit("careful", stat_name))); 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( map.insert(
"time".to_string(), "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`). /// Add a `key: value` pair to the TUI (port of `curse_add_stat`).
pub fn curse_add_stat(&self, key: &str, width: Option<usize>, header: &str, separator: &str, trailer: &str) -> Vec<StatLine> { pub fn curse_add_stat(
&self,
key: &str,
width: Option<usize>,
header: &str,
separator: &str,
trailer: &str,
) -> Vec<StatLine> {
let Some(value) = self.stats.get(key) else { let Some(value) = self.stats.get(key) else {
return Vec::new(); return Vec::new();
}; };
@@ -1006,7 +1144,9 @@ impl PluginModel {
let unit_short = definition.map(|f| f.short_unit()).unwrap_or(""); let unit_short = definition.map(|f| f.short_unit()).unwrap_or("");
let unit_type = definition.map(|f| f.unit_type()).unwrap_or("float"); let unit_type = definition.map(|f| f.unit_type()).unwrap_or("float");
let value = if definition.map(|f| f.rate).unwrap_or(false) { 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 { } else {
value value
}; };
@@ -1020,7 +1160,9 @@ impl PluginModel {
Some(v) => { Some(v) => {
if unit_type == "float" { if unit_type == "float" {
format!("{v:.1}{unit_short}") 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, "-")) format!("{}{unit_short}", auto_unit(v, false, &min_symbol, "-"))
} else { } else {
format!("{}{unit_short}", v as i64) format!("{}{unit_short}", v as i64)
@@ -1030,13 +1172,18 @@ impl PluginModel {
(item, value_text) (item, value_text)
} }
Some(width) => { Some(width) => {
let item = format!("{header}{key_name:<width$}{separator}", width = width.saturating_sub(7)); let item = format!(
"{header}{key_name:<width$}{separator}",
width = width.saturating_sub(7)
);
let value_text = match value { let value_text = match value {
None => format!("{:>5}", "-"), None => format!("{:>5}", "-"),
Some(v) => { Some(v) => {
if unit_type == "float" { if unit_type == "float" {
format!("{v:>5.1}{unit_short}") 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, "-")) format!("{:>5}{unit_short}", auto_unit(v, false, &min_symbol, "-"))
} else { } else {
format!("{:>5}{unit_short}", v as i64) format!("{:>5}{unit_short}", v as i64)
@@ -1048,7 +1195,11 @@ impl PluginModel {
}; };
let item = format!("{msg_item}{trailer}"); 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"); let optional = self.view_bool(None, key, "optional");
vec![ vec![
@@ -1088,12 +1239,14 @@ impl PluginModel {
} }
let history_max_size = self.history_size(); let history_max_size = self.history_size();
let items = self.items_history_list.clone(); let items = self.items_history_list.clone();
let key = self.item_key.clone(); let key = self.item_key;
for item in items { for item in items {
match &export { match &export {
Value::Array(list) => { Value::Array(list) => {
for entry in 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; continue;
}; };
if let Some(value) = entry.get(&item.name).and_then(as_f64) { if let Some(value) = entry.get(&item.name).and_then(as_f64) {
@@ -1108,7 +1261,12 @@ impl PluginModel {
} }
Value::Object(_) => { Value::Object(_) => {
if let Some(value) = export.get(&item.name).and_then(as_f64) { 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),
);
} }
} }
_ => {} _ => {}
+218 -75
View File
@@ -15,8 +15,15 @@ use sysinfo::{ProcessesToUpdate, System, Users};
pub const MANDATORY_PROCESS_STATS: [&str; 2] = ["pid", "name"]; pub const MANDATORY_PROCESS_STATS: [&str; 2] = ["pid", "name"];
/// Available process sort keys. /// Available process sort keys.
pub const SORT_PROCESSES_STATS: [&str; 7] = pub const SORT_PROCESSES_STATS: [&str; 7] = [
["cpu_percent", "memory_percent", "username", "cpu_times", "io_counters", "name", "cpu_num"]; "cpu_percent",
"memory_percent",
"username",
"cpu_times",
"io_counters",
"name",
"cpu_num",
];
/// Sort keys as displayed to humans. /// Sort keys as displayed to humans.
pub fn sort_for_human(key: &str) -> &str { pub fn sort_for_human(key: &str) -> &str {
@@ -155,7 +162,9 @@ impl Processes {
} }
pub fn pid_max(&self) -> Option<i64> { pub fn pid_max(&self) -> Option<i64> {
std::fs::read_to_string("/proc/sys/kernel/pid_max").ok().and_then(|v| v.trim().parse::<i64>().ok()) std::fs::read_to_string("/proc/sys/kernel/pid_max")
.ok()
.and_then(|v| v.trim().parse::<i64>().ok())
} }
pub fn processes_count(&self, max_processes: Option<i64>) -> i64 { pub fn processes_count(&self, max_processes: Option<i64>) -> i64 {
@@ -199,20 +208,28 @@ impl Processes {
let stat = read_proc_stat(pid); let stat = read_proc_stat(pid);
let name = process.name().to_string_lossy().to_string(); let name = process.name().to_string_lossy().to_string();
let cmdline: Vec<String> = let cmdline: Vec<String> = process
process.cmd().iter().map(|c| c.to_string_lossy().to_string()).collect(); .cmd()
.iter()
.map(|c| c.to_string_lossy().to_string())
.collect();
let username = process let username = process
.user_id() .user_id()
.and_then(|uid| self.users.get_user_by_id(uid)) .and_then(|uid| self.users.get_user_by_id(uid))
.map(|u| u.name().to_string()) .map(|u| u.name().to_string())
.unwrap_or_default(); .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 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 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 cpu_num = stat.as_ref().map(|s| s.processor).unwrap_or(0);
let (user_time, system_time) = let (user_time, system_time) = stat
stat.as_ref().map(|s| (s.utime, s.stime)).unwrap_or((0.0, 0.0)); .as_ref()
.map(|s| (s.utime, s.stime))
.unwrap_or((0.0, 0.0));
let rss = process.memory() as f64; let rss = process.memory() as f64;
let vms = process.virtual_memory() as f64; let vms = process.virtual_memory() as f64;
@@ -269,7 +286,12 @@ impl Processes {
} }
// Sort processes by the current sort key. // 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); self.update_processcount(&processlist);
@@ -284,23 +306,25 @@ impl Processes {
{ {
self.extended_process_pid = Some(pid); self.extended_process_pid = Some(pid);
} }
if let Some(target) = self.extended_process_pid { if let Some(target) = self.extended_process_pid
if target == pid { && target == pid
let extended = self.set_extended_stats(pid, proc); {
if let Value::Object(map) = extended { let extended = self.set_extended_stats(pid, proc);
if let Some(obj) = proc.as_object_mut() { if let Value::Object(map) = extended
for (k, v) in map { && let Some(obj) = proc.as_object_mut()
obj.insert(k, v); {
} for (k, v) in map {
} obj.insert(k, v);
} }
} }
} }
} }
// Remove non running processes from the IO cache. // Remove non running processes from the IO cache.
let running: std::collections::HashSet<i64> = let running: std::collections::HashSet<i64> = processlist
processlist.iter().filter_map(|p| p.get("pid").and_then(Value::as_i64)).collect(); .iter()
.filter_map(|p| p.get("pid").and_then(Value::as_i64))
.collect();
self.io_old.retain(|pid, _| running.contains(pid)); self.io_old.retain(|pid, _| running.contains(pid));
// Export list. // Export list.
@@ -343,8 +367,12 @@ impl Processes {
let mean_counter_key = format!("{stat_prefix}_mean_counter"); 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_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_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_sum = prev
let prev_count = prev.and_then(|p| p.get(&mean_counter_key)).and_then(Value::as_i64); .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 min = prev_min.map(|m| m.min(current)).unwrap_or(current);
let max = prev_max.map(|m| m.max(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); 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)); 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 cpu_percent = proc
let memory_percent = proc.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0); .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, "cpu", cpu_percent, previous.as_ref());
prefix(&mut ret, "memory", memory_percent, previous.as_ref()); prefix(&mut ret, "memory", memory_percent, previous.as_ref());
ret.insert("extended_stats".to_string(), json!(true)); ret.insert("extended_stats".to_string(), json!(true));
@@ -387,19 +421,31 @@ impl Processes {
fn update_list(&self, processlist: &[Value]) -> Vec<Value> { fn update_list(&self, processlist: &[Value]) -> Vec<Value> {
if !self.filter_focus.is_empty() { 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() { if self.filter.filter().is_none() {
return processlist.to_vec(); 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<Value> { fn update_export_list(&self, processlist: &[Value]) -> Vec<Value> {
if self.filter_export.is_empty() { if self.filter_export.is_empty() {
return Vec::new(); 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). /// Get the process list (sorted or not).
@@ -422,16 +468,23 @@ impl Processes {
} }
pub fn get_stats(&self, pid: i64) -> Option<&Value> { 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). /// Decrease the nice level of a process (higher priority).
pub fn nice_decrease(&self, pid: i64) { pub fn nice_decrease(&self, pid: i64) {
let current = read_nice(pid).unwrap_or(0) as i32; 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 { 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 { } 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) { pub fn nice_increase(&self, pid: i64) {
let current = read_nice(pid).unwrap_or(0) as i32; 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 { 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 { } 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. /// Kill a process.
pub fn kill(&self, pid: i64) { 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}"); tracing::debug!("Send kill signal to process: {pid}");
unsafe { unsafe {
libc::kill(pid as libc::pid_t, libc::SIGKILL); 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" => { "io_counters" => {
// [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag] // [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
let counters = process.get("io_counters").and_then(Value::as_array); 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 read = counters
let write = counters.and_then(|c| c.get(1)).and_then(Value::as_f64).unwrap_or(0.0); .and_then(|c| c.first())
let read_old = counters.and_then(|c| c.get(2)).and_then(Value::as_f64).unwrap_or(0.0); .and_then(Value::as_f64)
let write_old = counters.and_then(|c| c.get(3)).and_then(Value::as_f64).unwrap_or(0.0); .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 read - read_old + write - write_old
} }
"cpu_times" => { "cpu_times" => {
let times = process.get("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 user = times
let system = times.and_then(|t| t.get("system")).and_then(Value::as_f64).unwrap_or(0.0); .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 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 { 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| { stats.sort_by(|a, b| {
@@ -523,22 +609,53 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
let no_io = [0u64; 5]; let no_io = [0u64; 5];
let mut programs: indexmap::IndexMap<String, Value> = indexmap::IndexMap::new(); let mut programs: indexmap::IndexMap<String, Value> = indexmap::IndexMap::new();
for process in processlist { for process in processlist {
let name = process.get("name").and_then(Value::as_str).unwrap_or("?").to_string(); let name = process
let num_threads = process.get("num_threads").and_then(Value::as_f64).unwrap_or(0.0).max(0.0); .get("name")
let cpu_percent = process.get("cpu_percent").and_then(Value::as_f64).unwrap_or(0.0); .and_then(Value::as_str)
let memory_percent = process.get("memory_percent").and_then(Value::as_f64).unwrap_or(0.0); .unwrap_or("?")
let cpu_times = process.get("cpu_times").cloned().unwrap_or_else(|| json!({})); .to_string();
let memory_info = process.get("memory_info").cloned().unwrap_or_else(|| json!({})); 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<u64> = process let io_counters: Vec<u64> = process
.get("io_counters") .get("io_counters")
.and_then(Value::as_array) .and_then(Value::as_array)
.map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect()) .map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect())
.unwrap_or_else(|| no_io.to_vec()); .unwrap_or_else(|| no_io.to_vec());
let pid = process.get("pid").and_then(Value::as_i64).unwrap_or(0); 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 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 status = process
let time_since_update = process.get("time_since_update").cloned().unwrap_or(Value::Null); .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) { match programs.get_mut(&name) {
None => { None => {
@@ -575,20 +692,26 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
add(program, "memory_percent", memory_percent); add(program, "memory_percent", memory_percent);
for key in ["user", "system", "iowait"] { for key in ["user", "system", "iowait"] {
let value = cpu_times.get(key).and_then(Value::as_f64).unwrap_or(0.0); 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 (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 Some(times) =
let current = times.get(key).and_then(Value::as_f64).unwrap_or(0.0); program.get_mut("cpu_times").and_then(Value::as_object_mut)
times.insert(key.to_string(), json!(current + value)); {
} 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(program_memory) = program
if let Some(addition) = memory_info.as_object() { .get_mut("memory_info")
for (key, value) in addition { .and_then(Value::as_object_mut)
let current = program_memory.get(key).and_then(Value::as_f64).unwrap_or(0.0); && let Some(addition) = memory_info.as_object()
let value = value.as_f64().unwrap_or(0.0); {
program_memory.insert(key.clone(), json!(current + value)); 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. // io_counters: sum slots 0..4, OR the tag.
@@ -598,10 +721,15 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
.and_then(Value::as_array) .and_then(Value::as_array)
.map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect()) .map(|values| values.iter().map(|v| v.as_u64().unwrap_or(0)).collect())
.unwrap_or_else(|| no_io.to_vec()); .unwrap_or_else(|| no_io.to_vec());
for index in 0..4 { for (index, value) in summed.iter_mut().take(4).enumerate() {
summed[index] = existing.get(index).copied().unwrap_or(0) + io_counters.get(index).copied().unwrap_or(0); *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)); program.insert("io_counters".to_string(), json!(summed));
if let Some(children) = program.get_mut("childrens").and_then(Value::as_array_mut) { if let Some(children) = program.get_mut("childrens").and_then(Value::as_array_mut) {
children.push(json!(pid)); children.push(json!(pid));
@@ -625,7 +753,11 @@ pub fn processes_to_programs(processlist: &[Value]) -> Vec<Value> {
.into_values() .into_values()
.map(|mut program| { .map(|mut program| {
if let Some(program) = program.as_object_mut() { 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.insert("nprocs".to_string(), json!(nprocs));
} }
program program
@@ -657,7 +789,12 @@ fn read_proc_stat(pid: i64) -> Option<ProcStat> {
let fields: Vec<&str> = content[close + 1..].split_whitespace().collect(); 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]. // 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 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 = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
let ticks = if ticks <= 0.0 { 100.0 } else { ticks }; let ticks = if ticks <= 0.0 { 100.0 } else { ticks };
let mut stat = ProcStat { let mut stat = ProcStat {
@@ -680,13 +817,16 @@ fn read_proc_stat(pid: i64) -> Option<ProcStat> {
} }
} }
} }
if let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status")) { 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 Some(line) = status.lines().find(|l| l.starts_with("Gid:"))
let gids: Vec<i64> = line[4..].split_whitespace().filter_map(|v| v.parse().ok()).collect(); {
stat.gid_real = gids.first().copied().unwrap_or(0); let gids: Vec<i64> = line[4..]
stat.gid_effective = gids.get(1).copied().unwrap_or(0); .split_whitespace()
stat.gid_saved = gids.get(2).copied().unwrap_or(0); .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) Some(stat)
} }
@@ -712,7 +852,10 @@ mod tests {
#[test] #[test]
fn test_sort_stats() { 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); sort_stats(&mut stats, "cpu_percent", "memory_percent", true);
assert_eq!(stats[0]["name"], "b"); assert_eq!(stats[0]["name"], "b");
} }
+18 -9
View File
@@ -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) => {
serde_json::Value::Array(items.iter().map(sanitize_value).collect()) serde_json::Value::Array(items.iter().map(sanitize_value).collect())
} }
serde_json::Value::Object(map) => { serde_json::Value::Object(map) => serde_json::Value::Object(
serde_json::Value::Object(map.iter().map(|(k, v)| (k.clone(), sanitize_value(v))).collect()) map.iter()
} .map(|(k, v)| (k.clone(), sanitize_value(v)))
.collect(),
),
other => other.clone(), other => other.clone(),
} }
} }
/// Split a command string into an argument list. Spaces are separators except /// Split a command string into an argument list. Spaces are separators except
/// within single or double quotes (the surrounding quotes are removed). /// within single or double quotes (the surrounding quotes are removed).
pub fn split_args(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> Result<Vec<String>, String> { pub fn split_args(
cmd: &str,
render: Option<&dyn Fn(&str) -> Result<String, String>>,
) -> Result<Vec<String>, String> {
let mut args: Vec<String> = Vec::new(); let mut args: Vec<String> = Vec::new();
let mut current = String::new(); let mut current = String::new();
let mut in_single = false; let mut in_single = false;
@@ -182,16 +187,20 @@ fn secure_popen_one(cmd: &str, render: Option<&dyn Fn(&str) -> Result<String, St
return stderr; return stderr;
} }
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
if let Some(target) = stdout_redirect { if let Some(target) = stdout_redirect
if let Ok(mut file) = std::fs::File::create(&target) { && let Ok(mut file) = std::fs::File::create(&target)
let _ = file.write_all(stdout.as_bytes()); {
} let _ = file.write_all(stdout.as_bytes());
} }
stdout stdout
} }
/// Run a command line, interpreting (or not) the shell operators. /// Run a command line, interpreting (or not) the shell operators.
pub fn secure_popen(cmd: &str, allow_operators: bool, render: Option<&dyn Fn(&str) -> Result<String, String>>) -> String { pub fn secure_popen(
cmd: &str,
allow_operators: bool,
render: Option<&dyn Fn(&str) -> Result<String, String>>,
) -> String {
if !allow_operators { if !allow_operators {
return run_argv(cmd, render); return run_argv(cmd, render);
} }
+1
View File
@@ -13,6 +13,7 @@ pub enum Threshold {
} }
impl Threshold { impl Threshold {
#[allow(clippy::should_implement_trait)]
pub fn from_str(name: &str) -> Option<Self> { pub fn from_str(name: &str) -> Option<Self> {
match name { match name {
"OK" => Some(Threshold::Ok), "OK" => Some(Threshold::Ok),
+12 -3
View File
@@ -14,7 +14,10 @@ pub struct Timer {
impl Timer { impl Timer {
pub fn new(duration: f64) -> Self { 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.start();
timer timer
} }
@@ -32,7 +35,11 @@ impl Timer {
/// Elapsed seconds since the timer was started. /// Elapsed seconds since the timer was started.
pub fn get(&self) -> f64 { 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) { pub fn set(&mut self, duration: f64) {
@@ -62,7 +69,9 @@ impl Default for Counter {
impl Counter { impl Counter {
pub fn new() -> Self { pub fn new() -> Self {
Self { start: Instant::now() } Self {
start: Instant::now(),
}
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
+33 -11
View File
@@ -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. /// 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 { 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 value = percent.clamp(0.0, 100.0);
let scaled = size as f64 * value / 100.0; let scaled = size as f64 * value / 100.0;
let whole = scaled.floor() as usize; 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 { if (frac * 8.0) as usize >= 5 {
bar.push_str(bar_char); bar.push_str(bar_char);
} else { } else {
bar.push(' '); bar.push_str(empty_char);
} }
whole += 1; whole += 1;
} }
bar.push_str(&" ".repeat(size.saturating_sub(whole))); bar.push_str(&empty_char.repeat(size.saturating_sub(whole)));
if display_value { if display_value {
if percent >= 100.0 { 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 { } else {
bar = format!("{bar}{percent:5.1}%"); bar = format!("{bar}{percent:5.1}%");
} }
@@ -104,11 +123,9 @@ pub fn string_value_to_float(s: &str) -> Option<f64> {
let mut digits = String::new(); let mut digits = String::new();
let mut rest = String::new(); let mut rest = String::new();
for c in cleaned.chars() { for c in cleaned.chars() {
if c.is_ascii_digit() || c == '.' { if (c.is_ascii_digit() || c == '.') && rest.is_empty() {
if rest.is_empty() { digits.push(c);
digits.push(c); continue;
continue;
}
} }
rest.push(c); rest.push(c);
} }
@@ -195,7 +212,9 @@ pub fn py_repr(value: &Value) -> String {
pub fn as_f64(value: &Value) -> Option<f64> { pub fn as_f64(value: &Value) -> Option<f64> {
match value { match value {
Value::Number(n) => n.as_f64(), 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<String> {
/// shell by adding a `$` prefix in front of them. (Only the replacement /// shell by adding a `$` prefix in front of them. (Only the replacement
/// flavour is needed by the plugins.) /// flavour is needed by the plugins.)
pub fn replace_special_chars(input: &str, by: char) -> String { 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)] #[cfg(test)]
+86 -32
View File
@@ -94,7 +94,13 @@ fn clock_ticks() -> f64 {
fn parse_cpu_line(fields: &[&str]) -> CpuTimes { fn parse_cpu_line(fields: &[&str]) -> CpuTimes {
let ticks = clock_ticks(); let ticks = clock_ticks();
let value = |index: usize| -> f64 { fields.get(index).and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.0) / ticks }; let value = |index: usize| -> f64 {
fields
.get(index)
.and_then(|v| v.parse::<f64>().ok())
.unwrap_or(0.0)
/ ticks
};
CpuTimes { CpuTimes {
user: value(0), user: value(0),
nice: value(1), nice: value(1),
@@ -150,7 +156,11 @@ pub fn read_meminfo() -> HashMap<String, u64> {
continue; continue;
}; };
let unit = parts.next().unwrap_or(""); 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.insert(key.trim().to_string(), bytes);
} }
values values
@@ -163,10 +173,10 @@ pub fn read_vmstat() -> HashMap<String, u64> {
return values; return values;
}; };
for line in content.lines() { for line in content.lines() {
if let Some((key, value)) = line.split_once(' ') { if let Some((key, value)) = line.split_once(' ')
if let Ok(value) = value.trim().parse::<u64>() { && let Ok(value) = value.trim().parse::<u64>()
values.insert(key.to_string(), value); {
} values.insert(key.to_string(), value);
} }
} }
values values
@@ -212,7 +222,9 @@ pub fn uptime_seconds() -> Option<f64> {
/// Number of logical CPUs. /// Number of logical CPUs.
pub fn logical_core_count() -> usize { 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`. /// Number of physical cores, parsed from `/proc/cpuinfo`.
@@ -262,20 +274,21 @@ pub fn cpu_name() -> String {
let mut implementer: Option<String> = None; let mut implementer: Option<String> = None;
let mut ret = fallback; let mut ret = fallback;
for line in content.lines() { for line in content.lines() {
if line.starts_with("model name") || line.starts_with("Model") || line.starts_with("cpu model") { if (line.starts_with("model name")
if let Some((_, value)) = line.split_once(':') { || line.starts_with("Model")
return value.trim().to_string(); || line.starts_with("cpu model"))
} && let Some((_, value)) = line.split_once(':')
{
return value.trim().to_string();
} }
if line.starts_with("CPU implementer") { if line.starts_with("CPU implementer") {
implementer = line.split_once(':').map(|(_, v)| v.trim().to_string()); implementer = line.split_once(':').map(|(_, v)| v.trim().to_string());
} }
if line.starts_with("CPU part") { if line.starts_with("CPU part")
if let Some(part) = line.split_once(':').map(|(_, v)| v.trim().to_string()) { && let Some(part) = line.split_once(':').map(|(_, v)| v.trim().to_string())
if let Some(implementer) = &implementer { && let Some(implementer) = &implementer
ret = format!("{implementer} part {part}"); {
} ret = format!("{implementer} part {part}");
}
} }
} }
ret ret
@@ -316,7 +329,10 @@ pub fn read_net_dev() -> IndexMap<String, NetDev> {
let Some((interface, values)) = line.split_once(':') else { let Some((interface, values)) = line.split_once(':') else {
continue; continue;
}; };
let fields: Vec<u64> = values.split_whitespace().filter_map(|v| v.parse().ok()).collect(); let fields: Vec<u64> = values
.split_whitespace()
.filter_map(|v| v.parse().ok())
.collect();
if fields.len() < 16 { if fields.len() < 16 {
continue; continue;
} }
@@ -429,7 +445,8 @@ pub fn read_diskstats() -> IndexMap<String, DiskStat> {
continue; continue;
} }
let name = fields[2].to_string(); 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( result.insert(
name, name,
DiskStat { DiskStat {
@@ -520,12 +537,25 @@ pub fn disk_usage(path: &str) -> Option<FsUsage> {
if unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) } != 0 { if unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) } != 0 {
return None; 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 total = stat.f_blocks as u64 * block_size;
let free = stat.f_bavail 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 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 }; let percent = if used + free > 0 {
Some(FsUsage { total, used, free, percent }) (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`). /// Linux distribution name/version (`/etc/os-release`).
@@ -546,7 +576,8 @@ pub fn read_os_release() -> (String, String) {
pub fn hostname() -> String { pub fn hostname() -> String {
let mut buffer = [0u8; 256]; 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 { if result == 0 {
let end = buffer.iter().position(|c| *c == 0).unwrap_or(buffer.len()); let end = buffer.iter().position(|c| *c == 0).unwrap_or(buffer.len());
String::from_utf8_lossy(&buffer[..end]).to_string() String::from_utf8_lossy(&buffer[..end]).to_string()
@@ -571,10 +602,17 @@ pub fn get_ip_address() -> (Option<String>, Option<String>) {
addr.sin_family = libc::AF_INET as u16; addr.sin_family = libc::AF_INET as u16;
addr.sin_port = 53u16.to_be(); addr.sin_port = 53u16.to_be();
addr.sin_addr.s_addr = u32::from_be_bytes([192, 0, 2, 1]).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::<libc::sockaddr_in>() as u32) == 0 { if libc::connect(
sock,
&addr as *const _ as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_in>() as u32,
) == 0
{
let mut local: libc::sockaddr_in = std::mem::zeroed(); let mut local: libc::sockaddr_in = std::mem::zeroed();
let mut len = std::mem::size_of::<libc::sockaddr_in>() as u32; let mut len = std::mem::size_of::<libc::sockaddr_in>() 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 bytes = local.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.starts_with("127.") && candidate != "0.0.0.0" { if !candidate.starts_with("127.") && candidate != "0.0.0.0" {
@@ -599,11 +637,15 @@ pub fn get_ip_address() -> (Option<String>, Option<String>) {
if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET { if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET {
let sin = addr as *const libc::sockaddr_in; let sin = addr as *const libc::sockaddr_in;
let bytes = (*sin).sin_addr.s_addr.to_ne_bytes(); 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() { if &candidate == ip && !mask.is_null() {
let sin_mask = mask as *const libc::sockaddr_in; let sin_mask = mask as *const libc::sockaddr_in;
let mbytes = (*sin_mask).sin_addr.s_addr.to_ne_bytes(); 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; break;
} }
} }
@@ -624,18 +666,24 @@ pub fn get_ip_address() -> (Option<String>, Option<String>) {
while !current.is_null() { while !current.is_null() {
let addr = (*current).ifa_addr; let addr = (*current).ifa_addr;
if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET { 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; let flags = (*current).ifa_flags as u32;
if name != "lo" && flags & libc::IFF_UP as u32 != 0 { if name != "lo" && flags & libc::IFF_UP as u32 != 0 {
let sin = addr as *const libc::sockaddr_in; let sin = addr as *const libc::sockaddr_in;
let bytes = (*sin).sin_addr.s_addr.to_ne_bytes(); 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 mask = (*current).ifa_netmask;
let mut netmask = None; let mut netmask = None;
if !mask.is_null() { if !mask.is_null() {
let sin_mask = mask as *const libc::sockaddr_in; let sin_mask = mask as *const libc::sockaddr_in;
let mbytes = (*sin_mask).sin_addr.s_addr.to_ne_bytes(); 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); found = (Some(candidate), netmask);
break; break;
@@ -661,7 +709,11 @@ pub fn get_default_gateway() -> Option<String> {
let gateway = fields[2]; let gateway = fields[2];
if gateway.len() == 8 { if gateway.len() == 8 {
let bytes: Vec<String> = (0..4) let bytes: Vec<String> = (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(); .collect();
return Some(bytes.join(".")); return Some(bytes.join("."));
} }
@@ -684,7 +736,9 @@ pub fn ip_to_cidr(ip: Option<&str>) -> u32 {
/// Read a sysfs hwmon input value. /// Read a sysfs hwmon input value.
pub fn read_number_file(path: &str) -> Option<f64> { pub fn read_number_file(path: &str) -> Option<f64> {
std::fs::read_to_string(path).ok().and_then(|v| v.trim().parse::<f64>().ok()) std::fs::read_to_string(path)
.ok()
.and_then(|v| v.trim().parse::<f64>().ok())
} }
/// Get the current date formatted with a C strftime format (same semantics /// Get the current date formatted with a C strftime format (same semantics
+8 -6
View File
@@ -20,6 +20,7 @@ pub struct CpuPercentState {
prev_cpu: Option<CpuTimes>, prev_cpu: Option<CpuTimes>,
prev_percpu: Vec<CpuTimes>, prev_percpu: Vec<CpuTimes>,
cpu_percent: f64, cpu_percent: f64,
cpu_timer: std::time::Instant,
percpu_percent: Vec<Value>, percpu_percent: Vec<Value>,
} }
@@ -30,6 +31,7 @@ impl CpuPercentState {
prev_cpu: None, prev_cpu: None,
prev_percpu: Vec::new(), prev_percpu: Vec::new(),
cpu_percent: 0.0, cpu_percent: 0.0,
cpu_timer: std::time::Instant::now(),
percpu_percent: Vec::new(), percpu_percent: Vec::new(),
}; };
state.cpu_percent = state.compute_cpu(); state.cpu_percent = state.compute_cpu();
@@ -39,12 +41,12 @@ impl CpuPercentState {
/// Update and/or return the CPU usage (psutil `cpu_percent`). /// Update and/or return the CPU usage (psutil `cpu_percent`).
pub fn get_cpu(&mut self) -> f64 { pub fn get_cpu(&mut self) -> f64 {
// The Python implementation caches this value for `cached_timer_cpu` // The value is cached for `cached_timer_cpu` seconds like in the
// seconds. Since the engine refreshes at least every `args.time` // Python implementation.
// seconds, the value is recomputed on every call there; keep the cache if self.cpu_timer.elapsed().as_secs_f64() >= self.cached_timer_cpu {
// for direct calls. self.cpu_percent = self.compute_cpu();
let _ = self.cached_timer_cpu; self.cpu_timer = std::time::Instant::now();
self.cpu_percent = self.compute_cpu(); }
self.cpu_percent self.cpu_percent
} }
+124 -36
View File
@@ -27,37 +27,102 @@ impl Engine {
// Registration order mirrors the alphabetical plugin loading of the // Registration order mirrors the alphabetical plugin loading of the
// Python implementation (in particular processcount runs before // Python implementation (in particular processcount runs before
// processlist and programlist). // processlist and programlist).
plugins.insert("amps".to_string(), Box::new(AmpsPlugin::new(&config, &args))); plugins.insert(
plugins.insert("core".to_string(), Box::new(CorePlugin::new(&config, &args))); "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("cpu".to_string(), Box::new(CpuPlugin::new(&config, &args)));
plugins.insert("diskio".to_string(), Box::new(DiskioPlugin::new(&config, &args))); plugins.insert(
plugins.insert("folders".to_string(), Box::new(FoldersPlugin::new(&config, &args))); "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("fs".to_string(), Box::new(FsPlugin::new(&config, &args)));
plugins.insert("ip".to_string(), Box::new(IpPlugin::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("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("mem".to_string(), Box::new(MemPlugin::new(&config, &args)));
plugins.insert("memswap".to_string(), Box::new(MemSwapPlugin::new(&config, &args))); plugins.insert(
plugins.insert("network".to_string(), Box::new(NetworkPlugin::new(&config, &args))); "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("now".to_string(), Box::new(NowPlugin::new(&config, &args)));
plugins.insert("percpu".to_string(), Box::new(PerCpuPlugin::new(&config, &args))); plugins.insert(
plugins.insert("ports".to_string(), Box::new(PortsPlugin::new(&config, &args))); "percpu".to_string(),
plugins.insert("processcount".to_string(), Box::new(ProcessCountPlugin::new(&config, &args))); Box::new(PerCpuPlugin::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(
plugins.insert("psutilversion".to_string(), Box::new(PsutilVersionPlugin::new(&config, &args))); "ports".to_string(),
plugins.insert("quicklook".to_string(), Box::new(QuicklookPlugin::new(&config, &args))); Box::new(PortsPlugin::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(
plugins.insert("system".to_string(), Box::new(SystemPlugin::new(&config, &args))); "processcount".to_string(),
plugins.insert("uptime".to_string(), Box::new(UptimePlugin::new(&config, &args))); Box::new(ProcessCountPlugin::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(
"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 // The refresh time is set on the model, which was built with the
// command line value already. // command line value already.
let _ = refresh_time; 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_plugin_options();
engine.apply_process_options(); engine.apply_process_options();
engine.sync_enabled(); engine.sync_enabled();
@@ -77,11 +142,11 @@ impl Engine {
fn apply_plugin_options(&mut self) { fn apply_plugin_options(&mut self) {
// Disable plugins from the configuration file. // Disable plugins from the configuration file.
for section in self.config.sections() { for section in self.config.sections() {
if self.config.get_bool_value(section, "disable", false) { if self.config.get_bool_value(section, "disable", false)
if let Some(plugin) = self.plugins.get_mut(section) { && let Some(plugin) = self.plugins.get_mut(section)
debug!("{} disabled by the configuration file", section); {
plugin.set_enabled(false); debug!("{} disabled by the configuration file", section);
} plugin.set_enabled(false);
} }
} }
@@ -102,10 +167,18 @@ impl Engine {
// --disable-plugin / --enable-plugin. // --disable-plugin / --enable-plugin.
let mut explicit_enable: Vec<String> = Vec::new(); let mut explicit_enable: Vec<String> = Vec::new();
if let Some(enable) = &self.args.enable_plugin { 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 { if let Some(disable) = &self.args.disable_plugin {
let tokens: Vec<String> = disable.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); let tokens: Vec<String> = disable
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if tokens.iter().any(|t| t == "all") { if tokens.iter().any(|t| t == "all") {
for name in self.plugins.keys().cloned().collect::<Vec<_>>() { for name in self.plugins.keys().cloned().collect::<Vec<_>>() {
self.plugins.get_mut(&name).unwrap().set_enabled(false); self.plugins.get_mut(&name).unwrap().set_enabled(false);
@@ -134,12 +207,18 @@ impl Engine {
} }
// Processlist needs processcount to update the shared process list. // 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 { if !processcount_enabled {
for name in ["processlist", "programlist"] { for name in ["processlist", "programlist"] {
if let Some(plugin) = self.plugins.get_mut(name) { if let Some(plugin) = self.plugins.get_mut(name) {
if plugin.is_enabled() { 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); plugin.set_enabled(false);
} }
@@ -210,7 +289,10 @@ impl Engine {
/// Update every enabled plugin. /// Update every enabled plugin.
pub fn update(&mut self) { pub fn update(&mut self) {
let names: Vec<String> = self.plugins.keys().cloned().collect(); let names: Vec<String> = self.plugins.keys().cloned().collect();
let enabled: Vec<String> = names.into_iter().filter(|n| self.plugins[n].is_enabled()).collect(); let enabled: Vec<String> = names
.into_iter()
.filter(|n| self.plugins[n].is_enabled())
.collect();
self.update_plugins(&enabled); self.update_plugins(&enabled);
} }
@@ -222,7 +304,13 @@ impl Engine {
} }
fn update_plugin(&mut self, name: &str) { 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 { let Some(plugin) = plugins.get_mut(name) else {
return; return;
}; };
@@ -289,10 +377,10 @@ impl Engine {
}; };
let mut result = serde_json::Map::new(); let mut result = serde_json::Map::new();
for name in names { for name in names {
if let Some(plugin) = self.plugins.get(&name) { if let Some(plugin) = self.plugins.get(&name)
if plugin.is_enabled() { && plugin.is_enabled()
result.insert(name, plugin.stats().clone()); {
} result.insert(name, plugin.stats().clone());
} }
} }
result result
+2
View File
@@ -1,3 +1,5 @@
#![allow(clippy::type_complexity)]
//! glances-plugins: collectors, plugin registry and update engine. //! glances-plugins: collectors, plugin registry and update engine.
pub mod collectors; pub mod collectors;
+50 -14
View File
@@ -62,8 +62,12 @@ impl AmpsPlugin {
.get_value(section, "regex", None) .get_value(section, "regex", None)
.and_then(|pattern| regex::Regex::new(&pattern).ok()); .and_then(|pattern| regex::Regex::new(&pattern).ok());
let refresh = config.get_float_value(section, "refresh", 3.0); let refresh = config.get_float_value(section, "refresh", 3.0);
let countmin = config.get_value(section, "countmin", None).and_then(|v| v.parse::<f64>().ok()); let countmin = config
let countmax = config.get_value(section, "countmax", None).and_then(|v| v.parse::<f64>().ok()); .get_value(section, "countmin", None)
.and_then(|v| v.parse::<f64>().ok());
let countmax = config
.get_value(section, "countmax", None)
.and_then(|v| v.parse::<f64>().ok());
let one_line = config.get_bool_value(section, "one_line", false); let one_line = config.get_bool_value(section, "one_line", false);
amps.push(AmpConfig { amps.push(AmpConfig {
name: amp_display_name(section), name: amp_display_name(section),
@@ -142,7 +146,12 @@ impl Plugin for AmpsPlugin {
let cmdline = process let cmdline = process
.get("cmdline") .get("cmdline")
.and_then(Value::as_array) .and_then(Value::as_array)
.map(|a| a.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>().join(" ")) .map(|a| {
a.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default(); .unwrap_or_default();
regex.is_match(name) || regex.is_match(&cmdline) regex.is_match(name) || regex.is_match(&cmdline)
}) })
@@ -191,7 +200,8 @@ impl Plugin for AmpsPlugin {
} else { } else {
glances_core::Decoration::Ok 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<glances_core::StatLine> { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new(); 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; return ret;
} }
let name_max_width = ctx.max_width.saturating_sub(12); let name_max_width = ctx.max_width.saturating_sub(12);
@@ -212,7 +229,13 @@ impl Plugin for AmpsPlugin {
false, false,
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 items = self.model.sorted_stats();
let Some(items) = items.as_array() else { let Some(items) = items.as_array() else {
return ret; return ret;
@@ -221,15 +244,28 @@ impl Plugin for AmpsPlugin {
let name = i.get("name").and_then(Value::as_str).unwrap_or(""); 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 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 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_new_line());
ret.push(self.model.curse_add_line( ret.push(
format!("{:<width$}", display_name.chars().take(name_max_width).collect::<String>(), width = name_max_width), self.model.curse_add_line(
glances_core::Decoration::Default, format!(
false, "{:<width$}",
false, display_name
false, .chars()
)); .take(name_max_width)
.collect::<String>(),
width = name_max_width
),
glances_core::Decoration::Default,
false,
false,
false,
),
);
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{count:>9}"), format!("{count:>9}"),
self.model.view_decoration(Some(name), "count"), self.model.view_decoration(Some(name), "count"),
+75 -18
View File
@@ -48,7 +48,11 @@ impl CpuPlugin {
args.disable_config_exec, args.disable_config_exec,
); );
let nb_log_core = crate::collectors::logical_core_count(); 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 { fn times_percent(&mut self) -> CpuTimes {
@@ -105,11 +109,23 @@ impl Plugin for CpuPlugin {
let mut soft_interrupts = 0u64; let mut soft_interrupts = 0u64;
for line in content.lines() { for line in content.lines() {
if let Some(rest) = line.strip_prefix("ctxt ") { 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 ") { } 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 ") { } 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)); stats.insert("ctx_switches".to_string(), json!(ctx_switches));
@@ -125,13 +141,27 @@ impl Plugin for CpuPlugin {
fn update_views(&mut self) { fn update_views(&mut self) {
let key = self.get_key(); let key = self.get_key();
self.model.update_views(key); self.model.update_views(key);
if self.model.stats.get("time_since_update").and_then(Value::as_f64) != Some(0.0) { if self
if let Some(ctx_switches) = self.model.stats.get("ctx_switches").and_then(Value::as_f64) { .model
let maximum = 100.0 * self.nb_log_core as f64; .stats
let decoration = .get("time_since_update")
self.model.get_alert(ctx_switches, 0.0, maximum, true, false, Some("ctx_switches"), None, false); .and_then(Value::as_f64)
self.model.set_view_decoration(None, "ctx_switches", decoration); != 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(); let idle_tag = s.get("user").is_none();
// Line 1: total + idle + ctx_switches. // 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) { if let Some(total) = s.get("total").and_then(Value::as_f64) {
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{total:>4.1}%"), format!("{total:>4.1}%"),
@@ -160,7 +196,13 @@ impl Plugin for CpuPlugin {
} }
if s.get("idle").is_some() && !idle_tag { if s.get("idle").is_some() && !idle_tag {
let optional = self.model.view_bool(None, "idle", "optional"); 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) { if let Some(idle) = s.get("idle").and_then(Value::as_f64) {
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{idle:>4.1}%"), 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. // Line 2.
ret.push(self.model.curse_new_line()); 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("idle", Some(15), "", "", ""));
} }
ret.extend(self.model.curse_add_stat("irq", Some(14), " ", "", "")); 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. // Line 3.
ret.push(self.model.curse_new_line()); 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), " ", "", "")); ret.extend(self.model.curse_add_stat("nice", Some(14), " ", "", ""));
if s.get("soft_interrupts").is_some() { 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 { } 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. // Line 4.
@@ -208,7 +262,10 @@ impl Plugin for CpuPlugin {
if s.get("guest").is_some() { if s.get("guest").is_some() {
ret.extend(self.model.curse_add_stat("guest", Some(14), " ", "", "")); ret.extend(self.model.curse_add_stat("guest", Some(14), " ", "", ""));
} else { } else {
ret.extend(self.model.curse_add_stat("syscalls", Some(15), " ", "", "")); ret.extend(
self.model
.curse_add_stat("syscalls", Some(15), " ", "", ""),
);
} }
ret ret
} }
+285 -56
View File
@@ -39,9 +39,12 @@ impl DiskioPlugin {
); );
model.set_item_key("disk_name"); model.set_item_key("disk_name");
model.hide_zero = config.get_bool_value("diskio", "hide_zero", false); 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_threshold_bytes =
model.hide_zero_fields = config.get_int_value("diskio", "hide_threshold_bytes", 0) as f64;
vec!["read_bytes_rate_per_sec".to_string(), "write_bytes_rate_per_sec".to_string()]; model.hide_zero_fields = vec![
"read_bytes_rate_per_sec".to_string(),
"write_bytes_rate_per_sec".to_string(),
];
Self { model } Self { model }
} }
} }
@@ -75,8 +78,14 @@ impl Plugin for DiskioPlugin {
let mut item = serde_json::Map::new(); let mut item = serde_json::Map::new();
item.insert("read_count".to_string(), json!(disk_stat.read_count)); item.insert("read_count".to_string(), json!(disk_stat.read_count));
item.insert("write_count".to_string(), json!(disk_stat.write_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(
item.insert("write_bytes".to_string(), json!(disk_stat.sectors_written * SECTOR_SIZE)); "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("read_time".to_string(), json!(disk_stat.read_time));
item.insert("write_time".to_string(), json!(disk_stat.write_time)); item.insert("write_time".to_string(), json!(disk_stat.write_time));
item.insert("key".to_string(), json!("disk_name")); item.insert("key".to_string(), json!("disk_name"));
@@ -95,20 +104,34 @@ impl Plugin for DiskioPlugin {
let Some(item) = item.as_object_mut() else { let Some(item) = item.as_object_mut() else {
continue; continue;
}; };
let read_time = item.get("read_time_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); let read_time = item
let read_count = item.get("read_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); .get("read_time_rate_per_sec")
let write_time = item.get("write_time_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); .and_then(Value::as_f64)
let write_count = item.get("write_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); .unwrap_or(0.0);
let read_latency = if item.contains_key("read_time_rate_per_sec") && read_count > 0.0 { let read_count = item
(read_time / read_count).floor() as i64 .get("read_count_rate_per_sec")
} else { .and_then(Value::as_f64)
0 .unwrap_or(0.0);
}; let write_time = item
let write_latency = if item.contains_key("write_time_rate_per_sec") && write_count > 0.0 { .get("write_time_rate_per_sec")
(write_time / write_count).floor() as i64 .and_then(Value::as_f64)
} else { .unwrap_or(0.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("read_latency".to_string(), json!(read_latency));
item.insert("write_latency".to_string(), json!(write_latency)); item.insert("write_latency".to_string(), json!(write_latency));
} }
@@ -124,36 +147,104 @@ impl Plugin for DiskioPlugin {
.iter() .iter()
.filter_map(|item| { .filter_map(|item| {
let name = item.get("disk_name")?.as_str()?.to_string(); 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 read_rate = item
let write_rate = item.get("write_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); .get("read_bytes_rate_per_sec")
let read_latency = item.get("read_latency").and_then(Value::as_f64).unwrap_or(0.0); .and_then(Value::as_f64)
let write_latency = item.get("write_latency").and_then(Value::as_f64).unwrap_or(0.0); .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)) Some((name, read_rate, write_rate, read_latency, write_latency))
}) })
.collect(), .collect(),
_ => Vec::new(), _ => Vec::new(),
}; };
for (disk_name, read_rate, write_rate, read_latency, write_latency) in items { 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_rx = self.model.get_alert(
let alert_tx = self.model.get_alert(write_rate, 0.0, 100.0, true, false, Some("tx"), Some(&disk_name), false); 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"] { 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"] { 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 = let mut alert_latency_rx = self.model.get_alert(
self.model.get_alert(read_latency, 0.0, 100.0, true, false, Some("rx_latency"), Some(&disk_name), false); read_latency,
let mut alert_latency_tx = 0.0,
self.model.get_alert(write_latency, 0.0, 100.0, true, false, Some("tx_latency"), Some(&disk_name), false); 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 { 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 { 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
self.model.set_view_decoration(Some(&disk_name), "write_latency", alert_latency_tx); .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<glances_core::StatLine> { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new(); 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; return ret;
} }
let name_max_width = ctx.max_width.saturating_sub(13); let name_max_width = ctx.max_width.saturating_sub(13);
@@ -175,21 +273,61 @@ impl Plugin for DiskioPlugin {
false, false,
)); ));
if ctx.args.diskio_iops { 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(
ret.push(self.model.curse_add_line(format!("{:>7}", "IOW/s"), glances_core::Decoration::Default, false, false, false)); 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 { } 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(
ret.push(self.model.curse_add_line(format!("{:>7}", "ms/opW"), glances_core::Decoration::Default, false, false, false)); 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 { } 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(
ret.push(self.model.curse_add_line(format!("{:>7}", "W/s"), glances_core::Decoration::Default, false, false, false)); 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 stats = self.model.sorted_stats();
let Some(items) = stats.as_array() else { let Some(items) = stats.as_array() else {
return ret; return ret;
}; };
for i in items { 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 if self
.model .model
.hide_zero_fields .hide_zero_fields
@@ -204,7 +342,10 @@ impl Plugin for DiskioPlugin {
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or_else(|| disk_real_name.clone()); .unwrap_or_else(|| disk_real_name.clone());
if disk_name.chars().count() > name_max_width { if disk_name.chars().count() > name_max_width {
disk_name = format!("{}_", disk_name.chars().take(name_max_width).collect::<String>()); disk_name = format!(
"{}_",
disk_name.chars().take(name_max_width).collect::<String>()
);
} }
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
@@ -215,20 +356,108 @@ impl Plugin for DiskioPlugin {
false, false,
)); ));
if ctx.args.diskio_iops { 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 rx = self.model.auto_unit(
let tx = self.model.auto_unit(i.get("write_count_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K"); i.get("read_count_rate_per_sec")
ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_count"), false, false, false)); .and_then(Value::as_f64)
ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_count"), false, false, false)); .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 { } 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 rx = self.model.auto_unit(
let tx = self.model.auto_unit(i.get("write_latency").and_then(Value::as_f64).unwrap_or(0.0), true, "K"); i.get("read_latency").and_then(Value::as_f64).unwrap_or(0.0),
ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_latency"), false, false, false)); true,
ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_latency"), false, false, false)); "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 { } 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 rx = self.model.auto_unit(
let tx = self.model.auto_unit(i.get("write_bytes_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0), false, "K"); i.get("read_bytes_rate_per_sec")
ret.push(self.model.curse_add_line(format!("{rx:>7}"), self.model.view_decoration(Some(&disk_real_name), "read_bytes"), false, false, false)); .and_then(Value::as_f64)
ret.push(self.model.curse_add_line(format!("{tx:>7}"), self.model.view_decoration(Some(&disk_real_name), "write_bytes"), false, false, false)); .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 ret
+43 -23
View File
@@ -14,7 +14,6 @@ pub struct FoldersPlugin {
#[derive(Debug)] #[derive(Debug)]
struct FolderConfig { struct FolderConfig {
indice: String,
path: String, path: String,
refresh: f64, refresh: f64,
careful: Option<f64>, careful: Option<f64>,
@@ -59,12 +58,14 @@ impl FoldersPlugin {
if path.is_empty() { if path.is_empty() {
continue; 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<f64> { let threshold = |name: &str| -> Option<f64> {
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 { folders.push(FolderConfig {
indice: index.to_string(),
path, path,
refresh, refresh,
careful: threshold("careful"), careful: threshold("careful"),
@@ -123,21 +124,22 @@ impl Plugin for FoldersPlugin {
fn update_views(&mut self) { fn update_views(&mut self) {
let key = self.get_key(); let key = self.get_key();
self.model.update_views(key); self.model.update_views(key);
let items: Vec<(String, i64, u64, Option<f64>, Option<f64>, Option<f64>)> = match &self.model.stats { let items: Vec<(String, i64, u64, Option<f64>, Option<f64>, Option<f64>)> =
Value::Array(list) => list match &self.model.stats {
.iter() Value::Array(list) => list
.filter_map(|item| { .iter()
let path = item.get("path")?.as_str()?.to_string(); .filter_map(|item| {
let errno = item.get("errno")?.as_i64()?; let path = item.get("path")?.as_str()?.to_string();
let size = item.get("size")?.as_u64()?; let errno = item.get("errno")?.as_i64()?;
let careful = item.get("careful").and_then(Value::as_f64); let size = item.get("size")?.as_u64()?;
let warning = item.get("warning").and_then(Value::as_f64); let careful = item.get("careful").and_then(Value::as_f64);
let critical = item.get("critical").and_then(Value::as_f64); let warning = item.get("warning").and_then(Value::as_f64);
Some((path, errno, size, careful, warning, critical)) let critical = item.get("critical").and_then(Value::as_f64);
}) Some((path, errno, size, careful, warning, critical))
.collect(), })
_ => Vec::new(), .collect(),
}; _ => Vec::new(),
};
for (path, errno, size, careful, warning, critical) in items { for (path, errno, size, careful, warning, critical) in items {
if errno != 0 { if errno != 0 {
continue; continue;
@@ -151,7 +153,8 @@ impl Plugin for FoldersPlugin {
} else { } else {
glances_core::Decoration::Default 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<glances_core::StatLine> { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new(); 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; return ret;
} }
let name_max_width = ctx.max_width.saturating_sub(12); let name_max_width = ctx.max_width.saturating_sub(12);
@@ -172,7 +182,13 @@ impl Plugin for FoldersPlugin {
false, false,
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 items = self.model.sorted_stats();
let Some(items) = items.as_array() else { let Some(items) = items.as_array() else {
return ret; return ret;
@@ -183,7 +199,11 @@ impl Plugin for FoldersPlugin {
let errno = i.get("errno").and_then(Value::as_i64).unwrap_or(0); 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_new_line());
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{:<width$}", path.chars().take(name_max_width).collect::<String>(), width = name_max_width), format!(
"{:<width$}",
path.chars().take(name_max_width).collect::<String>(),
width = name_max_width
),
glances_core::Decoration::Default, glances_core::Decoration::Default,
false, false,
false, false,
+80 -15
View File
@@ -59,11 +59,16 @@ impl Plugin for FsPlugin {
let mut stats = Vec::new(); let mut stats = Vec::new();
for mount in crate::collectors::read_mounts() { for mount in crate::collectors::read_mounts() {
let is_physical = mount.device.starts_with("/dev/"); 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 { if !is_physical && !is_allowed {
continue; continue;
} }
if !self.model.is_display_any(&[&mount.mount_point, &mount.device], "") { if !self
.model
.is_display_any(&[&mount.mount_point, &mount.device], "")
{
continue; continue;
} }
let Some(usage) = crate::collectors::disk_usage(&mount.mount_point) else { 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("size".to_string(), json!(usage.total));
item.insert("used".to_string(), json!(usage.used)); item.insert("used".to_string(), json!(usage.used));
item.insert("free".to_string(), json!(usage.free)); 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")); item.insert("key".to_string(), json!("mnt_point"));
if let Some(alias) = self.model.has_alias(&mnt_point) { if let Some(alias) = self.model.has_alias(&mnt_point) {
item.insert("alias".to_string(), json!(alias)); item.insert("alias".to_string(), json!(alias));
@@ -96,7 +104,11 @@ impl Plugin for FsPlugin {
.iter() .iter()
.filter_map(|item| { .filter_map(|item| {
let mnt = item.get("mnt_point")?.as_str()?.to_string(); 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 size = item.get("size")?.as_f64()?;
let free = item.get("free")?.as_f64()?; let free = item.get("free")?.as_f64()?;
Some((mnt, size, free, options)) Some((mnt, size, free, options))
@@ -109,8 +121,18 @@ impl Plugin for FsPlugin {
if options.split(',').any(|o| o == "ro") { if options.split(',').any(|o| o == "ro") {
continue; continue;
} }
let decoration = self.model.get_alert(size - free, 0.0, size, true, false, Some(&mnt_point), None, false); let decoration = self.model.get_alert(
self.model.set_view_decoration(Some(&mnt_point), "used", decoration); 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<glances_core::StatLine> { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new(); 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; return ret;
} }
let name_max_width = ctx.max_width.saturating_sub(13); let name_max_width = ctx.max_width.saturating_sub(13);
@@ -132,30 +161,66 @@ impl Plugin for FsPlugin {
false, false,
)); ));
if ctx.runtime.fs_free_space { 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 { } 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 { let Some(items) = self.model.stats.as_array() else {
return ret; return ret;
}; };
let mut sorted = items.clone(); 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 { for i in &sorted {
let mnt_point_raw = i.get("mnt_point").and_then(Value::as_str).unwrap_or("").to_string(); let mnt_point_raw = i
let device = i.get("device_name").and_then(Value::as_str).unwrap_or("").to_string(); .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 let mut mnt_point = i
.get("alias") .get("alias")
.and_then(Value::as_str) .and_then(Value::as_str)
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or_else(|| mnt_point_raw.clone()); .unwrap_or_else(|| mnt_point_raw.clone());
let device_basename = device.rsplit('/').next().unwrap_or("").to_string(); 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})"); mnt_point = format!("{mnt_point} ({device_basename})");
} else if mnt_point.chars().count() > name_max_width { } else if mnt_point.chars().count() > name_max_width {
mnt_point = format!("{}_", mnt_point.chars().take(name_max_width).collect::<String>()); mnt_point = format!(
"{}_",
mnt_point.chars().take(name_max_width).collect::<String>()
);
} }
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
+31 -13
View File
@@ -80,12 +80,15 @@ impl IpPlugin {
if let (Some(username), Some(password)) = (&self.public_username, &self.public_password) { if let (Some(username), Some(password)) = (&self.public_username, &self.public_password) {
request = request.header("Authorization", &format!("Basic {}:{}", username, password)); request = request.header("Authorization", &format!("Basic {}:{}", username, password));
} }
if let Ok(mut response) = request.call() { if let Ok(mut response) = request.call()
if let Ok(value) = response.body_mut().read_json::<Value>() { && let Ok(value) = response.body_mut().read_json::<Value>()
self.public_address = {
value.get(&self.public_field).and_then(Value::as_str).unwrap_or("").to_string(); self.public_address = value
self.public_info = Some(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 { if let Some(mask) = &mask {
stats.insert("mask".to_string(), json!(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(); self.refresh_public();
if !self.public_address.is_empty() { if !self.public_address.is_empty() {
let public = if ctx.args.hide_public_info { let public = if ctx.args.hide_public_info {
@@ -158,14 +164,26 @@ impl Plugin for IpPlugin {
return ret; return ret;
}; };
let mut text = address.to_string(); let mut text = address.to_string();
if let Some(mask_cidr) = s.get("mask_cidr").and_then(Value::as_u64) { if let Some(mask_cidr) = s.get("mask_cidr").and_then(Value::as_u64)
if mask_cidr > 0 { && mask_cidr > 0
text += &format!("/{mask_cidr}"); {
} 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) { 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 ret
} }
+39 -7
View File
@@ -28,7 +28,10 @@ impl IrqPlugin {
args.disable_config_exec, args.disable_config_exec,
); );
model.set_item_key("irq_line"); 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)); return Ok(Value::Array(stats));
}; };
let mut lines = content.lines(); 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 { for line in lines {
let fields: Vec<&str> = line.split_whitespace().collect(); let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 2 { if fields.len() < 2 {
@@ -69,7 +75,10 @@ impl Plugin for IrqPlugin {
} else { } else {
first.to_string() first.to_string()
}; };
let current: u64 = fields[1..1 + cpu_number].iter().filter_map(|v| v.parse::<u64>().ok()).sum(); let current: u64 = fields[1..1 + cpu_number]
.iter()
.filter_map(|v| v.parse::<u64>().ok())
.sum();
let irq_rate = match self.lasts.get(&irq_line) { let irq_rate = match self.lasts.get(&irq_line) {
Some(last) if *last > 0 && time_since_update > 0.0 => { Some(last) if *last > 0 && time_since_update > 0.0 => {
((current.saturating_sub(*last)) as f64 / time_since_update) as i64 ((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<glances_core::StatLine> { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new(); 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; return ret;
} }
let name_max_width = ctx.max_width.saturating_sub(7); let name_max_width = ctx.max_width.saturating_sub(7);
@@ -112,7 +128,13 @@ impl Plugin for IrqPlugin {
false, false,
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 { let Some(items) = self.model.stats.as_array() else {
return ret; return ret;
}; };
@@ -121,13 +143,23 @@ impl Plugin for IrqPlugin {
let rate = i.get("irq_rate").and_then(Value::as_i64).unwrap_or(0); 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_new_line());
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{:<width$}", line.chars().take(name_max_width).collect::<String>(), width = name_max_width), format!(
"{:<width$}",
line.chars().take(name_max_width).collect::<String>(),
width = name_max_width
),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{rate:>9}"),
glances_core::Decoration::Default, glances_core::Decoration::Default,
false, false,
false, false,
false, false,
)); ));
ret.push(self.model.curse_add_line(format!("{rate:>9}"), glances_core::Decoration::Default, false, false, false));
} }
ret ret
} }
+42 -10
View File
@@ -64,13 +64,21 @@ impl Plugin for LoadPlugin {
fn update_views(&mut self) { fn update_views(&mut self) {
let key = self.get_key(); let key = self.get_key();
self.model.update_views(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) { if let Some(min15) = self.model.stats.get("min15").and_then(Value::as_f64) {
let decoration = self.model.get_alert_log(min15, max, None); let decoration = self.model.get_alert_log(min15, max, None);
self.model.set_view_decoration(None, "min15", decoration); self.model.set_view_decoration(None, "min15", decoration);
} }
if let Some(min5) = self.model.stats.get("min5").and_then(Value::as_f64) { 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); 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) { if !s.is_object() || s.as_object().map(|o| o.is_empty()).unwrap_or(true) {
return ret; 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( 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, glances_core::Decoration::Default,
false, false,
false, false,
false, false,
)); ));
if let Some(cpucore) = s.get("cpucore").and_then(Value::as_f64) { if let Some(cpucore) = s.get("cpucore").and_then(Value::as_f64)
if cpucore > 0.0 { && cpucore > 0.0
ret.push(self.model.curse_add_line(format!("{:3}core", cpucore as i64), glances_core::Decoration::Default, false, false, false)); {
} 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"] { for load_time in ["1", "5", "15"] {
ret.push(self.model.curse_new_line()); 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)); ret.push(self.model.curse_add_line(
let stat = s.get(&format!("min{load_time}")).and_then(Value::as_f64).unwrap_or(0.0); 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 value = if ctx.runtime.disable_irix {
let cores = s.get("cpucore").and_then(Value::as_f64).unwrap_or(0.0); let cores = s.get("cpucore").and_then(Value::as_f64).unwrap_or(0.0);
if cores != 0.0 { if cores != 0.0 {
+35 -6
View File
@@ -69,7 +69,11 @@ impl Plugin for MemPlugin {
let buffers = get("Buffers"); let buffers = get("Buffers");
// procps-ng "free" sums reclaimable slab to cached. // procps-ng "free" sums reclaimable slab to cached.
let cached = get("Cached") + get("SReclaimable"); 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 active = get("Active");
let inactive = get("Inactive"); let inactive = get("Inactive");
let mut available = get("MemAvailable"); let mut available = get("MemAvailable");
@@ -104,8 +108,18 @@ impl Plugin for MemPlugin {
fn update_views(&mut self) { fn update_views(&mut self) {
let key = self.get_key(); let key = self.get_key();
self.model.update_views(key); self.model.update_views(key);
let total = self.model.stats.get("total").and_then(Value::as_f64).unwrap_or(0.0); let total = self
let used = self.model.stats.get("used").and_then(Value::as_f64).unwrap_or(0.0); .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); let decoration = self.model.get_alert_log(used, total, None);
self.model.set_view_decoration(None, "percent", decoration); self.model.set_view_decoration(None, "percent", decoration);
} }
@@ -121,8 +135,20 @@ impl Plugin for MemPlugin {
return ret; return ret;
} }
// First line: total% + trend + active. // 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(
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)); "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) { if let Some(percent) = s.get("percent").and_then(Value::as_f64) {
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{:>6.1}%", percent), format!("{:>6.1}%", percent),
@@ -136,7 +162,10 @@ impl Plugin for MemPlugin {
// Second line: total + inactive. // Second line: total + inactive.
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
ret.extend(self.model.curse_add_stat("total", Some(15), "", "", "")); 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. // Third line: available + buffers.
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
if self.available { if self.available {
+35 -6
View File
@@ -53,7 +53,11 @@ impl Plugin for MemSwapPlugin {
let total = meminfo.get("SwapTotal").copied().unwrap_or(0); let total = meminfo.get("SwapTotal").copied().unwrap_or(0);
let free = meminfo.get("SwapFree").copied().unwrap_or(0); let free = meminfo.get("SwapFree").copied().unwrap_or(0);
let used = total.saturating_sub(free); 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(); let vmstat = crate::collectors::read_vmstat();
// pswpin/pswpout are expressed in 4 KiB pages. // pswpin/pswpout are expressed in 4 KiB pages.
let sin = vmstat.get("pswpin").copied().unwrap_or(0) * 4 * 1024; let sin = vmstat.get("pswpin").copied().unwrap_or(0) * 4 * 1024;
@@ -73,8 +77,18 @@ impl Plugin for MemSwapPlugin {
fn update_views(&mut self) { fn update_views(&mut self) {
let key = self.get_key(); let key = self.get_key();
self.model.update_views(key); self.model.update_views(key);
let total = self.model.stats.get("total").and_then(Value::as_f64).unwrap_or(0.0); let total = self
let used = self.model.stats.get("used").and_then(Value::as_f64).unwrap_or(0.0); .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); let decoration = self.model.get_alert_log(used, total, None);
self.model.set_view_decoration(None, "percent", decoration); self.model.set_view_decoration(None, "percent", decoration);
} }
@@ -89,10 +103,25 @@ impl Plugin for MemSwapPlugin {
if !s.is_object() { if !s.is_object() {
return ret; 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( 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"), self.model.view_decoration(None, "percent"),
false, false,
false, false,
+123 -24
View File
@@ -39,11 +39,19 @@ impl NetworkPlugin {
); );
model.set_item_key("interface_name"); model.set_item_key("interface_name");
model.hide_zero = config.get_bool_value("network", "hide_zero", false); model.hide_zero = config.get_bool_value("network", "hide_zero", false);
model.hide_zero_fields = model.hide_zero_fields = vec![
vec!["bytes_recv_rate_per_sec".to_string(), "bytes_sent_rate_per_sec".to_string()]; "bytes_recv_rate_per_sec".to_string(),
let hide_no_up = config.has_section("network") && config.get_bool_value("network", "hide_no_up", false); "bytes_sent_rate_per_sec".to_string(),
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 } 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("key".to_string(), json!("interface_name"));
item.insert("interface_name".to_string(), json!(interface_name)); item.insert("interface_name".to_string(), json!(interface_name));
item.insert("alias".to_string(), json!(alias)); 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)); stats.push(Value::Object(item));
} }
Ok(Value::Array(stats)) Ok(Value::Array(stats))
@@ -104,8 +115,14 @@ impl Plugin for NetworkPlugin {
.iter() .iter()
.filter_map(|item| { .filter_map(|item| {
let name = item.get("interface_name")?.as_str()?.to_string(); 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 rx = item
let tx = item.get("bytes_sent_rate_per_sec").and_then(Value::as_f64).unwrap_or(0.0); .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 speed = item.get("speed").and_then(Value::as_f64).unwrap_or(0.0);
let real_name = name.split(':').next().unwrap_or("").to_string(); let real_name = name.split(':').next().unwrap_or("").to_string();
Some((name, rx, tx, speed, Some(real_name))) 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_rx = rx as i64 * 8;
let bps_tx = tx as i64 * 8; let bps_tx = tx as i64 * 8;
let action_key = action_key.as_deref(); let action_key = action_key.as_deref();
let mut alert_rx = let mut alert_rx = self.model.get_alert(
self.model.get_alert(bps_rx as f64, 0.0, 100.0, true, false, Some("rx"), action_key, false); bps_rx as f64,
let mut alert_tx = 0.0,
self.model.get_alert(bps_tx as f64, 0.0, 100.0, true, false, Some("tx"), action_key, false); 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 { 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 { 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"] { 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"] { 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_cumul {
if ctx.runtime.network_sum { 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 { } 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(
ret.push(self.model.curse_add_line(format!("{:>7}", "Tx"), glances_core::Decoration::Default, false, false, false)); 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 { } 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 { } 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(
ret.push(self.model.curse_add_line(format!("{:>7}", "Tx/s"), glances_core::Decoration::Default, false, false, false)); format!("{:>7}", "Rx/s"),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
format!("{:>7}", "Tx/s"),
glances_core::Decoration::Default,
false,
false,
false,
));
} }
let stats = self.model.sorted_stats(); let stats = self.model.sorted_stats();
@@ -173,7 +262,11 @@ impl Plugin for NetworkPlugin {
return ret; return ret;
}; };
for i in items { 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. // Hide stats never different from zero.
if self if self
.model .model
@@ -227,7 +320,13 @@ impl Plugin for NetworkPlugin {
false, false,
)); ));
if ctx.runtime.network_sum { 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 { } else {
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{rx:>7}"), format!("{rx:>7}"),
+19 -4
View File
@@ -43,11 +43,15 @@ impl Plugin for NowPlugin {
} }
fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result<Value> { fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result<Value> {
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() { let strftime = if !ctx.args.strftime_format.is_empty() {
ctx.args.strftime_format.clone() ctx.args.strftime_format.clone()
} else { } 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() { let custom = if !strftime.is_empty() {
crate::collectors::strftime(&strftime) crate::collectors::strftime(&strftime)
@@ -72,7 +76,18 @@ impl Plugin for NowPlugin {
} }
fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> { fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let custom = self.model.stats.get("custom").and_then(Value::as_str).unwrap_or(""); let custom = self
vec![self.model.curse_add_line(format!("{custom:<23}"), glances_core::Decoration::Default, false, false, false)] .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,
)]
} }
} }
+69 -12
View File
@@ -41,7 +41,10 @@ impl PerCpuPlugin {
); );
model.set_item_key("cpu_number"); model.set_item_key("cpu_number");
let max_cpu_display = config.get_int_value("percpu", "max_cpu_display", 4).max(1) as usize; 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 items = &items;
let quicklook_disabled = !ctx.runtime.is_plugin_enabled("quicklook"); 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 { 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"); header.insert(0, "total");
} }
for stat in &header { 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. // Sort by total and keep the top max_cpu_display cores.
let mut sorted: Vec<Value> = items.clone(); let mut sorted: Vec<Value> = items.clone();
@@ -99,26 +116,54 @@ impl Plugin for PerCpuPlugin {
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
if quicklook_disabled { if quicklook_disabled {
let cpu_id = cpu.get("cpu_number").and_then(Value::as_i64).unwrap_or(0); 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} ") }; let msg = if cpu_id < 10 {
ret.push(self.model.curse_add_line(msg, glances_core::Decoration::Default, false, false, false)); 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 { for stat in &header {
let value = cpu.get(*stat).and_then(Value::as_f64); let value = cpu.get(*stat).and_then(Value::as_f64);
let decoration = match value { 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, None => glances_core::Decoration::Default,
}; };
let msg = match value { let msg = match value {
Some(value) => format!("{value:6.1}%"), Some(value) => format!("{value:6.1}%"),
None => format!("{:>6}%", "?"), 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 { if items.len() > self.max_cpu_display {
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
if quicklook_disabled { 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 { for stat in &header {
let values: Vec<f64> = sorted let values: Vec<f64> = sorted
@@ -126,9 +171,21 @@ impl Plugin for PerCpuPlugin {
.skip(self.max_cpu_display) .skip(self.max_cpu_display)
.filter_map(|cpu| cpu.get(*stat).and_then(Value::as_f64)) .filter_map(|cpu| cpu.get(*stat).and_then(Value::as_f64))
.collect(); .collect();
let mean = if values.is_empty() { 0.0 } else { values.iter().sum::<f64>() / values.len() as f64 }; let mean = if values.is_empty() {
let decoration = self.model.get_alert(mean, 0.0, 100.0, true, false, Some(stat), None, false); 0.0
ret.push(self.model.curse_add_line(format!("{mean:6.1}%"), decoration, false, false, false)); } else {
values.iter().sum::<f64>() / values.len() as f64
};
let decoration =
self.model
.get_alert(mean, 0.0, 100.0, true, false, Some(stat), None, false);
ret.push(self.model.curse_add_line(
format!("{mean:6.1}%"),
decoration,
false,
false,
false,
));
} }
} }
ret ret
+66 -27
View File
@@ -57,21 +57,21 @@ impl PortsPlugin {
let mut entries = Vec::new(); let mut entries = Vec::new();
if config.has_section("ports") { if config.has_section("ports") {
// Default gateway entry. // Default gateway entry.
if config.get_bool_value("ports", "port_default_gateway", false) { if config.get_bool_value("ports", "port_default_gateway", false)
if let Some(gateway) = crate::collectors::get_default_gateway() { && let Some(gateway) = crate::collectors::get_default_gateway()
entries.push(PortEntry { {
indice: "port_0".to_string(), entries.push(PortEntry {
host: gateway, indice: "port_0".to_string(),
port: None, host: gateway,
url: None, port: None,
description: "Default gateway".to_string(), url: None,
refresh, description: "Default gateway".to_string(),
timeout: default_timeout, refresh,
rtt_warning: None, timeout: default_timeout,
timer: Instant::now() - Duration::from_secs(3600), rtt_warning: None,
status: None, timer: Instant::now() - Duration::from_secs(3600),
}); status: None,
} });
} }
for index in 1..=255 { for index in 1..=255 {
let key = format!("port_{index}_"); let key = format!("port_{index}_");
@@ -81,14 +81,17 @@ impl PortsPlugin {
if host.is_empty() { if host.is_empty() {
continue; 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 let description = config
.get_value("ports", &format!("{key}description"), None) .get_value("ports", &format!("{key}description"), None)
.unwrap_or_else(|| match port { .unwrap_or_else(|| match port {
Some(port) => format!("{host}:{port}"), Some(port) => format!("{host}:{port}"),
None => host.clone(), 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 let rtt_warning = config
.get_value("ports", &format!("{key}rtt_warning"), None) .get_value("ports", &format!("{key}rtt_warning"), None)
.and_then(|v| v.parse::<f64>().ok()) .and_then(|v| v.parse::<f64>().ok())
@@ -114,8 +117,11 @@ impl PortsPlugin {
if url.is_empty() { if url.is_empty() {
continue; continue;
} }
let description = config.get_value("ports", &format!("{key}description"), None).unwrap_or_else(|| url.clone()); let description = config
let timeout = config.get_float_value("ports", &format!("{key}timeout"), default_timeout); .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 let rtt_warning = config
.get_value("ports", &format!("{key}rtt_warning"), None) .get_value("ports", &format!("{key}rtt_warning"), None)
.and_then(|v| v.parse::<f64>().ok()) .and_then(|v| v.parse::<f64>().ok())
@@ -134,14 +140,21 @@ impl PortsPlugin {
}); });
} }
} }
Self { model, entries, default_timeout } Self {
model,
entries,
default_timeout,
}
} }
fn check(entry: &PortEntry) -> Option<f64> { fn check(entry: &PortEntry) -> Option<f64> {
let start = Instant::now(); let start = Instant::now();
if let Some(url) = &entry.url { if let Some(url) = &entry.url {
let timeout = Duration::from_secs_f64(entry.timeout.max(0.1)); 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() { match request.call() {
Ok(_) => Some(start.elapsed().as_secs_f64()), Ok(_) => Some(start.elapsed().as_secs_f64()),
Err(_) => None, Err(_) => None,
@@ -158,7 +171,13 @@ impl PortsPlugin {
} else { } else {
// ICMP check with the ping command. // ICMP check with the ping command.
let output = std::process::Command::new("ping") 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() .output()
.ok()?; .ok()?;
let text = String::from_utf8_lossy(&output.stdout); let text = String::from_utf8_lossy(&output.stdout);
@@ -237,10 +256,13 @@ impl Plugin for PortsPlugin {
for (indice, status, warning) in items { for (indice, status, warning) in items {
let decoration = match status { let decoration = match status {
None => glances_core::Decoration::Critical, 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, 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<glances_core::StatLine> { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new(); 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; return ret;
} }
let name_max_width = ctx.max_width.saturating_sub(12); let name_max_width = ctx.max_width.saturating_sub(12);
@@ -261,7 +290,13 @@ impl Plugin for PortsPlugin {
false, false,
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 items = self.model.sorted_stats();
let Some(items) = items.as_array() else { let Some(items) = items.as_array() else {
return ret; return ret;
@@ -272,7 +307,11 @@ impl Plugin for PortsPlugin {
let status = i.get("status").and_then(Value::as_f64); let status = i.get("status").and_then(Value::as_f64);
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{:<width$}", description.chars().take(name_max_width).collect::<String>(), width = name_max_width), format!(
"{:<width$}",
description.chars().take(name_max_width).collect::<String>(),
width = name_max_width
),
glances_core::Decoration::Default, glances_core::Decoration::Default,
false, false,
false, false,
@@ -77,50 +77,127 @@ impl Plugin for ProcessCountPlugin {
return ret; return ret;
} }
if ctx.args.disable_process { 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; return ret;
} }
// Filter line. // Filter line.
{ {
let procs = glances_core::processes::processes().lock().unwrap(); let procs = glances_core::processes::processes().lock().unwrap();
if let Some(filter) = procs.filter.filter() { 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} "); let mut msg = format!(" {filter} ");
if let Some(key) = procs.filter.filter_key() { if let Some(key) = procs.filter.filter_key() {
msg += &format!("on column {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(
ret.push(self.model.curse_add_line("('ENTER' to edit, 'E' to reset)", glances_core::Decoration::Default, false, false, false)); 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_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); 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) { 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; let mut other = total;
if let Some(running) = s.get("running").and_then(Value::as_i64) { if let Some(running) = s.get("running").and_then(Value::as_i64) {
other -= running; 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) { if let Some(sleeping) = s.get("sleeping").and_then(Value::as_i64) {
other -= sleeping; 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 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); let sort_human = glances_core::processes::sort_for_human(&procs.sort_key);
if procs.auto_sort { if procs.auto_sort {
msg += " sorted automatically"; 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}"); msg = format!(" by {sort_human}");
} else { } else {
msg += &format!(" sorted by {sort_human}"); 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 ret
} }
} }
+320 -74
View File
@@ -43,33 +43,48 @@ impl ProcessListPlugin {
model.set_item_key("pid"); model.set_item_key("pid");
// Load the configuration. // Load the configuration.
if config.has_option("processlist", "sort_key") { if config.has_option("processlist", "sort_key")
if let Some(sort_key) = config.get_value("processlist", "sort_key", None) { && let Some(sort_key) = config.get_value("processlist", "sort_key", None)
let mut procs = processes().lock().unwrap(); {
procs.set_sort_key(&sort_key, false); let mut procs = processes().lock().unwrap();
} procs.set_sort_key(&sort_key, false);
} }
if config.has_option("processlist", "focus") { if config.has_option("processlist", "focus")
if let Some(focus) = config.get_value("processlist", "focus", None) { && let Some(focus) = config.get_value("processlist", "focus", None)
processes().lock().unwrap().filter_focus.set_filter(Some(&focus)); {
} processes()
.lock()
.unwrap()
.filter_focus
.set_filter(Some(&focus));
} }
if config.has_option("processlist", "disable_stats") { if config.has_option("processlist", "disable_stats")
if let Some(disabled) = config.get_value("processlist", "disable_stats", None) { && let Some(disabled) = config.get_value("processlist", "disable_stats", None)
let stats: Vec<String> = disabled {
.split(',') let stats: Vec<String> = disabled
.map(|s| s.trim().to_string()) .split(',')
.filter(|s| !s.is_empty() && !glances_core::processes::MANDATORY_PROCESS_STATS.contains(&s.as_str())) .map(|s| s.trim().to_string())
.collect(); .filter(|s| {
processes().lock().unwrap().disable_stats = stats; !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 { 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 nb_log_core = crate::collectors::logical_core_count();
let disable_virtual_memory = config.get_bool_value("processlist", "disable_virtual_memory", false); let disable_virtual_memory =
Self { model, nb_log_core, 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(); let procs = glances_core::processes::processes().lock().unwrap();
( (
procs.max_values.get("cpu_percent").copied().unwrap_or(0.0), 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(), procs.sort_key.clone(),
) )
}; };
@@ -140,33 +159,120 @@ impl Plugin for ProcessListPlugin {
// Header. // Header.
if ctx.runtime.disable_irix { 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 { } 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 wide {
if !self.disable_virtual_memory { 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(
ret.push(self.model.curse_add_line(format!("{:<10} ", "USER"), Decoration::Default, false, false, false)); 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 { 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(
ret.push(self.model.curse_add_line(format!("{:>3} ", "NI"), Decoration::Default, false, false, false)); format!("{:<3} ", "THR"),
ret.push(self.model.curse_add_line(format!("{:>1} ", "S"), Decoration::Default, false, false, false)); 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 { 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(
ret.push(self.model.curse_add_line(format!("{:<4} ", "W/s"), Decoration::Default, true, true, false)); 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 { 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 _ = sort_key;
let max_pid_size = { let max_pid_size = {
@@ -181,7 +287,11 @@ impl Plugin for ProcessListPlugin {
let selected = position as i64 == cursor; let selected = position as i64 == cursor;
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
if selected && !disable_cursor { ">" } else { " " }, if selected && !disable_cursor {
">"
} else {
" "
},
Decoration::Selected, Decoration::Selected,
false, false,
false, false,
@@ -205,10 +315,19 @@ impl Plugin for ProcessListPlugin {
None, None,
false, 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%. // 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( let mem_decoration = self.model.get_alert(
memory_percent, memory_percent,
0.0, 0.0,
@@ -219,13 +338,22 @@ impl Plugin for ProcessListPlugin {
None, None,
false, 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. // VIRT and RES.
let memory_info = p.get("memory_info"); let memory_info = p.get("memory_info");
if wide { if wide {
if !self.disable_virtual_memory { 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( ret.push(self.model.curse_add_line(
format!("{:<5} ", self.model.auto_unit(vms, false, "K")), format!("{:<5} ", self.model.auto_unit(vms, false, "K")),
Decoration::Default, Decoration::Default,
@@ -234,7 +362,10 @@ impl Plugin for ProcessListPlugin {
false, 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( ret.push(self.model.curse_add_line(
format!("{:<5} ", self.model.auto_unit(rss, false, "K")), format!("{:<5} ", self.model.auto_unit(rss, false, "K")),
Decoration::Default, Decoration::Default,
@@ -246,18 +377,36 @@ impl Plugin for ProcessListPlugin {
// PID. // PID.
let pid = p.get("pid").and_then(Value::as_i64).unwrap_or(0); 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. // USER.
let username = p.get("username").and_then(Value::as_str).unwrap_or("?"); let username = p.get("username").and_then(Value::as_str).unwrap_or("?");
let username: String = username.chars().take(9).collect(); 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+. // TIME+.
if wide { if wide {
let times = p.get("cpu_times"); 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 user = times
let system = times.and_then(|t| t.get("system")).and_then(Value::as_f64).unwrap_or(0.0); .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 total = (user + system) as i64;
let hours = total / 3600; let hours = total / 3600;
let minutes = (total % 3600) / 60; let minutes = (total % 3600) / 60;
@@ -265,29 +414,62 @@ impl Plugin for ProcessListPlugin {
let (msg, decoration) = if hours > 99 { let (msg, decoration) = if hours > 99 {
(format!("{hours:<7}h"), Decoration::Default) (format!("{hours:<7}h"), Decoration::Default)
} else if hours > 0 { } else if hours > 0 {
(format!("{hours}h{minutes}:{seconds:02}"), Decoration::CpuTime) (
format!("{hours}h{minutes}:{seconds:02}"),
Decoration::CpuTime,
)
} else { } else {
(format!("{minutes}:{seconds:02}"), Decoration::Default) (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. // THR.
if let Some(threads) = p.get("num_threads").and_then(Value::as_i64) { 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 { } 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. // NI.
let nice_value = p.get("nice").and_then(Value::as_i64).unwrap_or(0); let nice_value = p.get("nice").and_then(Value::as_i64).unwrap_or(0);
let nice_decoration = self.nice_alert(nice_value); 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. // Status.
let status = p.get("status").and_then(Value::as_str).unwrap_or("?"); let status = p.get("status").and_then(Value::as_str).unwrap_or("?");
let status_decoration = self.status_alert(status); 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. // IO rates.
if wide { if wide {
@@ -301,12 +483,28 @@ impl Plugin for ProcessListPlugin {
let current = counters.get(slot)?.as_f64()?; let current = counters.get(slot)?.as_f64()?;
let old = counters.get(if slot == 0 { 2 } else { 3 })?.as_f64()?; let old = counters.get(if slot == 0 { 2 } else { 3 })?.as_f64()?;
let io = (current - old) / time_since_update; 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)] { for (slot, width) in [(0usize, 4usize), (1, 4)] {
match rate(slot) { match rate(slot) {
Some(value) => ret.push(self.model.curse_add_line(format!("{value:>width$} "), Decoration::Default, true, true, false)), Some(value) => ret.push(self.model.curse_add_line(
None => ret.push(self.model.curse_add_line(format!("{:>width$} ", "?"), Decoration::Default, true, true, false)), 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} "), Some(value) if value >= 0 => format!("{value:>3} "),
_ => format!("{:>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. // Command.
@@ -326,9 +527,17 @@ impl Plugin for ProcessListPlugin {
let cmdline: Vec<String> = p let cmdline: Vec<String> = p
.get("cmdline") .get("cmdline")
.and_then(Value::as_array) .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(); .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() { if !cmdline.is_empty() {
let first = &cmdline[0]; let first = &cmdline[0];
let (path, cmd) = if first.starts_with(name) { let (path, cmd) = if first.starts_with(name) {
@@ -341,17 +550,50 @@ impl Plugin for ProcessListPlugin {
}; };
let arguments = cmdline[1..].join(" "); let arguments = cmdline[1..].join(" ");
if !path.is_empty() && std::path::Path::new(&path).is_dir() && !short_name { 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(
ret.push(self.model.curse_add_line(cmd, process_decoration, false, false, true)); format!("{path}/"),
Decoration::Default,
false,
false,
true,
));
ret.push(self.model.curse_add_line(
cmd,
process_decoration,
false,
false,
true,
));
} else { } 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() { if !arguments.is_empty() {
let msg = format!(" {}", glances_core::units::replace_special_chars(&arguments, ' ')); let msg = format!(
ret.push(self.model.curse_add_line(msg, Decoration::Default, false, false, true)); " {}",
glances_core::units::replace_special_chars(&arguments, ' ')
);
ret.push(self.model.curse_add_line(
msg,
Decoration::Default,
false,
false,
true,
));
} }
} else { } 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 ret
@@ -367,10 +609,10 @@ impl ProcessListPlugin {
("nice_careful", glances_core::Decoration::Careful), ("nice_careful", glances_core::Decoration::Careful),
("nice_ok", glances_core::Decoration::Ok), ("nice_ok", glances_core::Decoration::Ok),
] { ] {
if let Some(items) = self.model.get_limit_list(criticality, "processlist") { if let Some(items) = self.model.get_limit_list(criticality, "processlist")
if items.iter().any(|item| item == &value) { && items.iter().any(|item| item == &value)
return decoration; {
} return decoration;
} }
} }
glances_core::Decoration::Default glances_core::Decoration::Default
@@ -383,13 +625,17 @@ impl ProcessListPlugin {
("status_careful", glances_core::Decoration::Careful), ("status_careful", glances_core::Decoration::Careful),
("status_ok", glances_core::Decoration::Ok), ("status_ok", glances_core::Decoration::Ok),
] { ] {
if let Some(items) = self.model.get_limit_list(criticality, "processlist") { if let Some(items) = self.model.get_limit_list(criticality, "processlist")
if items.iter().any(|item| item == value) { && items.iter().any(|item| item == value)
return decoration; {
} 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
}
} }
} }
+139 -33
View File
@@ -51,13 +51,23 @@ impl QuicklookPlugin {
args.disable_config_exec, args.disable_config_exec,
); );
let stats_list = model.get_conf_value("list", ""); let stats_list = model.get_conf_value("list", "");
let mut stats_list: Vec<String> = let mut stats_list: Vec<String> = if stats_list.is_empty() {
if stats_list.is_empty() { DEFAULT_STATS_LIST.iter().map(|s| s.to_string()).collect() } else { stats_list }; DEFAULT_STATS_LIST.iter().map(|s| s.to_string()).collect()
if stats_list.iter().any(|s| !AVAILABLE_STATS_LIST.contains(&s.as_str())) { } 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(); 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; 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<f64>) -> Option<f64> { fn mhz_to_hz(value: Option<f64>) -> Option<f64> {
@@ -88,7 +98,10 @@ impl Plugin for QuicklookPlugin {
(info, cpu, percpu) (info, cpu, percpu)
}; };
stats.insert("cpu_name".to_string(), json!(info.cpu_name)); 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_hz".to_string(), json!(Self::mhz_to_hz(info.cpu_hz)));
stats.insert("cpu".to_string(), json!(cpu_value)); stats.insert("cpu".to_string(), json!(cpu_value));
stats.insert("percpu".to_string(), Value::Array(percpu_values)); 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; let available = meminfo.get("MemAvailable").copied().unwrap_or(0) as f64;
stats.insert( stats.insert(
"mem".to_string(), "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_total = meminfo.get("SwapTotal").copied().unwrap_or(0);
let swap_free = meminfo.get("SwapFree").copied().unwrap_or(0); let swap_free = meminfo.get("SwapFree").copied().unwrap_or(0);
stats.insert( stats.insert(
"swap".to_string(), "swap".to_string(),
json!(if swap_total > 0 { 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 { } else {
0.0 0.0
}), }),
@@ -113,9 +131,16 @@ impl Plugin for QuicklookPlugin {
let log_core = crate::collectors::logical_core_count(); let log_core = crate::collectors::logical_core_count();
stats.insert("cpu_log_core".to_string(), json!(log_core)); 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)| { 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("load".to_string(), json!(load));
stats.insert("gpu_mem".to_string(), Value::Null); stats.insert("gpu_mem".to_string(), Value::Null);
@@ -130,8 +155,16 @@ impl Plugin for QuicklookPlugin {
let stats_list = self.stats_list.clone(); let stats_list = self.stats_list.clone();
for stat_key in &stats_list { for stat_key in &stats_list {
if let Some(value) = self.model.stats.get(stat_key).and_then(Value::as_f64) { if let Some(value) = self.model.stats.get(stat_key).and_then(Value::as_f64) {
let decoration = let decoration = self.model.get_alert(
self.model.get_alert(value, 0.0, 100.0, true, false, Some(stat_key), None, false); value,
0.0,
100.0,
true,
false,
Some(stat_key),
None,
false,
);
self.model.set_view_decoration(None, stat_key, decoration); 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(); let mut percpu_decoration = serde_json::Map::new();
if let Some(percpu) = self.model.stats.get("percpu").and_then(Value::as_array) { if let Some(percpu) = self.model.stats.get("percpu").and_then(Value::as_array) {
for cpu in percpu { 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); 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 { if percpu.len() > self.max_cpu_display && self.max_cpu_display > 0 {
let mut hidden: Vec<f64> = let mut hidden: Vec<f64> = percpu
percpu.iter().filter_map(|cpu| cpu.get("total").and_then(Value::as_f64)).collect(); .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)); 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())..]; let hidden = &hidden[self.max_cpu_display.min(hidden.len())..];
if !hidden.is_empty() { if !hidden.is_empty() {
let mean = hidden.iter().sum::<f64>() / hidden.len() as f64; let mean = hidden.iter().sum::<f64>() / 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 { fn display_curse(&self) -> bool {
@@ -171,26 +218,45 @@ impl Plugin for QuicklookPlugin {
return ret; return ret;
} }
// System information: CPU name + frequency. // 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), Some(max)) => format!(" {:.2}/{:.2}GHz", current / 1e9, max / 1e9),
(Some(current), None) => format!(" {:.2}GHz", current / 1e9), (Some(current), None) => format!(" {:.2}GHz", current / 1e9),
_ => String::new(), _ => String::new(),
}; };
let cpu_name = s.get("cpu_name").and_then(Value::as_str).unwrap_or(""); 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 { let name = if (ctx.max_width as i64 - freq.len() as i64 + 7) > 0 {
format!("{:<width$}", cpu_name, width = ctx.max_width - freq.len() + 7) format!(
"{:<width$}",
cpu_name,
width = ctx.max_width - freq.len() + 7
)
} else if freq.is_empty() { } else if freq.is_empty() {
String::new() String::new()
} else { } else {
"Frequency".to_string() "Frequency".to_string()
}; };
ret.push(self.model.curse_add_line(name.clone(), glances_core::Decoration::Default, false, false, false)); ret.push(self.model.curse_add_line(
ret.push(self.model.curse_add_line(freq.clone(), glances_core::Decoration::Default, false, false, false)); name.clone(),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_add_line(
freq.clone(),
glances_core::Decoration::Default,
false,
false,
false,
));
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
let bar_size = std::cmp::max( let bar_size = std::cmp::max(
name.len().saturating_add(freq.len()).saturating_sub(7), name.len().saturating_add(freq.len()).saturating_sub(7),
(ctx.max_width as usize).saturating_sub(9), ctx.max_width.saturating_sub(9),
) )
.max(10); .max(10);
let bar_char = self let bar_char = self
@@ -201,14 +267,32 @@ impl Plugin for QuicklookPlugin {
.unwrap_or_else(|| "|".to_string()); .unwrap_or_else(|| "|".to_string());
let mut lines: Vec<Vec<glances_core::StatLine>> = Vec::new(); let mut lines: Vec<Vec<glances_core::StatLine>> = Vec::new();
let mut push_line = |msg: String, bar_key: &str, percent: f64, decoration: glances_core::Decoration| { let 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); let bar = glances_core::units::bar_percent(percent, bar_size, &bar_char, true);
lines.push(vec![ lines.push(vec![
self.model.curse_add_line(msg, glances_core::Decoration::Default, false, false, false), self.model.curse_add_line(
self.model.curse_add_line("[", glances_core::Decoration::Bold, false, false, false), msg,
self.model.curse_add_line(bar, decoration, false, false, false), glances_core::Decoration::Default,
self.model.curse_add_line("]", glances_core::Decoration::Bold, false, false, false), false,
self.model.curse_add_line(" ", glances_core::Decoration::Default, false, false, false), false,
false,
),
self.model
.curse_add_line("[", glances_core::Decoration::Bold, false, false, false),
self.model
.curse_add_line(bar, decoration, false, false, false),
self.model
.curse_add_line("]", glances_core::Decoration::Bold, false, false, false),
self.model.curse_add_line(
" ",
glances_core::Decoration::Default,
false,
false,
false,
),
]); ]);
let _ = bar_key; let _ = bar_key;
}; };
@@ -230,7 +314,11 @@ impl Plugin for QuicklookPlugin {
for cpu in sorted.iter().take(self.max_cpu_display) { 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 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 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); let decoration = self.decoration_for_percpu(cpu_id);
push_line(msg, key, total, decoration); push_line(msg, key, total, decoration);
} }
@@ -240,14 +328,28 @@ impl Plugin for QuicklookPlugin {
.skip(self.max_cpu_display) .skip(self.max_cpu_display)
.filter_map(|cpu| cpu.get("total").and_then(Value::as_f64)) .filter_map(|cpu| cpu.get("total").and_then(Value::as_f64))
.collect(); .collect();
let mean = if hidden.is_empty() { 0.0 } else { hidden.iter().sum::<f64>() / hidden.len() as f64 }; let mean = if hidden.is_empty() {
0.0
} else {
hidden.iter().sum::<f64>() / hidden.len() as f64
};
let decoration = self.decoration_for_percpu(-1); 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) { } else if let Some(value) = s.get(key).and_then(Value::as_f64) {
let decoration = self.model.view_decoration(None, key); 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 { for line in lines {
@@ -261,7 +363,11 @@ impl Plugin for QuicklookPlugin {
impl QuicklookPlugin { impl QuicklookPlugin {
fn decoration_for_percpu(&self, cpu_id: i64) -> glances_core::Decoration { 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) { match self.model.get_view(None, "percpu_decoration", &key) {
Value::String(name) => glances_core::Decoration::from_name(&name), Value::String(name) => glances_core::Decoration::from_name(&name),
_ => glances_core::Decoration::Default, _ => glances_core::Decoration::Default,
+72 -35
View File
@@ -64,16 +64,18 @@ impl Plugin for RaidPlugin {
let status = fields[2].to_string(); let status = fields[2].to_string();
let raid_type = fields let raid_type = fields
.iter() .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()) .map(|f| f.to_string())
.unwrap_or_else(|| "unknown".to_string()); .unwrap_or_else(|| "unknown".to_string());
// Devices (name[index]). // Devices (name[index]).
let mut components = serde_json::Map::new(); let mut components = serde_json::Map::new();
for field in fields.iter().skip(3) { for field in fields.iter().skip(3) {
if let Some(device) = field.split('[').next() { if let Some(device) = field.split('[').next()
if !device.is_empty() { && !device.is_empty()
components.insert(device.to_string(), json!("active sync")); {
} components.insert(device.to_string(), json!("active sync"));
} }
} }
// The following line contains blocks and [used/available] [UU]. // The following line contains blocks and [used/available] [UU].
@@ -81,30 +83,30 @@ impl Plugin for RaidPlugin {
let mut available = components.len(); let mut available = components.len();
if index + 1 < lines.len() { if index + 1 < lines.len() {
let detail = lines[index + 1]; let detail = lines[index + 1];
if let Some(start) = detail.rfind('[') { if let Some(start) = detail.rfind('[')
if let Some(end) = detail[start..].find(']') { && let Some(end) = detail[start..].find(']')
let counts = &detail[start + 1..start + end]; {
if let Some((u, a)) = counts.split_once('/') { let counts = &detail[start + 1..start + end];
used = u.parse().unwrap_or(used as i64) as usize; if let Some((u, a)) = counts.split_once('/') {
available = a.parse().unwrap_or(available as i64) as usize; 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(start) = detail.rfind('[')
if let Some(end) = detail[start..].find(']') { && let Some(end) = detail[start..].find(']')
let flags = &detail[start + 1..start + end]; {
if flags.len() == components.len() { let flags = &detail[start + 1..start + end];
let names: Vec<String> = components.keys().cloned().collect(); if flags.len() == components.len() {
for (position, flag) in flags.chars().enumerate() { let names: Vec<String> = components.keys().cloned().collect();
if let Some(device) = names.get(position) { for (position, flag) in flags.chars().enumerate() {
let status = match flag { if let Some(device) = names.get(position) {
'U' => "active sync", let status = match flag {
'_' => "faulty", 'U' => "active sync",
'S' => "spare", '_' => "faulty",
_ => "unknown", 'S' => "spare",
}; _ => "unknown",
components.insert(device.clone(), json!(status)); };
} components.insert(device.clone(), json!(status));
} }
} }
} }
@@ -146,8 +148,20 @@ impl Plugin for RaidPlugin {
false, 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(
ret.push(self.model.curse_add_line(format!("{:>7}", "Avail"), glances_core::Decoration::Default, false, false, false)); 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(); let mut names: Vec<&String> = arrays.keys().collect();
names.sort(); names.sort();
for name in names { 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 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 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 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); let decoration = raid_alert(status, used, available, array_type);
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{:<width$}", name.chars().take(name_max_width).collect::<String>(), width = name_max_width), format!(
"{:<width$}",
name.chars().take(name_max_width).collect::<String>(),
width = name_max_width
),
decoration, decoration,
false, false,
false, 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(
ret.push(self.model.curse_add_line(format!("{available:>7}"), glances_core::Decoration::Default, false, false, false)); 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 ret
} }
} }
fn raid_alert(
fn raid_alert(status: &str, used: i64, available: i64, array_type: &str) -> glances_core::Decoration { status: &str,
used: i64,
available: i64,
array_type: &str,
) -> glances_core::Decoration {
if status != "active" || used < available { if status != "active" || used < available {
return glances_core::Decoration::Critical; return glances_core::Decoration::Critical;
} }
+108 -40
View File
@@ -49,27 +49,36 @@ fn read_hwmon() -> Vec<Value> {
let name = file.file_name().to_string_lossy().to_string(); let name = file.file_name().to_string_lossy().to_string();
let path = file.path(); let path = file.path();
// Temperatures (millidegrees). // Temperatures (millidegrees).
if let Some(rest) = name.strip_prefix("temp") { if let Some(rest) = name.strip_prefix("temp")
if let Some(rest) = rest.strip_suffix("_input") { && let Some(rest) = rest.strip_suffix("_input")
let Some(raw) = read_value(&path) else { {
continue; 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 value = raw / 1000.0;
let warning = read_label_value(&dir, &format!("temp{rest}_max")).map(|v| v / 1000.0); let label = read_label(&dir, &format!("temp{rest}_label"), &format!("temp{rest}"));
let critical = read_label_value(&dir, &format!("temp{rest}_crit")).map(|v| v / 1000.0); let warning =
stats.push(sensor_item(&label, "C", value, warning, critical, "temperature_core")); 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). // Fans (RPM).
if let Some(rest) = name.strip_prefix("fan") { if let Some(rest) = name.strip_prefix("fan")
if let Some(rest) = rest.strip_suffix("_input") { && let Some(rest) = rest.strip_suffix("_input")
let Some(value) = read_value(&path) else { {
continue; 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")); 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<Value> {
let dir = entry.path(); let dir = entry.path();
let name = entry.file_name().to_string_lossy().to_string(); let name = entry.file_name().to_string_lossy().to_string();
let is_battery = name.starts_with("BAT") 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 { if !is_battery {
continue; continue;
} }
@@ -97,14 +108,23 @@ fn read_batteries() -> Vec<Value> {
}; };
let mut item = sensor_item("Battery", "%", value, None, None, "battery"); let mut item = sensor_item("Battery", "%", value, None, None, "battery");
if let Some(status) = read_string(&dir.join("status")) { 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.push(item);
} }
stats stats
} }
fn sensor_item(label: &str, unit: &str, value: f64, warning: Option<f64>, critical: Option<f64>, r#type: &str) -> Value { fn sensor_item(
label: &str,
unit: &str,
value: f64,
warning: Option<f64>,
critical: Option<f64>,
r#type: &str,
) -> Value {
json!({ json!({
"label": label, "label": label,
"unit": unit, "unit": unit,
@@ -117,7 +137,9 @@ fn sensor_item(label: &str, unit: &str, value: f64, warning: Option<f64>, critic
} }
fn read_string(path: &std::path::Path) -> Option<String> { fn read_string(path: &std::path::Path) -> Option<String> {
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<f64> { fn read_value(path: &std::path::Path) -> Option<f64> {
@@ -154,23 +176,40 @@ impl Plugin for SensorsPlugin {
stats.extend(read_batteries()); stats.extend(read_batteries());
// Apply show/hide filters and aliases. // Apply show/hide filters and aliases.
stats.retain(|stat| { 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, "") self.model.is_display(&label, "")
}); });
for stat in stats.iter_mut() { for stat in stats.iter_mut() {
if let (Some(label), Some(r#type)) = ( if let (Some(label), Some(r#type)) = (
stat.get("label").and_then(Value::as_str).map(|s| s.to_lowercase()), stat.get("label")
stat.get("type").and_then(Value::as_str).map(|s| s.to_lowercase()), .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)) { 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) { } 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. // 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)) Ok(Value::Array(stats))
} }
@@ -196,14 +235,34 @@ impl Plugin for SensorsPlugin {
if warning.is_some() || critical.is_some() { if warning.is_some() || critical.is_some() {
system_thresholds(value, warning, critical) system_thresholds(value, warning, critical)
} else { } 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" { } 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 { } 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, false,
)); ));
for i in items { 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 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 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 unit = i.get("unit").and_then(Value::as_str).unwrap_or("");
let value = i.get("value").and_then(Value::as_f64); let value = i.get("value").and_then(Value::as_f64);
ret.push(self.model.curse_new_line()); ret.push(self.model.curse_new_line());
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{:<width$}", label.chars().take(name_max_width).collect::<String>(), width = name_max_width), format!(
"{:<width$}",
label.chars().take(name_max_width).collect::<String>(),
width = name_max_width
),
glances_core::Decoration::Default, glances_core::Decoration::Default,
false, false,
false, false,
@@ -244,11 +311,12 @@ impl Plugin for SensorsPlugin {
let Some(value) = value else { let Some(value) = value else {
continue; continue;
}; };
let (value, unit) = if ctx.args.fahrenheit && sensor_type != "battery" && sensor_type != "fan_speed" { let (value, unit) =
(glances_core::to_fahrenheit(value), "F") if ctx.args.fahrenheit && sensor_type != "battery" && sensor_type != "fan_speed" {
} else { (glances_core::to_fahrenheit(value), "F")
(value, unit) } else {
}; (value, unit)
};
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{:>14}", format!("{value:.0}{unit}")), format!("{:>14}", format!("{value:.0}{unit}")),
self.model.view_decoration(Some(&item_key), "value"), self.model.view_decoration(Some(&item_key), "value"),
+14 -2
View File
@@ -85,9 +85,21 @@ impl Plugin for SystemPlugin {
return ret; return ret;
} }
let hostname = s.get("hostname").and_then(Value::as_str).unwrap_or(""); 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(""); 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 ret
} }
} }
+34 -7
View File
@@ -63,8 +63,14 @@ impl Plugin for WifiPlugin {
continue; continue;
} }
let ssid = fields[0].trim_end_matches(':').to_string(); let ssid = fields[0].trim_end_matches(':').to_string();
let quality_link = fields[2].trim_end_matches('.').parse::<f64>().unwrap_or(0.0); let quality_link = fields[2]
let quality_level = fields[3].trim_end_matches('.').parse::<f64>().unwrap_or(0.0); .trim_end_matches('.')
.parse::<f64>()
.unwrap_or(0.0);
let quality_level = fields[3]
.trim_end_matches('.')
.parse::<f64>()
.unwrap_or(0.0);
stats.push(json!({ stats.push(json!({
"key": "ssid", "key": "ssid",
"ssid": ssid, "ssid": ssid,
@@ -105,7 +111,8 @@ impl Plugin for WifiPlugin {
} else { } else {
Decoration::Ok 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<glances_core::StatLine> { fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new(); 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; return ret;
} }
let if_name_max_width = ctx.max_width.saturating_sub(5); let if_name_max_width = ctx.max_width.saturating_sub(5);
@@ -126,16 +140,29 @@ impl Plugin for WifiPlugin {
false, false,
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 { let Some(items) = self.model.stats.as_array() else {
return ret; return ret;
}; };
for i in items { for i in items {
let ssid = i.get("ssid").and_then(Value::as_str).unwrap_or(""); 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_new_line());
ret.push(self.model.curse_add_line( ret.push(self.model.curse_add_line(
format!("{:<width$}", ssid.chars().take(if_name_max_width).collect::<String>(), width = if_name_max_width), format!(
"{:<width$}",
ssid.chars().take(if_name_max_width).collect::<String>(),
width = if_name_max_width
),
glances_core::Decoration::Default, glances_core::Decoration::Default,
false, false,
false, false,
+288 -117
View File
@@ -3,7 +3,9 @@
use anyhow::Result; use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use crossterm::execute; 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::plugin::{Align, Decoration, StatLine};
use glances_core::processes::processes; use glances_core::processes::processes;
use glances_core::{Args, RenderContext}; use glances_core::{Args, RenderContext};
@@ -18,17 +20,7 @@ use std::io::Stdout;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
const LEFT_SIDEBAR: [&str; 11] = [ const LEFT_SIDEBAR: [&str; 11] = [
"network", "network", "ports", "wifi", "diskio", "fs", "irq", "folders", "raid", "sensors", "ip", "now",
"ports",
"wifi",
"diskio",
"fs",
"irq",
"folders",
"raid",
"sensors",
"ip",
"now",
]; ];
const TOP_PLUGINS: [&str; 6] = ["cpu", "percpu", "mem", "memswap", "load", "gpu"]; 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 backend = CrosstermBackend::new(stdout);
let mut terminal = ratatui::Terminal::new(backend)?; 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); let result = run_loop(&mut terminal, engine, &mut tui);
disable_raw_mode()?; disable_raw_mode()?;
@@ -76,10 +72,10 @@ fn run_loop(
tui.draw(terminal, engine)?; tui.draw(terminal, engine)?;
count += 1; count += 1;
if let Some(max) = engine.args.stop_after { if let Some(max) = engine.args.stop_after
if count >= max { && count >= max
break; {
} break;
} }
let elapsed = start.elapsed().as_secs_f64(); let elapsed = start.elapsed().as_secs_f64();
@@ -101,7 +97,11 @@ fn run_loop(
} }
impl Tui { impl Tui {
fn draw(&self, terminal: &mut ratatui::Terminal<CrosstermBackend<Stdout>>, engine: &mut Engine) -> Result<()> { fn draw(
&self,
terminal: &mut ratatui::Terminal<CrosstermBackend<Stdout>>,
engine: &mut Engine,
) -> Result<()> {
terminal.draw(|frame| draw_frame(frame, engine, self))?; terminal.draw(|frame| draw_frame(frame, engine, self))?;
Ok(()) Ok(())
} }
@@ -122,14 +122,39 @@ fn draw_frame(frame: &mut Frame, engine: &mut Engine, tui: &Tui) {
// Header. // Header.
let header_height = 1u16; let header_height = 1u16;
let top_height = tui.top_height(engine, args, area.width); 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 footer_height = if tui.popup == Popup::Filter || tui.popup == Popup::Kill {
let main_height = area.height.saturating_sub(header_height + top_height + footer_height); 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 header_area = Rect {
let top_area = Rect { x: 0, y: header_height, width: area.width, height: top_height }; x: 0,
let main_area = Rect { x: 0, y: header_height + top_height, width: area.width, height: main_height }; y: 0,
let footer_area = width: area.width,
Rect { x: 0, y: area.height.saturating_sub(footer_height), width: area.width, height: footer_height }; 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_header(frame, header_area, engine, args);
tui.draw_top(frame, top_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 { impl Tui {
fn top_height(&self, engine: &mut Engine, args: &Args, width: u16) -> u16 { fn top_height(&self, engine: &mut Engine, args: &Args, width: u16) -> u16 {
if args.disable_top { if args.disable_top {
return 1; return 1;
@@ -157,53 +181,67 @@ impl Tui {
} }
} }
// Quicklook. // Quicklook.
if let Some(plugin) = engine.plugins.get_mut("quicklook") { if let Some(plugin) = engine.plugins.get_mut("quicklook")
if plugin.is_enabled() && !args.disable_quicklook { && plugin.is_enabled()
let ql_width = quicklook_width(width); && !args.disable_quicklook
let ctx = render_ctx(args, &engine.runtime, ql_width as usize); {
let lines = split_lines(plugin.render(&ctx)); let ql_width = quicklook_width(width);
height = height.max(lines.len()); 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) (height.min(20) as u16).max(1)
} }
fn draw_header( fn draw_header(&self, frame: &mut Frame, area: Rect, engine: &mut Engine, args: &Args) {
&self,
frame: &mut Frame,
area: Rect,
engine: &mut Engine,
args: &Args,
) {
let x = area.x; let x = area.x;
// System information. // System information.
if let Some(plugin) = engine.plugins.get_mut("system") { if let Some(plugin) = engine.plugins.get_mut("system")
if plugin.is_enabled() { && plugin.is_enabled()
let ctx = render_ctx(args, &engine.runtime, area.width as usize); {
let lines = split_lines(plugin.render(&ctx)); let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let text: String = lines.first().map(|l| l.iter().map(|s| s.msg.clone()).collect()).unwrap_or_default(); let lines = split_lines(plugin.render(&ctx));
let style = lines let text: String = lines
.first() .first()
.and_then(|l| l.first()) .map(|l| l.iter().map(|s| s.msg.clone()).collect())
.map(|s| style_for(s.decoration, args)) .unwrap_or_default();
.unwrap_or_default(); let style = lines
let width = text.chars().count() as u16; .first()
frame.render_widget(Paragraph::new(text).style(style), Rect { x, y: area.y, width: width.min(area.width), height: 1 }); .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. // Uptime on the right.
if let Some(plugin) = engine.plugins.get_mut("uptime") { if let Some(plugin) = engine.plugins.get_mut("uptime")
if plugin.is_enabled() { && plugin.is_enabled()
let ctx = render_ctx(args, &engine.runtime, area.width as usize); {
let lines = split_lines(plugin.render(&ctx)); let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let text: String = lines.first().map(|l| l.iter().map(|s| s.msg.clone()).collect()).unwrap_or_default(); let lines = split_lines(plugin.render(&ctx));
let width = text.chars().count() as u16; let text: String = lines
if width + 2 < area.width { .first()
frame.render_widget( .map(|l| l.iter().map(|s| s.msg.clone()).collect())
Paragraph::new(text), .unwrap_or_default();
Rect { x: area.width.saturating_sub(width), y: area.y, width, height: 1 }, 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; let mut x = area.x;
// Collect the rendered blocks with their natural widths. // Collect the rendered blocks with their natural widths.
struct Block { struct Block {
name: String,
lines: Vec<Vec<StatLine>>, lines: Vec<Vec<StatLine>>,
natural_width: usize, natural_width: usize,
} }
let mut blocks: Vec<Block> = Vec::new(); let mut blocks: Vec<Block> = Vec::new();
if !args.disable_quicklook { if !args.disable_quicklook
if let Some(plugin) = engine.plugins.get_mut("quicklook") { && let Some(plugin) = engine.plugins.get_mut("quicklook")
if plugin.is_enabled() { && plugin.is_enabled()
let width = quicklook_width(area.width) as usize; {
let ctx = render_ctx(args, &engine.runtime, width); let width = quicklook_width(area.width) as usize;
let lines = split_lines(plugin.render(&ctx)); let ctx = render_ctx(args, &engine.runtime, width);
let natural = lines.iter().map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>()).max().unwrap_or(10); let lines = split_lines(plugin.render(&ctx));
blocks.push(Block { name: "quicklook".to_string(), lines, natural_width: natural.max(10) }); let natural = lines
} .iter()
} .map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>())
.max()
.unwrap_or(10);
blocks.push(Block {
lines,
natural_width: natural.max(10),
});
} }
for name in TOP_PLUGINS { for name in TOP_PLUGINS {
if let Some(plugin) = engine.plugins.get_mut(name) { 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 ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx)); let lines = split_lines(plugin.render(&ctx));
let natural = lines.iter().map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>()).max().unwrap_or(10); let natural = lines
blocks.push(Block { name: name.to_string(), lines, natural_width: natural.max(10) }); .iter()
.map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>())
.max()
.unwrap_or(10);
blocks.push(Block {
lines,
natural_width: natural.max(10),
});
} }
} }
if blocks.is_empty() { if blocks.is_empty() {
@@ -247,7 +297,9 @@ impl Tui {
// Allocate the available width proportionally to the natural widths. // Allocate the available width proportionally to the natural widths.
let total_natural: usize = blocks.iter().map(|b| b.natural_width).sum(); let total_natural: usize = blocks.iter().map(|b| b.natural_width).sum();
let spacing = 2usize * blocks.len().saturating_sub(1); 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<u16> = Vec::new(); let mut widths: Vec<u16> = Vec::new();
for block in &blocks { for block in &blocks {
let mut width = (available * block.natural_width / total_natural).max(8); let mut width = (available * block.natural_width / total_natural).max(8);
@@ -267,7 +319,17 @@ impl Tui {
if x + width > area.x + area.width { if x + width > area.x + area.width {
break; 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; x += width + 2;
} }
} }
@@ -304,7 +366,17 @@ impl Tui {
break; break;
} }
let height = height.min(area.y + area.height - y); 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; y += height;
} }
} }
@@ -318,14 +390,24 @@ impl Tui {
let mut y = area.y; let mut y = area.y;
// Process count. // Process count.
if let Some(plugin) = engine.plugins.get_mut("processcount") { if let Some(plugin) = engine.plugins.get_mut("processcount")
if plugin.is_enabled() { && plugin.is_enabled()
let ctx = render_ctx(args, &engine.runtime, right_width as usize); {
let lines = split_lines(plugin.render(&ctx)); let ctx = render_ctx(args, &engine.runtime, right_width as usize);
let height = (lines.len() as u16).min(2); let lines = split_lines(plugin.render(&ctx));
draw_lines(frame, Rect { x: right_x, y, width: right_width, height }, &lines, args); let height = (lines.len() as u16).min(2);
y += height; draw_lines(
} frame,
Rect {
x: right_x,
y,
width: right_width,
height,
},
&lines,
args,
);
y += height;
} }
// Alerts footer (up to 4 lines). // Alerts footer (up to 4 lines).
@@ -334,20 +416,40 @@ impl Tui {
let list_height = area.y + area.height - y; let list_height = area.y + area.height - y;
let process_height = list_height.saturating_sub(alert_height); let process_height = list_height.saturating_sub(alert_height);
let list_name = if args.programs { "programlist" } else { "processlist" }; let list_name = if args.programs {
if let Some(plugin) = engine.plugins.get_mut(list_name) { "programlist"
if plugin.is_enabled() && process_height > 1 { } else {
let ctx = render_ctx(args, &engine.runtime, right_width as usize); "processlist"
let mut lines = split_lines(plugin.render(&ctx)); };
lines.truncate(process_height as usize); if let Some(plugin) = engine.plugins.get_mut(list_name)
draw_lines(frame, Rect { x: right_x, y, width: right_width, height: process_height }, &lines, args); && 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; y += process_height;
if alert_height > 0 { if alert_height > 0 {
draw_lines( draw_lines(
frame, 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, &event_lines,
args, args,
); );
@@ -360,14 +462,20 @@ impl Tui {
if events.is_empty() { if events.is_empty() {
return lines; 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) { for event in events.events.iter().rev().take(4) {
let state = match event.state.as_str() { let state = match event.state.as_str() {
"CRITICAL" => Decoration::CriticalLog, "CRITICAL" => Decoration::CriticalLog,
"WARNING" => Decoration::WarningLog, "WARNING" => Decoration::WarningLog,
_ => Decoration::Default, _ => 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![ lines.push(vec![
StatLine::new(format!("{} ", event.state)).decoration(state), StatLine::new(format!("{} ", event.state)).decoration(state),
StatLine::new(format!("{} (avg {:.1}){}", event.r#type, event.avg, end)), StatLine::new(format!("{} (avg {:.1}){}", event.r#type, event.avg, end)),
@@ -401,7 +509,9 @@ impl Tui {
Line::from("4 Full quicklook"), Line::from("4 Full quicklook"),
Line::from("5 Disable top menu"), Line::from("5 Disable top menu"),
Line::from("6 Mean GPU mode"), 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("</> Cycle sort key"),
Line::from("UP/DOWN Select process"), Line::from("UP/DOWN Select process"),
Line::from("+ / - Increase/decrease nice level"), Line::from("+ / - Increase/decrease nice level"),
@@ -420,7 +530,12 @@ impl Tui {
]; ];
let block = Block::default().borders(Borders::ALL).title("Help"); let block = Block::default().borders(Borders::ALL).title("Help");
frame.render_widget(Clear, area); 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) { fn handle_key(&mut self, engine: &mut Engine, key: KeyEvent) {
@@ -435,7 +550,11 @@ impl Tui {
KeyCode::Enter => { KeyCode::Enter => {
let filter = self.input.clone(); let filter = self.input.clone();
let mut procs = processes().lock().unwrap(); 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; self.popup = Popup::None;
} }
KeyCode::Esc => self.popup = Popup::None, KeyCode::Esc => self.popup = Popup::None,
@@ -485,7 +604,9 @@ impl Tui {
args.meangpu = !args.meangpu; args.meangpu = !args.meangpu;
engine.runtime.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('a') => set_sort(engine, "auto"),
KeyCode::Char('c') => set_sort(engine, "cpu_percent"), KeyCode::Char('c') => set_sort(engine, "cpu_percent"),
KeyCode::Char('m') => set_sort(engine, "memory_percent"), KeyCode::Char('m') => set_sort(engine, "memory_percent"),
@@ -502,7 +623,13 @@ impl Tui {
KeyCode::Char('-') => nice_change(engine, false), KeyCode::Char('-') => nice_change(engine, false),
KeyCode::Char('k') => self.popup = Popup::Kill, KeyCode::Char('k') => self.popup = Popup::Kill,
KeyCode::Char('f') => { 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; self.popup = Popup::Filter;
} }
KeyCode::Char('E') => processes().lock().unwrap().filter.set_filter(None), 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('L') => args.diskio_latency = !args.diskio_latency,
KeyCode::Char('F') => engine.runtime.fs_free_space = !engine.runtime.fs_free_space, KeyCode::Char('F') => engine.runtime.fs_free_space = !engine.runtime.fs_free_space,
KeyCode::Char('S') => engine.runtime.sparkline = !engine.runtime.sparkline, 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') => { KeyCode::Char('j') => {
args.programs = !args.programs; args.programs = !args.programs;
engine.runtime.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); 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> { fn render_ctx<'a>(
RenderContext { args, runtime, max_width } args: &'a Args,
runtime: &'a glances_core::Runtime,
max_width: usize,
) -> RenderContext<'a> {
RenderContext {
args,
runtime,
max_width,
}
} }
fn quicklook_width(width: u16) -> u16 { fn quicklook_width(width: u16) -> u16 {
@@ -587,7 +726,11 @@ fn draw_lines(frame: &mut Frame, area: Rect, lines: &[Vec<StatLine>], args: &Arg
} }
fn style_for(decoration: Decoration, args: &Args) -> Style { 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; let bg = args.disable_bg;
match decoration { match decoration {
Decoration::Default | Decoration::Password => Style::default(), 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) Style::default().add_modifier(bold | Modifier::UNDERLINED)
} }
Decoration::Title => Style::default().add_modifier(bold), 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::Max => Style::default().fg(Color::Green).add_modifier(bold),
Decoration::Careful | Decoration::Info => Style::default().fg(Color::Blue), Decoration::Careful | Decoration::Info => Style::default().fg(Color::Blue),
Decoration::CarefulLog => { Decoration::CarefulLog => {
@@ -630,7 +775,9 @@ fn style_for(decoration: Decoration, args: &Args) -> Style {
} }
} }
Decoration::CpuTime => Style::default().fg(Color::Magenta), 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::Error => Style::default().fg(Color::Red),
Decoration::Separator => Style::default().fg(Color::DarkGray), 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) { 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 current = processes().lock().unwrap().sort_key.clone();
let index = KEYS.iter().position(|k| *k == current).unwrap_or(0); let index = KEYS.iter().position(|k| *k == current).unwrap_or(0);
let next = if forward { let next = if forward {
@@ -680,7 +835,11 @@ fn nice_change(engine: &mut Engine, increase: bool) {
} }
fn toggle_processes(engine: &mut Engine) { 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"] { for name in ["processcount", "processlist", "programlist"] {
if let Some(plugin) = engine.plugins.get_mut(name) { if let Some(plugin) = engine.plugins.get_mut(name) {
plugin.set_enabled(!enabled); plugin.set_enabled(!enabled);
@@ -752,7 +911,13 @@ mod tests {
fn test_tui_layout_renders() { fn test_tui_layout_renders() {
use clap::Parser; use clap::Parser;
use glances_core::Config; 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); args.time = Some(1.0);
let config = Config::empty(false); let config = Config::empty(false);
let mut engine = Engine::new(args.clone(), config); let mut engine = Engine::new(args.clone(), config);
@@ -760,8 +925,14 @@ mod tests {
engine.update(); engine.update();
let backend = TestBackend::new(140, 40); let backend = TestBackend::new(140, 40);
let mut terminal = ratatui::Terminal::new(backend).unwrap(); let mut terminal = ratatui::Terminal::new(backend).unwrap();
let tui = Tui { popup: Popup::None, input: String::new(), quit: false }; let tui = Tui {
terminal.draw(|frame| draw_frame(frame, &mut engine, &tui)).unwrap(); 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()); let text = buffer_to_string(terminal.backend().buffer());
println!("{text}"); println!("{text}");
assert!(text.contains("CPU")); assert!(text.contains("CPU"));