diff options
| author | pablu <me@pablu.de> | 2026-05-08 14:33:59 +0200 |
|---|---|---|
| committer | pablu <me@pablu.de> | 2026-05-08 14:33:59 +0200 |
| commit | 8a1f6b186bde43323003c440910d43e0db8a7bc3 (patch) | |
| tree | d2f191ce43cc3a1deedde40d25991a8b4fe537c5 /src | |
| parent | 0d85df1dfe0bd3fbb4c78f9bbc3cb24c464b901c (diff) | |
first dashboard implementation
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.rs | 15 | ||||
| -rw-r--r-- | src/event.rs | 2 | ||||
| -rw-r--r-- | src/ui.rs | 142 |
3 files changed, 135 insertions, 24 deletions
@@ -4,6 +4,12 @@ use ratatui_textarea::TextArea; use crate::gentoo::{Package, portage::Portage}; #[derive(Debug)] +pub enum ViewState { + Dashboard, + InstalledPackages, +} + +#[derive(Debug)] #[allow(dead_code)] pub struct App<'a> { portage: Portage, @@ -14,6 +20,7 @@ pub struct App<'a> { search_indexes: Option<Vec<usize>>, pub search_indexes_len: Option<usize>, pub current_search_index: Option<usize>, + pub view: ViewState, } #[allow(dead_code)] @@ -28,6 +35,7 @@ impl<'a> App<'a> { search_indexes: None, current_search_index: None, search_indexes_len: None, + view: ViewState::Dashboard, } } @@ -50,6 +58,13 @@ impl<'a> App<'a> { self.search_found = None; } + pub fn cycle_current_tab(&mut self) { + match self.view { + ViewState::Dashboard => self.view = ViewState::InstalledPackages, + ViewState::InstalledPackages => self.view = ViewState::Dashboard, + } + } + pub fn search(&mut self) -> bool { let indexes: Vec<usize> = self .portage diff --git a/src/event.rs b/src/event.rs index 4b0ac43..7045d6d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -15,6 +15,8 @@ pub fn handle_event(app: &mut App) -> io::Result<bool> { KeyCode::Char('n') => app.next_search(), KeyCode::Char('N') => app.prev_search(), + KeyCode::Tab => app.cycle_current_tab(), + KeyCode::Char('d') => match key.modifiers { KeyModifiers::CONTROL => app .list_state @@ -1,15 +1,116 @@ use ratatui::{ Frame, - layout::{Constraint, Direction, Layout}, + layout::{Alignment, Constraint, Direction, Layout}, prelude::Rect, style::{Color, Modifier, Style, Stylize}, text::{Line, Span}, widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}, }; -use crate::app::App; +use crate::{app::App, gentoo::Package}; pub fn ui(frame: &mut Frame, app: &mut App) { + match app.view { + crate::app::ViewState::Dashboard => render_dashboard(frame, app), + crate::app::ViewState::InstalledPackages => render_installed_packages(frame, app), + } +} + +fn render_dashboard(frame: &mut Frame, app: &mut App) { + let logo = " + ██████╗ ██╗ ██╗██╗███████╗██╗ ██╗ +██╔═══██╗██║ ██║██║██╔════╝██║ ██║ +██║ ██║██║ ██║██║█████╗ ██║ █╗ ██║ +██║▄▄ ██║╚██╗ ██╔╝██║██╔══╝ ██║███╗██║ +╚██████╔╝ ╚████╔╝ ██║███████╗╚███╔███╔╝ + ╚══▀▀═╝ ╚═══╝ ╚═╝╚══════╝ ╚══╝╚══╝ + gentoo portage dashboard + "; + + let constraints = [ + Constraint::Length(9), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Fill(1), + Constraint::Length(3), + ]; + + let layout = Layout::vertical(constraints); + + let [ + logo_top, + top_stats, + middle_stats, + bottom_stats, + _fill, + key_hints, + ] = frame.area().layout(&layout); + + let splits = Layout::horizontal([ + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Fill(1), + ]) + .split(top_stats); + + let logo = Paragraph::new(logo); + frame.render_widget(logo, logo_top); + + let keys = Paragraph::new("(q) to quit | (tab) to switch tabs".fg(Color::Yellow)) + .block(Block::default().borders(Borders::ALL)); + frame.render_widget(keys, key_hints); + + let installed_packages_len = app.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", "126"); + let repository_state = create_stats("Repository state", "synced 2h ago"); + 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), + ]) + .split(middle_stats); + + let world_packages = create_stats("World Packages", "203"); + let pending_updates = create_stats("Pending Updates", "16"); + let last_emerge = create_stats("Last Emerge", "11:24 today"); + + 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), + ]) + .split(bottom_stats); + + let installed_size = create_stats("Installed Size", "38.2 GB"); + let distfiles_cache = create_stats("Distfiles Cache", "12.8 GB"); + let portage_news = create_stats("Portage News", "3 unread"); + + frame.render_widget(installed_size, splits[0]); + frame.render_widget(distfiles_cache, splits[1]); + frame.render_widget(portage_news, splits[2]); +} + +fn create_stats<'a>(title: &'a str, value: &'a str) -> Paragraph<'a> { + Paragraph::new(vec![ + Line::from(Span::styled(title, Style::default().bold())), + Line::from(""), + Line::from(value), + ]) + .block(Block::default().borders(Borders::ALL)) + .alignment(Alignment::Center) +} + +fn render_installed_packages(frame: &mut Frame, app: &mut App) { let constraints = [Constraint::Fill(1), Constraint::Length(3)]; let layout = Layout::vertical(constraints); @@ -83,12 +184,12 @@ fn render_package_metadata(frame: &mut Frame, area: Rect, app: &mut App) { 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.repository), + Span::raw(&pkg.repository), ]); let license = Line::from_iter([ @@ -101,24 +202,25 @@ fn render_package_metadata(frame: &mut Frame, area: Rect, app: &mut App) { 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 homepage = homepage_lines(&pkg, bold_style); + let mut lines = vec![maintainer, version, repository, license, description]; + lines.append(&mut homepage); - let mut lines = Vec::new(); + let paragraph = Paragraph::new(lines) + .block(Block::default().title("Metadata").borders(Borders::ALL)) + .wrap(Wrap { trim: false }); + + frame.render_widget(paragraph, area); +} - // First line: label +fn homepage_lines(pkg: &Package, bold_style: Style) -> Vec<Line<'_>> { + let mut lines = Vec::new(); 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 { + 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 @@ -137,15 +239,7 @@ fn render_package_metadata(frame: &mut Frame, area: Rect, app: &mut App) { ])); } - 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); + return lines; } fn search_popup_rect(percent_x: u16, r: Rect) -> Rect { |
