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:
glances-rs
2026-10-09 23:59:26 +00:00
parent cb22e53aa6
commit 6819ce4e18
47 changed files with 4145 additions and 935 deletions
+288 -117
View File
@@ -3,7 +3,9 @@
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use glances_core::plugin::{Align, Decoration, StatLine};
use glances_core::processes::processes;
use glances_core::{Args, RenderContext};
@@ -18,17 +20,7 @@ use std::io::Stdout;
use std::time::{Duration, Instant};
const LEFT_SIDEBAR: [&str; 11] = [
"network",
"ports",
"wifi",
"diskio",
"fs",
"irq",
"folders",
"raid",
"sensors",
"ip",
"now",
"network", "ports", "wifi", "diskio", "fs", "irq", "folders", "raid", "sensors", "ip", "now",
];
const TOP_PLUGINS: [&str; 6] = ["cpu", "percpu", "mem", "memswap", "load", "gpu"];
@@ -54,7 +46,11 @@ pub fn run(engine: &mut Engine) -> Result<()> {
let backend = CrosstermBackend::new(stdout);
let mut terminal = ratatui::Terminal::new(backend)?;
let mut tui = Tui { popup: Popup::None, input: String::new(), quit: false };
let mut tui = Tui {
popup: Popup::None,
input: String::new(),
quit: false,
};
let result = run_loop(&mut terminal, engine, &mut tui);
disable_raw_mode()?;
@@ -76,10 +72,10 @@ fn run_loop(
tui.draw(terminal, engine)?;
count += 1;
if let Some(max) = engine.args.stop_after {
if count >= max {
break;
}
if let Some(max) = engine.args.stop_after
&& count >= max
{
break;
}
let elapsed = start.elapsed().as_secs_f64();
@@ -101,7 +97,11 @@ fn run_loop(
}
impl Tui {
fn draw(&self, terminal: &mut ratatui::Terminal<CrosstermBackend<Stdout>>, engine: &mut Engine) -> Result<()> {
fn draw(
&self,
terminal: &mut ratatui::Terminal<CrosstermBackend<Stdout>>,
engine: &mut Engine,
) -> Result<()> {
terminal.draw(|frame| draw_frame(frame, engine, self))?;
Ok(())
}
@@ -122,14 +122,39 @@ fn draw_frame(frame: &mut Frame, engine: &mut Engine, tui: &Tui) {
// Header.
let header_height = 1u16;
let top_height = tui.top_height(engine, args, area.width);
let footer_height = if tui.popup == Popup::Filter || tui.popup == Popup::Kill { 3 } else { 0 };
let main_height = area.height.saturating_sub(header_height + top_height + footer_height);
let footer_height = if tui.popup == Popup::Filter || tui.popup == Popup::Kill {
3
} else {
0
};
let main_height = area
.height
.saturating_sub(header_height + top_height + footer_height);
let header_area = Rect { x: 0, y: 0, width: area.width, height: header_height };
let top_area = Rect { x: 0, y: header_height, width: area.width, height: top_height };
let main_area = Rect { x: 0, y: header_height + top_height, width: area.width, height: main_height };
let footer_area =
Rect { x: 0, y: area.height.saturating_sub(footer_height), width: area.width, height: footer_height };
let header_area = Rect {
x: 0,
y: 0,
width: area.width,
height: header_height,
};
let top_area = Rect {
x: 0,
y: header_height,
width: area.width,
height: top_height,
};
let main_area = Rect {
x: 0,
y: header_height + top_height,
width: area.width,
height: main_height,
};
let footer_area = Rect {
x: 0,
y: area.height.saturating_sub(footer_height),
width: area.width,
height: footer_height,
};
tui.draw_header(frame, header_area, engine, args);
tui.draw_top(frame, top_area, engine, args);
@@ -140,7 +165,6 @@ fn draw_frame(frame: &mut Frame, engine: &mut Engine, tui: &Tui) {
}
impl Tui {
fn top_height(&self, engine: &mut Engine, args: &Args, width: u16) -> u16 {
if args.disable_top {
return 1;
@@ -157,53 +181,67 @@ impl Tui {
}
}
// Quicklook.
if let Some(plugin) = engine.plugins.get_mut("quicklook") {
if plugin.is_enabled() && !args.disable_quicklook {
let ql_width = quicklook_width(width);
let ctx = render_ctx(args, &engine.runtime, ql_width as usize);
let lines = split_lines(plugin.render(&ctx));
height = height.max(lines.len());
}
if let Some(plugin) = engine.plugins.get_mut("quicklook")
&& plugin.is_enabled()
&& !args.disable_quicklook
{
let ql_width = quicklook_width(width);
let ctx = render_ctx(args, &engine.runtime, ql_width as usize);
let lines = split_lines(plugin.render(&ctx));
height = height.max(lines.len());
}
(height.min(20) as u16).max(1)
}
fn draw_header(
&self,
frame: &mut Frame,
area: Rect,
engine: &mut Engine,
args: &Args,
) {
fn draw_header(&self, frame: &mut Frame, area: Rect, engine: &mut Engine, args: &Args) {
let x = area.x;
// System information.
if let Some(plugin) = engine.plugins.get_mut("system") {
if plugin.is_enabled() {
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx));
let text: String = lines.first().map(|l| l.iter().map(|s| s.msg.clone()).collect()).unwrap_or_default();
let style = lines
.first()
.and_then(|l| l.first())
.map(|s| style_for(s.decoration, args))
.unwrap_or_default();
let width = text.chars().count() as u16;
frame.render_widget(Paragraph::new(text).style(style), Rect { x, y: area.y, width: width.min(area.width), height: 1 });
}
if let Some(plugin) = engine.plugins.get_mut("system")
&& plugin.is_enabled()
{
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx));
let text: String = lines
.first()
.map(|l| l.iter().map(|s| s.msg.clone()).collect())
.unwrap_or_default();
let style = lines
.first()
.and_then(|l| l.first())
.map(|s| style_for(s.decoration, args))
.unwrap_or_default();
let width = text.chars().count() as u16;
frame.render_widget(
Paragraph::new(text).style(style),
Rect {
x,
y: area.y,
width: width.min(area.width),
height: 1,
},
);
}
// Uptime on the right.
if let Some(plugin) = engine.plugins.get_mut("uptime") {
if plugin.is_enabled() {
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx));
let text: String = lines.first().map(|l| l.iter().map(|s| s.msg.clone()).collect()).unwrap_or_default();
let width = text.chars().count() as u16;
if width + 2 < area.width {
frame.render_widget(
Paragraph::new(text),
Rect { x: area.width.saturating_sub(width), y: area.y, width, height: 1 },
);
}
if let Some(plugin) = engine.plugins.get_mut("uptime")
&& plugin.is_enabled()
{
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx));
let text: String = lines
.first()
.map(|l| l.iter().map(|s| s.msg.clone()).collect())
.unwrap_or_default();
let width = text.chars().count() as u16;
if width + 2 < area.width {
frame.render_widget(
Paragraph::new(text),
Rect {
x: area.width.saturating_sub(width),
y: area.y,
width,
height: 1,
},
);
}
}
}
@@ -212,22 +250,27 @@ impl Tui {
let mut x = area.x;
// Collect the rendered blocks with their natural widths.
struct Block {
name: String,
lines: Vec<Vec<StatLine>>,
natural_width: usize,
}
let mut blocks: Vec<Block> = Vec::new();
if !args.disable_quicklook {
if let Some(plugin) = engine.plugins.get_mut("quicklook") {
if plugin.is_enabled() {
let width = quicklook_width(area.width) as usize;
let ctx = render_ctx(args, &engine.runtime, width);
let lines = split_lines(plugin.render(&ctx));
let natural = lines.iter().map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>()).max().unwrap_or(10);
blocks.push(Block { name: "quicklook".to_string(), lines, natural_width: natural.max(10) });
}
}
if !args.disable_quicklook
&& let Some(plugin) = engine.plugins.get_mut("quicklook")
&& plugin.is_enabled()
{
let width = quicklook_width(area.width) as usize;
let ctx = render_ctx(args, &engine.runtime, width);
let lines = split_lines(plugin.render(&ctx));
let natural = lines
.iter()
.map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>())
.max()
.unwrap_or(10);
blocks.push(Block {
lines,
natural_width: natural.max(10),
});
}
for name in TOP_PLUGINS {
if let Some(plugin) = engine.plugins.get_mut(name) {
@@ -236,8 +279,15 @@ impl Tui {
}
let ctx = render_ctx(args, &engine.runtime, area.width as usize);
let lines = split_lines(plugin.render(&ctx));
let natural = lines.iter().map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>()).max().unwrap_or(10);
blocks.push(Block { name: name.to_string(), lines, natural_width: natural.max(10) });
let natural = lines
.iter()
.map(|l| l.iter().map(|s| s.msg.chars().count()).sum::<usize>())
.max()
.unwrap_or(10);
blocks.push(Block {
lines,
natural_width: natural.max(10),
});
}
}
if blocks.is_empty() {
@@ -247,7 +297,9 @@ impl Tui {
// Allocate the available width proportionally to the natural widths.
let total_natural: usize = blocks.iter().map(|b| b.natural_width).sum();
let spacing = 2usize * blocks.len().saturating_sub(1);
let available = (area.width as usize).saturating_sub(spacing).max(blocks.len() * 8);
let available = (area.width as usize)
.saturating_sub(spacing)
.max(blocks.len() * 8);
let mut widths: Vec<u16> = Vec::new();
for block in &blocks {
let mut width = (available * block.natural_width / total_natural).max(8);
@@ -267,7 +319,17 @@ impl Tui {
if x + width > area.x + area.width {
break;
}
draw_lines(frame, Rect { x, y: area.y, width, height: area.height }, &block.lines, args);
draw_lines(
frame,
Rect {
x,
y: area.y,
width,
height: area.height,
},
&block.lines,
args,
);
x += width + 2;
}
}
@@ -304,7 +366,17 @@ impl Tui {
break;
}
let height = height.min(area.y + area.height - y);
draw_lines(frame, Rect { x: area.x, y, width: left_width, height }, &lines, args);
draw_lines(
frame,
Rect {
x: area.x,
y,
width: left_width,
height,
},
&lines,
args,
);
y += height;
}
}
@@ -318,14 +390,24 @@ impl Tui {
let mut y = area.y;
// Process count.
if let Some(plugin) = engine.plugins.get_mut("processcount") {
if plugin.is_enabled() {
let ctx = render_ctx(args, &engine.runtime, right_width as usize);
let lines = split_lines(plugin.render(&ctx));
let height = (lines.len() as u16).min(2);
draw_lines(frame, Rect { x: right_x, y, width: right_width, height }, &lines, args);
y += height;
}
if let Some(plugin) = engine.plugins.get_mut("processcount")
&& plugin.is_enabled()
{
let ctx = render_ctx(args, &engine.runtime, right_width as usize);
let lines = split_lines(plugin.render(&ctx));
let height = (lines.len() as u16).min(2);
draw_lines(
frame,
Rect {
x: right_x,
y,
width: right_width,
height,
},
&lines,
args,
);
y += height;
}
// Alerts footer (up to 4 lines).
@@ -334,20 +416,40 @@ impl Tui {
let list_height = area.y + area.height - y;
let process_height = list_height.saturating_sub(alert_height);
let list_name = if args.programs { "programlist" } else { "processlist" };
if let Some(plugin) = engine.plugins.get_mut(list_name) {
if plugin.is_enabled() && process_height > 1 {
let ctx = render_ctx(args, &engine.runtime, right_width as usize);
let mut lines = split_lines(plugin.render(&ctx));
lines.truncate(process_height as usize);
draw_lines(frame, Rect { x: right_x, y, width: right_width, height: process_height }, &lines, args);
}
let list_name = if args.programs {
"programlist"
} else {
"processlist"
};
if let Some(plugin) = engine.plugins.get_mut(list_name)
&& plugin.is_enabled()
&& process_height > 1
{
let ctx = render_ctx(args, &engine.runtime, right_width as usize);
let mut lines = split_lines(plugin.render(&ctx));
lines.truncate(process_height as usize);
draw_lines(
frame,
Rect {
x: right_x,
y,
width: right_width,
height: process_height,
},
&lines,
args,
);
}
y += process_height;
if alert_height > 0 {
draw_lines(
frame,
Rect { x: right_x, y, width: right_width, height: alert_height },
Rect {
x: right_x,
y,
width: right_width,
height: alert_height,
},
&event_lines,
args,
);
@@ -360,14 +462,20 @@ impl Tui {
if events.is_empty() {
return lines;
}
lines.push(vec![StatLine::new("EVENTS HISTORY").decoration(Decoration::Title)]);
lines.push(vec![
StatLine::new("EVENTS HISTORY").decoration(Decoration::Title),
]);
for event in events.events.iter().rev().take(4) {
let state = match event.state.as_str() {
"CRITICAL" => Decoration::CriticalLog,
"WARNING" => Decoration::WarningLog,
_ => Decoration::Default,
};
let end = if event.is_ongoing() { String::new() } else { " (ended)".to_string() };
let end = if event.is_ongoing() {
String::new()
} else {
" (ended)".to_string()
};
lines.push(vec![
StatLine::new(format!("{} ", event.state)).decoration(state),
StatLine::new(format!("{} (avg {:.1}){}", event.r#type, event.avg, end)),
@@ -401,7 +509,9 @@ impl Tui {
Line::from("4 Full quicklook"),
Line::from("5 Disable top menu"),
Line::from("6 Mean GPU mode"),
Line::from("a c m i o p t u Sort processes (auto/cpu/mem/io/cpu_time/name/time/user)"),
Line::from(
"a c m i o p t u Sort processes (auto/cpu/mem/io/cpu_time/name/time/user)",
),
Line::from("</> Cycle sort key"),
Line::from("UP/DOWN Select process"),
Line::from("+ / - Increase/decrease nice level"),
@@ -420,7 +530,12 @@ impl Tui {
];
let block = Block::default().borders(Borders::ALL).title("Help");
frame.render_widget(Clear, area);
frame.render_widget(Paragraph::new(lines).block(block).wrap(Wrap { trim: false }), area);
frame.render_widget(
Paragraph::new(lines)
.block(block)
.wrap(Wrap { trim: false }),
area,
);
}
fn handle_key(&mut self, engine: &mut Engine, key: KeyEvent) {
@@ -435,7 +550,11 @@ impl Tui {
KeyCode::Enter => {
let filter = self.input.clone();
let mut procs = processes().lock().unwrap();
procs.filter.set_filter(if filter.is_empty() { None } else { Some(&filter) });
procs.filter.set_filter(if filter.is_empty() {
None
} else {
Some(&filter)
});
self.popup = Popup::None;
}
KeyCode::Esc => self.popup = Popup::None,
@@ -485,7 +604,9 @@ impl Tui {
args.meangpu = !args.meangpu;
engine.runtime.meangpu = args.meangpu;
}
KeyCode::Char('/') => engine.runtime.process_short_name = !engine.runtime.process_short_name,
KeyCode::Char('/') => {
engine.runtime.process_short_name = !engine.runtime.process_short_name
}
KeyCode::Char('a') => set_sort(engine, "auto"),
KeyCode::Char('c') => set_sort(engine, "cpu_percent"),
KeyCode::Char('m') => set_sort(engine, "memory_percent"),
@@ -502,7 +623,13 @@ impl Tui {
KeyCode::Char('-') => nice_change(engine, false),
KeyCode::Char('k') => self.popup = Popup::Kill,
KeyCode::Char('f') => {
self.input = processes().lock().unwrap().filter.filter_input().unwrap_or("").to_string();
self.input = processes()
.lock()
.unwrap()
.filter
.filter_input()
.unwrap_or("")
.to_string();
self.popup = Popup::Filter;
}
KeyCode::Char('E') => processes().lock().unwrap().filter.set_filter(None),
@@ -521,12 +648,16 @@ impl Tui {
KeyCode::Char('L') => args.diskio_latency = !args.diskio_latency,
KeyCode::Char('F') => engine.runtime.fs_free_space = !engine.runtime.fs_free_space,
KeyCode::Char('S') => engine.runtime.sparkline = !engine.runtime.sparkline,
KeyCode::Char('M') => engine.runtime.reset_minmax_tag = !engine.runtime.reset_minmax_tag,
KeyCode::Char('M') => {
engine.runtime.reset_minmax_tag = !engine.runtime.reset_minmax_tag
}
KeyCode::Char('j') => {
args.programs = !args.programs;
engine.runtime.programs = args.programs;
}
KeyCode::Char(c @ ('d' | 'n' | 's' | 'l' | 'D' | 'C' | 'I' | 'V' | 'W' | 'P' | 'K' | 'R')) => {
KeyCode::Char(
c @ ('d' | 'n' | 's' | 'l' | 'D' | 'C' | 'I' | 'V' | 'W' | 'P' | 'K' | 'R'),
) => {
toggle_plugin_by_key(engine, c);
}
_ => {}
@@ -534,8 +665,16 @@ impl Tui {
}
}
fn render_ctx<'a>(args: &'a Args, runtime: &'a glances_core::Runtime, max_width: usize) -> RenderContext<'a> {
RenderContext { args, runtime, max_width }
fn render_ctx<'a>(
args: &'a Args,
runtime: &'a glances_core::Runtime,
max_width: usize,
) -> RenderContext<'a> {
RenderContext {
args,
runtime,
max_width,
}
}
fn quicklook_width(width: u16) -> u16 {
@@ -587,7 +726,11 @@ fn draw_lines(frame: &mut Frame, area: Rect, lines: &[Vec<StatLine>], args: &Arg
}
fn style_for(decoration: Decoration, args: &Args) -> Style {
let bold = if args.disable_bold { Modifier::empty() } else { Modifier::BOLD };
let bold = if args.disable_bold {
Modifier::empty()
} else {
Modifier::BOLD
};
let bg = args.disable_bg;
match decoration {
Decoration::Default | Decoration::Password => Style::default(),
@@ -596,7 +739,9 @@ fn style_for(decoration: Decoration, args: &Args) -> Style {
Style::default().add_modifier(bold | Modifier::UNDERLINED)
}
Decoration::Title => Style::default().add_modifier(bold),
Decoration::Ok | Decoration::Process | Decoration::Status => Style::default().fg(Color::Green),
Decoration::Ok | Decoration::Process | Decoration::Status => {
Style::default().fg(Color::Green)
}
Decoration::Max => Style::default().fg(Color::Green).add_modifier(bold),
Decoration::Careful | Decoration::Info => Style::default().fg(Color::Blue),
Decoration::CarefulLog => {
@@ -630,7 +775,9 @@ fn style_for(decoration: Decoration, args: &Args) -> Style {
}
}
Decoration::CpuTime => Style::default().fg(Color::Magenta),
Decoration::ProcessSelected => Style::default().fg(Color::Green).add_modifier(bold | Modifier::UNDERLINED),
Decoration::ProcessSelected => Style::default()
.fg(Color::Green)
.add_modifier(bold | Modifier::UNDERLINED),
Decoration::Error => Style::default().fg(Color::Red),
Decoration::Separator => Style::default().fg(Color::DarkGray),
}
@@ -643,7 +790,15 @@ fn set_sort(_engine: &mut Engine, key: &str) {
}
fn cycle_sort(engine: &mut Engine, forward: bool) {
const KEYS: [&str; 7] = ["cpu_percent", "memory_percent", "username", "cpu_times", "io_counters", "name", "cpu_num"];
const KEYS: [&str; 7] = [
"cpu_percent",
"memory_percent",
"username",
"cpu_times",
"io_counters",
"name",
"cpu_num",
];
let current = processes().lock().unwrap().sort_key.clone();
let index = KEYS.iter().position(|k| *k == current).unwrap_or(0);
let next = if forward {
@@ -680,7 +835,11 @@ fn nice_change(engine: &mut Engine, increase: bool) {
}
fn toggle_processes(engine: &mut Engine) {
let enabled = engine.plugins.get("processlist").map(|p| p.is_enabled()).unwrap_or(false);
let enabled = engine
.plugins
.get("processlist")
.map(|p| p.is_enabled())
.unwrap_or(false);
for name in ["processcount", "processlist", "programlist"] {
if let Some(plugin) = engine.plugins.get_mut(name) {
plugin.set_enabled(!enabled);
@@ -752,7 +911,13 @@ mod tests {
fn test_tui_layout_renders() {
use clap::Parser;
use glances_core::Config;
let mut args = Args::parse_from(["glances", "--disable-plugin", "all", "--enable-plugin", "cpu,mem,load,processlist,processcount,network,fs,diskio,now,system,uptime,quicklook"]);
let mut args = Args::parse_from([
"glances",
"--disable-plugin",
"all",
"--enable-plugin",
"cpu,mem,load,processlist,processcount,network,fs,diskio,now,system,uptime,quicklook",
]);
args.time = Some(1.0);
let config = Config::empty(false);
let mut engine = Engine::new(args.clone(), config);
@@ -760,8 +925,14 @@ mod tests {
engine.update();
let backend = TestBackend::new(140, 40);
let mut terminal = ratatui::Terminal::new(backend).unwrap();
let tui = Tui { popup: Popup::None, input: String::new(), quit: false };
terminal.draw(|frame| draw_frame(frame, &mut engine, &tui)).unwrap();
let tui = Tui {
popup: Popup::None,
input: String::new(),
quit: false,
};
terminal
.draw(|frame| draw_frame(frame, &mut engine, &tui))
.unwrap();
let text = buffer_to_string(terminal.backend().buffer());
println!("{text}");
assert!(text.contains("CPU"));