Files
glances-rs/crates/glances-tui/src/lib.rs
T
glances-rs 6819ce4e18 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
2026-10-09 23:59:26 +00:00

943 lines
32 KiB
Rust

//! Terminal user interface (port of the Glances curses interface).
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 glances_core::plugin::{Align, Decoration, StatLine};
use glances_core::processes::processes;
use glances_core::{Args, RenderContext};
use glances_plugins::engine::Engine;
use ratatui::Frame;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Line;
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Wrap};
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",
];
const TOP_PLUGINS: [&str; 6] = ["cpu", "percpu", "mem", "memswap", "load", "gpu"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Popup {
None,
Help,
Filter,
Kill,
}
pub struct Tui {
popup: Popup,
input: String,
quit: bool,
}
/// Run the terminal interface until the user quits.
pub fn run(engine: &mut Engine) -> Result<()> {
enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen)?;
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 result = run_loop(&mut terminal, engine, &mut tui);
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
result
}
fn run_loop(
terminal: &mut ratatui::Terminal<CrosstermBackend<Stdout>>,
engine: &mut Engine,
tui: &mut Tui,
) -> Result<()> {
let refresh = engine.args.time.unwrap_or(2.0);
let mut count: i64 = 0;
while !tui.quit {
let start = Instant::now();
engine.update();
tui.draw(terminal, engine)?;
count += 1;
if let Some(max) = engine.args.stop_after
&& count >= max
{
break;
}
let elapsed = start.elapsed().as_secs_f64();
let timeout = Duration::from_secs_f64((refresh - elapsed).max(0.05));
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
let remaining = deadline.saturating_duration_since(Instant::now());
if !event::poll(remaining)? {
break;
}
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => tui.handle_key(engine, key),
Event::Resize(_, _) => break,
_ => {}
}
}
}
Ok(())
}
impl Tui {
fn draw(
&self,
terminal: &mut ratatui::Terminal<CrosstermBackend<Stdout>>,
engine: &mut Engine,
) -> Result<()> {
terminal.draw(|frame| draw_frame(frame, engine, self))?;
Ok(())
}
}
fn draw_frame(frame: &mut Frame, engine: &mut Engine, tui: &Tui) {
let args = engine.args.clone();
let args = &args;
let area = frame.area();
if area.width < 40 || area.height < 10 {
return;
}
if tui.popup == Popup::Help {
tui.draw_help(frame, area);
return;
}
// 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 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);
tui.draw_main(frame, main_area, engine, args);
if footer_height > 0 {
tui.draw_popup(frame, footer_area);
}
}
impl Tui {
fn top_height(&self, engine: &mut Engine, args: &Args, width: u16) -> u16 {
if args.disable_top {
return 1;
}
let mut height = 1usize;
for name in TOP_PLUGINS {
if let Some(plugin) = engine.plugins.get_mut(name) {
if !plugin.is_enabled() {
continue;
}
let ctx = render_ctx(args, &engine.runtime, width as usize);
let lines = split_lines(plugin.render(&ctx));
height = height.max(lines.len());
}
}
// Quicklook.
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) {
let x = area.x;
// System information.
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")
&& 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,
},
);
}
}
}
fn draw_top(&self, frame: &mut Frame, area: Rect, engine: &mut Engine, args: &Args) {
let mut x = area.x;
// Collect the rendered blocks with their natural widths.
struct Block {
lines: Vec<Vec<StatLine>>,
natural_width: usize,
}
let mut blocks: Vec<Block> = Vec::new();
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) {
if !plugin.is_enabled() {
continue;
}
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 {
lines,
natural_width: natural.max(10),
});
}
}
if blocks.is_empty() {
return;
}
// 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 mut widths: Vec<u16> = Vec::new();
for block in &blocks {
let mut width = (available * block.natural_width / total_natural).max(8);
width = width.min(block.natural_width + 2);
widths.push(width as u16);
}
// Adjust the last block if the total exceeds the screen.
let total: usize = widths.iter().map(|w| *w as usize).sum::<usize>() + spacing;
if total > area.width as usize {
let excess = total - area.width as usize;
if let Some(last) = widths.last_mut() {
*last = last.saturating_sub(excess as u16).max(8);
}
}
for (block, width) in blocks.into_iter().zip(widths) {
if x + width > area.x + area.width {
break;
}
draw_lines(
frame,
Rect {
x,
y: area.y,
width,
height: area.height,
},
&block.lines,
args,
);
x += width + 2;
}
}
fn draw_main(&self, frame: &mut Frame, area: Rect, engine: &mut Engine, args: &Args) {
if area.height == 0 {
return;
}
let left_width = if args.disable_left_sidebar {
0
} else {
(area.width as i32 - 105).clamp(23, 34) as u16
};
if left_width > 0 && area.width > left_width + 20 {
let mut y = area.y;
for name in LEFT_SIDEBAR {
if y >= area.y + area.height {
break;
}
let Some(plugin) = engine.plugins.get_mut(name) else {
continue;
};
if !plugin.is_enabled() {
continue;
}
let ctx = render_ctx(args, &engine.runtime, left_width as usize);
let lines = split_lines(plugin.render(&ctx));
if lines.is_empty() {
continue;
}
let height = (lines.len() as u16).min(area.y + area.height - y);
y += 1; // separator/blank line
if y >= area.y + area.height {
break;
}
let height = height.min(area.y + area.height - y);
draw_lines(
frame,
Rect {
x: area.x,
y,
width: left_width,
height,
},
&lines,
args,
);
y += height;
}
}
// Right side: process count + processes + alert footer.
let right_x = area.x + left_width + if left_width > 0 { 1 } else { 0 };
let right_width = area.width.saturating_sub(right_x - area.x);
if right_width < 10 {
return;
}
let mut y = area.y;
// Process count.
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).
let event_lines = self.event_lines(engine, right_width as usize);
let alert_height = event_lines.len().min(5) as u16;
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)
&& 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,
},
&event_lines,
args,
);
}
}
fn event_lines(&self, engine: &Engine, _width: usize) -> Vec<Vec<StatLine>> {
let events = glances_core::events::events().lock().unwrap();
let mut lines = Vec::new();
if events.is_empty() {
return lines;
}
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()
};
lines.push(vec![
StatLine::new(format!("{} ", event.state)).decoration(state),
StatLine::new(format!("{} (avg {:.1}){}", event.r#type, event.avg, end)),
]);
}
let _ = engine;
lines
}
fn draw_popup(&self, frame: &mut Frame, area: Rect) {
let (title, prompt) = match self.popup {
Popup::Filter => ("Filter", format!("Process filter: {}_", self.input)),
Popup::Kill => ("Kill process", "Confirm ([y]es/[n]o): ".to_string()),
_ => return,
};
let block = Block::default().borders(Borders::ALL).title(title);
frame.render_widget(Clear, area);
frame.render_widget(Paragraph::new(prompt).block(block), area);
}
fn draw_help(&self, frame: &mut Frame, area: Rect) {
let lines = vec![
Line::from("Glances-rs help"),
Line::from(""),
Line::from("q / ESC Quit"),
Line::from("h Toggle this help"),
Line::from("0 Irix mode (disable-irix)"),
Line::from("1 Per CPU mode"),
Line::from("2 Disable left sidebar"),
Line::from("3 Disable quicklook"),
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("</> Cycle sort key"),
Line::from("UP/DOWN Select process"),
Line::from("+ / - Increase/decrease nice level"),
Line::from("k Kill selected process"),
Line::from("f / E Edit/erase process filter"),
Line::from("z Disable/enable processes"),
Line::from("e Extended stats for the selected process"),
Line::from("w / x Clean warning/critical events"),
Line::from("b Byte mode (network)"),
Line::from("T / U Network sum / cumulative"),
Line::from("B / L Disk IOPS / latency"),
Line::from("F FS free space"),
Line::from("S Sparkline"),
Line::from("M Reset min/max"),
Line::from("d n s l D C I V W P K Toggle plugins"),
];
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,
);
}
fn handle_key(&mut self, engine: &mut Engine, key: KeyEvent) {
let args = &mut engine.args;
// Popup handling first.
match self.popup {
Popup::Help => {
self.popup = Popup::None;
return;
}
Popup::Filter => match key.code {
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)
});
self.popup = Popup::None;
}
KeyCode::Esc => self.popup = Popup::None,
KeyCode::Backspace => {
self.input.pop();
}
KeyCode::Char(c) => self.input.push(c),
_ => {}
},
Popup::Kill => match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => {
let cursor = engine.runtime.cursor_position.max(0) as usize;
let pid = processes()
.lock()
.unwrap()
.get_list(false)
.get(cursor)
.and_then(|p| p.get("pid"))
.and_then(serde_json::Value::as_i64);
if let Some(pid) = pid {
processes().lock().unwrap().kill(pid);
}
self.popup = Popup::None;
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => self.popup = Popup::None,
_ => {}
},
Popup::None => {}
}
if self.popup != Popup::None {
return;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.quit = true,
KeyCode::Char('h') => self.popup = Popup::Help,
KeyCode::Char('0') => engine.runtime.disable_irix = !engine.runtime.disable_irix,
KeyCode::Char('1') => {
args.percpu = !args.percpu;
engine.runtime.percpu = args.percpu;
}
KeyCode::Char('2') => args.disable_left_sidebar = !args.disable_left_sidebar,
KeyCode::Char('3') => args.disable_quicklook = !args.disable_quicklook,
KeyCode::Char('4') => args.full_quicklook = !args.full_quicklook,
KeyCode::Char('5') => args.disable_top = !args.disable_top,
KeyCode::Char('6') => {
args.meangpu = !args.meangpu;
engine.runtime.meangpu = args.meangpu;
}
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"),
KeyCode::Char('i') => set_sort(engine, "io_counters"),
KeyCode::Char('o') => set_sort(engine, "cpu_num"),
KeyCode::Char('p') => set_sort(engine, "name"),
KeyCode::Char('t') => set_sort(engine, "cpu_times"),
KeyCode::Char('u') => set_sort(engine, "username"),
KeyCode::Char('<') => cycle_sort(engine, false),
KeyCode::Char('>') => cycle_sort(engine, true),
KeyCode::Up => move_cursor(engine, -1),
KeyCode::Down => move_cursor(engine, 1),
KeyCode::Char('+') => nice_change(engine, true),
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.popup = Popup::Filter;
}
KeyCode::Char('E') => processes().lock().unwrap().filter.set_filter(None),
KeyCode::Char('z') => toggle_processes(engine),
KeyCode::Char('e') => toggle_extended(engine),
KeyCode::Char('w') => {
glances_core::events::events().lock().unwrap().clean(false);
}
KeyCode::Char('x') => {
glances_core::events::events().lock().unwrap().clean(true);
}
KeyCode::Char('b') => args.byte = !args.byte,
KeyCode::Char('T') => engine.runtime.network_sum = !engine.runtime.network_sum,
KeyCode::Char('U') => engine.runtime.network_cumul = !engine.runtime.network_cumul,
KeyCode::Char('B') => args.diskio_iops = !args.diskio_iops,
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('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'),
) => {
toggle_plugin_by_key(engine, c);
}
_ => {}
}
}
}
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 {
((width as f64 * 0.28) as u16).clamp(24, 48)
}
fn split_lines(lines: Vec<StatLine>) -> Vec<Vec<StatLine>> {
let mut result: Vec<Vec<StatLine>> = vec![Vec::new()];
for line in lines {
if line.msg == "\n" {
result.push(Vec::new());
} else {
result.last_mut().unwrap().push(line);
}
}
result
}
/// Draw a list of lines into a rect (a line group is a row).
fn draw_lines(frame: &mut Frame, area: Rect, lines: &[Vec<StatLine>], args: &Args) {
if area.width == 0 || area.height == 0 {
return;
}
let buffer = frame.buffer_mut();
for (row, line) in lines.iter().take(area.height as usize).enumerate() {
let y = area.y + row as u16;
let mut x = area.x;
let mut decoration = Decoration::Default;
for segment in line {
if segment.decoration != Decoration::Default {
decoration = segment.decoration;
}
let style = style_for(decoration, args);
for c in segment.msg.chars() {
if x >= area.x + area.width {
break;
}
if let Some(cell) = buffer.cell_mut((x, y)) {
cell.set_char(c);
cell.set_style(style);
}
x += 1;
}
if x >= area.x + area.width {
break;
}
}
}
}
fn style_for(decoration: Decoration, args: &Args) -> Style {
let bold = if args.disable_bold {
Modifier::empty()
} else {
Modifier::BOLD
};
let bg = args.disable_bg;
match decoration {
Decoration::Default | Decoration::Password => Style::default(),
Decoration::Underline => Style::default().add_modifier(Modifier::UNDERLINED),
Decoration::Bold | Decoration::Selected | Decoration::Sort | Decoration::Filter => {
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::Max => Style::default().fg(Color::Green).add_modifier(bold),
Decoration::Careful | Decoration::Info => Style::default().fg(Color::Blue),
Decoration::CarefulLog => {
if bg {
Style::default().fg(Color::Blue).add_modifier(bold)
} else {
Style::default().bg(Color::Blue).add_modifier(bold)
}
}
Decoration::Warning => Style::default().fg(Color::Magenta).add_modifier(bold),
Decoration::WarningLog => {
if bg {
Style::default().fg(Color::Magenta).add_modifier(bold)
} else {
Style::default().bg(Color::Magenta).add_modifier(bold)
}
}
Decoration::Critical => Style::default().fg(Color::Red).add_modifier(bold),
Decoration::CriticalLog => {
if bg {
Style::default().fg(Color::Red).add_modifier(bold)
} else {
Style::default().bg(Color::Red).add_modifier(bold)
}
}
Decoration::OkLog => {
if bg {
Style::default().fg(Color::Green).add_modifier(bold)
} else {
Style::default().bg(Color::Green).add_modifier(bold)
}
}
Decoration::CpuTime => Style::default().fg(Color::Magenta),
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),
}
}
fn set_sort(_engine: &mut Engine, key: &str) {
let mut procs = processes().lock().unwrap();
procs.set_sort_key(key, key != "auto");
procs.auto_sort = key == "auto";
}
fn cycle_sort(engine: &mut Engine, forward: bool) {
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 {
(index + 1) % KEYS.len()
} else {
(index + KEYS.len() - 1) % KEYS.len()
};
set_sort(engine, KEYS[next]);
}
fn move_cursor(engine: &mut Engine, delta: i64) {
let len = processes().lock().unwrap().get_list(false).len() as i64;
let max = (len - 1).max(0);
engine.runtime.cursor_position = (engine.runtime.cursor_position + delta).clamp(0, max);
}
fn nice_change(engine: &mut Engine, increase: bool) {
let cursor = engine.runtime.cursor_position.max(0) as usize;
let pid = processes()
.lock()
.unwrap()
.get_list(false)
.get(cursor)
.and_then(|p| p.get("pid"))
.and_then(serde_json::Value::as_i64);
if let Some(pid) = pid {
let procs = processes().lock().unwrap();
if increase {
procs.nice_increase(pid);
} else {
procs.nice_decrease(pid);
}
}
}
fn toggle_processes(engine: &mut Engine) {
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);
}
}
engine.sync_enabled();
}
fn toggle_extended(engine: &mut Engine) {
let procs = processes().lock().unwrap();
if procs.disable_extended_tag {
drop(procs);
processes().lock().unwrap().enable_extended();
engine.args.enable_process_extended = true;
} else {
drop(procs);
processes().lock().unwrap().disable_extended();
engine.args.enable_process_extended = false;
}
}
fn toggle_plugin_by_key(engine: &mut Engine, key: char) {
let name = match key {
'd' => "diskio",
'n' => "network",
's' => "sensors",
'l' => "alert",
'D' => "containers",
'C' => "cloud",
'I' => "ip",
'V' => "vms",
'W' => "wifi",
'P' => "ports",
'K' => "connections",
'R' => "raid",
_ => return,
};
if let Some(plugin) = engine.plugins.get_mut(name) {
let enabled = plugin.is_enabled();
plugin.set_enabled(!enabled);
engine.sync_enabled();
}
}
/// Keep the `Align` import alive for future use.
#[allow(dead_code)]
fn unused(_: Align) {}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::backend::TestBackend;
fn buffer_to_string(buffer: &ratatui::buffer::Buffer) -> String {
let area = buffer.area();
let mut out = String::new();
for y in 0..area.height {
for x in 0..area.width {
if let Some(cell) = buffer.cell((x, y)) {
out.push_str(cell.symbol());
}
}
out.push('\n');
}
out
}
#[test]
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",
]);
args.time = Some(1.0);
let config = Config::empty(false);
let mut engine = Engine::new(args.clone(), config);
engine.update();
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 text = buffer_to_string(terminal.backend().buffer());
println!("{text}");
assert!(text.contains("CPU"));
assert!(text.contains("MEM"));
assert!(text.contains("Command") || text.contains("python") || text.contains("COMMAND"));
}
}