diff --git a/src/commands/dotfiles/alacritty-setup.sh b/src/commands/applications-setup/alacritty-setup.sh similarity index 100% rename from src/commands/dotfiles/alacritty-setup.sh rename to src/commands/applications-setup/alacritty-setup.sh diff --git a/src/commands/dotfiles/kitty-setup.sh b/src/commands/applications-setup/kitty-setup.sh similarity index 100% rename from src/commands/dotfiles/kitty-setup.sh rename to src/commands/applications-setup/kitty-setup.sh diff --git a/src/commands/dotfiles/rofi-setup.sh b/src/commands/applications-setup/rofi-setup.sh similarity index 100% rename from src/commands/dotfiles/rofi-setup.sh rename to src/commands/applications-setup/rofi-setup.sh diff --git a/src/list.rs b/src/list.rs index b564bdea..7f61f053 100644 --- a/src/list.rs +++ b/src/list.rs @@ -18,6 +18,7 @@ macro_rules! with_common_script { }; } +#[derive(Clone)] struct ListNode { name: &'static str, command: &'static str, @@ -37,6 +38,10 @@ pub struct CustomList { /// This stores the preview windows state. If it is None, it will not be displayed. /// If it is Some, we show it with the content of the selected item preview_window_state: Option, + // This stores the current search query + filter_query: String, + // This stores the filtered tree + filtered_items: Vec, } /// This struct stores the preview window state @@ -62,18 +67,6 @@ impl CustomList { name: "root", command: "" } => { - ListNode { - name: "Full System Update", - command: with_common_script!("commands/system-update.sh"), - }, - ListNode { - name: "Setup Bash Prompt", - command: "bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\"" - }, - ListNode { - name: "Setup Neovim", - command: "bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/neovim/main/setup.sh)\"" - }, // ListNode { // name: "Just ls, nothing special, trust me", // command: include_str!("commands/special_ls.sh"), @@ -100,22 +93,35 @@ impl CustomList { } }, ListNode { - name: "Titus Dotfiles", + name: "Applications Setup", command: "" } => { ListNode { name: "Alacritty Setup", - command: with_common_script!("commands/dotfiles/alacritty-setup.sh"), + command: with_common_script!("commands/applications-setup/alacritty-setup.sh"), + }, ListNode { name: "Kitty Setup", - command: with_common_script!("commands/dotfiles/kitty-setup.sh"), + command: with_common_script!("commands/applications-setup/kitty-setup.sh"), + }, + ListNode { + name: "Bash Prompt Setup", + command: "bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\"" + }, + ListNode { + name: "Neovim Setup", + command: "bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/neovim/main/setup.sh)\"" }, ListNode { name: "Rofi Setup", - command: with_common_script!("commands/dotfiles/rofi-setup.sh"), + command: with_common_script!("commands/applications-setup/rofi-setup.sh"), }, - } + }, + ListNode { + name: "Full System Update", + command: with_common_script!("commands/system-update.sh"), + }, }); // We don't get a reference, but rather an id, because references are siginficantly more // paintfull to manage @@ -126,46 +132,59 @@ impl CustomList { list_state: ListState::default().with_selected(Some(0)), // By default the PreviewWindowState is set to None, so it is not being shown preview_window_state: None, + filter_query: String::new(), + filtered_items: vec![], } } - + /// Draw our custom widget to the frame - pub fn draw(&mut self, frame: &mut Frame, area: Rect) { - // Get the last element in the `visit_stack` vec + pub fn draw(&mut self, frame: &mut Frame, area: Rect, filter: String) { let theme = get_theme(); - let curr = self - .inner_tree - .get(*self.visit_stack.last().unwrap()) - .unwrap(); - let mut items = vec![]; + self.filter(filter); - // If we are not at the root of our filesystem tree, we need to add `..` path, to be able - // to go up the tree - // icons:   - if !self.at_root() { - items.push(Line::from(format!("{} ..", theme.dir_icon)).style(theme.dir_color)); - } - - // Iterate through all the children - for node in curr.children() { - // The difference between a "directory" and a "command" is simple: if it has children, - // it's a directory and will be handled as such - if node.has_children() { - items.push( - Line::from(format!("{} {}", theme.dir_icon, node.value().name)) - .style(theme.dir_color), - ); - } else { - items.push( - Line::from(format!("{} {}", theme.cmd_icon, node.value().name)) - .style(theme.cmd_color), - ); + let item_list: Vec = if self.filter_query.is_empty() { + let mut items: Vec = vec![]; + // If we are not at the root of our filesystem tree, we need to add `..` path, to be able + // to go up the tree + // icons:   + if !self.at_root() { + items.push(Line::from(format!("{} ..", theme.dir_icon)).style(theme.dir_color)); } - } + // Get the last element in the `visit_stack` vec + let curr = self + .inner_tree + .get(*self.visit_stack.last().unwrap()) + .unwrap(); + + // Iterate through all the children + for node in curr.children() { + // The difference between a "directory" and a "command" is simple: if it has children, + // it's a directory and will be handled as such + if node.has_children() { + items.push( + Line::from(format!("{} {}", theme.dir_icon, node.value().name)) + .style(theme.dir_color), + ); + } else { + items.push( + Line::from(format!("{} {}", theme.cmd_icon, node.value().name)) + .style(theme.cmd_color), + ); + } + } + items + } else { + self.filtered_items + .iter() + .map(|node| { + Line::from(format!("{} {}", theme.cmd_icon, node.name)).style(theme.cmd_color) + }) + .collect() + }; // create the normal list widget containing only item in our "working directory" / tree // node - let list = List::new(items) + let list = List::new(item_list) .highlight_style(Style::default().reversed()) .block(Block::default().borders(Borders::ALL).title(format!( "Linux Toolbox - {}", @@ -204,6 +223,26 @@ impl CustomList { } } + pub fn filter(&mut self, query: String) { + self.filter_query.clone_from(&query); + self.filtered_items.clear(); + + let query_lower = query.to_lowercase(); + let mut stack = vec![self.inner_tree.root().id()]; + + while let Some(node_id) = stack.pop() { + let node = self.inner_tree.get(node_id).unwrap(); + + if node.value().name.to_lowercase().contains(&query_lower) && !node.has_children() { + self.filtered_items.push(node.value().clone()); + } + + for child in node.children() { + stack.push(child.id()); + } + } + } + /// Handle key events, we are only interested in `Press` and `Repeat` events pub fn handle_key(&mut self, event: KeyEvent) -> Option<&'static str> { if event.kind == KeyEventKind::Release { @@ -268,18 +307,22 @@ impl CustomList { } } } + fn try_scroll_up(&mut self) { self.list_state .select(Some(self.list_state.selected().unwrap().saturating_sub(1))); } + fn try_scroll_down(&mut self) { - let curr = self - .inner_tree - .get(*self.visit_stack.last().unwrap()) - .unwrap(); - - let count = curr.children().count(); - + let count = if self.filter_query.is_empty() { + let curr = self + .inner_tree + .get(*self.visit_stack.last().unwrap()) + .unwrap(); + curr.children().count() + } else { + self.filtered_items.len() + }; let curr_selection = self.list_state.selected().unwrap(); if self.at_root() { self.list_state diff --git a/src/main.rs b/src/main.rs index 7acfcfb0..8f474e7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,10 @@ use crossterm::{ use list::CustomList; use ratatui::{ backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout}, + style::{Color, Style}, + text::Span, + widgets::{Block, Borders, Paragraph}, Terminal, }; use running_command::RunningCommand; @@ -57,13 +61,47 @@ fn main() -> std::io::Result<()> { fn run(terminal: &mut Terminal) -> io::Result<()> { let mut command_opt: Option = None; - let mut custom_list = CustomList::new(); + let mut search_input = String::new(); + let mut in_search_mode = false; + loop { // Always redraw terminal .draw(|frame| { - custom_list.draw(frame, frame.size()); + //Split the terminal into 2 vertical chunks + //One for the search bar and one for the command list + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref()) + .split(frame.size()); + + //Set the search bar text (If empty use the placeholder) + let display_text = if search_input.is_empty() { + if in_search_mode { + Span::raw("") + } else { + Span::raw("Press / to search") + } + } else { + Span::raw(&search_input) + }; + + //Create the search bar widget + let mut search_bar = Paragraph::new(display_text) + .block(Block::default().borders(Borders::ALL).title("Search")) + .style(Style::default().fg(Color::DarkGray)); + + //Change the color if in search mode + if in_search_mode { + search_bar = search_bar.clone().style(Style::default().fg(Color::Blue)); + } + + //Render the search bar (First chunk of the screen) + frame.render_widget(search_bar, chunks[0]); + //Render the command list (Second chunk of the screen) + custom_list.draw(frame, chunks[1], search_input.clone()); + if let Some(ref mut command) = &mut command_opt { command.draw(frame); } @@ -90,7 +128,27 @@ fn run(terminal: &mut Terminal) -> io::Result<()> { if key.code == KeyCode::Char('q') { return Ok(()); } - if let Some(cmd) = custom_list.handle_key(key) { + //Activate search mode if the forward slash key gets pressed + if key.code == KeyCode::Char('/') { + // Enter search mode + in_search_mode = true; + continue; + } + //Insert user input into the search bar + if in_search_mode { + match key.code { + KeyCode::Char(c) => search_input.push(c), + KeyCode::Backspace => { + search_input.pop(); + } + KeyCode::Esc => { + search_input = String::new(); + in_search_mode = false + } + KeyCode::Enter => in_search_mode = false, + _ => {} + } + } else if let Some(cmd) = custom_list.handle_key(key) { command_opt = Some(RunningCommand::new(cmd)); } }