M2: terminal UI (ratatui)
- layout: header, top menu (quicklook, cpu, mem, swap, load), left sidebar, process list and alert footer - decorations/colors, bars, per-plugin render implementations - hotkeys: quit, help, modes, sorting, cursor, nice, kill, filters, toggles - TestBackend layout test
This commit is contained in:
@@ -9,7 +9,7 @@ use crate::history::History;
|
||||
use crate::state::Runtime;
|
||||
use crate::thresholds::thresholds;
|
||||
use crate::timer::Timer;
|
||||
use crate::units::as_f64;
|
||||
use crate::units::{as_f64, auto_unit};
|
||||
use indexmap::IndexMap;
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::collections::VecDeque;
|
||||
@@ -399,6 +399,18 @@ impl PluginModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the list-valued limit (used for nice/status alerts).
|
||||
pub fn get_limit_list(&self, criticality: &str, stat_name: &str) -> Option<Vec<String>> {
|
||||
let stat_name = stat_name.to_lowercase();
|
||||
if let Some(Limit::List(items)) = self.limits.get(&format!("{stat_name}_{criticality}")) {
|
||||
return Some(items.clone());
|
||||
}
|
||||
if let Some(Limit::List(items)) = self.limits.get(&format!("{}_{criticality}", self.name)) {
|
||||
return Some(items.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Return the limit value for the given criticality.
|
||||
pub fn get_limit(&self, criticality: &str, stat_name: &str) -> Option<f64> {
|
||||
let stat_name = stat_name.to_lowercase();
|
||||
@@ -958,6 +970,113 @@ impl PluginModel {
|
||||
self.stats.clone()
|
||||
}
|
||||
|
||||
/// Create a TUI line (port of `curse_add_line`).
|
||||
pub fn curse_add_line(
|
||||
&self,
|
||||
msg: impl Into<String>,
|
||||
decoration: Decoration,
|
||||
optional: bool,
|
||||
additional: bool,
|
||||
splittable: bool,
|
||||
) -> StatLine {
|
||||
StatLine {
|
||||
msg: msg.into(),
|
||||
decoration,
|
||||
optional,
|
||||
additional,
|
||||
splittable,
|
||||
}
|
||||
}
|
||||
|
||||
/// New line marker (port of `curse_new_line`).
|
||||
pub fn curse_new_line(&self) -> StatLine {
|
||||
StatLine::new("\n")
|
||||
}
|
||||
|
||||
/// Add a `key: value` pair to the TUI (port of `curse_add_stat`).
|
||||
pub fn curse_add_stat(&self, key: &str, width: Option<usize>, header: &str, separator: &str, trailer: &str) -> Vec<StatLine> {
|
||||
let Some(value) = self.stats.get(key) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let definition = self.fields.get(key);
|
||||
|
||||
let key_name = definition
|
||||
.and_then(|f| f.short_name.clone())
|
||||
.unwrap_or_else(|| key.to_string());
|
||||
let unit_short = definition.map(|f| f.short_unit()).unwrap_or("");
|
||||
let unit_type = definition.map(|f| f.unit_type()).unwrap_or("float");
|
||||
let value = if definition.map(|f| f.rate).unwrap_or(false) {
|
||||
self.stats.get(&format!("{key}_rate_per_sec")).unwrap_or(&Value::Null)
|
||||
} else {
|
||||
value
|
||||
};
|
||||
let value = as_f64(value);
|
||||
|
||||
let (msg_item, msg_value) = match width {
|
||||
None => {
|
||||
let item = format!("{header}{key_name}{separator}");
|
||||
let value_text = match value {
|
||||
None => "-".to_string(),
|
||||
Some(v) => {
|
||||
if unit_type == "float" {
|
||||
format!("{v:.1}{unit_short}")
|
||||
} else if let Some(min_symbol) = definition.and_then(|f| f.min_symbol.clone()) {
|
||||
format!("{}{unit_short}", auto_unit(v, false, &min_symbol, "-"))
|
||||
} else {
|
||||
format!("{}{unit_short}", v as i64)
|
||||
}
|
||||
}
|
||||
};
|
||||
(item, value_text)
|
||||
}
|
||||
Some(width) => {
|
||||
let item = format!("{header}{key_name:<width$}{separator}", width = width.saturating_sub(7));
|
||||
let value_text = match value {
|
||||
None => format!("{:>5}", "-"),
|
||||
Some(v) => {
|
||||
if unit_type == "float" {
|
||||
format!("{v:>5.1}{unit_short}")
|
||||
} else if let Some(min_symbol) = definition.and_then(|f| f.min_symbol.clone()) {
|
||||
format!("{:>5}{unit_short}", auto_unit(v, false, &min_symbol, "-"))
|
||||
} else {
|
||||
format!("{:>5}{unit_short}", v as i64)
|
||||
}
|
||||
}
|
||||
};
|
||||
(item, value_text)
|
||||
}
|
||||
};
|
||||
let item = format!("{msg_item}{trailer}");
|
||||
|
||||
let decoration = if value.is_some() { self.view_decoration(None, key) } else { Decoration::Default };
|
||||
let optional = self.view_bool(None, key, "optional");
|
||||
|
||||
vec![
|
||||
self.curse_add_line(item, Decoration::Default, optional, false, false),
|
||||
self.curse_add_line(msg_value, decoration, optional, false, false),
|
||||
]
|
||||
}
|
||||
|
||||
/// Return the fields to display for a plugin (port of the display flags).
|
||||
pub fn field_optional(&self, field: &str) -> bool {
|
||||
self.fields.get(field).map(|f| f.optional).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Return the trend message (port of `trend_msg`).
|
||||
pub fn trend_msg(&self, trend: Option<f64>, significant: f64) -> String {
|
||||
match trend {
|
||||
None => " ".to_string(),
|
||||
Some(trend) if trend > significant => "↑".to_string(),
|
||||
Some(trend) if trend < significant => "↓".to_string(),
|
||||
_ => "-".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Human readable auto unit (port of the plugin `auto_unit` method).
|
||||
pub fn auto_unit(&self, number: f64, low_precision: bool, min_symbol: &str) -> String {
|
||||
auto_unit(number, low_precision, min_symbol, "-")
|
||||
}
|
||||
|
||||
/// Update the history with the current stats.
|
||||
pub fn update_stats_history(&mut self) {
|
||||
if !self.history_enabled {
|
||||
@@ -1137,7 +1256,7 @@ pub trait Plugin: Send + Sync {
|
||||
fn exit(&mut self) {}
|
||||
|
||||
/// Render the plugin block in the TUI.
|
||||
fn render(&self, ctx: &RenderContext) -> Vec<StatLine> {
|
||||
fn render(&mut self, ctx: &RenderContext) -> Vec<StatLine> {
|
||||
let _ = ctx;
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
/// Runtime state shared between the engine, the plugins and the TUI.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Runtime {
|
||||
/// Effective set of enabled plugins (kept up to date by the engine/TUI).
|
||||
pub enabled_plugins: std::collections::HashSet<String>,
|
||||
/// Irix mode (task's CPU usage divided by the number of CPUs).
|
||||
pub disable_irix: bool,
|
||||
/// Display Rx and Tx sum instead of per interface values.
|
||||
pub network_sum: bool,
|
||||
/// Display cumulative network values instead of rates.
|
||||
@@ -38,6 +42,8 @@ pub struct Runtime {
|
||||
impl Runtime {
|
||||
pub fn new(args: &crate::args::Args, fs_free_space_config: bool) -> Self {
|
||||
Self {
|
||||
enabled_plugins: std::collections::HashSet::new(),
|
||||
disable_irix: args.disable_irix,
|
||||
network_sum: false,
|
||||
network_cumul: false,
|
||||
percpu: args.percpu,
|
||||
@@ -55,4 +61,9 @@ impl Runtime {
|
||||
reset_minmax_tag: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return True if the given plugin is enabled.
|
||||
pub fn is_plugin_enabled(&self, name: &str) -> bool {
|
||||
self.enabled_plugins.contains(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,35 @@ pub fn auto_unit(number: f64, low_precision: bool, min_symbol: &str, none_symbol
|
||||
format!("{number:.decimal_precision$}")
|
||||
}
|
||||
|
||||
/// Render a progress bar the way the Python `Bar` class does.
|
||||
pub fn bar_percent(percent: f64, size: usize, bar_char: &str, display_value: bool) -> String {
|
||||
let size = if display_value { size.saturating_sub(6) } else { size };
|
||||
let value = percent.clamp(0.0, 100.0);
|
||||
let scaled = size as f64 * value / 100.0;
|
||||
let whole = scaled.floor() as usize;
|
||||
let frac = scaled - whole as f64;
|
||||
let mut bar = bar_char.repeat(whole);
|
||||
let mut whole = whole;
|
||||
if frac > 0.0 {
|
||||
// curses_bars = [empty; 5] + [bar; 5]: index 5..7 draws a partial block.
|
||||
if (frac * 8.0) as usize >= 5 {
|
||||
bar.push_str(bar_char);
|
||||
} else {
|
||||
bar.push(' ');
|
||||
}
|
||||
whole += 1;
|
||||
}
|
||||
bar.push_str(&" ".repeat(size.saturating_sub(whole)));
|
||||
if display_value {
|
||||
if percent >= 100.0 {
|
||||
bar = format!("{bar} {}{:3.0}%", if percent > 100.0 { ">" } else { " " }, 100.0);
|
||||
} else {
|
||||
bar = format!("{bar}{percent:5.1}%");
|
||||
}
|
||||
}
|
||||
bar
|
||||
}
|
||||
|
||||
/// Parse a value like `'12.5 MB'` into a float (port of `string_value_to_float`).
|
||||
pub fn string_value_to_float(s: &str) -> Option<f64> {
|
||||
let cleaned: String = s.chars().filter(|c| *c != ' ').collect();
|
||||
|
||||
Reference in New Issue
Block a user