- --fetch with a Jinja-subset renderer and the shipped templates - clippy clean, cargo fmt, all tests green - README status and differential testing instructions
970 lines
35 KiB
Rust
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)
|
|
}
|