use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, symbols, text::{Line, Span}, widgets::{Axis, Block, Borders, Chart, Dataset, List, ListItem, Paragraph, Tabs}, Frame, }; use crate::{App, AppTab}; pub fn draw(f: &mut Frame, app: &mut App) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ]) .split(f.area()); // Draw header with tabs let tab_items = vec![ Line::from(Span::styled( "Process List", Style::default().fg(Color::White), )), Line::from(Span::styled( "Detailed View", Style::default().fg(Color::White), )), ]; let tabs = Tabs::new(tab_items) .block( Block::default() .borders(Borders::ALL) .title("Energy Monitor"), ) .select(match app.selected_tab { AppTab::ProcessList => 0, AppTab::DetailedView(_) => 1, }) .style(Style::default().fg(Color::White)) .highlight_style(Style::default().fg(Color::Yellow)); f.render_widget(tabs, chunks[0]); // Draw content based on selected tab match app.selected_tab { AppTab::ProcessList => draw_process_list(f, app, chunks[1]), AppTab::DetailedView(pid) => draw_detailed_view(f, app, chunks[1], pid), } // Draw footer with instructions let footer_spans = vec![ Span::styled("q", Style::default().fg(Color::Yellow)), Span::raw(" Quit | "), Span::styled("↑/k ↓/j", Style::default().fg(Color::Yellow)), Span::raw(" Navigate | "), Span::styled("Space", Style::default().fg(Color::Yellow)), Span::raw(" Select | "), Span::styled("Tab", Style::default().fg(Color::Yellow)), Span::raw(" Change Tab"), Span::styled("+/-", Style::default().fg(Color::Yellow)), Span::raw(" Adjust Power Limit"), ]; let footer = Paragraph::new(Line::from(footer_spans)).block(Block::default().borders(Borders::ALL)); f.render_widget(footer, chunks[2]); } fn draw_process_list(f: &mut Frame, app: &mut App, area: Rect) { let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) .split(area); // Draw process list let processes: Vec = app .process_data .processes .iter() .map(|pid| { let p = &app.process_data.process_info[pid]; let is_selected = app.selected_processes.contains(&p.pid); let style = if is_selected { Style::default().fg(app .process_colors .get(&p.pid) .copied() .unwrap_or(Color::White)) } else { Style::default() }; let content = vec![ Span::styled( format!("{}{:>5} ", if is_selected { "* " } else { " " }, p.pid), style, ), Span::styled( format!("{:20}", p.name.chars().take(20).collect::()), style, ), Span::styled(format!(" {:8.2}J", p.energy), style), Span::styled(format!(" {:8.2}J", p.tree_energy), style), Span::styled( format!(" {:8.2}W", p.history.last().map(|(_, x)| x).unwrap_or(&0.)), style, ), ]; let line = Line::from(content); ListItem::new(line) }) .collect(); let processes_list = List::new(processes) .block( Block::default() .borders(Borders::ALL) .title("Processes by Energy"), ) .highlight_style(Style::default().add_modifier(Modifier::REVERSED)) .highlight_symbol("> "); f.render_stateful_widget(processes_list, chunks[0], &mut app.table_state); // Draw energy graph draw_energy_graph(f, app, chunks[1]); } fn draw_detailed_view(f: &mut Frame, app: &mut App, area: Rect, selected_pid: Option) { if let Some(pid) = selected_pid.as_ref() { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(5), Constraint::Min(0)]) .split(area); // Draw process details if let Some(process) = app.process_data.process_info.get(pid) { let details = vec![ Line::from(vec![ Span::raw("PID: "), Span::styled( format!("{}", process.pid), Style::default().fg(Color::Yellow), ), ]), Line::from(vec![ Span::raw("Name: "), Span::styled(&process.name, Style::default().fg(Color::Yellow)), ]), Line::from(vec![ Span::raw("Energy: "), Span::styled( format!("{:.2}J", process.energy), Style::default().fg(Color::Yellow), ), ]), Line::from(vec![ Span::raw("Tree Energy: "), Span::styled( format!("{:.2}J", process.tree_energy), Style::default().fg(Color::Yellow), ), ]), ]; let details_widget = Paragraph::new(details).block( Block::default() .borders(Borders::ALL) .title("Process Details"), ); f.render_widget(details_widget, chunks[0]); // Draw process energy history graph let start_time = if let Some((first_time, _)) = process.history.first() { *first_time } else { 0.0 }; let end_time = if let Some((last_time, _)) = process.history.last() { *last_time } else { start_time + 60.0 }; let dataset = Dataset::default() .name(process.name.clone()) .marker(symbols::Marker::HalfBlock) .style( Style::default().fg(app .process_colors .get(pid) .copied() .unwrap_or(Color::Yellow)), ) .graph_type(ratatui::widgets::GraphType::Line) .data(&process.history); let chart = Chart::new(vec![dataset]) .block( Block::default() .borders(Borders::ALL) .title("Energy Usage History"), ) .x_axis( Axis::default() .title("Time (s)") .style(Style::default().fg(Color::Gray)) .bounds([start_time, end_time]) .labels(vec![ Span::styled( format!("{:.0}", start_time), Style::default().fg(Color::White), ), Span::styled( format!("{:.0}", (start_time + end_time) / 2.0), Style::default().fg(Color::White), ), Span::styled( format!("{:.0}", end_time), Style::default().fg(Color::White), ), ]), ) .y_axis( Axis::default() .title("Energy (J)") .style(Style::default().fg(Color::Gray)) .bounds([ 0.0, process .history .iter() .map(|(_, e)| *e) .fold(0.0f64, f64::max) * 1.2, ]) .labels(vec![ Span::styled("0", Style::default().fg(Color::White)), Span::styled( format!( "{:.2}", process .history .iter() .map(|(_, e)| *e) .fold(0.0f64, f64::max) / 2.0 ), Style::default().fg(Color::White), ), Span::styled( format!( "{:.2}", process .history .iter() .map(|(_, e)| *e) .fold(0.0f64, f64::max) ), Style::default().fg(Color::White), ), ]), ); f.render_widget(chart, chunks[1]); } } else { let message = Paragraph::new("No process selected") .block(Block::default().borders(Borders::ALL)) .alignment(ratatui::layout::Alignment::Center); f.render_widget(message, area); } } fn draw_energy_graph(f: &mut Frame, app: &mut App, area: Rect) { // Create datasets for each selected process and the total let mut datasets = Vec::new(); // Add total system energy dataset if !app.process_data.total_energy_history.is_empty() { datasets.push( Dataset::default() .name("Total System Energy") .marker(symbols::Marker::HalfBlock) .style(Style::default().fg(Color::White)) .graph_type(ratatui::widgets::GraphType::Line) .data(&app.process_data.total_energy_history), ); } // Add selected processes datasets for pid in &app.selected_processes { if let Some(process) = app.process_data.process_info.get(pid) { if !process.history.is_empty() { datasets.push( Dataset::default() .name(process.name.clone()) .marker(symbols::Marker::Dot) .style( Style::default().fg(app .process_colors .get(pid) .copied() .unwrap_or(Color::White)), ) .graph_type(ratatui::widgets::GraphType::Line) .data(&process.history), ); } } } // Get time bounds let start_time = app .process_data .total_energy_history .first() .map(|(t, _)| *t) .unwrap_or(0.0); let end_time = app .process_data .total_energy_history .last() .map(|(t, _)| *t) .unwrap_or(start_time + 60.0); // Add power limit line as a horizontal line across the chart if app.power_limit > 0.0 { datasets.push( Dataset::default() .name(format!("Power Limit ({:.1}W)", app.power_limit)) .marker(symbols::Marker::Bar) .style(Style::default().fg(Color::Red)) .graph_type(ratatui::widgets::GraphType::Line) .data(&app.power_limit_history), ); } let max_total = app .process_data .total_energy_history .iter() .map(|(_, e)| *e) .fold(0.0f64, f64::max); let max_processes = app .selected_processes .iter() .filter_map(|pid| app.process_data.process_info.get(pid)) .flat_map(|process| process.history.iter().map(|(_, e)| *e)) .fold(0.0f64, f64::max); // Ensure the y-axis upper bound includes the power limit, if set let mut max_energy = f64::max(max_total, max_processes); if app.power_limit > 0.0 { max_energy = f64::max(max_energy, app.power_limit); } // Create chart widget let chart = Chart::new(datasets) .block(Block::default().borders(Borders::ALL).title(format!( "Energy Usage (Power Limit: {:.1}W)", app.power_limit ))) .x_axis( Axis::default() .title("Time (s)") .style(Style::default().fg(Color::Gray)) .bounds([start_time, end_time]) .labels(vec![ Span::styled( format!("{:.0}", start_time), Style::default().fg(Color::White), ), Span::styled( format!("{:.0}", (start_time + end_time) / 2.0), Style::default().fg(Color::White), ), Span::styled( format!("{:.0}", end_time), Style::default().fg(Color::White), ), ]), ) .y_axis( Axis::default() .title("Power (W)") .style(Style::default().fg(Color::Gray)) .bounds([0.0, max_energy * 1.2]) .labels(vec![ Span::styled("0", Style::default().fg(Color::White)), Span::styled( format!("{:.2}", max_energy * 1.2 / 2.0), Style::default().fg(Color::White), ), Span::styled( format!("{:.2}", max_energy * 1.2), Style::default().fg(Color::White), ), ]), ); f.render_widget(chart, area); }