aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.rs24
-rw-r--r--src/event.rs18
-rw-r--r--src/gentoo/mod.rs7
-rw-r--r--src/gentoo/package.rs20
-rw-r--r--src/gentoo/portage.rs85
-rw-r--r--src/gentoo/useflags.rs16
-rw-r--r--src/main.rs73
-rw-r--r--src/ui.rs77
8 files changed, 320 insertions, 0 deletions
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<Package> {
+ 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<bool> {
+ 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<UseFlag>,
+ pub version: String,
+}
+
+#[allow(dead_code)]
+impl Package {
+ pub fn new(name: String, version: String, use_flags: Vec<UseFlag>) -> 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<Package>,
+}
+
+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<UseFlag> = 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<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()>
+where
+ io::Error: From<B::Error>,
+{
+ loop {
+ terminal.draw(|frame| ui(frame, app))?;
+ let should_exit = handle_event(app)?;
+ if should_exit {
+ break;
+ }
+ }
+
+ Ok(())
+}
+
+fn init_tui() -> io::Result<ratatui::Terminal<CrosstermBackend<Stdout>>> {
+ 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);
+}