use super::app::VisualizeApp;
use super::panels;
use super::theme::borders;
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame,
};
pub fn draw(f: &mut Frame, app: &mut VisualizeApp) {
let size = f.area();
if size.width == 0 || size.height == 0 {
return;
}
let top_panel_count = app.count_top_panels();
if top_panel_count == 0 && !app.panels.process_syscalls {
draw_empty_state(f, size);
return;
}
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(if app.panels.process_syscalls {
vec![Constraint::Percentage(45), Constraint::Percentage(55)]
} else {
vec![Constraint::Percentage(100)]
})
.split(size);
if top_panel_count > 0 {
draw_top_panels(f, app, main_chunks[0]);
}
if app.panels.process_syscalls && main_chunks.len() > 1 {
panels::process_syscalls::draw(f, app, main_chunks[1]);
}
if app.show_help {
draw_help_overlay(f, size);
}
if app.show_filter_input {
draw_filter_input(f, app, size);
}
if app.config.show_fps {
draw_fps_overlay(f, app, size);
}
if app.trace_complete {
draw_trace_complete_overlay(f, size);
}
}
fn draw_top_panels(f: &mut Frame, app: &mut VisualizeApp, area: Rect) {
let panel_count = app.count_top_panels();
if panel_count == 0 {
return;
}
let constraints: Vec<Constraint> = match panel_count {
1 => vec![Constraint::Percentage(100)],
2 => vec![Constraint::Percentage(50), Constraint::Percentage(50)],
3 => {
vec![Constraint::Percentage(34), Constraint::Percentage(33), Constraint::Percentage(33)]
}
4 => vec![
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
],
_ => vec![
Constraint::Percentage(20),
Constraint::Percentage(20),
Constraint::Percentage(20),
Constraint::Percentage(20),
Constraint::Percentage(20),
],
};
let chunks =
Layout::default().direction(Direction::Horizontal).constraints(constraints).split(area);
let mut chunk_idx = 0;
if app.panels.syscall_heatmap && chunk_idx < chunks.len() {
panels::syscall_heatmap::draw(f, app, chunks[chunk_idx]);
chunk_idx += 1;
}
if app.panels.anomaly_timeline && chunk_idx < chunks.len() {
panels::anomaly_timeline::draw(f, app, chunks[chunk_idx]);
chunk_idx += 1;
}
if app.panels.ml_scatter && chunk_idx < chunks.len() {
panels::ml_scatter::draw(f, app, chunks[chunk_idx]);
chunk_idx += 1;
}
if app.panels.trace_waterfall && chunk_idx < chunks.len() {
panels::trace_waterfall::draw(f, app, chunks[chunk_idx]);
chunk_idx += 1;
}
if app.panels.stats_summary && chunk_idx < chunks.len() {
panels::stats_summary::draw(f, app, chunks[chunk_idx]);
}
}
fn draw_empty_state(f: &mut Frame, area: Rect) {
let text = vec![
Line::from("No panels visible"),
Line::from(""),
Line::from("Press 1-6 to toggle panels, ? for help"),
];
let paragraph = Paragraph::new(text).style(Style::default().fg(Color::DarkGray)).block(
Block::default()
.borders(Borders::ALL)
.border_type(borders::STYLE)
.title(" renacer visualize "),
);
f.render_widget(paragraph, area);
}
fn draw_help_overlay(f: &mut Frame, area: Rect) {
let popup_area = centered_rect(60, 70, area);
f.render_widget(Clear, popup_area);
let help_text = vec![
Line::from(Span::styled(
"Keyboard Shortcuts",
Style::default().add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(Span::styled("Navigation", Style::default().add_modifier(Modifier::UNDERLINED))),
Line::from(" j/↓ Move down"),
Line::from(" k/↑ Move up"),
Line::from(" g Go to top"),
Line::from(" G Go to bottom"),
Line::from(" PgUp Page up"),
Line::from(" PgDn Page down"),
Line::from(""),
Line::from(Span::styled("Panels", Style::default().add_modifier(Modifier::UNDERLINED))),
Line::from(" 1 Toggle Syscall Heatmap"),
Line::from(" 2 Toggle Anomaly Timeline"),
Line::from(" 3 Toggle ML Clusters"),
Line::from(" 4 Toggle Trace Waterfall"),
Line::from(" 5 Toggle Process Syscalls"),
Line::from(" 6 Toggle Stats Summary"),
Line::from(" 0 Reset all panels"),
Line::from(""),
Line::from(Span::styled(
"Sorting & Filtering",
Style::default().add_modifier(Modifier::UNDERLINED),
)),
Line::from(" s/Tab Cycle sort column"),
Line::from(" r Reverse sort order"),
Line::from(" f/ Open filter"),
Line::from(" Del Clear filter"),
Line::from(""),
Line::from(Span::styled("General", Style::default().add_modifier(Modifier::UNDERLINED))),
Line::from(" ?/F1 Toggle this help"),
Line::from(" q Quit"),
Line::from(" Esc Close overlay"),
];
let help = Paragraph::new(help_text)
.style(Style::default().fg(Color::White))
.block(
Block::default()
.borders(Borders::ALL)
.border_type(borders::STYLE)
.border_style(Style::default().fg(borders::HELP))
.title(" Help "),
)
.wrap(Wrap { trim: false });
f.render_widget(help, popup_area);
}
fn draw_filter_input(f: &mut Frame, app: &VisualizeApp, area: Rect) {
let popup_area = centered_rect(40, 10, area);
f.render_widget(Clear, popup_area);
let filter_text = vec![
Line::from("Filter processes:"),
Line::from(""),
Line::from(Span::styled(format!(">{}_", app.filter), Style::default().fg(Color::Yellow))),
Line::from(""),
Line::from(Span::styled(
"Enter to confirm, Esc to cancel",
Style::default().fg(Color::DarkGray),
)),
];
let filter = Paragraph::new(filter_text).style(Style::default().fg(Color::White)).block(
Block::default()
.borders(Borders::ALL)
.border_type(borders::STYLE)
.border_style(Style::default().fg(Color::Yellow))
.title(" Filter "),
);
f.render_widget(filter, popup_area);
}
fn draw_fps_overlay(f: &mut Frame, app: &VisualizeApp, area: Rect) {
let fps_text = format!(
" Frame: {} | Avg: {}us | Max: {}us ",
app.frame_id, app.avg_frame_time_us, app.max_frame_time_us
);
let text_len = fps_text.len();
let fps = Paragraph::new(fps_text).style(Style::default().fg(Color::DarkGray).bg(Color::Black));
let fps_area = Rect {
x: area.width.saturating_sub(text_len as u16 + 2),
y: 0,
width: text_len as u16 + 2,
height: 1,
};
f.render_widget(fps, fps_area);
}
fn draw_trace_complete_overlay(f: &mut Frame, area: Rect) {
let text = " TRACE COMPLETE - Press 'q' to exit ";
let text_len = text.len() as u16;
let banner = Paragraph::new(text)
.style(Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD));
let x = area.width.saturating_sub(text_len) / 2;
let banner_area = Rect { x, y: area.height.saturating_sub(1), width: text_len, height: 1 };
f.render_widget(banner, banner_area);
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::visualize::app::VisualizeApp;
use crate::visualize::VisualizeConfig;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
fn buffer_to_string(buffer: &ratatui::buffer::Buffer) -> String {
let mut s = String::new();
for y in 0..buffer.area.height {
for x in 0..buffer.area.width {
s.push(buffer[(x, y)].symbol().chars().next().unwrap_or(' '));
}
s.push('\n');
}
s
}
#[test]
fn test_centered_rect() {
let area = Rect::new(0, 0, 100, 50);
let centered = centered_rect(50, 50, area);
assert!(centered.x > 0);
assert!(centered.y > 0);
assert!(centered.width < 100);
assert!(centered.height < 50);
}
#[test]
fn test_draw_empty_state() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
draw_empty_state(f, f.area());
})
.unwrap();
let buffer = terminal.backend().buffer();
let content = buffer_to_string(buffer);
assert!(content.contains("No panels visible"));
}
#[test]
fn test_draw_help_overlay() {
let backend = TestBackend::new(100, 40);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
draw_help_overlay(f, f.area());
})
.unwrap();
let buffer = terminal.backend().buffer();
let content = buffer_to_string(buffer);
assert!(content.contains("Keyboard Shortcuts"));
assert!(content.contains("Navigation"));
}
#[test]
fn test_draw_filter_input() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
let app = VisualizeApp::new(VisualizeConfig::default());
terminal
.draw(|f| {
draw_filter_input(f, &app, f.area());
})
.unwrap();
let buffer = terminal.backend().buffer();
let content = buffer_to_string(buffer);
assert!(content.contains("Filter"));
}
#[test]
fn test_draw_fps_overlay() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
let app = VisualizeApp::new(VisualizeConfig::default());
terminal
.draw(|f| {
draw_fps_overlay(f, &app, f.area());
})
.unwrap();
let buffer = terminal.backend().buffer();
let content = buffer_to_string(buffer);
assert!(content.contains("Frame"));
}
#[test]
fn test_draw_trace_complete_overlay() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
draw_trace_complete_overlay(f, f.area());
})
.unwrap();
let buffer = terminal.backend().buffer();
let content = buffer_to_string(buffer);
assert!(content.contains("TRACE COMPLETE"));
}
#[test]
fn test_draw_with_zero_size() {
let backend = TestBackend::new(0, 0);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
let _ = terminal.draw(|f| {
draw(f, &mut app);
});
}
#[test]
fn test_draw_with_all_panels_disabled() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.panels.syscall_heatmap = false;
app.panels.anomaly_timeline = false;
app.panels.ml_scatter = false;
app.panels.trace_waterfall = false;
app.panels.stats_summary = false;
app.panels.process_syscalls = false;
terminal
.draw(|f| {
draw(f, &mut app);
})
.unwrap();
let buffer = terminal.backend().buffer();
let content = buffer_to_string(buffer);
assert!(content.contains("No panels visible"));
}
#[test]
fn test_draw_with_help_visible() {
let backend = TestBackend::new(100, 40);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.show_help = true;
terminal
.draw(|f| {
draw(f, &mut app);
})
.unwrap();
let buffer = terminal.backend().buffer();
let content = buffer_to_string(buffer);
assert!(content.contains("Keyboard Shortcuts"));
}
#[test]
fn test_draw_with_filter_visible() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.show_filter_input = true;
app.filter = "read".to_string();
terminal
.draw(|f| {
draw(f, &mut app);
})
.unwrap();
let buffer = terminal.backend().buffer();
let content = buffer_to_string(buffer);
assert!(content.contains("Filter"));
}
#[test]
fn test_draw_with_fps_visible() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig { show_fps: true, ..Default::default() });
terminal
.draw(|f| {
draw(f, &mut app);
})
.unwrap();
let buffer = terminal.backend().buffer();
let content = buffer_to_string(buffer);
assert!(content.contains("Frame"));
}
#[test]
fn test_draw_with_trace_complete() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.trace_complete = true;
terminal
.draw(|f| {
draw(f, &mut app);
})
.unwrap();
let buffer = terminal.backend().buffer();
let content = buffer_to_string(buffer);
assert!(content.contains("TRACE COMPLETE"));
}
#[test]
fn test_draw_top_panels_one() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.panels.syscall_heatmap = true;
app.panels.anomaly_timeline = false;
app.panels.ml_scatter = false;
app.panels.trace_waterfall = false;
app.panels.stats_summary = false;
terminal
.draw(|f| {
draw_top_panels(f, &mut app, f.area());
})
.unwrap();
}
#[test]
fn test_draw_top_panels_two() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.panels.syscall_heatmap = true;
app.panels.anomaly_timeline = true;
app.panels.ml_scatter = false;
app.panels.trace_waterfall = false;
app.panels.stats_summary = false;
terminal
.draw(|f| {
draw_top_panels(f, &mut app, f.area());
})
.unwrap();
}
#[test]
fn test_draw_top_panels_three() {
let backend = TestBackend::new(120, 24);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.panels.syscall_heatmap = true;
app.panels.anomaly_timeline = true;
app.panels.ml_scatter = true;
app.panels.trace_waterfall = false;
app.panels.stats_summary = false;
terminal
.draw(|f| {
draw_top_panels(f, &mut app, f.area());
})
.unwrap();
}
#[test]
fn test_draw_top_panels_four() {
let backend = TestBackend::new(120, 24);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.panels.syscall_heatmap = true;
app.panels.anomaly_timeline = true;
app.panels.ml_scatter = true;
app.panels.trace_waterfall = true;
app.panels.stats_summary = false;
terminal
.draw(|f| {
draw_top_panels(f, &mut app, f.area());
})
.unwrap();
}
#[test]
fn test_draw_top_panels_five() {
let backend = TestBackend::new(150, 24);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.panels.syscall_heatmap = true;
app.panels.anomaly_timeline = true;
app.panels.ml_scatter = true;
app.panels.trace_waterfall = true;
app.panels.stats_summary = true;
terminal
.draw(|f| {
draw_top_panels(f, &mut app, f.area());
})
.unwrap();
}
#[test]
fn test_draw_top_panels_zero() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.panels.syscall_heatmap = false;
app.panels.anomaly_timeline = false;
app.panels.ml_scatter = false;
app.panels.trace_waterfall = false;
app.panels.stats_summary = false;
terminal
.draw(|f| {
draw_top_panels(f, &mut app, f.area());
})
.unwrap();
}
#[test]
fn test_draw_with_process_syscalls() {
let backend = TestBackend::new(80, 40);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = VisualizeApp::new(VisualizeConfig::default());
app.panels.process_syscalls = true;
terminal
.draw(|f| {
draw(f, &mut app);
})
.unwrap();
}
#[test]
fn test_centered_rect_edge_cases() {
let area = Rect::new(0, 0, 10, 10);
let centered = centered_rect(80, 80, area);
assert!(centered.width > 0);
assert!(centered.height > 0);
let centered = centered_rect(0, 0, area);
assert_eq!(centered.width, 0);
assert_eq!(centered.height, 0);
let centered = centered_rect(100, 100, area);
assert_eq!(centered.x, 0);
assert_eq!(centered.y, 0);
}
}