Files
glances-rs/crates/glances-cli/src/fetch.rs
T
glances-rs 6819ce4e18 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
2026-10-09 23:59:26 +00:00

970 lines
35 KiB
Rust

//! 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)
}