aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpablu <me@pablu.de>2026-06-03 14:10:49 +0200
committerpablu <me@pablu.de>2026-06-03 14:10:49 +0200
commiteaa1b915da8480bf3a86960c94f3667f386aa0f4 (patch)
treeb85e1d2edb0a507ba480926d12fab074d52bb961
parent81cc9a0b347255629d411323e7ff8fee4d5ec61d (diff)
wip: browser screen implementation
-rw-r--r--src/app.rs13
-rw-r--r--src/gentoo/package.rs24
-rw-r--r--src/gentoo/portage.rs19
-rw-r--r--src/screens/available_packages.rs196
-rw-r--r--src/screens/installed_packages.rs21
-rw-r--r--src/screens/mod.rs1
-rw-r--r--src/widgets/helpers.rs6
-rw-r--r--src/widgets/package_metadata.rs66
-rw-r--r--src/widgets/use_flags.rs25
9 files changed, 314 insertions, 57 deletions
diff --git a/src/app.rs b/src/app.rs
index d6dd71e..70a3474 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -11,7 +11,8 @@ use crate::{
background_loader::PackageLoader,
gentoo::portage::Portage,
screens::{
- dashboard::DashboardScreen, installed_packages::InstalledPackagesScreen, screen::Screen,
+ available_packages::AvailablePackagesScreen, dashboard::DashboardScreen,
+ installed_packages::InstalledPackagesScreen, screen::Screen,
},
signal::Signal,
theme::Theme,
@@ -37,8 +38,9 @@ pub enum LoadingState {
pub struct App {
portage: Portage,
- installed_package_screen: InstalledPackagesScreen,
dashboard_screen: DashboardScreen,
+ installed_package_screen: InstalledPackagesScreen,
+ available_package_screen: AvailablePackagesScreen,
pub view: ViewState,
@@ -54,6 +56,7 @@ impl App {
dashboard_screen: DashboardScreen::default(),
installed_package_screen: InstalledPackagesScreen::default(),
+ available_package_screen: AvailablePackagesScreen::default(),
view: ViewState::Dashboard,
@@ -81,7 +84,8 @@ impl App {
.draw(frame, rest, &self.portage, &self.loading_state);
}
ViewState::AvailablePackages => {
- frame.render_widget(Text::from("NOT IMPLEMENTED"), rest);
+ self.available_package_screen
+ .draw(frame, rest, &self.portage, &self.loading_state);
}
}
}
@@ -92,6 +96,9 @@ impl App {
crate::app::ViewState::InstalledPackages => {
self.installed_package_screen.update(key, &self.portage)
}
+ ViewState::AvailablePackages => {
+ self.available_package_screen.update(key, &self.portage)
+ }
// Fallback, primarly while implementing
_ => match key.code {
diff --git a/src/gentoo/package.rs b/src/gentoo/package.rs
index 1d41b8c..fcd86eb 100644
--- a/src/gentoo/package.rs
+++ b/src/gentoo/package.rs
@@ -4,12 +4,34 @@ use chrono::{DateTime, Utc};
use crate::gentoo::UseFlag;
-#[derive(Debug, Clone, Hash, Eq, PartialEq)]
+#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct PackageKey {
+ // Field declaration matters because Ord, and PartialOrd check in order
pub category: String,
pub name: String,
}
+// impl Ord for PackageKey {
+// fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+// let cat_ord = self.category.cmp(&other.category);
+// if cat_ord == Ordering::Equal {
+// return self.name.cmp(&other.name);
+// }
+//
+// cat_ord
+// }
+// }
+//
+// impl PartialOrd for PackageKey {
+// fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+// match self.category.partial_cmp(&other.category) {
+// Some(core::cmp::Ordering::Equal) => {}
+// ord => return ord,
+// }
+// self.name.partial_cmp(&other.name)
+// }
+// }
+
impl PackageKey {
pub fn from_atom(s: &str) -> Option<Self> {
let (category, name) = s.split_once('/')?;
diff --git a/src/gentoo/portage.rs b/src/gentoo/portage.rs
index 935c1ce..6a1b797 100644
--- a/src/gentoo/portage.rs
+++ b/src/gentoo/portage.rs
@@ -41,16 +41,25 @@ impl Portage {
.collect()
}
- pub fn get_installed_package(&self, index: usize) -> Option<&InstalledPackage> {
- if self.installed.len() > index && index > 0 {
- return Some(&self.installed[index]);
+ pub fn get_installed_package(&self, key: &PackageKey) -> Option<&InstalledPackage> {
+ let pkg = self.installed.iter().find(|pkg| pkg.atom == *key);
+ if let Some(pkg) = pkg {
+ return Some(pkg);
}
None
}
- pub fn get_installed_package_key(&self, key: &PackageKey) -> Option<&InstalledPackage> {
- let pkg = self.installed.iter().find(|pkg| pkg.atom == *key);
+ pub fn available_packages(&self) -> Vec<&Package> {
+ let mut pkg: Vec<&Package> = self.available.iter().collect();
+
+ pkg.sort_by_key(|p| &p.atom);
+
+ pkg
+ }
+
+ pub fn get_available_package(&self, key: &PackageKey) -> Option<&Package> {
+ let pkg = self.available.iter().find(|pkg| pkg.atom == *key);
if let Some(pkg) = pkg {
return Some(pkg);
}
diff --git a/src/screens/available_packages.rs b/src/screens/available_packages.rs
new file mode 100644
index 0000000..1f61982
--- /dev/null
+++ b/src/screens/available_packages.rs
@@ -0,0 +1,196 @@
+use std::collections::HashSet;
+
+use ratatui::{
+ Frame,
+ crossterm::event::KeyCode,
+ layout::{Constraint, Layout, Rect},
+ style::Color,
+ text::Text,
+ widgets::{Block, Borders, List, ListState},
+};
+
+use crate::{
+ app::LoadingState,
+ gentoo::package::{Package, PackageKey, PackageVersion},
+ screens::screen::Screen,
+ signal::Signal,
+ theme::Theme,
+ widgets::{package_metadata::render_package_metadata, use_flags::render_use_flags},
+};
+
+#[derive(Debug)]
+enum CurrentList {
+ PackageList,
+ VariantList,
+}
+
+#[derive(Debug)]
+pub struct AvailablePackagesScreen {
+ chosen_list: CurrentList,
+
+ pkg_list_state: ListState,
+ variant_list_state: ListState,
+}
+
+fn version_to_variant(pkg_version: &PackageVersion) -> String {
+ format!(
+ "{}::{}",
+ pkg_version.version, pkg_version.metadata.repository
+ )
+}
+
+impl AvailablePackagesScreen {
+ // TODO: this is the same as InstalledPackagesScreen, replace this with a widget function
+ fn render_packages(&mut self, frame: &mut Frame, area: Rect, package_keys: Vec<String>) {
+ // TODO: Put this into screen, instead of rebuilding the list every frame
+ let list = List::new(package_keys)
+ .style(Color::White)
+ .highlight_style(Theme::selected())
+ .highlight_symbol("> ")
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_style(Theme::block())
+ .title("Available packages"),
+ );
+
+ frame.render_stateful_widget(list, area, &mut self.pkg_list_state);
+ }
+
+ fn render_package_variants(
+ &mut self,
+ frame: &mut Frame,
+ area: Rect,
+ variants: Vec<&PackageVersion>,
+ ) {
+ // TODO: Put this into screen, instead of rebuilding the list every frame
+ let list = List::new(variants.iter().map(|v| version_to_variant(v)))
+ .style(Color::White)
+ .highlight_style(Theme::selected())
+ .highlight_symbol("> ")
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_style(Theme::block())
+ .title("Variants"),
+ );
+
+ frame.render_stateful_widget(list, area, &mut self.variant_list_state);
+ }
+
+ fn cycle_list(&mut self) {
+ self.variant_list_state.select(Some(0));
+
+ self.chosen_list = match self.chosen_list {
+ CurrentList::PackageList => CurrentList::VariantList,
+ CurrentList::VariantList => CurrentList::PackageList,
+ }
+ }
+}
+
+impl Default for AvailablePackagesScreen {
+ fn default() -> Self {
+ let mut list_state = ListState::default();
+ list_state.select(Some(0));
+
+ let mut variant_state = ListState::default();
+ variant_state.select(Some(0));
+
+ Self {
+ chosen_list: CurrentList::PackageList,
+ pkg_list_state: list_state,
+ variant_list_state: variant_state,
+ }
+ }
+}
+
+impl Screen for AvailablePackagesScreen {
+ fn draw(
+ &mut self,
+ frame: &mut ratatui::Frame,
+ area: ratatui::prelude::Rect,
+ repo: &crate::gentoo::Portage,
+ loading_state: &crate::app::LoadingState,
+ ) {
+ if !matches!(loading_state, LoadingState::Complete) {
+ // TODO: Implement loading screen
+ frame.render_widget(Text::styled("Not implemented yet :P", Theme::text()), area);
+ return;
+ }
+
+ let constraints = [Constraint::Percentage(50), Constraint::Percentage(50)];
+ let [left, right] = area.layout(&Layout::horizontal(constraints));
+
+ let [package_list, use_flags] = left.layout(&Layout::vertical(constraints));
+ let [package_variants, metadata] = right.layout(&Layout::vertical(constraints));
+
+ let packages: Vec<&PackageKey> =
+ repo.available_packages().iter().map(|x| &x.atom).collect();
+
+ let selected_package_key = packages.get(self.pkg_list_state.selected().unwrap_or(0));
+ let pkg: Option<&Package> = match selected_package_key {
+ Some(pkg_key) => repo.get_available_package(pkg_key),
+ None => None,
+ };
+
+ self.render_packages(
+ frame,
+ package_list,
+ packages.iter().map(|x| x.qualified_name()).collect(),
+ );
+
+ if let Some(pkg) = pkg {
+ self.render_package_variants(frame, package_variants, pkg.versions.iter().collect());
+
+ let selected_version = &pkg.versions[self.variant_list_state.selected().unwrap_or(0)];
+
+ render_use_flags(
+ frame,
+ use_flags,
+ selected_version.iuse.iter().collect(),
+ // Get default active use flags for package, and globally defines ones
+ HashSet::new(),
+ );
+
+ render_package_metadata(
+ frame,
+ metadata,
+ &selected_version.version,
+ &selected_version.metadata,
+ None,
+ None,
+ );
+ }
+ }
+
+ fn update(
+ &mut self,
+ key: ratatui::crossterm::event::KeyEvent,
+ _repo: &crate::gentoo::Portage,
+ ) -> Option<crate::signal::Signal> {
+ match key.code {
+ KeyCode::Char('q') => return Some(Signal::Quit),
+ KeyCode::Tab => return Some(Signal::CycleTab),
+
+ // TODO: This should cycle, also space isnt a good key for this
+ KeyCode::Char(' ') => self.cycle_list(),
+
+ _ => match self.chosen_list {
+ CurrentList::PackageList => match key.code {
+ KeyCode::Char('j') => self.pkg_list_state.select_next(),
+ KeyCode::Char('k') => self.pkg_list_state.select_previous(),
+
+ _ => {}
+ },
+ CurrentList::VariantList => match key.code {
+ KeyCode::Char('j') => self.variant_list_state.select_next(),
+ KeyCode::Char('k') => self.variant_list_state.select_previous(),
+
+ _ => {}
+ },
+ },
+ }
+
+ None
+ }
+}
diff --git a/src/screens/installed_packages.rs b/src/screens/installed_packages.rs
index 4421837..8f0cc5d 100644
--- a/src/screens/installed_packages.rs
+++ b/src/screens/installed_packages.rs
@@ -162,7 +162,7 @@ impl Screen for InstalledPackagesScreen {
// (this) package installed
let selected_package_key = items.get(self.list_state.selected().unwrap_or(0));
let pkg: Option<&InstalledPackage> = match selected_package_key {
- Some(selected_package_key) => repo.get_installed_package_key(selected_package_key),
+ Some(selected_package_key) => repo.get_installed_package(selected_package_key),
None => None,
};
@@ -171,8 +171,23 @@ impl Screen for InstalledPackagesScreen {
split[0],
items.iter().map(|x| x.qualified_name()).collect(),
);
- render_use_flags(frame, split_vert[0], pkg);
- render_package_metadata(frame, split_vert[1], pkg);
+ if let Some(pkg) = pkg {
+ render_use_flags(
+ frame,
+ split_vert[0],
+ pkg.iuse.iter().collect(),
+ // TODO: Dont use clone here if possible
+ pkg.enabled_use_flags.clone(),
+ );
+ render_package_metadata(
+ frame,
+ split_vert[1],
+ &pkg.version,
+ &pkg.metadata,
+ Some(pkg.size),
+ Some(pkg.build_time),
+ );
+ }
let text = if self.search_popup.visible {
"(esc) to quit search | (enter) to search".to_string()
diff --git a/src/screens/mod.rs b/src/screens/mod.rs
index 45ca33f..8e39692 100644
--- a/src/screens/mod.rs
+++ b/src/screens/mod.rs
@@ -1,3 +1,4 @@
+pub mod available_packages;
pub mod dashboard;
pub mod installed_packages;
pub mod screen;
diff --git a/src/widgets/helpers.rs b/src/widgets/helpers.rs
index 0bde326..1e76b5f 100644
--- a/src/widgets/helpers.rs
+++ b/src/widgets/helpers.rs
@@ -4,7 +4,7 @@ use ratatui::{
text::{Line, Span},
};
-use crate::gentoo::InstalledPackage;
+use crate::gentoo::{InstalledPackage, package::Metadata};
pub fn search_popup_rect(percent_x: u16, r: Rect) -> Rect {
// Cut the given rectangle into three vertical pieces
@@ -24,14 +24,14 @@ pub fn search_popup_rect(percent_x: u16, r: Rect) -> Rect {
.split(popup_layout[1])[1] // Return the middle chunk
}
-pub fn homepage_lines(pkg: &InstalledPackage, bold_style: Style) -> Vec<Line<'_>> {
+pub fn homepage_lines(metadata: &Metadata, bold_style: Style) -> Vec<Line<'_>> {
let mut lines = Vec::new();
lines.push(Line::from(vec![
Span::styled("Homepage: ", bold_style),
Span::raw(""),
]));
- for url in pkg.metadata.homepage.iter().filter(|s| !s.is_empty()) {
+ for url in metadata.homepage.iter().filter(|s| !s.is_empty()) {
lines.push(Line::from(vec![
Span::raw(" - "), // indentation
Span::styled(
diff --git a/src/widgets/package_metadata.rs b/src/widgets/package_metadata.rs
index ee46f62..d6c82cd 100644
--- a/src/widgets/package_metadata.rs
+++ b/src/widgets/package_metadata.rs
@@ -1,3 +1,4 @@
+use chrono::{DateTime, Utc};
use ratatui::{
Frame,
layout::Rect,
@@ -6,64 +7,69 @@ use ratatui::{
};
use crate::{
- gentoo::InstalledPackage,
+ gentoo::{
+ InstalledPackage,
+ package::{Metadata, PackageKey},
+ },
theme::Theme,
widgets::helpers::{homepage_lines, human_size},
};
-pub fn render_package_metadata(frame: &mut Frame, area: Rect, pkg: Option<&InstalledPackage>) {
- let Some(pkg) = pkg else {
- return;
- };
-
+pub fn render_package_metadata(
+ frame: &mut Frame,
+ area: Rect,
+ pkg_version: &String,
+ metadata: &Metadata,
+ size: Option<usize>,
+ build_time: Option<DateTime<Utc>>,
+) {
let bold_style = Theme::title(); //Style::default().add_modifier(Modifier::BOLD);
let maintainer = Line::from_iter([
Span::styled("Maintainer: ", bold_style),
- Span::raw(pkg.metadata.maintainer.as_deref().unwrap_or("Unknown")),
+ Span::raw(metadata.maintainer.as_deref().unwrap_or("Unknown")),
]);
let version = Line::from_iter([
Span::styled("Version: ", bold_style),
- Span::raw(&pkg.version),
+ Span::raw(pkg_version),
]);
let repository = Line::from_iter([
Span::styled("Repository: ", bold_style),
- Span::raw(&pkg.metadata.repository),
+ Span::raw(&metadata.repository),
]);
let license = Line::from_iter([
Span::styled("License: ", bold_style),
- Span::raw(pkg.metadata.license.as_deref().unwrap_or("Unknown")),
+ Span::raw(metadata.license.as_deref().unwrap_or("Unknown")),
]);
let description = Line::from_iter([
Span::styled("Description: ", bold_style),
- Span::raw(pkg.metadata.description.as_deref().unwrap_or("Unknown")),
+ Span::raw(metadata.description.as_deref().unwrap_or("Unknown")),
]);
- let size = Line::from_iter([
- Span::styled("Size: ", bold_style),
- Span::raw(human_size(pkg.size)),
- ]);
+ let mut lines = vec![maintainer, version, repository, license, description];
- let build_time = Line::from_iter([
- Span::styled("Build time: ", bold_style),
- Span::raw(pkg.build_time.to_string()),
- ]);
+ let mut homepage = homepage_lines(metadata, bold_style);
+ lines.append(&mut homepage);
- let mut lines = vec![
- maintainer,
- version,
- repository,
- build_time,
- license,
- description,
- size,
- ];
+ if let Some(size) = size {
+ let size = Line::from_iter([
+ Span::styled("Size: ", bold_style),
+ Span::raw(human_size(size)),
+ ]);
+ lines.push(size);
+ }
- let mut homepage = homepage_lines(pkg, bold_style);
- lines.append(&mut homepage);
+ if let Some(build_time) = build_time {
+ let build_time = Line::from_iter([
+ Span::styled("Build time: ", bold_style),
+ Span::raw(build_time.to_string()),
+ ]);
+
+ lines.push(build_time);
+ }
let paragraph = Paragraph::new(lines)
.block(
diff --git a/src/widgets/use_flags.rs b/src/widgets/use_flags.rs
index 5017160..76e314b 100644
--- a/src/widgets/use_flags.rs
+++ b/src/widgets/use_flags.rs
@@ -1,3 +1,5 @@
+use std::collections::HashSet;
+
use ratatui::{
Frame,
layout::Rect,
@@ -6,22 +8,21 @@ use ratatui::{
widgets::{Block, Borders, Paragraph},
};
-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;
- };
-
- let flags: Vec<_> = package.iuse.iter().collect();
+use crate::{gentoo::UseFlag, theme::Theme};
- let max_len = flags.iter().map(|f| f.name.len()).max().unwrap_or(1);
+pub fn render_use_flags(
+ frame: &mut Frame,
+ area: Rect,
+ use_flags: Vec<&UseFlag>,
+ enabled_use_flags: HashSet<String>,
+) {
+ let max_len = use_flags.iter().map(|f| f.name.len()).max().unwrap_or(1);
// width per column (+ padding between columns)
let col_width = max_len as u16 + 3;
let columns = (area.width / col_width).max(1) as usize;
- let rows = flags.len().div_ceil(columns);
+ let rows = use_flags.len().div_ceil(columns);
let mut lines = Vec::new();
@@ -31,8 +32,8 @@ pub fn render_use_flags(frame: &mut Frame, area: Rect, package: Option<&Installe
for col in 0..columns {
let idx = row * columns + col;
- if let Some(flag) = flags.get(idx) {
- let mut style = if package.enabled_use_flags.contains(&flag.name) {
+ if let Some(flag) = use_flags.get(idx) {
+ let mut style = if enabled_use_flags.contains(&flag.name) {
Theme::success()
} else if flag.default {
Theme::info()