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