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};
use crate::apt::query::PackageInfo;
pub struct PackageTable {
pub packages: Vec<PackageInfo>,
pub changes: Vec<PackageChange>,
pub state: TableState,
pub mode: PackageTableMode,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PackageTableMode {
Browse,
UpgradeDiff,
}
impl PackageTable {
pub fn new(packages: Vec<PackageInfo>, changes: Vec<PackageChange>) -> Self {
let mode = if changes.is_empty() {
PackageTableMode::Browse
} else {
PackageTableMode::UpgradeDiff
};
let mut state = TableState::default();
if !packages.is_empty() || !changes.is_empty() {
state.select(Some(0));
}
Self { packages, changes, state, mode }
}
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 = match self.mode {
PackageTableMode::Browse => self.packages.len(),
PackageTableMode::UpgradeDiff => 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) {
match self.mode {
PackageTableMode::Browse => self.render_browse(f, area),
PackageTableMode::UpgradeDiff => self.render_diff(f, area),
}
}
fn render_browse(&self, f: &mut Frame, area: Rect) {
let rows: Vec<Row> = self
.packages
.iter()
.map(|p| {
let installed = p.installed_version.as_deref().unwrap_or("—");
let candidate = p.candidate_version.as_deref().unwrap_or("—");
let style = if p.installed_version.is_some() && p.candidate_version.is_some() {
Style::default().fg(Color::Yellow)
} else if p.installed_version.is_some() {
Style::default().fg(Color::Green)
} else {
Style::default()
};
Row::new(vec![
Cell::from(p.name.clone()),
Cell::from(installed),
Cell::from(candidate),
Cell::from(p.description.as_deref().unwrap_or("").chars().take(60).collect::<String>()),
])
.style(style)
})
.collect();
let widths = [
Constraint::Length(30),
Constraint::Length(20),
Constraint::Length(20),
Constraint::Min(20),
];
let table = Table::new(rows, widths)
.header(
Row::new(vec!["Package", "Installed", "Available", "Description"])
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
)
.block(Block::default().borders(Borders::ALL).title(" Packages "))
.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, area, &mut state);
}
fn render_diff(&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 rows: Vec<Row> = self
.changes
.iter()
.filter(|c| c.kind != ChangeKind::Configure)
.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 table = Table::new(rows, widths)
.header(
Row::new(vec!["Action", "Package", "Old Version", "New Version"])
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
)
.block(Block::default().borders(Borders::ALL).title(" Pending Changes "))
.row_highlight_style(Style::default().bg(Color::DarkGray));
let mut state = self.state.clone();
f.render_stateful_widget(table, chunks[0], &mut state);
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 upgrades = self.changes.iter().filter(|c| c.kind == ChangeKind::Upgrade).count();
let summary = format!(
" {} to install {} to remove {} to upgrade",
installs, removes, upgrades
);
let summary_widget = Paragraph::new(summary)
.block(Block::default().borders(Borders::ALL).title(" Summary "));
f.render_widget(summary_widget, chunks[1]);
let actions = Paragraph::new(" [Y] Proceed [n] Abort [?] Help")
.block(Block::default().borders(Borders::ALL).title(" Actions "));
f.render_widget(actions, chunks[2]);
}
}