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:
1 parent
975385da5d
commit
8385900ac1
27 files changed
+2053
-31
No files matched your search
@@ -1,14 +1,759 @@
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
//! 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; 8] = ["network", "diskio", "fs", "irq", "sensors", "now", "folders", "raid"];
|
||||
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 {
|
||||
if 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") {
|
||||
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());
|
||||
}
|
||||
}
|
||||
(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") {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
// 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
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 { name: name.to_string(), 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") {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user