use std::path::Path;
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
symbols::Marker,
text::{Line, Span},
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, Paragraph, Wrap},
Frame,
};
use crate::process::display_name;
use crate::process::info::ProcessInfo;
use super::format::{format_duration_full, format_memory};
use super::styles::{GraphStyle, Palette};
#[allow(clippy::too_many_arguments)]
fn render_chart(
f: &mut Frame,
area: Rect,
title: &str,
points: &[(f64, f64)],
y_max: f64,
y_labels: Vec<String>,
style: Style,
graph_style: GraphStyle,
palette: &Palette,
) {
let (marker, graph_type) = match graph_style {
GraphStyle::Dots => (Marker::Dot, GraphType::Scatter),
GraphStyle::Bars => (Marker::Bar, GraphType::Bar),
};
let dataset = Dataset::default()
.data(points)
.marker(marker)
.graph_type(graph_type)
.style(style);
let x_max = (points.len().saturating_sub(1)).max(1) as f64;
let x_axis = Axis::default().bounds([0.0, x_max]);
let y_axis = Axis::default()
.bounds([0.0, y_max])
.labels(y_labels)
.style(palette.label_style());
let block = Block::default()
.title(title)
.title_style(palette.title_style())
.borders(Borders::ALL)
.border_style(palette.border_style());
let chart = Chart::new(vec![dataset])
.block(block)
.x_axis(x_axis)
.y_axis(y_axis);
f.render_widget(chart, area);
}
fn kv<'a>(key: &'a str, val: &'a str, palette: &Palette) -> Line<'a> {
Line::from(vec![
Span::styled(key, palette.label_style()),
Span::styled(val, Style::new().fg(palette.foreground)),
])
}
fn basename(path: &str) -> &str {
Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("—")
}
fn git_branch(cwd: &str) -> Option<String> {
let head = std::fs::read_to_string(Path::new(cwd).join(".git/HEAD")).ok()?;
let trimmed = head.trim();
if let Some(rest) = trimmed.strip_prefix("ref: refs/heads/") {
Some(rest.to_string())
} else if trimmed.len() >= 7 {
Some(format!("({})", &trimmed[..7]))
} else {
None
}
}
fn render_info_table(f: &mut Frame, area: Rect, info: &ProcessInfo, palette: &Palette) {
let parent = info.parent_pid.map(|p| p.to_string()).unwrap_or_else(|| "—".into());
let exe = info.exe_path.as_deref().unwrap_or("—");
let cwd = info.cwd.as_deref().unwrap_or("—");
let project = info.cwd.as_deref().map(basename).unwrap_or("—");
let branch = info
.cwd
.as_deref()
.and_then(git_branch)
.unwrap_or_else(|| "—".into());
let env_str = info.environ_count.to_string();
let pid_str = info.pid.to_string();
let runtime_str = format_duration_full(info.run_time);
let lines = vec![
kv("PID: ", &pid_str, palette),
kv("Parent PID: ", &parent, palette),
kv("Name: ", display_name(info), palette),
kv("Status: ", &info.status, palette),
kv("Project: ", project, palette),
kv("Git Branch: ", &branch, palette),
kv("Working Dir: ", cwd, palette),
kv("Exe Path: ", exe, palette),
kv("Env Vars: ", &env_str, palette),
kv("Run Time: ", &runtime_str, palette),
];
let block = Block::default()
.title(" Info ")
.title_style(palette.title_style())
.borders(Borders::ALL)
.border_style(palette.border_style());
f.render_widget(Paragraph::new(lines).block(block), area);
}
pub fn render_detail_view(
f: &mut Frame,
area: Rect,
info: &ProcessInfo,
cpu_history: &[f32],
mem_history: &[u64],
graph_style: GraphStyle,
palette: &Palette,
) {
let sections = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Length(12), Constraint::Fill(1), Constraint::Fill(1), Constraint::Length(4), ])
.split(area);
render_header(f, sections[0], info, palette);
render_info_table(f, sections[1], info, palette);
let cpu_points: Vec<(f64, f64)> = cpu_history
.iter()
.enumerate()
.map(|(i, &v)| (i as f64, v as f64))
.collect();
let cpu_observed_max = cpu_history.iter().copied().fold(0.0_f32, f32::max) as f64;
let cpu_axis_max = (cpu_observed_max * 1.15).max(1.0);
let cpu_labels = vec!["0".to_string(), format!("{:.1}%", cpu_observed_max)];
let cpu_title = format!(" cpu {:.2}% ", info.cpu_usage);
render_chart(
f,
sections[2],
&cpu_title,
&cpu_points,
cpu_axis_max,
cpu_labels,
palette.claude_style(),
graph_style,
palette,
);
let mem_points: Vec<(f64, f64)> = mem_history
.iter()
.enumerate()
.map(|(i, &b)| (i as f64, b as f64 / (1024.0 * 1024.0)))
.collect();
let mem_observed_max = mem_history.iter().copied().max().unwrap_or(0) as f64 / (1024.0 * 1024.0);
let mem_axis_max = (mem_observed_max * 1.15).max(1.0);
let mem_labels = vec!["0".to_string(), format!("{:.1} MB", mem_observed_max)];
let mem_title = format!(" memory {} ", format_memory(info.memory_bytes));
render_chart(
f,
sections[3],
&mem_title,
&mem_points,
mem_axis_max,
mem_labels,
palette.codex_style(),
graph_style,
palette,
);
render_command(f, sections[4], info, palette);
}
fn render_header(f: &mut Frame, area: Rect, info: &ProcessInfo, palette: &Palette) {
let title = format!(" {} — PID {} — {} ", display_name(info), info.pid, info.status);
let block = Block::default()
.title(title)
.title_style(palette.title_style().add_modifier(Modifier::BOLD))
.borders(Borders::ALL)
.border_style(palette.border_style());
f.render_widget(block, area);
}
fn render_command(f: &mut Frame, area: Rect, info: &ProcessInfo, palette: &Palette) {
let cmd_text = info.cmd.join(" ");
let block = Block::default()
.title(" Command ")
.title_style(palette.title_style())
.borders(Borders::ALL)
.border_style(palette.border_style());
f.render_widget(
Paragraph::new(cmd_text)
.block(block)
.style(Style::new().fg(palette.foreground))
.wrap(Wrap { trim: false }),
area,
);
}