M1: core engine, config, plugin model and core plugins

- Cargo workspace (core, plugins, tui, cli) with drop-in compatible CLI
- Python-compatible config file handling, limits, views, rates, MMM, history
- process collection via sysinfo + procfs, filters, thresholds, events, actions
- 20 core plugins (cpu, percpu, mem, memswap, load, fs, network, diskio, ...)
- stdout, stdout-csv, stdout-json, --issue, --modules-list output modes
- differential test harness against the Python implementation
This commit is contained in:
glances-rs
2026-10-09 23:25:35 +00:00
commit 975385da5d
61 changed files with 12049 additions and 0 deletions
@@ -0,0 +1,183 @@
//! Quicklook plugin (condensed stats displayed in the top-left corner).
use crate::cpu_percent::cpu_percent;
use crate::fields;
use glances_core::plugin::{Decoration, Field, HistoryItem, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
pub struct QuicklookPlugin {
pub model: PluginModel,
pub stats_list: Vec<String>,
pub max_cpu_display: usize,
}
const AVAILABLE_STATS_LIST: [&str; 6] = ["cpu", "mem", "swap", "load", "gpu_mem", "gpu_proc"];
const DEFAULT_STATS_LIST: [&str; 3] = ["cpu", "mem", "load"];
impl QuicklookPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"cpu" => Field::new("CPU percent usage").unit("percent"),
"percpu" => Field::new("PERCPU percent usage").unit("percent"),
"mem" => Field::new("MEM percent usage").unit("percent"),
"swap" => Field::new("SWAP percent usage").unit("percent"),
"load" => Field::new("LOAD percent usage").unit("percent").alert(),
"cpu_log_core" => Field::new("Number of logical CPU core").unit("number"),
"cpu_phys_core" => Field::new("Number of physical CPU core").unit("number"),
"cpu_name" => Field::new("CPU name"),
"cpu_hz_current" => Field::new("CPU current frequency").unit("hertz"),
"cpu_hz" => Field::new("CPU max frequency").unit("hertz"),
"gpu_mem" => Field::new("Average GPU Memory consumption").unit("percent"),
"gpu_proc" => Field::new("Average GPU Processor consumption").unit("percent"),
};
let items_history = vec![
HistoryItem::new("cpu", "CPU percent usage"),
HistoryItem::new("percpu", "PERCPU percent usage"),
HistoryItem::new("mem", "MEM percent usage"),
HistoryItem::new("swap", "SWAP percent usage"),
HistoryItem::new("load", "LOAD percent usage"),
HistoryItem::new("gpu_mem", "GPU memory percent usage"),
HistoryItem::new("gpu_proc", "GPU processor percent usage"),
];
let model = PluginModel::new(
"quicklook",
fields,
items_history,
json!({}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
let stats_list = model.get_conf_value("list", "");
let mut stats_list: Vec<String> =
if stats_list.is_empty() { DEFAULT_STATS_LIST.iter().map(|s| s.to_string()).collect() } else { stats_list };
if stats_list.iter().any(|s| !AVAILABLE_STATS_LIST.contains(&s.as_str())) {
stats_list = DEFAULT_STATS_LIST.iter().map(|s| s.to_string()).collect();
}
let max_cpu_display = config.get_int_value("percpu", "max_cpu_display", 4).max(0) as usize;
Self { model, stats_list, max_cpu_display }
}
fn mhz_to_hz(value: Option<f64>) -> Option<f64> {
value.map(|v| v * 1_000_000.0)
}
}
impl Plugin for QuicklookPlugin {
fn name(&self) -> &str {
"quicklook"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let mut stats = serde_json::Map::new();
let (info, cpu_value, percpu_values) = {
let mut state = cpu_percent().lock().unwrap();
let info = state.get_info();
let cpu = state.get_cpu();
let percpu = state.get_percpu().to_vec();
(info, cpu, percpu)
};
stats.insert("cpu_name".to_string(), json!(info.cpu_name));
stats.insert("cpu_hz_current".to_string(), json!(Self::mhz_to_hz(info.cpu_hz_current)));
stats.insert("cpu_hz".to_string(), json!(Self::mhz_to_hz(info.cpu_hz)));
stats.insert("cpu".to_string(), json!(cpu_value));
stats.insert("percpu".to_string(), Value::Array(percpu_values));
let meminfo = crate::collectors::read_meminfo();
let total = meminfo.get("MemTotal").copied().unwrap_or(0) as f64;
let available = meminfo.get("MemAvailable").copied().unwrap_or(0) as f64;
stats.insert(
"mem".to_string(),
json!(if total > 0.0 { ((total - available) / total * 100.0 * 10.0).round() / 10.0 } else { 0.0 }),
);
let swap_total = meminfo.get("SwapTotal").copied().unwrap_or(0);
let swap_free = meminfo.get("SwapFree").copied().unwrap_or(0);
stats.insert(
"swap".to_string(),
json!(if swap_total > 0 {
(((swap_total - swap_free) as f64 * 100.0 / swap_total as f64) * 10.0).round() / 10.0
} else {
0.0
}),
);
let log_core = crate::collectors::logical_core_count();
stats.insert("cpu_log_core".to_string(), json!(log_core));
stats.insert("cpu_phys_core".to_string(), json!(crate::collectors::physical_core_count()));
let load = crate::collectors::read_loadavg().map(|(_, _, min15)| {
if log_core > 0 { min15 / log_core as f64 * 100.0 } else { 0.0 }
});
stats.insert("load".to_string(), json!(load));
stats.insert("gpu_mem".to_string(), Value::Null);
stats.insert("gpu_proc".to_string(), Value::Null);
Ok(Value::Object(stats))
}
fn update_views(&mut self) {
let key = self.get_key();
self.model.update_views(key);
let stats_list = self.stats_list.clone();
for stat_key in &stats_list {
if let Some(value) = self.model.stats.get(stat_key).and_then(Value::as_f64) {
let decoration =
self.model.get_alert(value, 0.0, 100.0, true, false, Some(stat_key), None, false);
self.model.set_view_decoration(None, stat_key, decoration);
}
}
// Store the list of stats to display and the per-CPU decorations.
self.model
.views
.insert("list".to_string(), json!(stats_list));
let mut percpu_decoration = serde_json::Map::new();
if let Some(percpu) = self.model.stats.get("percpu").and_then(Value::as_array) {
for cpu in percpu {
let number = cpu.get("cpu_number").map(glances_core::py_str).unwrap_or_default();
let total = cpu.get("total").and_then(Value::as_f64).unwrap_or(0.0);
percpu_decoration.insert(number, json!(decoration_for_percent(&self.model, total).name()));
}
if percpu.len() > self.max_cpu_display && self.max_cpu_display > 0 {
let mut hidden: Vec<f64> =
percpu.iter().filter_map(|cpu| cpu.get("total").and_then(Value::as_f64)).collect();
hidden.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let hidden = &hidden[self.max_cpu_display.min(hidden.len())..];
if !hidden.is_empty() {
let mean = hidden.iter().sum::<f64>() / hidden.len() as f64;
percpu_decoration.insert("other".to_string(), json!(decoration_for_percent(&self.model, mean).name()));
}
}
}
self.model.views.insert("percpu_decoration".to_string(), Value::Object(percpu_decoration));
}
fn display_curse(&self) -> bool {
true
}
}
/// Return the style for a CPU percentage without the get_alert side effects.
fn decoration_for_percent(model: &PluginModel, percent: f64) -> Decoration {
let stat_name = model.get_stat_name(Some("cpu"), None).to_lowercase();
let critical = model.get_limit("critical", &stat_name).unwrap_or(0.0);
let warning = model.get_limit("warning", &stat_name).unwrap_or(0.0);
let careful = model.get_limit("careful", &stat_name).unwrap_or(0.0);
if critical > 0.0 && percent >= critical {
Decoration::Critical
} else if warning > 0.0 && percent >= warning {
Decoration::Warning
} else if careful > 0.0 && percent >= careful {
Decoration::Careful
} else {
Decoration::Default
}
}