diff options
| author | pablu <me@pablu.de> | 2026-05-07 13:07:32 +0200 |
|---|---|---|
| committer | pablu <me@pablu.de> | 2026-05-07 13:07:32 +0200 |
| commit | cc8e924ce828d237d7cdd245b794c532a4ee80d8 (patch) | |
| tree | 5fc7c118879eb0107fe20a12ff6c3096a3a36926 | |
| parent | 3346222550bc18edc457d9b509f00d0eba9d0f4f (diff) | |
working search
| -rw-r--r-- | Cargo.lock | 14 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/app.rs | 32 | ||||
| -rw-r--r-- | src/event.rs | 27 | ||||
| -rw-r--r-- | src/ui.rs | 84 |
5 files changed, 136 insertions, 22 deletions
@@ -1009,6 +1009,7 @@ version = "0.1.0" dependencies = [ "color-eyre", "ratatui", + "ratatui-textarea", ] [[package]] @@ -1105,6 +1106,19 @@ dependencies = [ ] [[package]] +name = "ratatui-textarea" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2950274c0e155944158cf766848dd87bd6db6dad27da1c23ee4a7c8de71dbf1f" +dependencies = [ + "ratatui-core", + "ratatui-crossterm", + "ratatui-widgets", + "unicode-segmentation", + "unicode-width", +] + +[[package]] name = "ratatui-widgets" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6,3 +6,4 @@ edition = "2024" [dependencies] color-eyre = "0.6.5" ratatui = "0.30.0" +ratatui-textarea = "0.9.1" @@ -1,24 +1,52 @@ use ratatui::widgets::ListState; +use ratatui_textarea::TextArea; use crate::gentoo::{Package, portage::Portage}; #[derive(Debug)] #[allow(dead_code)] -pub struct App { +pub struct App<'a> { portage: Portage, pub list_state: ListState, + pub showing_search_window: bool, + pub textarea: TextArea<'a>, + pub search_found: Option<bool>, } #[allow(dead_code)] -impl App { +impl<'a> App<'a> { 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, } } pub fn installed_packages(&self) -> Vec<Package> { self.portage.installed_packages.clone() } + + pub fn toggle_search_window(&mut self) { + self.showing_search_window = !self.showing_search_window; + self.textarea.clear(); + self.search_found = None; + } + + pub fn search(&mut self) -> bool { + let index = self.portage.installed_packages.iter().position(|s| { + s.name + .to_lowercase() + .contains(&self.textarea.lines()[0].to_lowercase()) + }); + + if let Some(index) = index { + self.list_state.select(Some(index)); + return true; + } + + return false; + } } diff --git a/src/event.rs b/src/event.rs index bfe1fa5..52013a5 100644 --- a/src/event.rs +++ b/src/event.rs @@ -6,11 +6,28 @@ use crate::app::App; pub fn handle_event(app: &mut App) -> io::Result<bool> { if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('q') => return Ok(true), - KeyCode::Char('j') => app.list_state.select_next(), - KeyCode::Char('k') => app.list_state.select_previous(), - _ => return Ok(false), + if !app.showing_search_window { + match key.code { + KeyCode::Char('q') => return Ok(true), + KeyCode::Char('j') => app.list_state.select_next(), + KeyCode::Char('k') => app.list_state.select_previous(), + KeyCode::Char('/') => app.toggle_search_window(), + _ => return Ok(false), + } + } else { + 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()); + () + } + } } } @@ -1,32 +1,82 @@ use ratatui::{ Frame, - layout::{Constraint, Layout}, + layout::{Constraint, Direction, Layout}, prelude::Rect, - style::{Color, Modifier, Style, Stylize}, + style::{Color, Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, ListState}, + widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph}, }; use crate::app::App; pub fn ui(frame: &mut Frame, app: &mut App) { - let constraints = [Constraint::Length(1), Constraint::Fill(1)]; + let constraints = [Constraint::Fill(1), Constraint::Length(3)]; - let layout = Layout::vertical(constraints).spacing(1); - let [top, first] = frame.area().layout(&layout); + 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(first); - - let title = Line::from_iter([ - Span::from("Installed Packages").bold(), - Span::from(" (Press 'q' to quit and arrow keys to navigate)"), - ]); - frame.render_widget(title.centered(), top); + .split(top); render_packages(frame, split[0], app); render_use_flags(frame, split[1], app); + + let key_hint = match app.showing_search_window { + false => Span::styled( + "(q) to quit | (/) to search | (j) down | (k) up", + Style::default().fg(Color::Yellow), + ), + true => Span::styled( + "(esc) to quit search | (enter) to search", + Style::default().fg(Color::Yellow), + ), + }; + + let key_notes_footer = + Paragraph::new(Line::from(key_hint)).block(Block::default().borders(Borders::ALL)); + frame.render_widget(key_notes_footer, bottom); + + if app.showing_search_window { + let mut popup_block = Block::default() + .title("Search") + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Rounded); + + if let Some(found) = app.search_found { + if found { + popup_block = popup_block.border_style(Style::default().fg(Color::Green)) + } else { + popup_block = popup_block.border_style(Style::default().fg(Color::Red)) + } + } + + app.textarea.set_block(popup_block); + app.textarea.set_style(Style::default().fg(Color::Yellow)); + app.textarea.set_cursor_line_style(Style::default()); + + let area = search_popup_rect(70, frame.area()); + frame.render_widget(Clear, area); + frame.render_widget(&app.textarea, area); + } +} + +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 } fn render_packages(frame: &mut Frame, area: Rect, app: &mut App) { @@ -35,7 +85,11 @@ fn render_packages(frame: &mut Frame, area: Rect, app: &mut App) { .style(Color::White) .highlight_style(Modifier::REVERSED) .highlight_symbol("> ") - .block(Block::new().borders(Borders::ALL)); + .block( + Block::new() + .borders(Borders::ALL) + .title("Installed packages"), + ); frame.render_stateful_widget(list, area, &mut app.list_state); } @@ -68,7 +122,7 @@ fn render_use_flags(frame: &mut Frame, area: Rect, app: &mut App) { .style(Color::White) .scroll_padding(1) .direction(ratatui::widgets::ListDirection::TopToBottom) - .block(Block::new().borders(Borders::ALL)); + .block(Block::new().borders(Borders::ALL).title("USE Flags")); let mut list_state = ListState::default(); list_state.select_first(); |
