diff options
| author | pablu <me@pablu.de> | 2026-05-19 20:51:10 +0200 |
|---|---|---|
| committer | pablu <me@pablu.de> | 2026-05-19 20:51:10 +0200 |
| commit | afe649e0efc31fcd33a828e54bd6313cd37f1784 (patch) | |
| tree | c2cec9450cc1778f62c6339dad9c8f7d43c46b16 | |
| parent | a6b4b29aaa222fa5c2f30cad233e97cb8125f96f (diff) | |
Add available package loading, update README.md, use eyre_color::Result
| -rw-r--r-- | Cargo.lock | 128 | ||||
| -rw-r--r-- | Cargo.toml | 7 | ||||
| -rw-r--r-- | README.md | 35 | ||||
| -rw-r--r-- | src/app.rs | 14 | ||||
| -rw-r--r-- | src/event.rs | 16 | ||||
| -rw-r--r-- | src/gentoo/mod.rs | 2 | ||||
| -rw-r--r-- | src/gentoo/package.rs | 92 | ||||
| -rw-r--r-- | src/gentoo/portage.rs | 281 | ||||
| -rw-r--r-- | src/gentoo/useflags.rs | 11 | ||||
| -rw-r--r-- | src/main.rs | 6 | ||||
| -rw-r--r-- | src/ui.rs | 64 |
11 files changed, 511 insertions, 145 deletions
@@ -33,6 +33,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -132,6 +141,16 @@ dependencies = [ ] [[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -144,6 +163,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] name = "color-eyre" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -194,6 +226,12 @@ dependencies = [ ] [[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -402,6 +440,12 @@ dependencies = [ ] [[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] name = "finl_unicode" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -535,6 +579,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1016,6 +1084,7 @@ dependencies = [ name = "qview" version = "0.1.0" dependencies = [ + "chrono", "color-eyre", "quick-xml", "ratatui", @@ -1301,6 +1370,12 @@ dependencies = [ ] [[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] name = "signal-hook" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1862,12 +1937,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -8,7 +8,14 @@ license = "MIT" [dependencies] +chrono = "0.4.44" color-eyre = "0.6.5" quick-xml = "0.39.3" ratatui = "0.30.0" ratatui-textarea = "0.9.1" + +[profile.release] +codegen-units = 1 +lto = true +opt-level = 3 +strip = true @@ -141,23 +141,28 @@ nix develop --- -## Planned Features +## Roadmap The project is still early in development. Planned features include: -* Public Gentoo ebuild -* Async package metadata loading -* Portage news reader -* Portage package search/browser -* Filtering installed packages - * World packages - * Explicitly installed packages - * Repository filters - * USE flag filters -* Improved dashboard statistics -* Search improvements -* Better package metadata rendering -* Sorting options -* Performance improvements for large systems +- [x] Public guru ebuild +- [ ] Portage package search/browser +- [ ] Improved information in package metadata + - [ ] RDEPEND + - [ ] BDEPEND + - [ ] SLOT + - [ ] Keywords + - [ ] New version available? +- [ ] Filtering installed packages + - [ ] World packages + - [ ] Explicitly installed packages + - [ ] Repository filters + - [ ] USE flag filters +- [ ] Better package USE Flag rendering +- [ ] Improved dashboard statistics +- [ ] Portage news reader +- [ ] Search improvements +- [ ] Sorting options +- [ ] Async package metadata loading --- @@ -1,17 +1,19 @@ use ratatui::widgets::ListState; use ratatui_textarea::TextArea; -use crate::gentoo::{Package, portage::Portage}; +use crate::gentoo::{InstalledPackage, portage::Portage}; #[derive(Debug)] pub enum ViewState { Dashboard = 0, InstalledPackages = 1, + AvailablePackages = 2, } #[derive(Debug)] pub struct App<'a> { portage: Portage, + pub list_state: ListState, pub showing_search_window: bool, pub textarea: TextArea<'a>, @@ -41,11 +43,11 @@ impl App<'_> { self.portage.world_packages.len() } - pub fn installed_packages(&self) -> &[Package] { + pub fn installed_packages(&self) -> &[InstalledPackage] { &self.portage.installed_packages } - pub fn current_package(&self) -> &Package { + pub fn current_package(&self) -> &InstalledPackage { let selected_package_index = self.list_state.selected().unwrap_or_default(); &self.portage.installed_packages[selected_package_index] } @@ -63,7 +65,8 @@ impl App<'_> { pub fn cycle_current_tab(&mut self) { match self.view { ViewState::Dashboard => self.view = ViewState::InstalledPackages, - ViewState::InstalledPackages => self.view = ViewState::Dashboard, + ViewState::InstalledPackages => self.view = ViewState::AvailablePackages, + ViewState::AvailablePackages => self.view = ViewState::Dashboard, } } @@ -74,7 +77,8 @@ impl App<'_> { .iter() .enumerate() .filter(|(_, s)| { - s.name + s.atom + .qualified_name() .to_lowercase() .contains(&self.textarea.lines()[0].to_lowercase()) }) diff --git a/src/event.rs b/src/event.rs index 81cf6bd..44a4c3e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -19,12 +19,28 @@ pub fn handle_event(app: &mut App) -> io::Result<bool> { 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(), diff --git a/src/gentoo/mod.rs b/src/gentoo/mod.rs index ff343bc..e2cc3df 100644 --- a/src/gentoo/mod.rs +++ b/src/gentoo/mod.rs @@ -2,6 +2,6 @@ pub mod package; pub mod portage; pub mod useflags; -pub use package::Package; +pub use package::InstalledPackage; pub use portage::Portage; pub use useflags::UseFlag; diff --git a/src/gentoo/package.rs b/src/gentoo/package.rs index a74a09c..d1596f7 100644 --- a/src/gentoo/package.rs +++ b/src/gentoo/package.rs @@ -1,44 +1,70 @@ use std::collections::HashSet; +use chrono::{DateTime, Utc}; + use crate::gentoo::UseFlag; -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct Package { +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct PackageKey { + pub category: String, pub name: String, - pub use_flags: HashSet<UseFlag>, - pub version: String, - pub repository: String, +} + +impl PackageKey { + pub fn from_atom(s: &str) -> Option<Self> { + let (category, name) = s.split_once('/')?; + Some(Self { + category: category.to_string(), + name: name.to_string(), + }) + } + + pub fn qualified_name(&self) -> String { + format!("{}/{}", self.category, self.name) + } +} + +#[derive(Debug, Clone)] +pub struct Metadata { pub maintainer: Option<String>, pub description: Option<String>, - pub homepage: Option<Vec<String>>, + pub homepage: Vec<String>, pub license: Option<String>, + pub repository: String, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct InstalledPackage { + pub atom: PackageKey, + + pub version: String, + + pub build_time: DateTime<Utc>, + + pub slot: i32, + + pub metadata: Metadata, + + pub enabled_use_flags: HashSet<String>, + pub iuse: Vec<UseFlag>, + pub size: usize, } -// #[allow(dead_code)] -// impl Package { -// pub fn new( -// name: String, -// version: String, -// repository: String, -// size: usize, -// homepage: Option<Vec<String>>, -// license: Option<String>, -// description: Option<String>, -// maintainer: Option<String>, -// use_flags: HashSet<UseFlag>, -// ) -> Self { -// Package { -// name, -// use_flags, -// version, -// repository, -// maintainer, -// description, -// homepage, -// license, -// size, -// } -// } -// } +#[derive(Debug)] +pub struct Package { + pub atom: PackageKey, + + pub versions: Vec<Version>, +} + +#[derive(Debug, Clone)] +pub struct Version { + pub version: String, + + pub metadata: Metadata, + + pub keywords: Vec<String>, + pub iuse: Vec<UseFlag>, +} diff --git a/src/gentoo/portage.rs b/src/gentoo/portage.rs index bc9bb32..51dc60a 100644 --- a/src/gentoo/portage.rs +++ b/src/gentoo/portage.rs @@ -1,20 +1,26 @@ use std::{ - collections::HashSet, - fs, io, + collections::{HashMap, HashSet}, + fs, path::{Path, PathBuf}, }; +use chrono::DateTime; +use color_eyre::eyre::Context; use quick_xml::{Reader, events::Event}; -use crate::gentoo::{Package, UseFlag}; +use crate::gentoo::{ + InstalledPackage, UseFlag, + package::{Metadata, Package, PackageKey, Version}, +}; #[derive(Debug, Default)] pub struct Portage { - pub installed_packages: Vec<Package>, - pub world_packages: Vec<String>, + pub installed_packages: Vec<InstalledPackage>, + pub world_packages: HashSet<PackageKey>, + pub available_packages: Vec<Package>, } -fn split_pkg(input: &str) -> (&str, Option<&str>) { +fn split_pkg(input: &str) -> (&str, &str) { let bytes = input.as_bytes(); for i in (0..bytes.len()).rev() { @@ -24,14 +30,14 @@ fn split_pkg(input: &str) -> (&str, Option<&str>) { { let name = &input[..i]; let version = &input[i + 1..]; - return (name, Some(version)); + return (name, version); } } - (input, None) + (input, "") } -fn extract_maintainer(path: &Path) -> io::Result<Option<String>> { +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); @@ -69,32 +75,189 @@ fn extract_maintainer(path: &Path) -> io::Result<Option<String>> { 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 { installed_packages: vec![], - world_packages: vec![], + world_packages: HashSet::new(), + available_packages: vec![], } } - pub fn load_world_packages(&mut self) -> io::Result<()> { + pub fn load_world_packages() -> color_eyre::Result<HashSet<PackageKey>> { let world = fs::read_to_string("/var/lib/portage/world")?; - self.world_packages = world + Ok(world .split_whitespace() - .map(std::string::ToString::to_string) - .collect(); + .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); + } + } + } + } + + self.available_packages = packages.into_values().collect(); Ok(()) } - pub fn load_installed_packages(&mut self) -> io::Result<()> { + 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 cat_name = category.file_name(); + 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 { @@ -109,37 +272,36 @@ impl Portage { let (pkg_name, pkg_version) = split_pkg(pkg_name); let use_flags_file = fs::read_to_string(pkg.path().join("USE"))?; - let use_flags: HashSet<&str> = use_flags_file.trim().split(' ').collect(); + 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 iuse_flags: Vec<&str> = iuse_flags_file.trim().split(' ').collect(); - let use_flags: HashSet<UseFlag> = iuse_flags - .iter() - .filter(|x| !x.trim().is_empty()) - .map(|&original_iuse| { + let use_flags: Vec<UseFlag> = iuse_flags_file + .split_whitespace() + .map(|original_iuse| { let iuse = original_iuse.strip_prefix("+").unwrap_or(original_iuse); - let is_default = iuse != original_iuse; - - let is_active = use_flags.contains(&iuse); - UseFlag::new(iuse.into(), is_active, is_default) + 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: Option<Vec<String>> = - match fs::read_to_string(pkg.path().join("HOMEPAGE")) { - Ok(homepage) => Some( - homepage - .split_whitespace() - .map(std::string::ToString::to_string) - .collect(), - ), - Err(_) => None, - }; + + 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()), @@ -152,33 +314,50 @@ impl Portage { Err(_) => None, }; - let size: usize = match fs::read_to_string(pkg.path().join("SIZE")) { - Ok(size) => size.trim().parse().unwrap_or(0), - Err(_) => 0, - }; + 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.to_str().unwrap_or_default()) + .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)?; - self.installed_packages.push(Package { - name: format!("{}/{}", cat_name.to_str().unwrap_or_default(), pkg_name), - use_flags, - version: pkg_version.unwrap_or_default().into(), - repository, - maintainer, - description, - homepage, - license, + 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(()) + Ok(v) } } diff --git a/src/gentoo/useflags.rs b/src/gentoo/useflags.rs index 1e83b32..2bcd678 100644 --- a/src/gentoo/useflags.rs +++ b/src/gentoo/useflags.rs @@ -1,16 +1,5 @@ #[derive(Debug, Clone, Eq, PartialEq, Hash)] 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 index 56016d3..ae3ff96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,8 +38,10 @@ fn main() -> color_eyre::Result<()> { let mut tui = init_tui()?; let mut p = Portage::new(); - p.load_world_packages()?; - p.load_installed_packages()?; + p.world_packages = Portage::load_world_packages()?; + + p.installed_packages = Portage::load_installed_packages()?; + p.load_available_packages()?; let mut app = App::new(p); @@ -12,7 +12,7 @@ use ratatui::{ use crate::{ app::{App, ViewState}, - gentoo::Package, + gentoo::InstalledPackage, theme::Theme, }; @@ -27,6 +27,9 @@ pub fn ui(frame: &mut Frame, app: &mut App) { match app.view { crate::app::ViewState::Dashboard => render_dashboard(frame, rest, app), crate::app::ViewState::InstalledPackages => render_installed_packages(frame, rest, app), + crate::app::ViewState::AvailablePackages => { + frame.render_widget(Text::from("NOT IMPLEMENTED"), rest) + } } } @@ -34,9 +37,10 @@ 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", "Search"]) + let tabs = Tabs::new(vec!["Dashboard", "Installed", "Browse"]) .block(Block::default().borders(Borders::ALL).style(Theme::block())) .highlight_style(Theme::success()) .select(selected_index) @@ -58,7 +62,7 @@ fn render_dashboard(frame: &mut Frame, area: Rect, app: &App) { let constraints = [ Constraint::Length(9), - Constraint::Percentage(30), + Constraint::Min(15), Constraint::Fill(1), Constraint::Length(3), ]; @@ -254,7 +258,7 @@ fn render_package_metadata(frame: &mut Frame, area: Rect, app: &App) { let bold_style = Theme::title(); //Style::default().add_modifier(Modifier::BOLD); let maintainer = Line::from_iter([ Span::styled("Maintainer: ", bold_style), - Span::raw(pkg.maintainer.as_deref().unwrap_or("Unknown")), + Span::raw(pkg.metadata.maintainer.as_deref().unwrap_or("Unknown")), ]); let version = Line::from_iter([ @@ -264,17 +268,17 @@ fn render_package_metadata(frame: &mut Frame, area: Rect, app: &App) { let repository = Line::from_iter([ Span::styled("Repository: ", bold_style), - Span::raw(&pkg.repository), + Span::raw(&pkg.metadata.repository), ]); let license = Line::from_iter([ Span::styled("License: ", bold_style), - Span::raw(pkg.license.as_deref().unwrap_or("Unknown")), + Span::raw(pkg.metadata.license.as_deref().unwrap_or("Unknown")), ]); let description = Line::from_iter([ Span::styled("Description: ", bold_style), - Span::raw(pkg.description.as_deref().unwrap_or("Unknown")), + Span::raw(pkg.metadata.description.as_deref().unwrap_or("Unknown")), ]); let size = Line::from_iter([ @@ -282,7 +286,20 @@ fn render_package_metadata(frame: &mut Frame, area: Rect, app: &App) { Span::raw(human_size(pkg.size)), ]); - let mut lines = vec![maintainer, version, repository, license, description, size]; + let build_time = Line::from_iter([ + Span::styled("Build time: ", bold_style), + Span::raw(pkg.build_time.to_string()), + ]); + + let mut lines = vec![ + maintainer, + version, + repository, + build_time, + license, + description, + size, + ]; let mut homepage = homepage_lines(pkg, bold_style); lines.append(&mut homepage); @@ -299,29 +316,22 @@ fn render_package_metadata(frame: &mut Frame, area: Rect, app: &App) { frame.render_widget(paragraph, area); } -fn homepage_lines(pkg: &Package, bold_style: Style) -> Vec<Line<'_>> { +fn homepage_lines(pkg: &InstalledPackage, bold_style: Style) -> Vec<Line<'_>> { let mut lines = Vec::new(); lines.push(Line::from(vec![ Span::styled("Homepage: ", bold_style), Span::raw(""), ])); - if let Some(homepages) = &pkg.homepage { - for url in homepages.iter().filter(|s| !s.is_empty()) { - lines.push(Line::from(vec![ - Span::raw(" - "), // indentation - Span::styled( - url, - Style::default() - .fg(Color::LightBlue) - .add_modifier(Modifier::UNDERLINED), - ), - ])); - } - } else { + for url in pkg.metadata.homepage.iter().filter(|s| !s.is_empty()) { lines.push(Line::from(vec![ - Span::raw(" "), - Span::styled("Unknown", Style::default().fg(Color::DarkGray)), + Span::raw(" - "), // indentation + Span::styled( + url, + Style::default() + .fg(Color::LightBlue) + .add_modifier(Modifier::UNDERLINED), + ), ])); } @@ -350,7 +360,7 @@ fn render_packages(frame: &mut Frame, area: Rect, app: &mut App) { let items: Vec<String> = app .installed_packages() .iter() - .map(|x| x.name.clone()) + .map(|x| x.atom.qualified_name()) .collect(); let list = List::new(items) @@ -370,8 +380,8 @@ fn render_packages(frame: &mut Frame, area: Rect, app: &mut App) { fn render_use_flags(frame: &mut Frame, area: Rect, app: &App) { let selected_package = app.current_package(); - let items = selected_package.use_flags.iter().map(|x| { - let text_style = if x.enabled { + let items = selected_package.iuse.iter().map(|x| { + let text_style = if selected_package.enabled_use_flags.contains(&x.name) { Theme::success() } else if x.default { Theme::info() |
