use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::{Alignment, Frame},
style::{Color, Style},
widgets::{Block, BorderType, Borders, Paragraph},
};
use crate::action_menu::MenuAction;
use crate::app::App;
use crate::appdata::WindowFocus;
use crate::numbers::{format_duration, ClampNumExt, MyIntExt, PercentFormatterExt};
use crate::strings::apply_scroll;
use crate::sysinfo::ProcessStat;
const VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn render(app: &mut App, frame: &mut Frame) {
let area = frame.area();
let h = area.height as f32;
let system_panel_height = (h * 0.35).clamp(8.0, 32.0) as u16;
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Min(8), Constraint::Length(system_panel_height)])
.split(area);
render_main_view(app, frame, layout[0]);
render_system_view(app, frame, layout[1]);
if app.window_focus == WindowFocus::SignalPick {
render_signal_panel(app, frame);
}
if app.info_message.is_some() {
render_info_popup(app, frame);
}
if app.error_message.is_some() {
render_error_popup(app, frame);
}
}
fn render_main_view(app: &mut App, frame: &mut Frame, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Max(3), Constraint::Min(5), Constraint::Max(3)])
.split(area);
render_info_panel(app, frame, layout[0]);
render_proc_list(app, frame, layout[1]);
render_filter_panel(app, frame, layout[2]);
}
fn render_info_panel(_app: &mut App, frame: &mut Frame, area: Rect) {
let p_text = "`?` for controls. `Ctrl+F` to filter. `R` to refresh. `S` to sort. `Enter` to execute.";
let widget = Paragraph::new(p_text)
.wrap(Wrap { trim: true })
.style(Style::default().fg(Color::White))
.block(
Block::default()
.title(format!("PSycho KILLer {}", VERSION))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Style::default().fg(Color::LightRed)),
)
.alignment(Alignment::Center);
frame.render_widget(widget, area);
}
fn render_filter_panel(app: &mut App, frame: &mut Frame, area: Rect) {
let p_text = match app.window_focus {
WindowFocus::ProcessFilter => format!("{}\u{2588}", app.filter_text), _ => app.filter_text.clone(),
};
let panel_color = match app.window_focus {
WindowFocus::ProcessFilter => Color::LightYellow,
_ => Color::White,
};
let mut title = Block::default().title("Filter (Ctrl+F)");
if app.window_focus == WindowFocus::ProcessFilter {
title = title.title_style(Style::new().bold());
}
let widget = Paragraph::new(p_text)
.block(
title
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
)
.style(Style::default().fg(panel_color))
.alignment(Alignment::Left);
frame.render_widget(widget, area);
}
fn render_proc_list(app: &mut App, frame: &mut Frame, area: Rect) {
let rows: Vec<Row> = app
.filtered_processes
.iter()
.map(|it: &ProcessStat| {
Row::new(vec![
it.pid.clone(),
apply_scroll(&it.display_name, app.horizontal_scroll),
format_duration(it.run_time),
it.memory_usage.to_percent1(),
it.format_cpu_usage(),
])
})
.collect();
let col_pid_length: i32 = app
.filtered_processes
.iter()
.map(|it| it.pid.to_string().len())
.max()
.unwrap_or(0) as i32;
let w = area.width as i32;
let uptime_col_w = 9;
let mem_col_w = 5;
let cpu_col_w = 6;
let rest_width = (w - col_pid_length - uptime_col_w - mem_col_w - cpu_col_w - 4 - 2 - 2).clamp_min(3); let widths = [
Constraint::Length(col_pid_length as u16), Constraint::Min(rest_width as u16), Constraint::Max(uptime_col_w as u16), Constraint::Max(mem_col_w as u16), Constraint::Max(cpu_col_w as u16), ];
let headers = match app.ordering {
crate::appdata::Ordering::ByUptime => ["PID", "Name", "Uptime↓", "MEM", "CPU"],
crate::appdata::Ordering::ByMemory => ["PID", "Name", "Uptime", "MEM↑", "CPU"],
crate::appdata::Ordering::ByCpu => ["PID", "Name", "Uptime", "MEM", "CPU↑"],
};
let panel_color = match app.window_focus {
WindowFocus::Browse => Color::LightYellow,
_ => Color::White,
};
let title = match app.group_by_exe {
false => "Running Processes",
true => "Running Processes (grouped by executable)",
};
let mut title = Block::default().title(title);
if app.window_focus == WindowFocus::Browse {
title = title.title_style(Style::new().bold());
}
let table = Table::new(rows, widths)
.column_spacing(1)
.header(Row::new(headers).style(Style::new().bold()).bottom_margin(1))
.block(
title
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(panel_color)),
)
.style(Style::default().fg(Color::White))
.row_highlight_style(Style::new().add_modifier(Modifier::REVERSED))
.highlight_symbol(">>");
frame.render_stateful_widget(table, area, &mut app.proc_list_table_state);
}
fn render_system_view(app: &mut App, frame: &mut Frame, area: Rect) {
let panel_color = match app.window_focus {
WindowFocus::SystemStats => Color::LightYellow,
_ => Color::Yellow,
};
let mut title = Block::default().title("System");
if app.window_focus == WindowFocus::SystemStats {
title = title.title_style(Style::new().bold());
}
let widget = Paragraph::new(app.format_sys_stats())
.wrap(Wrap { trim: true })
.block(
title
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
)
.style(Style::default().fg(panel_color))
.alignment(Alignment::Left);
frame.render_widget(widget, area);
}
fn render_signal_panel(app: &mut App, frame: &mut Frame) {
let list_items: Vec<ListItem> = app
.known_menu_actions
.iter()
.map(|it: &MenuAction| ListItem::new(it.name))
.collect();
let mut list_state = ListState::default().with_selected(Some(app.menu_action_cursor));
let widget = List::new(list_items)
.block(
Block::default()
.title("Choose a command")
.borders(Borders::ALL)
.bg(Color::DarkGray),
)
.style(Style::default().fg(Color::White).bg(Color::DarkGray))
.highlight_style(Style::new().add_modifier(Modifier::REVERSED))
.highlight_symbol(">> ");
let height = app.known_menu_actions.len() as u16 + 2;
let width: u16 = app
.known_menu_actions
.iter()
.map(|it: &MenuAction| it.name.len() as u16)
.max()
.unwrap_or(0)
+ 8;
let area = centered_rect(width, height, frame.area());
let buffer = frame.buffer_mut();
Clear.render(area, buffer);
frame.render_stateful_widget(widget, area, &mut list_state);
}
fn render_error_popup(app: &mut App, frame: &mut Frame) {
if app.error_message.is_none() {
return;
}
let error_message: String = app.error_message.clone().unwrap();
let title = Block::default()
.title("Error")
.title_style(Style::new().bold())
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.bg(Color::Red)
.border_type(BorderType::Rounded);
let error_window = Paragraph::new(error_message)
.wrap(Wrap { trim: true })
.block(title)
.style(Style::default().fg(Color::White));
let ok_label = Paragraph::new("OK")
.style(Style::default().bold().fg(Color::LightRed).bg(Color::White))
.alignment(Alignment::Center);
let width: u16 = (frame.area().width as f32 * 0.75f32) as u16;
let height: u16 = frame.area().height / 2;
let area = centered_rect(width, height, frame.area());
let ok_label_area = Rect {
x: area.x + 1,
y: area.y + area.height - 2,
width: area.width - 2,
height: 1,
};
let buffer = frame.buffer_mut();
Clear.render(area, buffer);
frame.render_widget(error_window, area);
frame.render_widget(ok_label, ok_label_area);
}
fn render_info_popup(app: &mut App, frame: &mut Frame) {
if app.info_message.is_none() {
return;
}
let width: u16 = frame.area().width.fraction(0.75);
let info_message: String = app.info_message.clone().unwrap();
let wrapped_lines = textwrap::wrap(info_message.as_str(), (width - 3) as usize);
let wrapped_message = wrapped_lines.join("\n");
app.info_lines_num = Some(wrapped_lines.len());
let skipped_lines = wrapped_message
.lines()
.into_iter()
.map(|s| s.to_string())
.skip(app.info_message_scroll)
.collect::<Vec<String>>();
let display_message: String = skipped_lines.join("\n");
let max_height: u16 = frame.area().height.fraction(0.75);
let text_height: u16 = wrapped_message
.lines()
.count()
.clamp_min(5)
.clamp_max(max_height.into()) as u16;
let title_block = Block::default()
.title("Info")
.title_style(Style::new().bold())
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.bg(Color::Blue)
.padding(Padding::bottom(1))
.border_type(BorderType::Rounded);
let popup_window = Paragraph::new(Text::raw(display_message))
.wrap(Wrap { trim: false })
.block(title_block)
.style(Style::default().fg(Color::White));
let ok_label = Paragraph::new("OK")
.style(Style::default().bold().fg(Color::LightBlue).bg(Color::White))
.alignment(Alignment::Center);
let area = centered_rect(width, text_height + 3, frame.area());
let ok_label_area = Rect {
x: area.x + 1,
y: area.y + area.height - 2,
width: area.width - 2,
height: 1,
};
Clear.render(area, frame.buffer_mut());
frame.render_widget(popup_window, area);
frame.render_widget(ok_label, ok_label_area);
}
fn centered_rect(w: u16, h: u16, r: Rect) -> Rect {
let x_gap = (r.width as i32 - w as i32).clamp_min(0) / 2;
let y_gap = (r.height as i32 - h as i32).clamp_min(0) / 2;
Rect {
x: r.x + x_gap as u16,
y: r.y + y_gap as u16,
width: w,
height: h,
}
}