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:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user