rustpm 0.2.2

A fast, friendly APT frontend with kernel, desktop, and sources management
use anyhow::Result;
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::{
    backend::Backend,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Clear, Paragraph, Tabs},
    Frame, Terminal,
};
use std::sync::mpsc;
use std::thread;

use crate::apt::executor::{AptOperation, execute};
use crate::apt::query::PackageInfo;
use crate::apt::parser::PackageChange;
use crate::desktop::manager::InstalledStatus;
use crate::history::HistoryEntry;
use crate::kernel::detector::KernelEntry;
use crate::kernel::vanilla::VanillaRelease;
use crate::sources::parser::AptSource;

use super::events::{AppEvent, EventHandler};
use super::widgets::{
    desktop_panel::DesktopPanel,
    history_panel::HistoryPanel,
    kernel_panel::KernelPanel,
    package_table::PackageTable,
    search_panel::SearchPanel,
    sources_panel::SourcesPanel,
    update_panel::UpdatePanel,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tab {
    Packages = 0,
    Updates = 1,
    Kernels = 2,
    Desktops = 3,
    Sources = 4,
    History = 5,
}

impl Tab {
    pub fn from_index(i: usize) -> Option<Self> {
        match i {
            0 => Some(Tab::Packages),
            1 => Some(Tab::Updates),
            2 => Some(Tab::Kernels),
            3 => Some(Tab::Desktops),
            4 => Some(Tab::Sources),
            5 => Some(Tab::History),
            _ => None,
        }
    }

    pub fn title(&self) -> &'static str {
        match self {
            Tab::Packages => "Packages",
            Tab::Updates => "Updates",
            Tab::Kernels => "Kernels",
            Tab::Desktops => "Desktops",
            Tab::Sources => "Sources",
            Tab::History => "History",
        }
    }

    pub fn all() -> &'static [Tab] {
        &[Tab::Packages, Tab::Updates, Tab::Kernels, Tab::Desktops, Tab::Sources, Tab::History]
    }
}

pub struct App {
    pub active_tab: Tab,
    pub status_messages: Vec<String>,
    pub should_quit: bool,
    pub show_help: bool,
    apt_tx: Option<mpsc::Sender<String>>,

    pub package_table: PackageTable,
    pub update_panel: UpdatePanel,
    pub search_panel: SearchPanel,
    pub kernel_panel: KernelPanel,
    pub desktop_panel: DesktopPanel,
    pub sources_panel: SourcesPanel,
    pub history_panel: HistoryPanel,
}

impl App {
    pub fn new(
        packages: Vec<PackageInfo>,
        changes: Vec<PackageChange>,
        upgrade_changes: Vec<PackageChange>,
        kernels: Vec<KernelEntry>,
        vanilla: Vec<VanillaRelease>,
        desktops: Vec<InstalledStatus>,
        sources: Vec<AptSource>,
        history: Vec<HistoryEntry>,
    ) -> Self {
        Self {
            active_tab: Tab::Packages,
            status_messages: Vec::new(),
            should_quit: false,
            show_help: false,
            apt_tx: None,
            package_table: PackageTable::new(packages, changes),
            update_panel: UpdatePanel::new(upgrade_changes),
            search_panel: SearchPanel::new(),
            kernel_panel: KernelPanel::new(kernels, vanilla),
            desktop_panel: DesktopPanel::new(desktops),
            sources_panel: SourcesPanel::new(sources),
            history_panel: HistoryPanel::new(history),
        }
    }

    pub fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<()> {
        let (apt_tx, apt_rx) = mpsc::channel::<String>();
        self.apt_tx = Some(apt_tx);
        let handler = EventHandler::new(Some(apt_rx));

        loop {
            terminal.draw(|f| self.draw(f))?;

            match handler.next()? {
                AppEvent::Key(key) => {
                    if self.handle_global_key(key.code, key.modifiers) {
                        continue;
                    }
                    // Run upgrade from Updates tab
                    if self.active_tab == Tab::Updates && key.code == KeyCode::Char('u') {
                        self.spawn_upgrade();
                        continue;
                    }
                    // Delegate to active tab
                    match self.active_tab {
                        Tab::Packages => self.package_table.handle_key(key.code),
                        Tab::Updates  => self.update_panel.handle_key(key.code),
                        Tab::Kernels  => self.kernel_panel.handle_key(key.code),
                        Tab::Desktops => self.desktop_panel.handle_key(key.code),
                        Tab::Sources  => self.sources_panel.handle_key(key.code),
                        Tab::History  => self.history_panel.handle_key(key.code),
                    }
                }
                AppEvent::AptOutput(line) => {
                    self.status_messages.push(line);
                    if self.status_messages.len() > 100 {
                        self.status_messages.drain(0..1);
                    }
                }
                AppEvent::Tick => {}
            }

            if self.should_quit {
                break;
            }
        }

        Ok(())
    }

    fn spawn_upgrade(&mut self) {
        if let Some(tx) = self.apt_tx.clone() {
            let _ = tx.send("Running apt-get upgrade...".into());
            thread::spawn(move || {
                let op = AptOperation::Upgrade { full: false };
                match execute(&op, Some(tx.clone())) {
                    Ok(status) if status.success() => {
                        let _ = tx.send("Upgrade complete.".into());
                    }
                    Ok(_) => {
                        let _ = tx.send("Upgrade failed (check you have root privileges).".into());
                    }
                    Err(e) => {
                        let _ = tx.send(format!("Upgrade error: {}", e));
                    }
                }
            });
        }
    }

    /// Returns true if the key was consumed globally.
    fn handle_global_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> bool {
        match code {
            KeyCode::Char('q') => {
                if self.show_help {
                    self.show_help = false;
                } else {
                    self.should_quit = true;
                }
                true
            }
            KeyCode::Char('?') => {
                self.show_help = !self.show_help;
                true
            }
            KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => {
                self.should_quit = true;
                true
            }
            KeyCode::Tab => {
                let idx = self.active_tab as usize;
                let next = (idx + 1) % Tab::all().len();
                self.active_tab = Tab::from_index(next).unwrap_or(Tab::Packages);
                true
            }
            KeyCode::BackTab => {
                let idx = self.active_tab as usize;
                let prev = if idx == 0 { Tab::all().len() - 1 } else { idx - 1 };
                self.active_tab = Tab::from_index(prev).unwrap_or(Tab::Packages);
                true
            }
            KeyCode::Char('1') => { self.active_tab = Tab::Packages; true }
            KeyCode::Char('2') => { self.active_tab = Tab::Updates; true }
            KeyCode::Char('3') => { self.active_tab = Tab::Kernels; true }
            KeyCode::Char('4') => { self.active_tab = Tab::Desktops; true }
            KeyCode::Char('5') => { self.active_tab = Tab::Sources; true }
            KeyCode::Char('6') => { self.active_tab = Tab::History; true }
            _ => false,
        }
    }

    fn draw(&self, f: &mut Frame) {
        let area = f.area();

        // Main layout: tab bar (3), content (rest - 3), status bar (3)
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(3),
                Constraint::Min(0),
                Constraint::Length(3),
            ])
            .split(area);

        self.draw_tab_bar(f, chunks[0]);
        self.draw_content(f, chunks[1]);
        self.draw_status_bar(f, chunks[2]);

        if self.show_help {
            self.draw_help_overlay(f, area);
        }
    }

    fn draw_tab_bar(&self, f: &mut Frame, area: Rect) {
        let titles: Vec<Line> = Tab::all()
            .iter()
            .map(|t| Line::from(t.title()))
            .collect();

        let tabs = Tabs::new(titles)
            .block(Block::default().borders(Borders::ALL).title(" rustpm "))
            .select(self.active_tab as usize)
            .style(Style::default().fg(Color::White))
            .highlight_style(
                Style::default()
                    .fg(Color::Yellow)
                    .add_modifier(Modifier::BOLD),
            );

        f.render_widget(tabs, area);
    }

    fn draw_content(&self, f: &mut Frame, area: Rect) {
        match self.active_tab {
            Tab::Packages => self.package_table.render(f, area),
            Tab::Updates  => self.update_panel.render(f, area),
            Tab::Kernels  => self.kernel_panel.render(f, area),
            Tab::Desktops => self.desktop_panel.render(f, area),
            Tab::Sources  => self.sources_panel.render(f, area),
            Tab::History  => self.history_panel.render(f, area),
        }
    }

    fn draw_status_bar(&self, f: &mut Frame, area: Rect) {
        let hint = " Tab: switch  1-6: jump  PgUp/Dn: scroll  ?: help  q: quit";
        let last_msg = self.status_messages.last().map(|s| s.as_str()).unwrap_or("");

        let text = Line::from(vec![
            Span::styled(hint, Style::default().fg(Color::DarkGray)),
            Span::raw("    "),
            Span::styled(last_msg, Style::default().fg(Color::Cyan)),
        ]);

        let bar = Paragraph::new(text)
            .block(Block::default().borders(Borders::ALL));

        f.render_widget(bar, area);
    }

    fn draw_help_overlay(&self, f: &mut Frame, area: Rect) {
        let popup_area = centered_rect(60, 14, area);
        f.render_widget(Clear, popup_area);

        let help_text = vec![
            Line::from(vec![
                Span::styled("Global", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow)),
                Span::raw("                    "),
                Span::styled("Panels", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow)),
            ]),
            Line::from("Tab / 1-6  Switch tab       /     Live search"),
            Line::from("q / Esc    Quit/back         Enter Select / confirm"),
            Line::from("?          This help         j/k ↑↓ Navigate rows"),
            Line::from("Ctrl-C     Force quit        PgUp/Dn Scroll 10 rows"),
            Line::from(""),
            Line::from("i     Install selected"),
            Line::from("r     Remove selected"),
            Line::from("u     Undo (history panel)"),
            Line::from("p     Pin/unpin (kernel panel)"),
            Line::from("v     Vanilla tab (kernel panel)"),
            Line::from(""),
            Line::from(Span::styled("Press ? or q to close", Style::default().fg(Color::DarkGray))),
        ];

        let block = Block::default()
            .borders(Borders::ALL)
            .title(" rustpm keybindings ")
            .style(Style::default().bg(Color::Black));

        let para = Paragraph::new(help_text).block(block);
        f.render_widget(para, popup_area);
    }
}

fn centered_rect(percent_x: u16, height: u16, r: Rect) -> Rect {
    let popup_width = r.width * percent_x / 100;
    let x = r.x + (r.width - popup_width) / 2;
    let y = r.y + (r.height.saturating_sub(height)) / 2;

    Rect {
        x,
        y,
        width: popup_width,
        height: height.min(r.height),
    }
}