- --fetch with a Jinja-subset renderer and the shipped templates - clippy clean, cargo fmt, all tests green - README status and differential testing instructions
394 lines
15 KiB
Rust
394 lines
15 KiB
Rust
//! 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
|
|
}
|
|
|
|
fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
|
|
let mut ret = Vec::new();
|
|
let s = &self.model.stats;
|
|
if !s.is_object() || ctx.max_width == 0 {
|
|
return ret;
|
|
}
|
|
// System information: CPU name + frequency.
|
|
let freq = match (
|
|
s.get("cpu_hz_current").and_then(Value::as_f64),
|
|
s.get("cpu_hz").and_then(Value::as_f64),
|
|
) {
|
|
(Some(current), Some(max)) => format!(" {:.2}/{:.2}GHz", current / 1e9, max / 1e9),
|
|
(Some(current), None) => format!(" {:.2}GHz", current / 1e9),
|
|
_ => String::new(),
|
|
};
|
|
let cpu_name = s.get("cpu_name").and_then(Value::as_str).unwrap_or("");
|
|
let name = if (ctx.max_width as i64 - freq.len() as i64 + 7) > 0 {
|
|
format!(
|
|
"{:<width$}",
|
|
cpu_name,
|
|
width = ctx.max_width - freq.len() + 7
|
|
)
|
|
} else if freq.is_empty() {
|
|
String::new()
|
|
} else {
|
|
"Frequency".to_string()
|
|
};
|
|
ret.push(self.model.curse_add_line(
|
|
name.clone(),
|
|
glances_core::Decoration::Default,
|
|
false,
|
|
false,
|
|
false,
|
|
));
|
|
ret.push(self.model.curse_add_line(
|
|
freq.clone(),
|
|
glances_core::Decoration::Default,
|
|
false,
|
|
false,
|
|
false,
|
|
));
|
|
ret.push(self.model.curse_new_line());
|
|
|
|
let bar_size = std::cmp::max(
|
|
name.len().saturating_add(freq.len()).saturating_sub(7),
|
|
ctx.max_width.saturating_sub(9),
|
|
)
|
|
.max(10);
|
|
let bar_char = self
|
|
.model
|
|
.get_conf_value("bar_char", "")
|
|
.first()
|
|
.cloned()
|
|
.unwrap_or_else(|| "|".to_string());
|
|
|
|
let mut lines: Vec<Vec<glances_core::StatLine>> = Vec::new();
|
|
let mut push_line = |msg: String,
|
|
bar_key: &str,
|
|
percent: f64,
|
|
decoration: glances_core::Decoration| {
|
|
let bar = glances_core::units::bar_percent(percent, bar_size, &bar_char, true);
|
|
lines.push(vec![
|
|
self.model.curse_add_line(
|
|
msg,
|
|
glances_core::Decoration::Default,
|
|
false,
|
|
false,
|
|
false,
|
|
),
|
|
self.model
|
|
.curse_add_line("[", glances_core::Decoration::Bold, false, false, false),
|
|
self.model
|
|
.curse_add_line(bar, decoration, false, false, false),
|
|
self.model
|
|
.curse_add_line("]", glances_core::Decoration::Bold, false, false, false),
|
|
self.model.curse_add_line(
|
|
" ",
|
|
glances_core::Decoration::Default,
|
|
false,
|
|
false,
|
|
false,
|
|
),
|
|
]);
|
|
let _ = bar_key;
|
|
};
|
|
|
|
let stats_list = self.stats_list.clone();
|
|
for key in &stats_list {
|
|
if key == "cpu" && ctx.runtime.percpu {
|
|
if let Some(percpu) = s.get("percpu").and_then(Value::as_array) {
|
|
let mut sorted = percpu.clone();
|
|
if sorted.len() > self.max_cpu_display {
|
|
sorted.sort_by(|a, b| {
|
|
b.get("total")
|
|
.and_then(Value::as_f64)
|
|
.unwrap_or(0.0)
|
|
.partial_cmp(&a.get("total").and_then(Value::as_f64).unwrap_or(0.0))
|
|
.unwrap_or(std::cmp::Ordering::Equal)
|
|
});
|
|
}
|
|
for cpu in sorted.iter().take(self.max_cpu_display) {
|
|
let cpu_id = cpu.get("cpu_number").and_then(Value::as_i64).unwrap_or(0);
|
|
let total = cpu.get("total").and_then(Value::as_f64).unwrap_or(0.0);
|
|
let msg = if cpu_id < 10 {
|
|
format!("{:<3}{cpu_id} ", key.to_uppercase())
|
|
} else {
|
|
format!("{cpu_id:4} ")
|
|
};
|
|
let decoration = self.decoration_for_percpu(cpu_id);
|
|
push_line(msg, key, total, decoration);
|
|
}
|
|
if percpu.len() > self.max_cpu_display {
|
|
let hidden: Vec<f64> = sorted
|
|
.iter()
|
|
.skip(self.max_cpu_display)
|
|
.filter_map(|cpu| cpu.get("total").and_then(Value::as_f64))
|
|
.collect();
|
|
let mean = if hidden.is_empty() {
|
|
0.0
|
|
} else {
|
|
hidden.iter().sum::<f64>() / hidden.len() as f64
|
|
};
|
|
let decoration = self.decoration_for_percpu(-1);
|
|
push_line(
|
|
format!("{:<3}* ", key.to_uppercase()),
|
|
key,
|
|
mean,
|
|
decoration,
|
|
);
|
|
}
|
|
}
|
|
} else if let Some(value) = s.get(key).and_then(Value::as_f64) {
|
|
let decoration = self.model.view_decoration(None, key);
|
|
push_line(
|
|
format!("{:<4} ", key.to_uppercase()),
|
|
key,
|
|
value,
|
|
decoration,
|
|
);
|
|
}
|
|
}
|
|
for line in lines {
|
|
ret.extend(line);
|
|
ret.push(self.model.curse_new_line());
|
|
}
|
|
ret.pop();
|
|
ret
|
|
}
|
|
}
|
|
|
|
impl QuicklookPlugin {
|
|
fn decoration_for_percpu(&self, cpu_id: i64) -> glances_core::Decoration {
|
|
let key = if cpu_id < 0 {
|
|
"other".to_string()
|
|
} else {
|
|
cpu_id.to_string()
|
|
};
|
|
match self.model.get_view(None, "percpu_decoration", &key) {
|
|
Value::String(name) => glances_core::Decoration::from_name(&name),
|
|
_ => glances_core::Decoration::Default,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
}
|