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::query::PackageInfo;

pub struct SearchPanel {
    pub query: String,
    pub results: Vec<PackageInfo>,
    pub state: TableState,
    pub typing: bool,
}

impl SearchPanel {
    pub fn new() -> Self {
        Self {
            query: String::new(),
            results: Vec::new(),
            state: TableState::default(),
            typing: false,
        }
    }

    pub fn set_results(&mut self, results: Vec<PackageInfo>) {
        self.results = results;
        if !self.results.is_empty() {
            self.state.select(Some(0));
        } else {
            self.state.select(None);
        }
    }

    pub fn handle_key(&mut self, code: KeyCode) {
        match code {
            KeyCode::Char('/') => { self.typing = true; }
            KeyCode::Char(c) if self.typing => { self.query.push(c); }
            KeyCode::Backspace if self.typing => { self.query.pop(); }
            KeyCode::Enter | KeyCode::Esc => { self.typing = false; }
            KeyCode::Char('j') | KeyCode::Down if !self.typing => self.next(1),
            KeyCode::Char('k') | KeyCode::Up if !self.typing => self.prev(1),
            KeyCode::PageDown if !self.typing => self.next(10),
            KeyCode::PageUp if !self.typing => self.prev(10),
            _ => {}
        }
    }

    fn next(&mut self, step: usize) {
        let len = self.results.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::Length(3),
                Constraint::Min(0),
                Constraint::Length(3),
            ])
            .split(area);

        // Search bar
        let cursor = if self.typing { "_" } else { "" };
        let search_text = format!("> {}{}", self.query, cursor);
        let search_bar = Paragraph::new(search_text)
            .block(Block::default().borders(Borders::ALL).title(" Search "))
            .style(if self.typing {
                Style::default().fg(Color::Yellow)
            } else {
                Style::default()
            });
        f.render_widget(search_bar, chunks[0]);

        // Split results + details
        let content_chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
            .split(chunks[1]);

        // Results table
        let rows: Vec<Row> = self
            .results
            .iter()
            .map(|p| {
                let ver = p
                    .installed_version
                    .as_deref()
                    .or(p.candidate_version.as_deref())
                    .unwrap_or("");
                let style = if p.installed_version.is_some() {
                    Style::default().fg(Color::Green)
                } else {
                    Style::default()
                };
                Row::new(vec![Cell::from(p.name.clone()), Cell::from(ver)]).style(style)
            })
            .collect();

        let widths = [Constraint::Min(25), Constraint::Length(20)];
        let title = format!(" Results ({}) ", self.results.len());
        let table = Table::new(rows, widths)
            .header(
                Row::new(vec!["Package", "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, content_chunks[0], &mut state);

        // Detail pane
        let detail_text = if let Some(idx) = self.state.selected() {
            if let Some(pkg) = self.results.get(idx) {
                let installed = pkg.installed_version.as_deref().unwrap_or("(not installed)");
                let candidate = pkg.candidate_version.as_deref().unwrap_or("");
                let desc = pkg.description.as_deref().unwrap_or("No description.");
                format!(
                    "{}\n\nInstalled: {}\nCandidate: {}\n\n{}",
                    pkg.name, installed, candidate, desc
                )
            } else {
                String::new()
            }
        } else {
            "Select a package to see details".to_string()
        };

        let details = Paragraph::new(detail_text)
            .block(Block::default().borders(Borders::ALL).title(" Details "))
            .wrap(ratatui::widgets::Wrap { trim: true });
        f.render_widget(details, content_chunks[1]);

        let actions = Paragraph::new("  Enter: install/remove  /: search  j/k: navigate  [?] Help")
            .block(Block::default().borders(Borders::ALL).title(" Actions "));
        f.render_widget(actions, chunks[2]);
    }
}