//! 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, 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 = 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) -> Option { 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 { 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 = 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::() / 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 { 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!( "{:> = 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 = 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::() / 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 } }