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);
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]);
let content_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[1]);
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);
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]);
}
}