From 3346222550bc18edc457d9b509f00d0eba9d0f4f Mon Sep 17 00:00:00 2001 From: pablu Date: Wed, 6 May 2026 17:04:31 +0200 Subject: initial working --- src/app.rs | 24 ++++++++++++++ src/event.rs | 18 +++++++++++ src/gentoo/mod.rs | 7 +++++ src/gentoo/package.rs | 20 ++++++++++++ src/gentoo/portage.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/gentoo/useflags.rs | 16 ++++++++++ src/main.rs | 73 +++++++++++++++++++++++++++++++++++++++++++ src/ui.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 320 insertions(+) create mode 100644 src/app.rs create mode 100644 src/event.rs create mode 100644 src/gentoo/mod.rs create mode 100644 src/gentoo/package.rs create mode 100644 src/gentoo/portage.rs create mode 100644 src/gentoo/useflags.rs create mode 100644 src/main.rs create mode 100644 src/ui.rs (limited to 'src') diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..8f8a209 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,24 @@ +use ratatui::widgets::ListState; + +use crate::gentoo::{Package, portage::Portage}; + +#[derive(Debug)] +#[allow(dead_code)] +pub struct App { + portage: Portage, + pub list_state: ListState, +} + +#[allow(dead_code)] +impl App { + pub fn new(portage: Portage) -> Self { + App { + portage, + list_state: ListState::default().with_selected(Some(0)), + } + } + + pub fn installed_packages(&self) -> Vec { + self.portage.installed_packages.clone() + } +} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..bfe1fa5 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,18 @@ +use std::io; + +use ratatui::crossterm::event::{self, Event, KeyCode}; + +use crate::app::App; + +pub fn handle_event(app: &mut App) -> io::Result { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(true), + KeyCode::Char('j') => app.list_state.select_next(), + KeyCode::Char('k') => app.list_state.select_previous(), + _ => return Ok(false), + } + } + + Ok(false) +} diff --git a/src/gentoo/mod.rs b/src/gentoo/mod.rs new file mode 100644 index 0000000..ff343bc --- /dev/null +++ b/src/gentoo/mod.rs @@ -0,0 +1,7 @@ +pub mod package; +pub mod portage; +pub mod useflags; + +pub use package::Package; +pub use portage::Portage; +pub use useflags::UseFlag; diff --git a/src/gentoo/package.rs b/src/gentoo/package.rs new file mode 100644 index 0000000..0fe9a6c --- /dev/null +++ b/src/gentoo/package.rs @@ -0,0 +1,20 @@ +use crate::gentoo::UseFlag; + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct Package { + pub name: String, + pub use_flags: Vec, + pub version: String, +} + +#[allow(dead_code)] +impl Package { + pub fn new(name: String, version: String, use_flags: Vec) -> Self { + Package { + name: name, + use_flags: use_flags, + version: version, + } + } +} diff --git a/src/gentoo/portage.rs b/src/gentoo/portage.rs new file mode 100644 index 0000000..40b03c0 --- /dev/null +++ b/src/gentoo/portage.rs @@ -0,0 +1,85 @@ +use std::fs; + +use crate::gentoo::{Package, UseFlag}; + +#[derive(Debug)] +pub struct Portage { + pub installed_packages: Vec, +} + +fn split_pkg(input: &str) -> (&str, Option<&str>) { + let bytes = input.as_bytes(); + + for i in (0..bytes.len()).rev() { + if bytes[i] == b'-' { + if let Some(next) = bytes.get(i + 1) { + if next.is_ascii_digit() { + let name = &input[..i]; + let version = &input[i + 1..]; + return (name, Some(version)); + } + } + } + } + + (input, None) +} + +impl Portage { + pub fn new() -> Self { + Self { + installed_packages: vec![], + } + } + + pub fn load_installed_packages(&mut self) -> Result<(), std::io::Error> { + let categories = fs::read_dir("/var/db/pkg")?; + + for category in categories { + let category = category.unwrap(); + let cat_name = category.file_name(); + let pkgs = fs::read_dir(category.path())?; + for pkg in pkgs { + let pkg = pkg.unwrap(); + let pkg_name = pkg.file_name(); + let pkg_name = pkg_name.to_str().unwrap(); + let (pkg_name, pkg_version) = split_pkg(pkg_name); + + let use_flags_file = fs::read(pkg.path().join("USE"))?; + let use_flags: Vec<&str> = str::from_utf8(&use_flags_file) + .unwrap() + .trim() + .split(' ') + .collect(); + + let iuse_flags_file = fs::read(pkg.path().join("IUSE"))?; + let iuse_flags: Vec<&str> = str::from_utf8(&iuse_flags_file) + .unwrap() + .trim() + .split(' ') + .collect(); + + let use_flags: Vec = iuse_flags + .iter() + .filter(|x| !x.trim().is_empty()) + .map(|&iuse| { + let is_default = iuse.starts_with("+"); + let iuse = iuse.strip_prefix("+").unwrap_or(iuse); + + let is_active = use_flags.contains(&iuse); + + UseFlag::new(iuse.into(), is_active, is_default) + }) + .collect(); + + self.installed_packages.push(Package::new( + format!("{}/{}", cat_name.to_str().unwrap(), pkg_name), + pkg_version.unwrap().into(), + use_flags, + )); + } + } + + Ok(()) + } +} diff --git a/src/gentoo/useflags.rs b/src/gentoo/useflags.rs new file mode 100644 index 0000000..0b7d981 --- /dev/null +++ b/src/gentoo/useflags.rs @@ -0,0 +1,16 @@ +#[derive(Debug, Clone)] +pub struct UseFlag { + pub name: String, + pub enabled: bool, + pub default: bool, +} + +impl UseFlag { + pub fn new(name: String, enabled: bool, default: bool) -> Self { + UseFlag { + name, + enabled, + default, + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..55e721a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,73 @@ +mod app; +mod event; +mod gentoo; +mod ui; + +use std::{ + io::{self, Stdout, stdout}, + panic::{set_hook, take_hook}, +}; + +use ratatui::{ + Terminal, + backend::Backend, + crossterm::{ + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, + }, + prelude::CrosstermBackend, +}; + +use crate::{app::App, event::handle_event, gentoo::Portage, ui::ui}; + +fn main() -> color_eyre::Result<()> { + color_eyre::install()?; + init_panic_hook(); + let mut tui = init_tui()?; + + let mut p = Portage::new(); + p.load_installed_packages()?; + + let mut app = App::new(p); + + run(&mut tui, &mut app)?; + + restore_tui()?; + + Ok(()) +} + +fn run(terminal: &mut Terminal, app: &mut App) -> io::Result<()> +where + io::Error: From, +{ + loop { + terminal.draw(|frame| ui(frame, app))?; + let should_exit = handle_event(app)?; + if should_exit { + break; + } + } + + Ok(()) +} + +fn init_tui() -> io::Result>> { + enable_raw_mode()?; + execute!(stdout(), EnterAlternateScreen)?; + Terminal::new(CrosstermBackend::new(stdout())) +} + +fn init_panic_hook() { + let original_hook = take_hook(); + set_hook(Box::new(move |panic_info| { + let _ = restore_tui(); + original_hook(panic_info); + })); +} + +fn restore_tui() -> io::Result<()> { + disable_raw_mode()?; + execute!(stdout(), LeaveAlternateScreen)?; + Ok(()) +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..a1c3440 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,77 @@ +use ratatui::{ + Frame, + layout::{Constraint, Layout}, + prelude::Rect, + style::{Color, Modifier, Style, Stylize}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, ListState}, +}; + +use crate::app::App; + +pub fn ui(frame: &mut Frame, app: &mut App) { + let constraints = [Constraint::Length(1), Constraint::Fill(1)]; + + let layout = Layout::vertical(constraints).spacing(1); + let [top, first] = frame.area().layout(&layout); + + let split = Layout::horizontal([Constraint::Percentage(70), Constraint::Percentage(30)]) + .spacing(1) + .split(first); + + let title = Line::from_iter([ + Span::from("Installed Packages").bold(), + Span::from(" (Press 'q' to quit and arrow keys to navigate)"), + ]); + frame.render_widget(title.centered(), top); + + render_packages(frame, split[0], app); + render_use_flags(frame, split[1], app); +} + +fn render_packages(frame: &mut Frame, area: Rect, app: &mut App) { + let items = app.installed_packages().into_iter().map(|x| x.name.clone()); + let list = List::new(items) + .style(Color::White) + .highlight_style(Modifier::REVERSED) + .highlight_symbol("> ") + .block(Block::new().borders(Borders::ALL)); + + frame.render_stateful_widget(list, area, &mut app.list_state); +} + +fn render_use_flags(frame: &mut Frame, area: Rect, app: &mut App) { + let selected_package_index = match app.list_state.selected() { + Some(selected) => selected, + None => 0, + }; + let selected_package = app.installed_packages()[selected_package_index].clone(); + + let items = selected_package.use_flags.into_iter().map(|x| { + let color = if x.enabled { + Color::Green + } else if x.default { + Color::Blue + } else { + Color::Red + }; + + let text_style = if x.default { + Style::default().add_modifier(Modifier::ITALIC).fg(color) + } else { + Style::default().fg(color) + }; + + ListItem::new(x.name).style(text_style) + }); + let list = List::new(items) + .style(Color::White) + .scroll_padding(1) + .direction(ratatui::widgets::ListDirection::TopToBottom) + .block(Block::new().borders(Borders::ALL)); + + let mut list_state = ListState::default(); + list_state.select_first(); + + frame.render_stateful_widget(list, area, &mut list_state); +} -- cgit 1.3.1