use crossterm::event::KeyCode;
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState},
Frame,
};
use crate::apt::parser::{ChangeKind, PackageChange};
pub struct UpdatePanel {
pub changes: Vec<PackageChange>,
pub state: TableState,
}
impl UpdatePanel {
pub fn new(changes: Vec<PackageChange>) -> Self {
let mut state = TableState::default();
if !changes.is_empty() {
state.select(Some(0));
}
Self { changes, state }
}
pub fn handle_key(&mut self, code: KeyCode) {
match code {
KeyCode::Char('j') | KeyCode::Down => self.next(1),
KeyCode::Char('k') | KeyCode::Up => self.prev(1),
KeyCode::PageDown => self.next(10),
KeyCode::PageUp => self.prev(10),
_ => {}
}
}
fn next(&mut self, step: usize) {
let len = self.changes.len();
if len == 0 { return; }
let i = self.state.selected().map_or(0, |i| (i + step).min(len - 1));
self.state.select(Some(i));
}
fn prev(&mut self, step: usize) {
let i = self.state.selected().map_or(0, |i| i.saturating_sub(step));
self.state.select(Some(i));
}
pub fn render(&self, f: &mut Frame, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(5),
Constraint::Length(3),
Constraint::Length(3),
])
.split(area);
let visible: Vec<&PackageChange> = self.changes.iter()
.filter(|c| c.kind != ChangeKind::Configure)
.collect();
let rows: Vec<Row> = visible.iter().map(|c| {
let (action, style) = match c.kind {
ChangeKind::Install => ("install", Style::default().fg(Color::Green)),
ChangeKind::Remove => ("remove", Style::default().fg(Color::Red)),
ChangeKind::Upgrade => ("upgrade", Style::default().fg(Color::Yellow)),
ChangeKind::Configure => ("configure", Style::default().fg(Color::DarkGray)),
};
let old = c.old_version.as_deref().unwrap_or("(new)");
let new = c.new_version.as_deref().unwrap_or("—");
Row::new(vec![
Cell::from(action),
Cell::from(c.name.clone()),
Cell::from(old),
Cell::from(new),
])
.style(style)
}).collect();
let widths = [
Constraint::Length(10),
Constraint::Length(30),
Constraint::Length(20),
Constraint::Length(20),
];
let title = if self.changes.is_empty() {
" Available Updates — System is up to date "
} else {
" Available Updates "
};
let table = Table::new(rows, widths)
.header(
Row::new(vec!["Action", "Package", "Current Version", "New Version"])
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
)
.block(Block::default().borders(Borders::ALL).title(title))
.row_highlight_style(Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD))
.highlight_symbol("▶ ");
let mut state = self.state.clone();
f.render_stateful_widget(table, chunks[0], &mut state);
let upgrades = self.changes.iter().filter(|c| c.kind == ChangeKind::Upgrade).count();
let installs = self.changes.iter().filter(|c| c.kind == ChangeKind::Install).count();
let removes = self.changes.iter().filter(|c| c.kind == ChangeKind::Remove).count();
let summary = if self.changes.is_empty() {
" System is up to date".to_string()
} else {
format!(" {} to upgrade {} to install {} to remove", upgrades, installs, removes)
};
f.render_widget(
Paragraph::new(summary).block(Block::default().borders(Borders::ALL).title(" Summary ")),
chunks[1],
);
f.render_widget(
Paragraph::new(" [u] Run upgrade [?] Help")
.block(Block::default().borders(Borders::ALL).title(" Actions ")),
chunks[2],
);
}
}