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;
}
if self.active_tab == Tab::Updates && key.code == KeyCode::Char('u') {
self.spawn_upgrade();
continue;
}
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));
}
}
});
}
}
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();
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),
}
}