diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.rs | 9 | ||||
| -rw-r--r-- | src/event.rs | 24 | ||||
| -rw-r--r-- | src/gentoo/package.rs | 21 | ||||
| -rw-r--r-- | src/gentoo/portage.rs | 79 | ||||
| -rw-r--r-- | src/main.rs | 13 | ||||
| -rw-r--r-- | src/ui.rs | 97 |
6 files changed, 227 insertions, 16 deletions
@@ -35,6 +35,15 @@ impl<'a> App<'a> { self.portage.installed_packages.clone() } + pub fn current_package(&self) -> Package { + let selected_package_index = match self.list_state.selected() { + Some(selected) => selected, + None => 0, + }; + + self.portage.installed_packages[selected_package_index].clone() + } + pub fn toggle_search_window(&mut self) { self.showing_search_window = !self.showing_search_window; self.textarea.clear(); diff --git a/src/event.rs b/src/event.rs index 530e244..4b0ac43 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,6 +1,6 @@ use std::io; -use ratatui::crossterm::event::{self, Event, KeyCode}; +use ratatui::crossterm::event::{self, Event, KeyCode, KeyModifiers}; use crate::app::App; @@ -15,6 +15,28 @@ pub fn handle_event(app: &mut App) -> io::Result<bool> { KeyCode::Char('n') => app.next_search(), KeyCode::Char('N') => app.prev_search(), + KeyCode::Char('d') => match key.modifiers { + KeyModifiers::CONTROL => app + .list_state + .select(Some(app.list_state.selected().unwrap_or(0) + 30)), + + _ => return Ok(false), + }, + + KeyCode::Char('u') => match key.modifiers { + KeyModifiers::CONTROL => { + 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)) + } + } + } + + _ => return Ok(false), + }, + _ => return Ok(false), } } else { diff --git a/src/gentoo/package.rs b/src/gentoo/package.rs index 0fe9a6c..1d7b3f3 100644 --- a/src/gentoo/package.rs +++ b/src/gentoo/package.rs @@ -6,15 +6,34 @@ pub struct Package { pub name: String, pub use_flags: Vec<UseFlag>, pub version: String, + pub repository: String, + pub maintainer: Option<String>, + pub description: Option<String>, + pub homepage: Option<Vec<String>>, + pub license: Option<String>, } #[allow(dead_code)] impl Package { - pub fn new(name: String, version: String, use_flags: Vec<UseFlag>) -> Self { + pub fn new( + name: String, + version: String, + repository: String, + homepage: Option<Vec<String>>, + license: Option<String>, + description: Option<String>, + maintainer: Option<String>, + use_flags: Vec<UseFlag>, + ) -> Self { Package { name: name, use_flags: use_flags, version: version, + repository: repository, + maintainer: maintainer, + description: description, + homepage: homepage, + license: license, } } } diff --git a/src/gentoo/portage.rs b/src/gentoo/portage.rs index 40b03c0..882cc5f 100644 --- a/src/gentoo/portage.rs +++ b/src/gentoo/portage.rs @@ -1,4 +1,6 @@ -use std::fs; +use std::{fs, io, path::PathBuf}; + +use quick_xml::{Reader, events::Event}; use crate::gentoo::{Package, UseFlag}; @@ -25,6 +27,45 @@ fn split_pkg(input: &str) -> (&str, Option<&str>) { (input, None) } +fn extract_maintainer(path: &PathBuf) -> io::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) => break, + Err(_) => break, + + _ => {} + } + + buf.clear(); + } + + Ok(None) +} + impl Portage { pub fn new() -> Self { Self { @@ -32,7 +73,7 @@ impl Portage { } } - pub fn load_installed_packages(&mut self) -> Result<(), std::io::Error> { + pub fn load_installed_packages(&mut self) -> io::Result<()> { let categories = fs::read_dir("/var/db/pkg")?; for category in categories { @@ -72,9 +113,43 @@ impl Portage { }) .collect(); + // let category = fs::read_to_string(pkg.path().join("CATEGORY"))?; + let repository = fs::read_to_string(pkg.path().join("repository"))? + .trim() + .to_string(); + let homepage: Option<Vec<String>> = + match fs::read_to_string(pkg.path().join("HOMEPAGE")) { + Ok(homepage) => Some(homepage.split('\n').map(|s| s.to_string()).collect()), + Err(_) => None, + }; + + 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 repo_path = PathBuf::from("/var/db/repos/") + .join(&repository) + .join(cat_name.to_str().unwrap()) + .join(pkg_name) + .join("metadata.xml"); + + let maintainer = extract_maintainer(&repo_path)?; + self.installed_packages.push(Package::new( format!("{}/{}", cat_name.to_str().unwrap(), pkg_name), pkg_version.unwrap().into(), + repository, + homepage, + license, + description, + maintainer, use_flags, )); } diff --git a/src/main.rs b/src/main.rs index 55e721a..1c25018 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,9 +20,20 @@ use ratatui::{ use crate::{app::App, event::handle_event, gentoo::Portage, ui::ui}; +struct TuiGuard; + +impl Drop for TuiGuard { + fn drop(&mut self) { + _ = restore_tui(); + } +} + fn main() -> color_eyre::Result<()> { color_eyre::install()?; init_panic_hook(); + + let _guard = TuiGuard; + let mut tui = init_tui()?; let mut p = Portage::new(); @@ -32,7 +43,7 @@ fn main() -> color_eyre::Result<()> { run(&mut tui, &mut app)?; - restore_tui()?; + // restore_tui()?; Ok(()) } @@ -2,9 +2,9 @@ use ratatui::{ Frame, layout::{Constraint, Direction, Layout}, prelude::Rect, - style::{Color, Modifier, Style}, + style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, - widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph}, + widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}, }; use crate::app::App; @@ -15,12 +15,15 @@ pub fn ui(frame: &mut Frame, app: &mut App) { let layout = Layout::vertical(constraints); let [top, bottom] = frame.area().layout(&layout); - let split = Layout::horizontal([Constraint::Percentage(70), Constraint::Percentage(30)]) - .spacing(1) - .split(top); + 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]); render_packages(frame, split[0], app); - render_use_flags(frame, split[1], app); + render_use_flags(frame, split_vert[0], app); + render_package_metadata(frame, split_vert[1], app); let text = match app.showing_search_window { false => { @@ -69,6 +72,82 @@ pub fn ui(frame: &mut Frame, app: &mut App) { } } +fn render_package_metadata(frame: &mut Frame, area: Rect, app: &mut App) { + let pkg = app.current_package(); + + let bold_style = Style::default().add_modifier(Modifier::BOLD); + let maintainer = Line::from_iter([ + Span::styled("Maintainer: ", bold_style), + Span::raw(pkg.maintainer.as_deref().unwrap_or("Unknown")), + ]); + + 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.repository), + ]); + + let license = Line::from_iter([ + Span::styled("License: ", bold_style), + Span::raw(pkg.license.as_deref().unwrap_or("Unknown")), + ]); + + let description = Line::from_iter([ + Span::styled("Description: ", bold_style), + Span::raw(pkg.description.as_deref().unwrap_or("Unknown")), + ]); + + // let homepage = Line::from_iter([ + // Span::styled("Homepage: ", bold_style), + // Span::styled( + // pkg.homepage.as_deref().unwrap_or("Unknown"), + // Style::default().fg(Color::LightBlue), + // ), + // ]); + + let mut lines = Vec::new(); + + // First line: label + lines.push(Line::from(vec![ + Span::styled("Homepage: ", bold_style), + Span::raw(""), + ])); + + // Following lines: one URL per line, indented + if let Some(homepages) = pkg.homepage { + for url in homepages.into_iter().filter(|s| !s.is_empty()) { + lines.push(Line::from(vec![ + Span::raw(" "), // indentation + Span::styled( + url, + Style::default() + .fg(Color::LightBlue) + .add_modifier(Modifier::UNDERLINED), + ), + ])); + } + } else { + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled("Unknown", Style::default().fg(Color::DarkGray)), + ])); + } + + let mut homepage = lines; + let mut lines = vec![maintainer, version, repository, license, description]; + lines.append(&mut homepage); + + let paragraph = Paragraph::new(lines) + .block(Block::default().title("Metadata").borders(Borders::ALL)) + .wrap(Wrap { trim: false }); + + frame.render_widget(paragraph, area); +} + fn search_popup_rect(percent_x: u16, r: Rect) -> Rect { // Cut the given rectangle into three vertical pieces let popup_layout = Layout::default() @@ -103,11 +182,7 @@ fn render_packages(frame: &mut Frame, area: Rect, app: &mut App) { } fn render_use_flags(frame: &mut Frame, area: Rect, app: &mut App) { - let selected_package_index = match app.list_state.selected() { - Some(selected) => selected, - None => 0, - }; - let selected_package = app.installed_packages()[selected_package_index].clone(); + let selected_package = app.current_package(); let items = selected_package.use_flags.into_iter().map(|x| { let color = if x.enabled { |
