aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpablu <me@pablu.de>2026-06-03 15:48:10 +0200
committerpablu <me@pablu.de>2026-06-03 15:48:10 +0200
commit73fea832488e1b6c1e3f036531dcd9668760fe68 (patch)
tree956fdbcc64f3ff316db388b332ed94daa7c1d4e8
parent282871b88f2414de492e97a88636551094ee04cb (diff)
search implemented for available packages, added some qol aswell
-rw-r--r--src/gentoo/package.rs23
-rw-r--r--src/screens/available_packages.rs113
-rw-r--r--src/screens/installed_packages.rs108
-rw-r--r--src/widgets/mod.rs1
-rw-r--r--src/widgets/search_popup.rs90
5 files changed, 214 insertions, 121 deletions
diff --git a/src/gentoo/package.rs b/src/gentoo/package.rs
index fcd86eb..8ddb2ad 100644
--- a/src/gentoo/package.rs
+++ b/src/gentoo/package.rs
@@ -46,7 +46,7 @@ impl PackageKey {
}
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Metadata {
pub maintainer: Option<String>,
pub description: Option<String>,
@@ -81,7 +81,7 @@ pub struct Package {
pub versions: Vec<PackageVersion>,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackageVersion {
pub version: String,
@@ -90,3 +90,22 @@ pub struct PackageVersion {
pub keywords: Vec<String>,
pub iuse: Vec<UseFlag>,
}
+
+impl PartialOrd for PackageVersion {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ match self.version.partial_cmp(&other.version) {
+ Some(core::cmp::Ordering::Equal) => {}
+ ord => return ord,
+ }
+
+ self.metadata
+ .repository
+ .partial_cmp(&other.metadata.repository)
+ }
+}
+
+impl Ord for PackageVersion {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.version.cmp(&other.version)
+ }
+}
diff --git a/src/screens/available_packages.rs b/src/screens/available_packages.rs
index 46d9f39..87f31e1 100644
--- a/src/screens/available_packages.rs
+++ b/src/screens/available_packages.rs
@@ -5,8 +5,8 @@ use ratatui::{
crossterm::event::KeyCode,
layout::{Constraint, Layout, Rect},
style::Color,
- text::Text,
- widgets::{Block, Borders, List, ListState},
+ text::{Line, Span, Text},
+ widgets::{Block, Borders, List, ListState, Paragraph},
};
use crate::{
@@ -15,7 +15,10 @@ use crate::{
screens::screen::Screen,
signal::{Event, Signal},
theme::Theme,
- widgets::{package_metadata::render_package_metadata, use_flags::render_use_flags},
+ widgets::{
+ package_metadata::render_package_metadata, search_popup::SearchPopup,
+ use_flags::render_use_flags,
+ },
};
#[derive(Debug)]
@@ -34,6 +37,8 @@ pub struct AvailablePackagesScreen {
variant_list_state: ListState,
loading_state: LoadingState,
+
+ search_popup: SearchPopup,
}
fn version_to_variant(pkg_version: &PackageVersion) -> String {
@@ -44,10 +49,8 @@ fn version_to_variant(pkg_version: &PackageVersion) -> String {
}
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) {
- // TODO: Put this into screen, instead of rebuilding the list every frame
-
+ // might not need to be own function
if let Some(list) = &self.pkg_list {
frame.render_stateful_widget(list, area, &mut self.pkg_list_state);
}
@@ -98,6 +101,7 @@ impl Default for AvailablePackagesScreen {
pkg_list_state: list_state,
variant_list_state: variant_state,
loading_state: LoadingState::Idle,
+ search_popup: SearchPopup::default(),
}
}
}
@@ -115,8 +119,13 @@ impl Screen for AvailablePackagesScreen {
return;
}
+ let [top, bottom] = area.layout(&Layout::vertical([
+ Constraint::Fill(1),
+ Constraint::Length(3),
+ ]));
+
let constraints = [Constraint::Percentage(50), Constraint::Percentage(50)];
- let [left, right] = area.layout(&Layout::horizontal(constraints));
+ let [left, right] = top.layout(&Layout::horizontal(constraints));
let [package_list, use_flags] = left.layout(&Layout::vertical(constraints));
let [package_variants, metadata] = right.layout(&Layout::vertical(constraints));
@@ -133,7 +142,11 @@ impl Screen for AvailablePackagesScreen {
self.render_packages(frame, package_list);
if let Some(pkg) = pkg {
- self.render_package_variants(frame, package_variants, pkg.versions.iter().collect());
+ let mut pkg_versions: Vec<&PackageVersion> = pkg.versions.iter().collect();
+ pkg_versions.sort();
+ pkg_versions.reverse();
+
+ self.render_package_variants(frame, package_variants, pkg_versions);
let selected_version = &pkg.versions[self.variant_list_state.selected().unwrap_or(0)];
@@ -141,7 +154,7 @@ impl Screen for AvailablePackagesScreen {
frame,
use_flags,
selected_version.iuse.iter().collect(),
- // Get default active use flags for package, and globally defines ones
+ // Get default active use flags for package, and globally defined ones
HashSet::new(),
);
@@ -154,6 +167,23 @@ impl Screen for AvailablePackagesScreen {
None,
);
}
+
+ let text = if self.search_popup.visible {
+ "(esc) to quit search | (enter) to search".to_string()
+ } else {
+ "(q) to quit | (j) down | (k) up | (space) switch panel | (/) to search".to_string()
+ };
+
+ let key_hint = Span::styled(text, Theme::muted());
+
+ let key_notes_footer = Paragraph::new(Line::from(key_hint)).block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_style(Theme::block()),
+ );
+ frame.render_widget(key_notes_footer, bottom);
+
+ self.search_popup.draw(frame, area);
}
fn update(
@@ -162,28 +192,57 @@ impl Screen for AvailablePackagesScreen {
repo: &crate::gentoo::Portage,
) -> Option<crate::signal::Signal> {
match event {
- Event::KeyEvent(key) => match key.code {
- KeyCode::Char('q') => return Some(Signal::Quit),
- KeyCode::Tab => return Some(Signal::CycleTab),
+ Event::KeyEvent(key) => {
+ if self.search_popup.visible {
+ match key.code {
+ KeyCode::Esc => self.search_popup.toggle(),
+ KeyCode::Enter => {
+ let result = self.search_popup.search(
+ repo.available_packages().iter().map(|p| &p.atom).collect(),
+ );
+ self.search_popup.toggle();
- // TODO: This should cycle, also space isnt a good key for this
- KeyCode::Char(' ') => self.cycle_list(),
+ if let Some(index) = result {
+ self.pkg_list_state.select(Some(index));
+ }
+ }
+ _ => {
+ self.search_popup.textarea.input_without_shortcuts(*key);
+ let result = self.search_popup.search(
+ repo.available_packages().iter().map(|p| &p.atom).collect(),
+ );
- _ => 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(),
+ if let Some(index) = result {
+ self.pkg_list_state.select(Some(index));
+ }
+ }
+ }
+ } else {
+ match key.code {
+ KeyCode::Char('q') => return Some(Signal::Quit),
+ KeyCode::Tab => return Some(Signal::CycleTab),
- _ => {}
- },
- CurrentList::VariantList => match key.code {
- KeyCode::Char('j') => self.variant_list_state.select_next(),
- KeyCode::Char('k') => self.variant_list_state.select_previous(),
+ // TODO: This should cycle, also space isnt a good key for this
+ KeyCode::Char(' ') => self.cycle_list(),
+ KeyCode::Char('/') => self.search_popup.toggle(),
- _ => {}
- },
- },
- },
+ _ => 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(),
+
+ _ => {}
+ },
+ },
+ }
+ }
+ }
Event::LoadStateUpdate(loading_state) => match loading_state {
LoadingState::Complete => {
let packages: Vec<String> = repo
diff --git a/src/screens/installed_packages.rs b/src/screens/installed_packages.rs
index 1b0061a..7d5057e 100644
--- a/src/screens/installed_packages.rs
+++ b/src/screens/installed_packages.rs
@@ -1,13 +1,3 @@
-use ratatui::{
- Frame,
- crossterm::event::{KeyCode, KeyModifiers},
- layout::{Constraint, Layout, Rect},
- style::{Color, Modifier, Style},
- text::{Line, Span},
- widgets::{Block, Borders, Clear, List, ListState, Paragraph},
-};
-use ratatui_textarea::TextArea;
-
use crate::{
gentoo::{InstalledPackage, Portage, package::PackageKey},
screens::screen::Screen,
@@ -15,62 +5,17 @@ use crate::{
theme::Theme,
widgets::{
helpers::search_popup_rect, package_metadata::render_package_metadata,
- use_flags::render_use_flags,
+ search_popup::SearchPopup, use_flags::render_use_flags,
},
};
-
-#[derive(Debug)]
-struct SearchPopup {
- pub(crate) visible: bool,
- pub(crate) textarea: TextArea<'static>,
- pub(crate) result: Option<usize>,
-}
-
-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());
-
- search_popup
- }
-}
-
-impl SearchPopup {
- pub fn toggle(&mut self) {
- self.visible = !self.visible;
- self.textarea.clear();
- }
-
- pub fn search(&mut self, packages: Vec<&InstalledPackage>) -> Option<usize> {
- let indexes: Vec<usize> = packages
- .iter()
- .enumerate()
- .filter(|(_, s)| {
- s.atom
- .qualified_name()
- .to_lowercase()
- .contains(&self.textarea.lines()[0].to_lowercase())
- })
- .map(|(i, _)| i)
- .collect();
-
- if indexes.is_empty() {
- self.result = None;
- None
- } else {
- self.result = Some(indexes[0]);
- Some(indexes[0])
- }
- }
-}
+use ratatui::{
+ Frame,
+ crossterm::event::{KeyCode, KeyModifiers},
+ layout::{Constraint, Layout, Rect},
+ style::{Color, Modifier, Style},
+ text::{Line, Span},
+ widgets::{Block, Borders, Clear, List, ListState, Paragraph},
+};
#[derive(Debug)]
enum FilterState {
@@ -208,32 +153,7 @@ impl Screen for InstalledPackagesScreen {
);
frame.render_widget(key_notes_footer, bottom);
- if self.search_popup.visible {
- let mut popup_block = Block::default()
- .title("Search")
- .borders(Borders::ALL)
- .border_type(ratatui::widgets::BorderType::Rounded)
- .border_style(Theme::block())
- .style(Style::default().bg(Theme::BG));
-
- if self.search_popup.result.is_some() {
- popup_block = popup_block.border_style(Theme::success());
- } else {
- popup_block = popup_block.border_style(Theme::error());
- }
-
- self.search_popup.textarea.set_block(popup_block);
-
- let area = search_popup_rect(70, area);
-
- frame.render_widget(
- Block::default().style(Style::default().bg(Theme::BG).add_modifier(Modifier::DIM)),
- frame.area(),
- );
- frame.render_widget(Clear, area);
- frame.render_widget(Block::default().style(Style::default().bg(Theme::BG)), area);
- frame.render_widget(&self.search_popup.textarea, area);
- }
+ self.search_popup.draw(frame, area);
}
fn update(&mut self, event: &Event, repo: &Portage) -> Option<Signal> {
@@ -248,7 +168,9 @@ impl Screen for InstalledPackagesScreen {
match key.code {
KeyCode::Esc => self.search_popup.toggle(),
KeyCode::Enter => {
- let result = self.search_popup.search(packages);
+ let result = self
+ .search_popup
+ .search(packages.iter().map(|p| &p.atom).collect());
self.search_popup.toggle();
if let Some(index) = result {
@@ -257,7 +179,9 @@ impl Screen for InstalledPackagesScreen {
}
_ => {
self.search_popup.textarea.input_without_shortcuts(*key);
- let result = self.search_popup.search(packages);
+ let result = self
+ .search_popup
+ .search(packages.iter().map(|p| &p.atom).collect());
if let Some(index) = result {
self.list_state.select(Some(index));
diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs
index 7bf8fde..7b015b8 100644
--- a/src/widgets/mod.rs
+++ b/src/widgets/mod.rs
@@ -1,4 +1,5 @@
pub mod helpers;
pub mod package_metadata;
+pub mod search_popup;
pub mod tabs;
pub mod use_flags;
diff --git a/src/widgets/search_popup.rs b/src/widgets/search_popup.rs
new file mode 100644
index 0000000..0f47b10
--- /dev/null
+++ b/src/widgets/search_popup.rs
@@ -0,0 +1,90 @@
+use ratatui::{
+ Frame,
+ layout::Rect,
+ style::{Modifier, Style},
+ widgets::{Block, Borders, Clear},
+};
+use ratatui_textarea::TextArea;
+
+use crate::{gentoo::package::PackageKey, theme::Theme, widgets::helpers::search_popup_rect};
+
+#[derive(Debug)]
+pub struct SearchPopup {
+ pub(crate) visible: bool,
+ pub(crate) textarea: TextArea<'static>,
+ pub(crate) result: Option<usize>,
+}
+
+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());
+
+ search_popup
+ }
+}
+
+impl SearchPopup {
+ pub fn toggle(&mut self) {
+ self.visible = !self.visible;
+ self.textarea.clear();
+ }
+
+ pub fn search(&mut self, packages: Vec<&PackageKey>) -> Option<usize> {
+ let indexes: Vec<usize> = packages
+ .iter()
+ .enumerate()
+ .filter(|(_, s)| {
+ s.qualified_name()
+ .to_lowercase()
+ .contains(&self.textarea.lines()[0].to_lowercase())
+ })
+ .map(|(i, _)| i)
+ .collect();
+
+ if indexes.is_empty() {
+ self.result = None;
+ None
+ } else {
+ self.result = Some(indexes[0]);
+ Some(indexes[0])
+ }
+ }
+
+ pub fn draw(&mut self, frame: &mut Frame, area: Rect) {
+ if self.visible {
+ let mut popup_block = Block::default()
+ .title("Search")
+ .borders(Borders::ALL)
+ .border_type(ratatui::widgets::BorderType::Rounded)
+ .border_style(Theme::block())
+ .style(Style::default().bg(Theme::BG));
+
+ if self.result.is_some() {
+ popup_block = popup_block.border_style(Theme::success());
+ } else {
+ popup_block = popup_block.border_style(Theme::error());
+ }
+
+ self.textarea.set_block(popup_block);
+
+ let area = search_popup_rect(70, area);
+
+ frame.render_widget(
+ Block::default().style(Style::default().bg(Theme::BG).add_modifier(Modifier::DIM)),
+ frame.area(),
+ );
+ frame.render_widget(Clear, area);
+ frame.render_widget(Block::default().style(Style::default().bg(Theme::BG)), area);
+ frame.render_widget(&self.textarea, area);
+ }
+ }
+}