rustpm 0.2.2

A fast, friendly APT frontend with kernel, desktop, and sources management
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) {
        // Split into table + summary + action bar
        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]);
    }
}