- --fetch with a Jinja-subset renderer and the shipped templates - clippy clean, cargo fmt, all tests green - README status and differential testing instructions
943 lines
32 KiB
Rust
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"));
|
|
}
|
|
}
|