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.rs5
-rw-r--r--src/actions/signal.rs4
-rw-r--r--src/app.rs243
-rw-r--r--src/event.rs110
-rw-r--r--src/gentoo/portage.rs2
-rw-r--r--src/main.rs25
-rw-r--r--src/screens/installed_packages.rs170
-rw-r--r--src/screens/mod.rs2
-rw-r--r--src/screens/screen.rs11
-rw-r--r--src/widgets/helpers.rs65
-rw-r--r--src/widgets/mod.rs3
-rw-r--r--src/widgets/package_metadata.rs75
-rw-r--r--src/widgets/use_flags.rs43
14 files changed, 601 insertions, 169 deletions
diff --git a/src/actions/action.rs b/src/actions/action.rs
new file mode 100644
index 0000000..b2070d6
--- /dev/null
+++ b/src/actions/action.rs
@@ -0,0 +1,12 @@
+#[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
new file mode 100644
index 0000000..ed1dfa9
--- /dev/null
+++ b/src/actions/mod.rs
@@ -0,0 +1,5 @@
+pub mod action;
+pub mod signal;
+
+pub use action::Action;
+pub use signal::Signal;
diff --git a/src/actions/signal.rs b/src/actions/signal.rs
new file mode 100644
index 0000000..1210e17
--- /dev/null
+++ b/src/actions/signal.rs
@@ -0,0 +1,4 @@
+pub enum Signal {
+ Quit,
+ CycleTab,
+}
diff --git a/src/app.rs b/src/app.rs
index 6e9e8ab..c014a49 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,7 +1,19 @@
-use ratatui::widgets::ListState;
-use ratatui_textarea::TextArea;
+use color_eyre::eyre::Ok;
+use ratatui::{
+ Frame,
+ crossterm::event::{self, Event},
+ layout::{Constraint, Layout},
+ style::Stylize,
+ text::Text,
+ widgets::Block,
+};
-use crate::gentoo::{InstalledPackage, portage::Portage};
+use crate::{
+ actions::{Signal, signal},
+ gentoo::{InstalledPackage, portage::Portage},
+ screens::{installed_packages::InstalledPackagesScreen, screen::Screen},
+ theme::Theme,
+};
#[derive(Debug)]
pub enum ViewState {
@@ -11,133 +23,166 @@ pub enum ViewState {
}
#[derive(Debug)]
-pub struct App<'a> {
+pub struct App {
portage: Portage,
- pub list_state: ListState,
- pub showing_search_window: bool,
- pub textarea: TextArea<'a>,
- pub search_found: Option<bool>,
- search_indexes: Option<Vec<usize>>,
- pub search_indexes_len: Option<usize>,
- pub current_search_index: Option<usize>,
+ installed_package_screen: InstalledPackagesScreen,
+
+ // 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,
}
-impl App<'_> {
+impl App {
pub fn new(portage: Portage) -> Self {
App {
portage,
- list_state: ListState::default().with_selected(Some(0)),
- showing_search_window: false,
- textarea: TextArea::default(),
- search_found: None,
- search_indexes: None,
- current_search_index: None,
- search_indexes_len: None,
- view: ViewState::Dashboard,
- }
- }
- pub fn world_count(&self) -> usize {
- self.portage.world_packages.len()
- }
+ installed_package_screen: InstalledPackagesScreen::default(),
- pub fn installed_packages(&self) -> &[InstalledPackage] {
- &self.portage.installed_packages
+ view: ViewState::InstalledPackages,
+ }
}
- 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 draw(&mut self, frame: &mut Frame) {
+ frame.render_widget(Block::default().bg(Theme::BG), frame.area());
- pub fn total_installed_size(&self) -> usize {
- self.portage.installed_packages.iter().map(|p| p.size).sum()
- }
+ let layout = Layout::vertical(vec![Constraint::Length(3), Constraint::Fill(1)]);
+ let [tab_bar, rest] = frame.area().layout(&layout);
- pub fn toggle_search_window(&mut self) {
- self.showing_search_window = !self.showing_search_window;
- self.textarea.clear();
- self.search_found = None;
- }
+ // render_tab(frame, tab_bar, &app.view);
- pub fn cycle_current_tab(&mut self) {
match self.view {
- ViewState::Dashboard => self.view = ViewState::InstalledPackages,
- ViewState::InstalledPackages => self.view = ViewState::AvailablePackages,
- ViewState::AvailablePackages => self.view = ViewState::Dashboard,
+ crate::app::ViewState::InstalledPackages => {
+ self.installed_package_screen
+ .draw(frame, rest, &self.portage)
+ }
+ _ => frame.render_widget(Text::from("NOT IMPLEMENTED"), rest),
}
}
- 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();
+ pub fn update(&mut self) -> color_eyre::Result<bool> {
+ if let Event::Key(key) = event::read()? {
+ let signal = match self.view {
+ crate::app::ViewState::InstalledPackages => {
+ self.installed_package_screen.update(key)
+ }
- 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);
+ _ => None,
+ };
- return true;
+ if let Some(signal) = signal {
+ match signal {
+ Signal::Quit => return Ok(true),
+ Signal::CycleTab => todo!(),
+ }
+ }
}
- self.search_indexes = None;
-
- false
+ Ok(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;
- };
+ pub fn world_count(&self) -> usize {
+ self.portage.world_packages.len()
+ }
- if current_index + 1 >= search_indexes.len() {
- current_index = 0;
- } else {
- current_index += 1;
- }
+ pub fn installed_packages(&self) -> &[InstalledPackage] {
+ &self.portage.installed_packages
+ }
- let list_index = search_indexes[current_index];
+ // pub fn current_package(&self) -> &InstalledPackage {
+ // let selected_package_index = self.list_state.selected().unwrap_or_default();
+ // &self.portage.installed_packages[selected_package_index]
+ // }
- self.list_state.select(Some(list_index));
- self.current_search_index = Some(current_index);
+ pub fn total_installed_size(&self) -> usize {
+ self.portage.installed_packages.iter().map(|p| p.size).sum()
}
- pub fn prev_search(&mut self) {
- let Some(mut current_index) = self.current_search_index else {
- return;
- };
+ // pub fn toggle_search_window(&mut self) {
+ // self.showing_search_window = !self.showing_search_window;
+ // self.textarea.clear();
+ // self.search_found = None;
+ // }
- let Some(search_indexes) = &self.search_indexes else {
- return;
- };
-
- if current_index == 0 {
- current_index = search_indexes.len() - 1;
- } else {
- current_index -= 1;
+ pub fn cycle_current_tab(&mut self) {
+ match self.view {
+ ViewState::Dashboard => self.view = ViewState::InstalledPackages,
+ ViewState::InstalledPackages => self.view = ViewState::AvailablePackages,
+ ViewState::AvailablePackages => self.view = ViewState::Dashboard,
}
-
- let list_index = search_indexes[current_index];
-
- self.list_state.select(Some(list_index));
- self.current_search_index = Some(current_index);
}
+
+ // 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
index 44a4c3e..95eece2 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -13,11 +13,11 @@ pub fn handle_event(app: &mut App) -> io::Result<bool> {
}
}
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);
- }
+ // 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) {
@@ -30,7 +30,7 @@ pub fn handle_event(app: &mut App) -> io::Result<bool> {
Ok(false)
}
-fn handle_available_packages_keybinds(key: KeyEvent, app: &mut App<'_>) -> bool {
+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(),
@@ -40,55 +40,55 @@ fn handle_available_packages_keybinds(key: KeyEvent, app: &mut App<'_>) -> bool
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_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,
diff --git a/src/gentoo/portage.rs b/src/gentoo/portage.rs
index 51dc60a..b444a5b 100644
--- a/src/gentoo/portage.rs
+++ b/src/gentoo/portage.rs
@@ -332,7 +332,7 @@ impl Portage {
.and_then(DateTime::from_timestamp_secs)
.unwrap_or(DateTime::from_timestamp_nanos(0));
- let maintainer = extract_maintainer(&repo_path)?;
+ let maintainer = extract_maintainer(&repo_path).unwrap_or(None);
let atom = PackageKey {
category: cat_name.clone(),
diff --git a/src/main.rs b/src/main.rs
index ae3ff96..b768cc7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,10 @@
+mod actions;
mod app;
mod event;
mod gentoo;
+mod screens;
mod theme;
-mod ui;
+mod widgets;
use std::{
io::{self, Stdout, stdout},
@@ -11,7 +13,6 @@ use std::{
use ratatui::{
Terminal,
- backend::Backend,
crossterm::{
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
@@ -19,7 +20,7 @@ use ratatui::{
prelude::CrosstermBackend,
};
-use crate::{app::App, event::handle_event, gentoo::Portage, ui::ui};
+use crate::{app::App, gentoo::Portage};
struct TuiGuard;
@@ -31,11 +32,9 @@ impl Drop for TuiGuard {
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
- init_panic_hook();
-
let _guard = TuiGuard;
- let mut tui = init_tui()?;
+ let tui = init_tui()?;
let mut p = Portage::new();
p.world_packages = Portage::load_world_packages()?;
@@ -45,18 +44,15 @@ fn main() -> color_eyre::Result<()> {
let mut app = App::new(p);
- run(&mut tui, &mut app)?;
+ run(tui, &mut app)?;
Ok(())
}
-fn run<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()>
-where
- io::Error: From<B::Error>,
-{
+fn run(mut terminal: Terminal<CrosstermBackend<Stdout>>, app: &mut App) -> color_eyre::Result<()> {
loop {
- terminal.draw(|frame| ui(frame, app))?;
- let should_exit = handle_event(app)?;
+ terminal.draw(|frame| app.draw(frame))?;
+ let should_exit = app.update()?;
if should_exit {
break;
}
@@ -66,8 +62,9 @@ where
}
fn init_tui() -> io::Result<ratatui::Terminal<CrosstermBackend<Stdout>>> {
- enable_raw_mode()?;
execute!(stdout(), EnterAlternateScreen)?;
+ enable_raw_mode()?;
+ init_panic_hook();
Terminal::new(CrosstermBackend::new(stdout()))
}
diff --git a/src/screens/installed_packages.rs b/src/screens/installed_packages.rs
new file mode 100644
index 0000000..7e65fd5
--- /dev/null
+++ b/src/screens/installed_packages.rs
@@ -0,0 +1,170 @@
+use ratatui::{
+ Frame,
+ crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
+ layout::{Constraint, Layout, Rect},
+ style::{Color, Modifier, Style},
+ text::{Line, Span},
+ widgets::{Block, Borders, Clear, List, ListState, Paragraph},
+};
+use ratatui_textarea::{Key, TextArea};
+
+use crate::{
+ actions::Signal,
+ gentoo::Portage,
+ screens::screen::Screen,
+ theme::Theme,
+ widgets::{
+ helpers::search_popup_rect, package_metadata::render_package_metadata,
+ use_flags::render_use_flags,
+ },
+};
+
+#[derive(Debug, Default)]
+struct SearchPopup {
+ pub(crate) visible: bool,
+ pub(crate) textarea: TextArea<'static>,
+}
+
+#[derive(Debug, Default)]
+pub struct InstalledPackagesScreen {
+ list_state: ListState,
+
+ search_popup: SearchPopup,
+}
+
+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![];
+
+ let list = List::new(items)
+ .style(Color::White)
+ .highlight_style(Theme::selected())
+ .highlight_symbol("> ")
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .border_style(Theme::block())
+ .title("Installed packages"),
+ );
+
+ frame.render_stateful_widget(list, area, &mut self.list_state);
+ }
+}
+
+impl Screen for InstalledPackagesScreen {
+ fn draw(&mut self, frame: &mut Frame, area: Rect, repo: &Portage) {
+ let constraints = [Constraint::Fill(1), Constraint::Length(3)];
+
+ let layout = Layout::vertical(constraints);
+ let [top, bottom] = area.layout(&layout);
+
+ let split =
+ Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]).split(top);
+
+ let split_vert = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)])
+ .split(split[1]);
+
+ 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]);
+
+ let text = if self.search_popup.visible {
+ "(esc) to quit search | (enter) to search".to_string()
+ } else {
+ let mut main_key_hint = "(q) to quit | (j) down | (k) up | (/) to search".to_string();
+ // if let (Some(current), Some(total)) = (app.current_search_index, app.search_indexes_len)
+ // {
+ // let _ = write!(
+ // main_key_hint,
+ // " | (n) for next search | (N) for previous search | Searches found: {} / {}",
+ // current + 1,
+ // total
+ // );
+ // }
+
+ main_key_hint
+ };
+
+ 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);
+
+ 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 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());
+ // }
+ // }
+
+ // 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);
+
+ 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);
+ }
+ }
+
+ fn update(&mut self, key: KeyEvent) -> 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,
+ };
+ } else {
+ match key.modifiers {
+ KeyModifiers::CONTROL => match key.code {
+ KeyCode::Char('d') => todo!("next page"),
+ KeyCode::Char('u') => todo!("prev page"),
+ _ => {}
+ },
+
+ _ => match key.code {
+ KeyCode::Char('q') => return Some(Signal::Quit),
+ 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('n') => todo!("Next search"),
+ KeyCode::Char('N') => todo!("prev search"),
+
+ _ => {}
+ },
+ }
+ }
+
+ None
+ }
+}
diff --git a/src/screens/mod.rs b/src/screens/mod.rs
new file mode 100644
index 0000000..db3be11
--- /dev/null
+++ b/src/screens/mod.rs
@@ -0,0 +1,2 @@
+pub mod installed_packages;
+pub mod screen;
diff --git a/src/screens/screen.rs b/src/screens/screen.rs
new file mode 100644
index 0000000..a87329b
--- /dev/null
+++ b/src/screens/screen.rs
@@ -0,0 +1,11 @@
+use ratatui::{Frame, crossterm::event::KeyEvent, layout::Rect};
+
+use crate::{
+ actions::{Action, 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>;
+}
diff --git a/src/widgets/helpers.rs b/src/widgets/helpers.rs
new file mode 100644
index 0000000..0bde326
--- /dev/null
+++ b/src/widgets/helpers.rs
@@ -0,0 +1,65 @@
+use ratatui::{
+ layout::{Constraint, Direction, Layout, Rect},
+ style::{Color, Modifier, Style},
+ text::{Line, Span},
+};
+
+use crate::gentoo::InstalledPackage;
+
+pub fn search_popup_rect(percent_x: u16, r: Rect) -> Rect {
+ // Cut the given rectangle into three vertical pieces
+ let popup_layout = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints([Constraint::Length(10), Constraint::Length(3)])
+ .split(r);
+
+ // Then cut the middle vertical piece into three width-wise pieces
+ Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints([
+ Constraint::Percentage((100 - percent_x) / 2),
+ Constraint::Percentage(percent_x),
+ Constraint::Percentage((100 - percent_x) / 2),
+ ])
+ .split(popup_layout[1])[1] // Return the middle chunk
+}
+
+pub 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(""),
+ ]));
+
+ for url in pkg.metadata.homepage.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),
+ ),
+ ]));
+ }
+
+ lines
+}
+
+pub fn human_size(bytes: usize) -> String {
+ const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
+
+ let mut size = bytes as f64;
+ let mut unit = 0;
+
+ while size >= 1024.0 && unit < UNITS.len() - 1 {
+ size /= 1024.0;
+ unit += 1;
+ }
+
+ if unit == 0 {
+ format!("{} {}", bytes, UNITS[unit])
+ } else {
+ format!("{:.1} {}", size, UNITS[unit])
+ }
+}
diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs
new file mode 100644
index 0000000..66f4b70
--- /dev/null
+++ b/src/widgets/mod.rs
@@ -0,0 +1,3 @@
+pub mod helpers;
+pub mod package_metadata;
+pub mod use_flags;
diff --git a/src/widgets/package_metadata.rs b/src/widgets/package_metadata.rs
new file mode 100644
index 0000000..e4f83bb
--- /dev/null
+++ b/src/widgets/package_metadata.rs
@@ -0,0 +1,75 @@
+use ratatui::{
+ Frame,
+ layout::Rect,
+ text::{Line, Span},
+ widgets::{Block, Borders, Paragraph, Wrap},
+};
+
+use crate::{
+ app::App,
+ gentoo::InstalledPackage,
+ theme::Theme,
+ widgets::helpers::{homepage_lines, human_size},
+};
+
+pub fn render_package_metadata(frame: &mut Frame, area: Rect, pkg: &InstalledPackage) {
+ 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")),
+ ]);
+
+ let version = Line::from_iter([
+ Span::styled("Version: ", bold_style),
+ Span::raw(&pkg.version),
+ ]);
+
+ let repository = Line::from_iter([
+ Span::styled("Repository: ", bold_style),
+ Span::raw(&pkg.metadata.repository),
+ ]);
+
+ let license = Line::from_iter([
+ Span::styled("License: ", bold_style),
+ Span::raw(pkg.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")),
+ ]);
+
+ let size = Line::from_iter([
+ Span::styled("Size: ", bold_style),
+ Span::raw(human_size(pkg.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);
+
+ let paragraph = Paragraph::new(lines)
+ .block(
+ Block::default()
+ .title("Metadata")
+ .borders(Borders::ALL)
+ .border_style(Theme::block()),
+ )
+ .wrap(Wrap { trim: false });
+
+ frame.render_widget(paragraph, area);
+}
diff --git a/src/widgets/use_flags.rs b/src/widgets/use_flags.rs
new file mode 100644
index 0000000..8e2ecf4
--- /dev/null
+++ b/src/widgets/use_flags.rs
@@ -0,0 +1,43 @@
+use ratatui::{
+ Frame,
+ layout::Rect,
+ style::{Color, Modifier},
+ widgets::{Block, Borders, List, ListItem, ListState},
+};
+
+use crate::{app::App, gentoo::InstalledPackage, theme::Theme};
+
+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()
+ } else if x.default {
+ Theme::info()
+ } else {
+ Theme::error()
+ };
+
+ let text_style = if x.default {
+ text_style.add_modifier(Modifier::ITALIC)
+ } else {
+ text_style
+ };
+
+ ListItem::new(x.name.clone()).style(text_style)
+ });
+ let list = List::new(items)
+ .style(Color::White)
+ .scroll_padding(1)
+ .direction(ratatui::widgets::ListDirection::TopToBottom)
+ .block(
+ Block::default()
+ .borders(Borders::ALL)
+ .title("USE Flags")
+ .border_style(Theme::block()),
+ );
+
+ let mut list_state = ListState::default();
+ list_state.select_first();
+
+ frame.render_stateful_widget(list, area, &mut list_state);
+}