aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/actions/action.rs12
-rw-r--r--src/actions/mod.rs2
-rw-r--r--src/app.rs129
-rw-r--r--src/event.rs101
-rw-r--r--src/gentoo/mod.rs2
-rw-r--r--src/gentoo/portage.rs347
-rw-r--r--src/gentoo/portage_loader.rs344
-rw-r--r--src/main.rs13
-rw-r--r--src/screens/dashboard.rs144
-rw-r--r--src/screens/installed_packages.rs148
-rw-r--r--src/screens/mod.rs1
-rw-r--r--src/screens/screen.rs7
-rw-r--r--src/widgets/mod.rs1
-rw-r--r--src/widgets/package_metadata.rs6
-rw-r--r--src/widgets/tabs.rs24
-rw-r--r--src/widgets/use_flags.rs8
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;
diff --git a/src/app.rs b/src/app.rs
index c014a49..2fb3b23 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -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()