Added a Search bar and organized command structure

This commit is contained in:
afonsofrancof 2024-07-26 16:04:34 +01:00
parent 8e8476cda5
commit 42014998b4
No known key found for this signature in database
5 changed files with 159 additions and 58 deletions

View File

@ -18,6 +18,7 @@ macro_rules! with_common_script {
}; };
} }
#[derive(Clone)]
struct ListNode { struct ListNode {
name: &'static str, name: &'static str,
command: &'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. /// 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 /// If it is Some, we show it with the content of the selected item
preview_window_state: Option<PreviewWindowState>, preview_window_state: Option<PreviewWindowState>,
// This stores the current search query
filter_query: String,
// This stores the filtered tree
filtered_items: Vec<ListNode>,
} }
/// This struct stores the preview window state /// This struct stores the preview window state
@ -62,18 +67,6 @@ impl CustomList {
name: "root", name: "root",
command: "" 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 { // ListNode {
// name: "Just ls, nothing special, trust me", // name: "Just ls, nothing special, trust me",
// command: include_str!("commands/special_ls.sh"), // command: include_str!("commands/special_ls.sh"),
@ -100,22 +93,35 @@ impl CustomList {
} }
}, },
ListNode { ListNode {
name: "Titus Dotfiles", name: "Applications Setup",
command: "" command: ""
} => { } => {
ListNode { ListNode {
name: "Alacritty Setup", name: "Alacritty Setup",
command: with_common_script!("commands/dotfiles/alacritty-setup.sh"), command: with_common_script!("commands/applications-setup/alacritty-setup.sh"),
}, },
ListNode { ListNode {
name: "Kitty Setup", 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 { ListNode {
name: "Rofi Setup", 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 // We don't get a reference, but rather an id, because references are siginficantly more
// paintfull to manage // paintfull to manage
@ -126,46 +132,59 @@ impl CustomList {
list_state: ListState::default().with_selected(Some(0)), list_state: ListState::default().with_selected(Some(0)),
// By default the PreviewWindowState is set to None, so it is not being shown // By default the PreviewWindowState is set to None, so it is not being shown
preview_window_state: None, preview_window_state: None,
filter_query: String::new(),
filtered_items: vec![],
} }
} }
/// Draw our custom widget to the frame /// Draw our custom widget to the frame
pub fn draw(&mut self, frame: &mut Frame, area: Rect) { pub fn draw(&mut self, frame: &mut Frame, area: Rect, filter: String) {
// Get the last element in the `visit_stack` vec
let theme = get_theme(); let theme = get_theme();
let curr = self self.filter(filter);
.inner_tree
.get(*self.visit_stack.last().unwrap())
.unwrap();
let mut items = vec![];
// If we are not at the root of our filesystem tree, we need to add `..` path, to be able let item_list: Vec<Line> = if self.filter_query.is_empty() {
// to go up the tree let mut items: Vec<Line> = vec![];
// icons:   // If we are not at the root of our filesystem tree, we need to add `..` path, to be able
if !self.at_root() { // to go up the tree
items.push(Line::from(format!("{} ..", theme.dir_icon)).style(theme.dir_color)); // 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),
);
} }
} // 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 // create the normal list widget containing only item in our "working directory" / tree
// node // node
let list = List::new(items) let list = List::new(item_list)
.highlight_style(Style::default().reversed()) .highlight_style(Style::default().reversed())
.block(Block::default().borders(Borders::ALL).title(format!( .block(Block::default().borders(Borders::ALL).title(format!(
"Linux Toolbox - {}", "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 /// Handle key events, we are only interested in `Press` and `Repeat` events
pub fn handle_key(&mut self, event: KeyEvent) -> Option<&'static str> { pub fn handle_key(&mut self, event: KeyEvent) -> Option<&'static str> {
if event.kind == KeyEventKind::Release { if event.kind == KeyEventKind::Release {
@ -268,18 +307,22 @@ impl CustomList {
} }
} }
} }
fn try_scroll_up(&mut self) { fn try_scroll_up(&mut self) {
self.list_state self.list_state
.select(Some(self.list_state.selected().unwrap().saturating_sub(1))); .select(Some(self.list_state.selected().unwrap().saturating_sub(1)));
} }
fn try_scroll_down(&mut self) { fn try_scroll_down(&mut self) {
let curr = self let count = if self.filter_query.is_empty() {
.inner_tree let curr = self
.get(*self.visit_stack.last().unwrap()) .inner_tree
.unwrap(); .get(*self.visit_stack.last().unwrap())
.unwrap();
let count = curr.children().count(); curr.children().count()
} else {
self.filtered_items.len()
};
let curr_selection = self.list_state.selected().unwrap(); let curr_selection = self.list_state.selected().unwrap();
if self.at_root() { if self.at_root() {
self.list_state self.list_state

View File

@ -19,6 +19,10 @@ use crossterm::{
use list::CustomList; use list::CustomList;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
text::Span,
widgets::{Block, Borders, Paragraph},
Terminal, Terminal,
}; };
use running_command::RunningCommand; use running_command::RunningCommand;
@ -57,13 +61,47 @@ fn main() -> std::io::Result<()> {
fn run<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> { fn run<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut command_opt: Option<RunningCommand> = None; let mut command_opt: Option<RunningCommand> = None;
let mut custom_list = CustomList::new(); let mut custom_list = CustomList::new();
let mut search_input = String::new();
let mut in_search_mode = false;
loop { loop {
// Always redraw // Always redraw
terminal terminal
.draw(|frame| { .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 { if let Some(ref mut command) = &mut command_opt {
command.draw(frame); command.draw(frame);
} }
@ -90,7 +128,27 @@ fn run<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
if key.code == KeyCode::Char('q') { if key.code == KeyCode::Char('q') {
return Ok(()); 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)); command_opt = Some(RunningCommand::new(cmd));
} }
} }