diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/actions/action.rs | 12 | ||||
| -rw-r--r-- | src/actions/mod.rs | 2 | ||||
| -rw-r--r-- | src/app.rs | 129 | ||||
| -rw-r--r-- | src/event.rs | 101 | ||||
| -rw-r--r-- | src/gentoo/mod.rs | 2 | ||||
| -rw-r--r-- | src/gentoo/portage.rs | 347 | ||||
| -rw-r--r-- | src/gentoo/portage_loader.rs | 344 | ||||
| -rw-r--r-- | src/main.rs | 13 | ||||
| -rw-r--r-- | src/screens/dashboard.rs | 144 | ||||
| -rw-r--r-- | src/screens/installed_packages.rs | 148 | ||||
| -rw-r--r-- | src/screens/mod.rs | 1 | ||||
| -rw-r--r-- | src/screens/screen.rs | 7 | ||||
| -rw-r--r-- | src/widgets/mod.rs | 1 | ||||
| -rw-r--r-- | src/widgets/package_metadata.rs | 6 | ||||
| -rw-r--r-- | src/widgets/tabs.rs | 24 | ||||
| -rw-r--r-- | src/widgets/use_flags.rs | 8 |
16 files changed, 683 insertions, 606 deletions
diff --git a/src/actions/action.rs b/src/actions/action.rs deleted file mode 100644 index b2070d6..0000000 --- a/src/actions/action.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[derive(Debug, Clone)] -pub enum Action { - Quit, - MoveUp, - MoveDown, - OpenPopup, - ClosePopup, - Select, - NextSearchResult, - PrevSearchResult, - ChangeTab, -} diff --git a/src/actions/mod.rs b/src/actions/mod.rs index ed1dfa9..39af0f6 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -1,5 +1,3 @@ -pub mod action; pub mod signal; -pub use action::Action; pub use signal::Signal; @@ -1,7 +1,7 @@ use color_eyre::eyre::Ok; use ratatui::{ Frame, - crossterm::event::{self, Event}, + crossterm::event::{self, Event, KeyCode}, layout::{Constraint, Layout}, style::Stylize, text::Text, @@ -9,10 +9,13 @@ use ratatui::{ }; use crate::{ - actions::{Signal, signal}, - gentoo::{InstalledPackage, portage::Portage}, - screens::{installed_packages::InstalledPackagesScreen, screen::Screen}, + actions::Signal, + gentoo::portage::Portage, + screens::{ + dashboard::DashboardScreen, installed_packages::InstalledPackagesScreen, screen::Screen, + }, theme::Theme, + widgets::tabs::render_tab, }; #[derive(Debug)] @@ -27,11 +30,8 @@ pub struct App { portage: Portage, installed_package_screen: InstalledPackagesScreen, + dashboard_screen: DashboardScreen, - // pub search_found: Option<bool>, - // search_indexes: Option<Vec<usize>>, - // pub search_indexes_len: Option<usize>, - // pub current_search_index: Option<usize>, pub view: ViewState, } @@ -40,6 +40,7 @@ impl App { App { portage, + dashboard_screen: DashboardScreen::default(), installed_package_screen: InstalledPackagesScreen::default(), view: ViewState::InstalledPackages, @@ -52,13 +53,16 @@ impl App { let layout = Layout::vertical(vec![Constraint::Length(3), Constraint::Fill(1)]); let [tab_bar, rest] = frame.area().layout(&layout); - // render_tab(frame, tab_bar, &app.view); + render_tab(frame, tab_bar, &self.view); match self.view { - crate::app::ViewState::InstalledPackages => { + ViewState::InstalledPackages => { self.installed_package_screen .draw(frame, rest, &self.portage) } + ViewState::Dashboard => { + self.dashboard_screen.draw(frame, rest, &self.portage); + } _ => frame.render_widget(Text::from("NOT IMPLEMENTED"), rest), } } @@ -67,16 +71,21 @@ impl App { if let Event::Key(key) = event::read()? { let signal = match self.view { crate::app::ViewState::InstalledPackages => { - self.installed_package_screen.update(key) + self.installed_package_screen.update(key, &self.portage) } - _ => None, + // Fallback, primarly while implementing + _ => match key.code { + KeyCode::Char('q') => Some(Signal::Quit), + KeyCode::Tab => Some(Signal::CycleTab), + _ => None, + }, }; if let Some(signal) = signal { match signal { Signal::Quit => return Ok(true), - Signal::CycleTab => todo!(), + Signal::CycleTab => self.cycle_current_tab(), } } } @@ -84,29 +93,6 @@ impl App { Ok(false) } - pub fn world_count(&self) -> usize { - self.portage.world_packages.len() - } - - pub fn installed_packages(&self) -> &[InstalledPackage] { - &self.portage.installed_packages - } - - // pub fn current_package(&self) -> &InstalledPackage { - // let selected_package_index = self.list_state.selected().unwrap_or_default(); - // &self.portage.installed_packages[selected_package_index] - // } - - pub fn total_installed_size(&self) -> usize { - self.portage.installed_packages.iter().map(|p| p.size).sum() - } - - // pub fn toggle_search_window(&mut self) { - // self.showing_search_window = !self.showing_search_window; - // self.textarea.clear(); - // self.search_found = None; - // } - pub fn cycle_current_tab(&mut self) { match self.view { ViewState::Dashboard => self.view = ViewState::InstalledPackages, @@ -114,75 +100,4 @@ impl App { ViewState::AvailablePackages => self.view = ViewState::Dashboard, } } - - // pub fn search(&mut self) -> bool { - // let indexes: Vec<usize> = self - // .portage - // .installed_packages - // .iter() - // .enumerate() - // .filter(|(_, s)| { - // s.atom - // .qualified_name() - // .to_lowercase() - // .contains(&self.textarea.lines()[0].to_lowercase()) - // }) - // .map(|(i, _)| i) - // .collect(); - // - // if let Some(index) = indexes.first() { - // self.list_state.select(Some(*index)); - // self.search_indexes_len = Some(indexes.len()); - // self.search_indexes = Some(indexes); - // self.current_search_index = Some(0); - // - // return true; - // } - // - // self.search_indexes = None; - // - // false - // } - // - // pub fn next_search(&mut self) { - // let Some(mut current_index) = self.current_search_index else { - // return; - // }; - // - // let Some(search_indexes) = &self.search_indexes else { - // return; - // }; - // - // if current_index + 1 >= search_indexes.len() { - // current_index = 0; - // } else { - // current_index += 1; - // } - // - // let list_index = search_indexes[current_index]; - // - // self.list_state.select(Some(list_index)); - // self.current_search_index = Some(current_index); - // } - // - // pub fn prev_search(&mut self) { - // let Some(mut current_index) = self.current_search_index else { - // return; - // }; - // - // let Some(search_indexes) = &self.search_indexes else { - // return; - // }; - // - // if current_index == 0 { - // current_index = search_indexes.len() - 1; - // } else { - // current_index -= 1; - // } - // - // let list_index = search_indexes[current_index]; - // - // self.list_state.select(Some(list_index)); - // self.current_search_index = Some(current_index); - // } } diff --git a/src/event.rs b/src/event.rs deleted file mode 100644 index 95eece2..0000000 --- a/src/event.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::io; - -use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}; - -use crate::app::App; - -pub fn handle_event(app: &mut App) -> io::Result<bool> { - if let Event::Key(key) = event::read()? { - match app.view { - crate::app::ViewState::Dashboard => { - if handle_dashboard_keybinds(key, app) { - return Ok(true); - } - } - crate::app::ViewState::InstalledPackages => { - // if app.showing_search_window { - // handle_search_keybinds(key, app); - // } else if handle_installed_packages_keybinds(key, app) { - // return Ok(true); - // } - } - crate::app::ViewState::AvailablePackages => { - if handle_available_packages_keybinds(key, app) { - return Ok(true); - } - } - } - } - - Ok(false) -} - -fn handle_available_packages_keybinds(key: KeyEvent, app: &mut App) -> bool { - match key.code { - KeyCode::Char('q') => return true, - KeyCode::Tab => app.cycle_current_tab(), - - _ => {} - } - - false -} -// -// fn handle_search_keybinds(key: KeyEvent, app: &mut App) { -// match key.code { -// KeyCode::Esc => app.toggle_search_window(), -// KeyCode::Enter => { -// app.search(); -// app.toggle_search_window(); -// } -// _ => { -// app.textarea.input_without_shortcuts(key); -// app.search_found = Some(app.search()); -// } -// } -// } -// -// fn handle_installed_packages_keybinds(key: KeyEvent, app: &mut App) -> bool { -// match key.modifiers { -// KeyModifiers::CONTROL => match key.code { -// KeyCode::Char('d') => app -// .list_state -// .select(Some(app.list_state.selected().unwrap_or(0) + 30)), -// KeyCode::Char('u') => { -// if let Some(selected) = app.list_state.selected() { -// if selected < 30 { -// app.list_state.select(Some(0)); -// } else { -// app.list_state.select(Some(selected - 30)); -// } -// } -// } -// _ => {} -// }, -// _ => match key.code { -// KeyCode::Char('q') => return true, -// KeyCode::Char('j') => app.list_state.select_next(), -// KeyCode::Char('k') => app.list_state.select_previous(), -// KeyCode::Char('/') => app.toggle_search_window(), -// KeyCode::Char('n') => app.next_search(), -// KeyCode::Char('N') => app.prev_search(), -// -// KeyCode::Tab => app.cycle_current_tab(), -// -// _ => {} -// }, -// } -// -// false -// } -// -fn handle_dashboard_keybinds(key: KeyEvent, app: &mut App) -> bool { - match key.code { - KeyCode::Char('q') => return true, - KeyCode::Tab => app.cycle_current_tab(), - - _ => {} - } - - false -} diff --git a/src/gentoo/mod.rs b/src/gentoo/mod.rs index e2cc3df..f1bc744 100644 --- a/src/gentoo/mod.rs +++ b/src/gentoo/mod.rs @@ -1,7 +1,9 @@ pub mod package; pub mod portage; +pub mod portage_loader; pub mod useflags; pub use package::InstalledPackage; pub use portage::Portage; pub use useflags::UseFlag; +// pub use portage_loader diff --git a/src/gentoo/portage.rs b/src/gentoo/portage.rs index b444a5b..c837eb6 100644 --- a/src/gentoo/portage.rs +++ b/src/gentoo/portage.rs @@ -1,16 +1,8 @@ -use std::{ - collections::{HashMap, HashSet}, - fs, - path::{Path, PathBuf}, -}; - -use chrono::DateTime; -use color_eyre::eyre::Context; -use quick_xml::{Reader, events::Event}; +use std::collections::HashSet; use crate::gentoo::{ - InstalledPackage, UseFlag, - package::{Metadata, Package, PackageKey, Version}, + InstalledPackage, + package::{Package, PackageKey}, }; #[derive(Debug, Default)] @@ -20,71 +12,6 @@ pub struct Portage { pub available_packages: Vec<Package>, } -fn split_pkg(input: &str) -> (&str, &str) { - let bytes = input.as_bytes(); - - for i in (0..bytes.len()).rev() { - if bytes[i] == b'-' - && let Some(next) = bytes.get(i + 1) - && next.is_ascii_digit() - { - let name = &input[..i]; - let version = &input[i + 1..]; - return (name, version); - } - } - - (input, "") -} - -fn extract_maintainer(path: &Path) -> color_eyre::Result<Option<String>> { - let s = fs::read_to_string(path)?; - let mut reader = Reader::from_str(s.as_str()); - reader.config_mut().trim_text(true); - - let mut buf = Vec::new(); - let mut in_maintainer = false; - - loop { - match reader.read_event_into(&mut buf) { - Ok(Event::Start(e)) if e.name().as_ref() == b"maintainer" => { - in_maintainer = true; - } - - Ok(Event::Text(e)) if in_maintainer => { - match e.xml_content() { - Ok(s) => { - return Ok(Some(s.into())); - } - Err(_) => return Ok(None), - }; - } - - Ok(Event::End(e)) if e.name().as_ref() == b"maintainer" => { - in_maintainer = false; - } - - Ok(Event::Eof) | Err(_) => break, - - _ => {} - } - - buf.clear(); - } - - Ok(None) -} - -fn parse_cache_file(content: String) -> HashMap<String, String> { - content - .lines() - .filter_map(|line| { - line.split_once('=') - .map(|(k, v)| (k.to_string(), v.to_string())) - }) - .collect() -} - impl Portage { pub fn new() -> Self { Self { @@ -94,270 +21,16 @@ impl Portage { } } - pub fn load_world_packages() -> color_eyre::Result<HashSet<PackageKey>> { - let world = fs::read_to_string("/var/lib/portage/world")?; - Ok(world - .split_whitespace() - .filter_map(PackageKey::from_atom) - .collect()) - } - - pub fn load_available_packages(&mut self) -> color_eyre::Result<()> { - let repos = fs::read_dir("/var/db/repos/").wrap_err("failed to read /var/db/repos")?; - - let mut packages: HashMap<PackageKey, Package> = HashMap::new(); - - for repo in repos { - let Ok(repo) = repo else { - continue; - }; - let repo_name = repo.file_name(); - let Some(repo_name) = repo_name.to_str() else { - continue; - }; - - let Ok(categories) = fs::read_dir(repo.path().join("metadata/md5-cache")) else { - // TODO: Someday parse actual ebuilds instead of md5-cache - continue; - }; - - for category in categories { - let Ok(category) = category else { - continue; - }; - - let Ok(cat_name) = category.file_name().into_string() else { - continue; - }; - - if cat_name == "Manifest.gz" { - continue; - } - - let pkgs = fs::read_dir(category.path())?; - - for pkg in pkgs { - let Ok(pkg) = pkg else { - continue; - }; - - let Ok(pkg_name) = pkg.file_name().into_string() else { - continue; - }; - - if pkg_name == "Manifest.gz" { - continue; - } - - let (pkg_name, pkg_version) = split_pkg(&pkg_name); - - let version_file = fs::read_to_string(pkg.path()).wrap_err_with(|| { - format!( - "failed to read version_file for {}/{}-{} in repo {}", - cat_name, pkg_name, pkg_version, repo_name - ) - })?; - let data = parse_cache_file(version_file); - - let homepages: Vec<String> = data - .get("HOMEPAGE") - .and_then(|s| { - Some( - s.split_whitespace() - .map(std::string::ToString::to_string) - .collect(), - ) - }) - .unwrap_or(vec![]); - - let keywords: Vec<String> = data - .get("KEYWORDS") - .and_then(|s| { - Some( - s.split_whitespace() - .map(std::string::ToString::to_string) - .collect(), - ) - }) - .unwrap_or(vec![]); - - let iuse: Vec<UseFlag> = data - .get("IUSE") - .and_then(|s| { - Some( - s.split_whitespace() - .map(|original_iuse| { - let iuse = original_iuse - .strip_prefix("+") - .unwrap_or(original_iuse); - - UseFlag { - name: iuse.to_string(), - default: iuse != original_iuse, - } - }) - .collect(), - ) - }) - .unwrap_or(vec![]); - - let repo_path = PathBuf::from("/var/db/repos/") - .join(&repo_name) - .join(&cat_name) - .join(pkg_name) - .join("metadata.xml"); - - let maintainer = extract_maintainer(&repo_path).unwrap_or(None); - - let version = Version { - version: pkg_version.to_string(), - metadata: Metadata { - maintainer: maintainer, - description: data.get("DESCRIPTION").cloned(), - homepage: homepages, - license: data.get("LICENSE").cloned(), - repository: repo_name.to_string(), - }, - keywords: keywords, - iuse: iuse, - }; - - let pkg_key = PackageKey { - category: cat_name.clone(), - name: pkg_name.to_string(), - }; - - if let Some(package) = packages.get_mut(&pkg_key) { - package.versions.push(version); - } else { - let pkg = Package { - atom: pkg_key.clone(), - versions: vec![version], - }; - packages.insert(pkg_key, pkg); - } - } - } + pub fn get_installed_package(&self, key: PackageKey) -> Option<InstalledPackage> { + let pkg = self.installed_packages.iter().find(|pkg| pkg.atom == key); + if let Some(pkg) = pkg { + return Some(pkg.clone()); } - self.available_packages = packages.into_values().collect(); - Ok(()) + None } - pub fn load_installed_packages() -> color_eyre::Result<Vec<InstalledPackage>> { - let categories = fs::read_dir("/var/db/pkg")?; - - let mut v = vec![]; - - for category in categories { - let Ok(category) = category else { - continue; - }; - - let Ok(cat_name) = category.file_name().into_string() else { - continue; - }; - - let pkgs = fs::read_dir(category.path())?; - for pkg in pkgs { - let Ok(pkg) = pkg else { - continue; - }; - - let pkg_name = pkg.file_name(); - let Some(pkg_name) = pkg_name.to_str() else { - continue; - }; - - let (pkg_name, pkg_version) = split_pkg(pkg_name); - - let use_flags_file = fs::read_to_string(pkg.path().join("USE"))?; - let enabled_use_flags: HashSet<String> = use_flags_file - .split_whitespace() - .map(|s| s.to_string()) - .collect(); - - let iuse_flags_file = fs::read_to_string(pkg.path().join("IUSE"))?; - - let use_flags: Vec<UseFlag> = iuse_flags_file - .split_whitespace() - .map(|original_iuse| { - let iuse = original_iuse.strip_prefix("+").unwrap_or(original_iuse); - - UseFlag { - name: iuse.to_string(), - default: iuse != original_iuse, - } - }) - .collect(); - - let repository = fs::read_to_string(pkg.path().join("repository"))? - .trim() - .to_string(); - - let homepage: Vec<String> = match fs::read_to_string(pkg.path().join("HOMEPAGE")) { - Ok(homepage) => homepage - .split_whitespace() - .map(std::string::ToString::to_string) - .collect(), - Err(_) => vec![], - }; - - let license: Option<String> = match fs::read_to_string(pkg.path().join("LICENSE")) { - Ok(license) => Some(license.trim().to_string()), - Err(_) => None, - }; - - let description: Option<String> = - match fs::read_to_string(pkg.path().join("DESCRIPTION")) { - Ok(description) => Some(description.trim().to_string()), - Err(_) => None, - }; - - let size: usize = fs::read_to_string(pkg.path().join("SIZE"))? - .trim() - .parse() - .unwrap_or(0); - - let repo_path = PathBuf::from("/var/db/repos/") - .join(&repository) - .join(&cat_name) - .join(pkg_name) - .join("metadata.xml"); - - let build_time = fs::read_to_string(pkg.path().join("BUILD_TIME"))? - .trim() - .parse::<i64>() - .ok() - .and_then(DateTime::from_timestamp_secs) - .unwrap_or(DateTime::from_timestamp_nanos(0)); - - let maintainer = extract_maintainer(&repo_path).unwrap_or(None); - - let atom = PackageKey { - category: cat_name.clone(), - name: pkg_name.to_string(), - }; - - v.push(InstalledPackage { - atom: atom, - version: pkg_version.to_string(), - build_time, - slot: 0, - metadata: Metadata { - maintainer, - description, - homepage: homepage, - license, - repository: repository, - }, - enabled_use_flags: enabled_use_flags, - iuse: use_flags, - size, - }); - } - } - - Ok(v) + pub fn total_installed_size(&self) -> usize { + self.installed_packages.iter().map(|p| p.size).sum() } } diff --git a/src/gentoo/portage_loader.rs b/src/gentoo/portage_loader.rs new file mode 100644 index 0000000..792e53d --- /dev/null +++ b/src/gentoo/portage_loader.rs @@ -0,0 +1,344 @@ +use std::{ + collections::{HashMap, HashSet}, + fs, + path::{Path, PathBuf}, +}; + +use chrono::DateTime; +use color_eyre::eyre::Context; +use quick_xml::{Reader, events::Event}; + +use crate::gentoo::{ + InstalledPackage, UseFlag, + package::{Metadata, Package, PackageKey, Version}, +}; + +fn split_pkg(input: &str) -> (&str, &str) { + let bytes = input.as_bytes(); + + for i in (0..bytes.len()).rev() { + if bytes[i] == b'-' + && let Some(next) = bytes.get(i + 1) + && next.is_ascii_digit() + { + let name = &input[..i]; + let version = &input[i + 1..]; + return (name, version); + } + } + + (input, "") +} + +fn extract_maintainer(path: &Path) -> color_eyre::Result<Option<String>> { + let s = fs::read_to_string(path)?; + let mut reader = Reader::from_str(s.as_str()); + reader.config_mut().trim_text(true); + + let mut buf = Vec::new(); + let mut in_maintainer = false; + + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(e)) if e.name().as_ref() == b"maintainer" => { + in_maintainer = true; + } + + Ok(Event::Text(e)) if in_maintainer => { + match e.xml_content() { + Ok(s) => { + return Ok(Some(s.into())); + } + Err(_) => return Ok(None), + }; + } + + Ok(Event::End(e)) if e.name().as_ref() == b"maintainer" => { + in_maintainer = false; + } + + Ok(Event::Eof) | Err(_) => break, + + _ => {} + } + + buf.clear(); + } + + Ok(None) +} + +fn parse_cache_file(content: String) -> HashMap<String, String> { + content + .lines() + .filter_map(|line| { + line.split_once('=') + .map(|(k, v)| (k.to_string(), v.to_string())) + }) + .collect() +} + +pub fn load_world_packages() -> color_eyre::Result<HashSet<PackageKey>> { + let world = fs::read_to_string("/var/lib/portage/world")?; + Ok(world + .split_whitespace() + .filter_map(PackageKey::from_atom) + .collect()) +} + +pub fn load_available_packages() -> color_eyre::Result<Vec<Package>> { + let repos = fs::read_dir("/var/db/repos/").wrap_err("failed to read /var/db/repos")?; + + let mut packages: HashMap<PackageKey, Package> = HashMap::new(); + + for repo in repos { + let Ok(repo) = repo else { + continue; + }; + let repo_name = repo.file_name(); + let Some(repo_name) = repo_name.to_str() else { + continue; + }; + + let Ok(categories) = fs::read_dir(repo.path().join("metadata/md5-cache")) else { + // TODO: Someday parse actual ebuilds instead of md5-cache + continue; + }; + + for category in categories { + let Ok(category) = category else { + continue; + }; + + let Ok(cat_name) = category.file_name().into_string() else { + continue; + }; + + if cat_name == "Manifest.gz" { + continue; + } + + let pkgs = fs::read_dir(category.path())?; + + for pkg in pkgs { + let Ok(pkg) = pkg else { + continue; + }; + + let Ok(pkg_name) = pkg.file_name().into_string() else { + continue; + }; + + if pkg_name == "Manifest.gz" { + continue; + } + + let (pkg_name, pkg_version) = split_pkg(&pkg_name); + + let version_file = fs::read_to_string(pkg.path()).wrap_err_with(|| { + format!( + "failed to read version_file for {}/{}-{} in repo {}", + cat_name, pkg_name, pkg_version, repo_name + ) + })?; + let data = parse_cache_file(version_file); + + let homepages: Vec<String> = data + .get("HOMEPAGE") + .and_then(|s| { + Some( + s.split_whitespace() + .map(std::string::ToString::to_string) + .collect(), + ) + }) + .unwrap_or(vec![]); + + let keywords: Vec<String> = data + .get("KEYWORDS") + .and_then(|s| { + Some( + s.split_whitespace() + .map(std::string::ToString::to_string) + .collect(), + ) + }) + .unwrap_or(vec![]); + + let iuse: Vec<UseFlag> = data + .get("IUSE") + .and_then(|s| { + Some( + s.split_whitespace() + .map(|original_iuse| { + let iuse = + original_iuse.strip_prefix("+").unwrap_or(original_iuse); + + UseFlag { + name: iuse.to_string(), + default: iuse != original_iuse, + } + }) + .collect(), + ) + }) + .unwrap_or(vec![]); + + let repo_path = PathBuf::from("/var/db/repos/") + .join(&repo_name) + .join(&cat_name) + .join(pkg_name) + .join("metadata.xml"); + + let maintainer = extract_maintainer(&repo_path).unwrap_or(None); + + let version = Version { + version: pkg_version.to_string(), + metadata: Metadata { + maintainer: maintainer, + description: data.get("DESCRIPTION").cloned(), + homepage: homepages, + license: data.get("LICENSE").cloned(), + repository: repo_name.to_string(), + }, + keywords: keywords, + iuse: iuse, + }; + + let pkg_key = PackageKey { + category: cat_name.clone(), + name: pkg_name.to_string(), + }; + + if let Some(package) = packages.get_mut(&pkg_key) { + package.versions.push(version); + } else { + let pkg = Package { + atom: pkg_key.clone(), + versions: vec![version], + }; + packages.insert(pkg_key, pkg); + } + } + } + } + + Ok(packages.into_values().collect()) +} + +pub fn load_installed_packages() -> color_eyre::Result<Vec<InstalledPackage>> { + let categories = fs::read_dir("/var/db/pkg")?; + + let mut v = vec![]; + + for category in categories { + let Ok(category) = category else { + continue; + }; + + let Ok(cat_name) = category.file_name().into_string() else { + continue; + }; + + let pkgs = fs::read_dir(category.path())?; + for pkg in pkgs { + let Ok(pkg) = pkg else { + continue; + }; + + let pkg_name = pkg.file_name(); + let Some(pkg_name) = pkg_name.to_str() else { + continue; + }; + + let (pkg_name, pkg_version) = split_pkg(pkg_name); + + let use_flags_file = fs::read_to_string(pkg.path().join("USE"))?; + let enabled_use_flags: HashSet<String> = use_flags_file + .split_whitespace() + .map(|s| s.to_string()) + .collect(); + + let iuse_flags_file = fs::read_to_string(pkg.path().join("IUSE"))?; + + let use_flags: Vec<UseFlag> = iuse_flags_file + .split_whitespace() + .map(|original_iuse| { + let iuse = original_iuse.strip_prefix("+").unwrap_or(original_iuse); + + UseFlag { + name: iuse.to_string(), + default: iuse != original_iuse, + } + }) + .collect(); + + let repository = fs::read_to_string(pkg.path().join("repository"))? + .trim() + .to_string(); + + let homepage: Vec<String> = match fs::read_to_string(pkg.path().join("HOMEPAGE")) { + Ok(homepage) => homepage + .split_whitespace() + .map(std::string::ToString::to_string) + .collect(), + Err(_) => vec![], + }; + + let license: Option<String> = match fs::read_to_string(pkg.path().join("LICENSE")) { + Ok(license) => Some(license.trim().to_string()), + Err(_) => None, + }; + + let description: Option<String> = + match fs::read_to_string(pkg.path().join("DESCRIPTION")) { + Ok(description) => Some(description.trim().to_string()), + Err(_) => None, + }; + + let size: usize = fs::read_to_string(pkg.path().join("SIZE"))? + .trim() + .parse() + .unwrap_or(0); + + let repo_path = PathBuf::from("/var/db/repos/") + .join(&repository) + .join(&cat_name) + .join(pkg_name) + .join("metadata.xml"); + + let build_time = fs::read_to_string(pkg.path().join("BUILD_TIME"))? + .trim() + .parse::<i64>() + .ok() + .and_then(DateTime::from_timestamp_secs) + .unwrap_or(DateTime::from_timestamp_nanos(0)); + + let maintainer = extract_maintainer(&repo_path).unwrap_or(None); + + let atom = PackageKey { + category: cat_name.clone(), + name: pkg_name.to_string(), + }; + + v.push(InstalledPackage { + atom: atom, + version: pkg_version.to_string(), + build_time, + slot: 0, + metadata: Metadata { + maintainer, + description, + homepage: homepage, + license, + repository: repository, + }, + enabled_use_flags: enabled_use_flags, + iuse: use_flags, + size, + }); + } + } + + Ok(v) +} diff --git a/src/main.rs b/src/main.rs index b768cc7..120721b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ mod actions; mod app; -mod event; mod gentoo; mod screens; mod theme; @@ -20,7 +19,10 @@ use ratatui::{ prelude::CrosstermBackend, }; -use crate::{app::App, gentoo::Portage}; +use crate::{ + app::App, + gentoo::{Portage, portage_loader}, +}; struct TuiGuard; @@ -37,10 +39,9 @@ fn main() -> color_eyre::Result<()> { let tui = init_tui()?; let mut p = Portage::new(); - p.world_packages = Portage::load_world_packages()?; - - p.installed_packages = Portage::load_installed_packages()?; - p.load_available_packages()?; + p.world_packages = portage_loader::load_world_packages()?; + p.installed_packages = portage_loader::load_installed_packages()?; + // p.available_packages = portage_loader::load_available_packages()?; let mut app = App::new(p); diff --git a/src/screens/dashboard.rs b/src/screens/dashboard.rs new file mode 100644 index 0000000..e03624d --- /dev/null +++ b/src/screens/dashboard.rs @@ -0,0 +1,144 @@ +use ratatui::{ + crossterm::event::KeyCode, + layout::{Alignment, Constraint, Layout, Spacing}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Paragraph}, +}; + +use crate::{actions::Signal, screens::screen::Screen, theme::Theme, widgets::helpers::human_size}; + +#[derive(Debug, Default)] +pub struct DashboardScreen {} + +fn create_stats<'a>(title: &'a str, value: &'a str) -> Paragraph<'a> { + Paragraph::new(vec![ + Line::from(Span::styled(title, Theme::title())), + Line::from(""), + Line::from(value), + ]) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Theme::block()) + .merge_borders(ratatui::symbols::merge::MergeStrategy::Exact), + ) + .style(Theme::text()) + .alignment(Alignment::Center) +} + +impl Screen for DashboardScreen { + fn draw( + &mut self, + frame: &mut ratatui::Frame, + area: ratatui::prelude::Rect, + repo: &crate::gentoo::Portage, + ) { + let logo = " + ██████╗ ██╗ ██╗██╗███████╗██╗ ██╗ + ██╔═══██╗██║ ██║██║██╔════╝██║ ██║ + ██║ ██║██║ ██║██║█████╗ ██║ █╗ ██║ + ██║▄▄ ██║╚██╗ ██╔╝██║██╔══╝ ██║███╗██║ + ╚██████╔╝ ╚████╔╝ ██║███████╗╚███╔███╔╝ + ╚══▀▀═╝ ╚═══╝ ╚═╝╚══════╝ ╚══╝╚══╝ + gentoo portage dashboard + "; + + let constraints = [ + Constraint::Length(9), + Constraint::Min(15), + Constraint::Fill(1), + Constraint::Length(3), + ]; + + let layout = Layout::vertical(constraints); + + let [logo_top, stats, _fill, key_hints] = area.layout(&layout); + + let [stats_top, stats_middle, stats_bottom] = Layout::vertical([ + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Fill(1), + ]) + .spacing(Spacing::Overlap(1)) + .areas(stats); + + let splits = Layout::horizontal([ + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Fill(1), + ]) + .spacing(Spacing::Overlap(1)) + .split(stats_top); + + let logo = Paragraph::new(Text::styled(logo, Theme::title())); + frame.render_widget(logo, logo_top); + + let keys = Paragraph::new(Span::styled( + "(q) to quit | (tab) to switch tabs", + Theme::muted(), + )) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Theme::block()), + ); + frame.render_widget(keys, key_hints); + + let installed_packages_len = repo.installed_packages.len().to_string(); + let installed_packages = + create_stats("Installed Packages", installed_packages_len.as_str()); + let global_use_flags = create_stats("Global USE Flags", "COMING SOON"); + let repository_state = create_stats("Repository state", "COMING SOON"); + frame.render_widget(installed_packages, splits[0]); + frame.render_widget(global_use_flags, splits[1]); + frame.render_widget(repository_state, splits[2]); + + let splits = Layout::horizontal([ + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Fill(1), + ]) + .spacing(Spacing::Overlap(1)) + .split(stats_middle); + + let world_package_count = repo.world_packages.len().to_string(); + let world_packages = create_stats("World Packages", &world_package_count); + let pending_updates = create_stats("Pending Updates", "COMING SOON"); + let last_emerge = create_stats("Last Emerge", "COMING SOON"); + + frame.render_widget(world_packages, splits[0]); + frame.render_widget(pending_updates, splits[1]); + frame.render_widget(last_emerge, splits[2]); + + let splits = Layout::horizontal([ + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Fill(1), + ]) + .spacing(Spacing::Overlap(1)) + .split(stats_bottom); + + let installed_size = human_size(repo.total_installed_size()); + + let installed_size = create_stats("Installed Size", &installed_size); + let distfiles_cache = create_stats("Distfiles Cache", "COMING SOON"); + let portage_news = create_stats("Portage News", "COMING SOON"); + + frame.render_widget(installed_size, splits[0]); + frame.render_widget(distfiles_cache, splits[1]); + frame.render_widget(portage_news, splits[2]); + } + + fn update( + &mut self, + key: ratatui::crossterm::event::KeyEvent, + _: &crate::gentoo::Portage, + ) -> Option<crate::actions::Signal> { + match key.code { + KeyCode::Char('q') => Some(Signal::Quit), + KeyCode::Tab => Some(Signal::CycleTab), + + _ => None, + } + } +} diff --git a/src/screens/installed_packages.rs b/src/screens/installed_packages.rs index 7e65fd5..48b0513 100644 --- a/src/screens/installed_packages.rs +++ b/src/screens/installed_packages.rs @@ -6,11 +6,11 @@ use ratatui::{ text::{Line, Span}, widgets::{Block, Borders, Clear, List, ListState, Paragraph}, }; -use ratatui_textarea::{Key, TextArea}; +use ratatui_textarea::TextArea; use crate::{ actions::Signal, - gentoo::Portage, + gentoo::{InstalledPackage, Portage}, screens::screen::Screen, theme::Theme, widgets::{ @@ -19,13 +19,61 @@ use crate::{ }, }; -#[derive(Debug, Default)] +#[derive(Debug)] struct SearchPopup { pub(crate) visible: bool, pub(crate) textarea: TextArea<'static>, + pub(crate) result: Option<usize>, } -#[derive(Debug, Default)] +impl Default for SearchPopup { + fn default() -> Self { + let mut search_popup = Self { + visible: bool::default(), + textarea: TextArea::default(), + result: Option::default(), + }; + + search_popup.textarea.set_style(Theme::muted()); + search_popup + .textarea + .set_cursor_line_style(Style::default()); + + return search_popup; + } +} + +impl SearchPopup { + pub fn toggle(&mut self) { + self.visible = !self.visible; + self.textarea.clear(); + } + + pub fn search(&mut self, repo: &Portage) -> Option<usize> { + let indexes: Vec<usize> = repo + .installed_packages + .iter() + .enumerate() + .filter(|(_, s)| { + s.atom + .qualified_name() + .to_lowercase() + .contains(&self.textarea.lines()[0].to_lowercase()) + }) + .map(|(i, _)| i) + .collect(); + + if indexes.len() >= 1 { + self.result = Some(indexes[0]); + return Some(indexes[0]); + } else { + self.result = None; + return None; + } + } +} + +#[derive(Debug)] pub struct InstalledPackagesScreen { list_state: ListState, @@ -34,12 +82,12 @@ pub struct InstalledPackagesScreen { impl InstalledPackagesScreen { fn render_packages(&mut self, frame: &mut Frame, area: Rect, repo: &Portage) { - // let items: Vec<String> = repo - // .installed_packages() - // .iter() - // .map(|x| x.atom.qualified_name()) - // .collect(); - let items: Vec<String> = vec![]; + // TODO: Put this into portage method + let items: Vec<String> = repo + .installed_packages + .iter() + .map(|x| x.atom.qualified_name()) + .collect(); let list = List::new(items) .style(Color::White) @@ -56,6 +104,17 @@ impl InstalledPackagesScreen { } } +impl Default for InstalledPackagesScreen { + fn default() -> Self { + let mut list_state = ListState::default(); + list_state.select(Some(0)); + Self { + list_state: list_state, + search_popup: Default::default(), + } + } +} + impl Screen for InstalledPackagesScreen { fn draw(&mut self, frame: &mut Frame, area: Rect, repo: &Portage) { let constraints = [Constraint::Fill(1), Constraint::Length(3)]; @@ -69,9 +128,16 @@ impl Screen for InstalledPackagesScreen { let split_vert = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(split[1]); + // TODO: Atleast this doesnt crash but its still weird, it should show some Info that this + // package is completly unkown or placeholder data, or at best a warning + let mut pkg: Option<&InstalledPackage> = None; + if repo.installed_packages.len() > self.list_state.selected().unwrap_or(0) { + pkg = Some(&repo.installed_packages[self.list_state.selected().unwrap_or(0)]); + } + self.render_packages(frame, split[0], repo); - render_use_flags(frame, split_vert[0], &repo.installed_packages[0]); - render_package_metadata(frame, split_vert[1], &repo.installed_packages[0]); + render_use_flags(frame, split_vert[0], pkg); + render_package_metadata(frame, split_vert[1], pkg); let text = if self.search_popup.visible { "(esc) to quit search | (enter) to search".to_string() @@ -107,20 +173,13 @@ impl Screen for InstalledPackagesScreen { .border_style(Theme::block()) .style(Style::default().bg(Theme::BG)); - // if let Some(found) = app.search_found { - // if found { - // popup_block = popup_block.border_style(Theme::success()); - // } else { - // popup_block = popup_block.border_style(Theme::error()); - // } - // } + if let Some(_) = self.search_popup.result { + popup_block = popup_block.border_style(Theme::success()); + } else { + popup_block = popup_block.border_style(Theme::error()); + } - // TODO: Only do this in init, not every time self.search_popup.textarea.set_block(popup_block); - self.search_popup.textarea.set_style(Theme::muted()); - self.search_popup - .textarea - .set_cursor_line_style(Style::default()); let area = search_popup_rect(70, area); @@ -134,20 +193,43 @@ impl Screen for InstalledPackagesScreen { } } - fn update(&mut self, key: KeyEvent) -> Option<Signal> { + fn update(&mut self, key: KeyEvent, repo: &Portage) -> Option<Signal> { if self.search_popup.visible { match key.code { - KeyCode::Esc => todo!("Toggle search window"), - KeyCode::Enter => todo!("Send search, get back matches"), - _ => self.search_popup.textarea.input_without_shortcuts(key), //TODO: Run "search - //logic" to get - //jumping while typing, + KeyCode::Esc => self.search_popup.toggle(), + KeyCode::Enter => { + let result = self.search_popup.search(repo); + self.search_popup.toggle(); + + if let Some(index) = result { + self.list_state.select(Some(index)); + } + } + _ => { + self.search_popup.textarea.input_without_shortcuts(key); + let result = self.search_popup.search(repo); + + if let Some(index) = result { + self.list_state.select(Some(index)); + } + } }; } else { match key.modifiers { KeyModifiers::CONTROL => match key.code { - KeyCode::Char('d') => todo!("next page"), - KeyCode::Char('u') => todo!("prev page"), + KeyCode::Char('d') => self + .list_state + .select(Some(self.list_state.selected().unwrap_or(0) + 30)), + KeyCode::Char('u') => { + if let Some(selected) = self.list_state.selected() { + if selected < 30 { + self.list_state.select(Some(0)); + } else { + self.list_state.select(Some(selected - 30)); + } + } + } + _ => {} }, @@ -156,7 +238,7 @@ impl Screen for InstalledPackagesScreen { KeyCode::Tab => return Some(Signal::CycleTab), KeyCode::Char('j') => self.list_state.select_next(), KeyCode::Char('k') => self.list_state.select_previous(), - KeyCode::Char('/') => todo!("Toggle search window"), + KeyCode::Char('/') => self.search_popup.toggle(), KeyCode::Char('n') => todo!("Next search"), KeyCode::Char('N') => todo!("prev search"), diff --git a/src/screens/mod.rs b/src/screens/mod.rs index db3be11..45ca33f 100644 --- a/src/screens/mod.rs +++ b/src/screens/mod.rs @@ -1,2 +1,3 @@ +pub mod dashboard; pub mod installed_packages; pub mod screen; diff --git a/src/screens/screen.rs b/src/screens/screen.rs index a87329b..f299578 100644 --- a/src/screens/screen.rs +++ b/src/screens/screen.rs @@ -1,11 +1,8 @@ use ratatui::{Frame, crossterm::event::KeyEvent, layout::Rect}; -use crate::{ - actions::{Action, Signal}, - gentoo::Portage, -}; +use crate::{actions::Signal, gentoo::Portage}; pub(crate) trait Screen { fn draw(&mut self, frame: &mut Frame, area: Rect, repo: &Portage); - fn update(&mut self, action: KeyEvent) -> Option<Signal>; + fn update(&mut self, key: KeyEvent, repo: &Portage) -> Option<Signal>; } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 66f4b70..7bf8fde 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,3 +1,4 @@ pub mod helpers; pub mod package_metadata; +pub mod tabs; pub mod use_flags; diff --git a/src/widgets/package_metadata.rs b/src/widgets/package_metadata.rs index e4f83bb..7377cbc 100644 --- a/src/widgets/package_metadata.rs +++ b/src/widgets/package_metadata.rs @@ -12,7 +12,11 @@ use crate::{ widgets::helpers::{homepage_lines, human_size}, }; -pub fn render_package_metadata(frame: &mut Frame, area: Rect, pkg: &InstalledPackage) { +pub fn render_package_metadata(frame: &mut Frame, area: Rect, pkg: Option<&InstalledPackage>) { + let Some(pkg) = pkg else { + return; + }; + let bold_style = Theme::title(); //Style::default().add_modifier(Modifier::BOLD); let maintainer = Line::from_iter([ Span::styled("Maintainer: ", bold_style), diff --git a/src/widgets/tabs.rs b/src/widgets/tabs.rs new file mode 100644 index 0000000..aca7685 --- /dev/null +++ b/src/widgets/tabs.rs @@ -0,0 +1,24 @@ +use ratatui::{ + Frame, + layout::Rect, + symbols, + widgets::{Block, Borders, Tabs}, +}; + +use crate::{app::ViewState, theme::Theme}; + +pub fn render_tab(frame: &mut Frame, area: Rect, selected_tab: &ViewState) { + let selected_index = match selected_tab { + ViewState::Dashboard => 0, + ViewState::InstalledPackages => 1, + ViewState::AvailablePackages => 2, + }; + + let tabs = Tabs::new(vec!["Dashboard", "Installed", "Browse"]) + .block(Block::default().borders(Borders::ALL).style(Theme::block())) + .highlight_style(Theme::success()) + .select(selected_index) + .divider(symbols::DOT) + .padding(" ", " "); + frame.render_widget(tabs, area); +} diff --git a/src/widgets/use_flags.rs b/src/widgets/use_flags.rs index 8e2ecf4..711a45a 100644 --- a/src/widgets/use_flags.rs +++ b/src/widgets/use_flags.rs @@ -5,9 +5,13 @@ use ratatui::{ widgets::{Block, Borders, List, ListItem, ListState}, }; -use crate::{app::App, gentoo::InstalledPackage, theme::Theme}; +use crate::{gentoo::InstalledPackage, theme::Theme}; + +pub fn render_use_flags(frame: &mut Frame, area: Rect, package: Option<&InstalledPackage>) { + let Some(package) = package else { + return; + }; -pub fn render_use_flags(frame: &mut Frame, area: Rect, package: &InstalledPackage) { let items = package.iuse.iter().map(|x| { let text_style = if package.enabled_use_flags.contains(&x.name) { Theme::success() |
