use crossterm::event::KeyCode;
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState, Tabs},
text::Line,
Frame,
};
use crate::kernel::detector::KernelEntry;
use crate::kernel::vanilla::{ReleaseType, VanillaRelease};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum KernelTab {
Debian,
Vanilla,
}
pub struct KernelPanel {
pub kernels: Vec<KernelEntry>,
pub vanilla: Vec<VanillaRelease>,
pub active_tab: KernelTab,
pub debian_state: TableState,
pub vanilla_state: TableState,
}
impl KernelPanel {
pub fn new(kernels: Vec<KernelEntry>, vanilla: Vec<VanillaRelease>) -> Self {
let mut debian_state = TableState::default();
if !kernels.is_empty() {
debian_state.select(Some(0));
}
let mut vanilla_state = TableState::default();
if !vanilla.is_empty() {
vanilla_state.select(Some(0));
}
Self {
kernels,
vanilla,
active_tab: KernelTab::Debian,
debian_state,
vanilla_state,
}
}
pub fn handle_key(&mut self, code: KeyCode) {
match code {
KeyCode::Char('v') => self.active_tab = KernelTab::Vanilla,
KeyCode::Char('d') => self.active_tab = KernelTab::Debian,
KeyCode::Char('j') | KeyCode::Down => self.next(),
KeyCode::Char('k') | KeyCode::Up => self.prev(),
_ => {}
}
}
fn next(&mut self) {
match self.active_tab {
KernelTab::Debian => {
let len = self.kernels.len();
if len == 0 { return; }
let i = self.debian_state.selected().map_or(0, |i| (i + 1).min(len - 1));
self.debian_state.select(Some(i));
}
KernelTab::Vanilla => {
let len = self.vanilla.len();
if len == 0 { return; }
let i = self.vanilla_state.selected().map_or(0, |i| (i + 1).min(len - 1));
self.vanilla_state.select(Some(i));
}
}
}
fn prev(&mut self) {
match self.active_tab {
KernelTab::Debian => {
let i = self.debian_state.selected().map_or(0, |i| i.saturating_sub(1));
self.debian_state.select(Some(i));
}
KernelTab::Vanilla => {
let i = self.vanilla_state.selected().map_or(0, |i| i.saturating_sub(1));
self.vanilla_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 tab_titles = vec![Line::from("Debian packages"), Line::from("Vanilla / mainline")];
let inner_tabs = Tabs::new(tab_titles)
.block(Block::default().borders(Borders::ALL))
.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(inner_tabs, chunks[0]);
match self.active_tab {
KernelTab::Debian => self.render_debian(f, chunks[1]),
KernelTab::Vanilla => self.render_vanilla(f, chunks[1]),
}
let actions = match self.active_tab {
KernelTab::Debian => " [i] Install [r] Remove [p] Pin [u] Update [v] Vanilla tab [?] Help",
KernelTab::Vanilla => " [i] Download & Install [r] Remove [Enter] Details [d] Debian tab [?] Help",
};
let action_bar = Paragraph::new(actions)
.block(Block::default().borders(Borders::ALL).title(" Actions "));
f.render_widget(action_bar, chunks[2]);
}
fn render_debian(&self, f: &mut Frame, area: Rect) {
let rows: Vec<Row> = self
.kernels
.iter()
.map(|k| {
let marker = if k.is_running { "* " } else { " " };
let version = format!("{}{}", marker, k.version);
let image_status = if k.installed { "installed" } else { "—" };
let headers_status = if k.headers_installed { "installed" } else { "—" };
let apt_ver = k.apt_version.as_deref().unwrap_or("—");
let held = if k.is_held { "[held]" } else { "" };
let style = if k.is_running {
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
} else if k.installed {
Style::default().fg(Color::White)
} else {
Style::default().fg(Color::DarkGray)
};
Row::new(vec![
Cell::from(version),
Cell::from(image_status),
Cell::from(headers_status),
Cell::from(apt_ver),
Cell::from(held),
])
.style(style)
})
.collect();
let widths = [
Constraint::Length(35),
Constraint::Length(12),
Constraint::Length(12),
Constraint::Length(20),
Constraint::Length(8),
];
let table = Table::new(rows, widths)
.header(
Row::new(vec!["Version", "Image", "Headers", "APT Version", "Hold"])
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
)
.block(Block::default().borders(Borders::ALL).title(" Debian Kernels (* = running) "))
.highlight_style(Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD))
.highlight_symbol("▶ ");
let mut state = self.debian_state.clone();
f.render_stateful_widget(table, area, &mut state);
}
fn render_vanilla(&self, f: &mut Frame, area: Rect) {
let rows: Vec<Row> = self
.vanilla
.iter()
.map(|v| {
let installed_marker = if v.installed { "✓" } else { "" };
let released = v
.released
.map(|d| d.format("%Y-%m-%d").to_string())
.unwrap_or_else(|| "—".into());
let style = match v.release_type {
ReleaseType::Stable => Style::default().fg(Color::Green),
ReleaseType::Mainline => Style::default().fg(Color::Yellow),
ReleaseType::Longterm => Style::default().fg(Color::Cyan),
ReleaseType::Eol => Style::default().fg(Color::DarkGray),
ReleaseType::Unknown => Style::default(),
};
Row::new(vec![
Cell::from(v.version.clone()),
Cell::from(v.release_type.label()),
Cell::from(released),
Cell::from(installed_marker),
])
.style(style)
})
.collect();
let widths = [
Constraint::Length(20),
Constraint::Length(12),
Constraint::Length(14),
Constraint::Length(10),
];
let table = Table::new(rows, widths)
.header(
Row::new(vec!["Release", "Type", "Released", "Installed"])
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
)
.block(Block::default().borders(Borders::ALL).title(" Vanilla / Mainline Kernels "))
.highlight_style(Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD))
.highlight_symbol("▶ ");
let mut state = self.vanilla_state.clone();
f.render_stateful_widget(table, area, &mut state);
}
}