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
+1
View File
@@ -15,6 +15,7 @@ ctrlc = "3.5.2"
glances-core = { version = "0.1.0", path = "../glances-core" }
glances-plugins = { version = "0.1.0", path = "../glances-plugins" }
glances-tui = { version = "0.1.0", path = "../glances-tui" }
indexmap.workspace = true
serde_json.workspace = true
tracing.workspace = true
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
+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()
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join(".local").join("share")));
if let Some(base) = base {
if base.is_dir() {
let dir = base.join("glances");
let _ = std::fs::create_dir_all(&dir);
return dir.join("glances.log");
}
.or_else(|| {
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".local").join("share"))
});
if let Some(base) = base
&& base.is_dir()
{
let dir = base.join("glances");
let _ = std::fs::create_dir_all(&dir);
return dir.join("glances.log");
}
std::env::temp_dir().join(format!("glances-{}.log", std::env::var("USER").unwrap_or_else(|_| "user".into())))
std::env::temp_dir().join(format!(
"glances-{}.log",
std::env::var("USER").unwrap_or_else(|_| "user".into())
))
}
/// Initialise the logger: everything goes to the log file, warnings and
+44 -18
View File
@@ -1,13 +1,13 @@
//! The `glances` binary.
mod fetch;
mod logging;
mod outputs;
mod tui_placeholder;
use anyhow::Result;
use clap::{CommandFactory, Parser};
use glances_core::config::Config;
use glances_core::Args;
use glances_core::config::Config;
use glances_plugins::engine::Engine;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -21,7 +21,10 @@ fn main() {
let args = Args::parse();
if args.version {
println!("{}", args.version_message(&logging::log_filename().display().to_string()));
println!(
"{}",
args.version_message(&logging::log_filename().display().to_string())
);
return;
}
if let Some(shell) = &args.print_completion {
@@ -85,7 +88,7 @@ fn run(mut args: Args) -> Result<()> {
}
if args.stdout_fetch {
return tui_placeholder::run_fetch(&mut engine, &args);
return fetch::run_fetch(&mut engine, &args);
}
if args.quiet {
@@ -105,11 +108,28 @@ fn display_modules_list(engine: &Engine) {
fn print_completion(shell: &str) {
let mut command = Args::command();
match shell {
"bash" => clap_complete::generate(clap_complete::Shell::Bash, &mut command, "glances", &mut std::io::stdout()),
"zsh" => clap_complete::generate(clap_complete::Shell::Zsh, &mut command, "glances", &mut std::io::stdout()),
"fish" => clap_complete::generate(clap_complete::Shell::Fish, &mut command, "glances", &mut std::io::stdout()),
"bash" => clap_complete::generate(
clap_complete::Shell::Bash,
&mut command,
"glances",
&mut std::io::stdout(),
),
"zsh" => clap_complete::generate(
clap_complete::Shell::Zsh,
&mut command,
"glances",
&mut std::io::stdout(),
),
"fish" => clap_complete::generate(
clap_complete::Shell::Fish,
&mut command,
"glances",
&mut std::io::stdout(),
),
other => {
eprintln!("glances: error: unsupported shell for completion: {other} (use bash, zsh or fish)");
eprintln!(
"glances: error: unsupported shell for completion: {other} (use bash, zsh or fish)"
);
std::process::exit(2);
}
}
@@ -134,13 +154,19 @@ fn run_stdout(engine: &mut Engine, args: &Args) -> Result<()> {
let mut csv = outputs::CsvRenderer::new();
let stdout_items = args.stdout.as_deref().map(outputs::build_stdout_list);
let json_plugins = args.stdout_json.as_deref().map(|value| {
value.split(',').map(|p| p.trim().to_string()).filter(|p| !p.is_empty()).collect::<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| {
value
.split(',')
.map(|p| match p.split_once('.') {
Some((plugin, attribute)) => (plugin.trim().to_string(), Some(attribute.to_string())),
Some((plugin, attribute)) => {
(plugin.trim().to_string(), Some(attribute.to_string()))
}
None => (p.trim().to_string(), None),
})
.filter(|(p, _)| !p.is_empty())
@@ -164,10 +190,10 @@ fn run_stdout(engine: &mut Engine, args: &Args) -> Result<()> {
std::io::stdout().flush().ok();
count += 1;
if let Some(max) = iterations {
if count >= max {
break;
}
if let Some(max) = iterations
&& count >= max
{
break;
}
let elapsed = start.elapsed().as_secs_f64();
sleep_with_signal(refresh - elapsed);
@@ -182,10 +208,10 @@ fn run_quiet(engine: &mut Engine, args: &Args) -> Result<()> {
let start = Instant::now();
engine.update();
count += 1;
if let Some(max) = args.stop_after {
if count >= max {
break;
}
if let Some(max) = args.stop_after
&& count >= max
{
break;
}
let elapsed = start.elapsed().as_secs_f64();
sleep_with_signal(refresh - elapsed);
+56 -15
View File
@@ -17,7 +17,11 @@ pub fn build_stdout_list(value: &str) -> Vec<StdoutItem> {
.map(|item| {
let parts: Vec<&str> = item.split('.').collect();
match parts.len() {
1 => StdoutItem { plugin: parts[0].to_string(), key: None, attribute: None },
1 => StdoutItem {
plugin: parts[0].to_string(),
key: None,
attribute: None,
},
2 => StdoutItem {
plugin: parts[0].to_string(),
key: None,
@@ -48,9 +52,18 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String {
Some(attribute) => match &stat {
Value::Object(map) => {
if let Some(value) = map.get(attribute) {
out.push_str(&format!("{}.{}: {}\n", item.plugin, attribute, glances_core::py_str(value)));
out.push_str(&format!(
"{}.{}: {}\n",
item.plugin,
attribute,
glances_core::py_str(value)
));
} else {
tracing::error!("Can not display stat {}.{} (KeyError)", item.plugin, attribute);
tracing::error!(
"Can not display stat {}.{} (KeyError)",
item.plugin,
attribute
);
}
}
Value::Array(list) => {
@@ -66,10 +79,10 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String {
let Some(entry_key) = entry_key else {
continue;
};
if let Some(wanted) = &item.key {
if wanted != &entry_key {
continue;
}
if let Some(wanted) = &item.key
&& wanted != &entry_key
{
continue;
}
if let Some(value) = entry.get(attribute) {
out.push_str(&format!(
@@ -85,7 +98,11 @@ pub fn render_stdout(engine: &Engine, items: &[StdoutItem]) -> String {
_ => {}
},
None => {
out.push_str(&format!("{}: {}\n", item.plugin, glances_core::py_repr(&stat)));
out.push_str(&format!(
"{}: {}\n",
item.plugin,
glances_core::py_repr(&stat)
));
}
}
}
@@ -116,7 +133,10 @@ pub struct CsvRenderer {
impl CsvRenderer {
pub fn new() -> Self {
Self { header: true, ..Default::default() }
Self {
header: true,
..Default::default()
}
}
pub fn render(&mut self, engine: &Engine, items: &[(String, Option<String>)]) -> String {
@@ -172,7 +192,8 @@ impl CsvRenderer {
}
self.list_keys.insert(plugin.to_string(), keys_order);
if let Some(first) = list.iter().find_map(|entry| entry.as_object()) {
self.header_field_names.insert(plugin.to_string(), first.keys().cloned().collect());
self.header_field_names
.insert(plugin.to_string(), first.keys().cloned().collect());
}
}
_ => {
@@ -186,7 +207,10 @@ impl CsvRenderer {
let mut line = String::new();
if let Some(attribute) = attribute {
let text = match stat {
Value::Object(map) => map.get(attribute).map(glances_core::py_str).unwrap_or_else(|| "N/A".to_string()),
Value::Object(map) => map
.get(attribute)
.map(glances_core::py_str)
.unwrap_or_else(|| "N/A".to_string()),
_ => "N/A".to_string(),
};
line.push_str(&format!("{text},"));
@@ -213,9 +237,19 @@ impl CsvRenderer {
current.insert(glances_core::py_str(identity), entry.clone());
}
let empty = Map::new();
for identity in self.list_keys.get(plugin).map(|k| k.as_slice()).unwrap_or(&[]) {
for identity in self
.list_keys
.get(plugin)
.map(|k| k.as_slice())
.unwrap_or(&[])
{
let entry = current.get(identity).unwrap_or(&empty);
for field in self.header_field_names.get(plugin).map(|f| f.as_slice()).unwrap_or(&[]) {
for field in self
.header_field_names
.get(plugin)
.map(|f| f.as_slice())
.unwrap_or(&[])
{
let value = entry.get(field.as_str());
let text = match value {
Some(value) => glances_core::py_str(value),
@@ -241,8 +275,15 @@ pub fn render_issue(engine: &Engine) -> String {
let Some(plugin) = engine.get_plugin(&name) else {
continue;
};
let enabled = if plugin.is_enabled() { "enabled" } else { "disabled" };
out.push_str(&format!("{name} ({enabled}): {}\n", glances_core::py_repr(plugin.stats())));
let enabled = if plugin.is_enabled() {
"enabled"
} else {
"disabled"
};
out.push_str(&format!(
"{name} ({enabled}): {}\n",
glances_core::py_repr(plugin.stats())
));
}
out
}
-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")
}