Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a Search bar and organized command structure #80

Merged
merged 3 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 97 additions & 54 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ macro_rules! with_common_script {
};
}

#[derive(Clone)]
struct ListNode {
name: &'static str,
command: &'static str,
Expand All @@ -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<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
Expand All @@ -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"),
Expand All @@ -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
Expand All @@ -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<Line> = if self.filter_query.is_empty() {
let mut items: Vec<Line> = 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 - {}",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();

fn try_scroll_down(&mut self) {
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
Expand Down
64 changes: 61 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,13 +61,47 @@ fn main() -> std::io::Result<()> {

fn run<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut command_opt: Option<RunningCommand> = 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);
}
Expand All @@ -90,7 +128,27 @@ fn run<B: Backend>(terminal: &mut Terminal<B>) -> 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));
}
}
Expand Down