mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2024-11-22 05:12:27 +00:00
refactor: Move search and filter logic to a separate module
This commit is contained in:
parent
81c8fa8883
commit
388c073116
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -971,6 +971,7 @@ dependencies = [
|
||||||
"tempdir",
|
"tempdir",
|
||||||
"toml",
|
"toml",
|
||||||
"tui-term",
|
"tui-term",
|
||||||
|
"unicode-width",
|
||||||
"which",
|
"which",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ tempdir = "0.3.7"
|
||||||
serde = { version = "1.0.205", features = ["derive"] }
|
serde = { version = "1.0.205", features = ["derive"] }
|
||||||
toml = "0.8.19"
|
toml = "0.8.19"
|
||||||
which = "6.0.2"
|
which = "6.0.2"
|
||||||
|
unicode-width = "0.1.13"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "linutil"
|
name = "linutil"
|
||||||
|
|
160
src/filter.rs
Normal file
160
src/filter.rs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
use crate::{state::ListEntry, tabs::Tab, theme::Theme};
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use ego_tree::NodeId;
|
||||||
|
use ratatui::{
|
||||||
|
layout::Rect,
|
||||||
|
style::Style,
|
||||||
|
text::Span,
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
|
pub enum SearchAction {
|
||||||
|
None,
|
||||||
|
Exit,
|
||||||
|
Update,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FilterInstance {
|
||||||
|
search_input: Vec<char>,
|
||||||
|
in_search_mode: bool,
|
||||||
|
input_position: usize,
|
||||||
|
items: Vec<ListEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterInstance {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
search_input: vec![],
|
||||||
|
in_search_mode: false,
|
||||||
|
input_position: 0,
|
||||||
|
items: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn item_list(&self) -> &[ListEntry] {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
|
pub fn activate_search(&mut self) {
|
||||||
|
self.in_search_mode = true;
|
||||||
|
}
|
||||||
|
pub fn deactivate_search(&mut self) {
|
||||||
|
self.in_search_mode = false;
|
||||||
|
}
|
||||||
|
pub fn update_items(&mut self, tabs: &[Tab], current_tab: usize, node: NodeId) {
|
||||||
|
if self.search_input.is_empty() {
|
||||||
|
let curr = tabs[current_tab].tree.get(node).unwrap();
|
||||||
|
|
||||||
|
self.items = curr
|
||||||
|
.children()
|
||||||
|
.map(|node| ListEntry {
|
||||||
|
node: node.value().clone(),
|
||||||
|
id: node.id(),
|
||||||
|
has_children: node.has_children(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
} else {
|
||||||
|
self.items.clear();
|
||||||
|
|
||||||
|
let query_lower = self.search_input.iter().collect::<String>().to_lowercase();
|
||||||
|
for tab in tabs.iter() {
|
||||||
|
let mut stack = vec![tab.tree.root().id()];
|
||||||
|
while let Some(node_id) = stack.pop() {
|
||||||
|
let node = tab.tree.get(node_id).unwrap();
|
||||||
|
|
||||||
|
if node.value().name.to_lowercase().contains(&query_lower)
|
||||||
|
&& !node.has_children()
|
||||||
|
{
|
||||||
|
self.items.push(ListEntry {
|
||||||
|
node: node.value().clone(),
|
||||||
|
id: node.id(),
|
||||||
|
has_children: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.extend(node.children().map(|child| child.id()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn draw(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
|
||||||
|
//Set the search bar text (If empty use the placeholder)
|
||||||
|
let display_text = if !self.in_search_mode && self.search_input.is_empty() {
|
||||||
|
Span::raw("Press / to search")
|
||||||
|
} else {
|
||||||
|
Span::raw(self.search_input.iter().collect::<String>())
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_color = if self.in_search_mode {
|
||||||
|
theme.focused_color()
|
||||||
|
} else {
|
||||||
|
theme.unfocused_color()
|
||||||
|
};
|
||||||
|
|
||||||
|
//Create the search bar widget
|
||||||
|
let search_bar = Paragraph::new(display_text)
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("Search"))
|
||||||
|
.style(Style::default().fg(search_color));
|
||||||
|
|
||||||
|
//Render the search bar (First chunk of the screen)
|
||||||
|
frame.render_widget(search_bar, area);
|
||||||
|
|
||||||
|
// Render cursor in search bar
|
||||||
|
if self.in_search_mode {
|
||||||
|
let cursor_position: usize = self.search_input[..self.input_position]
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.width().unwrap_or(1))
|
||||||
|
.sum();
|
||||||
|
let x = area.x + cursor_position as u16 + 1;
|
||||||
|
let y = area.y + 1;
|
||||||
|
frame.set_cursor(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handles key events. Returns true if search must be exited
|
||||||
|
pub fn handle_key(&mut self, event: &KeyEvent) -> SearchAction {
|
||||||
|
//Insert user input into the search bar
|
||||||
|
match event.code {
|
||||||
|
KeyCode::Char(c) => self.insert_char(c),
|
||||||
|
KeyCode::Backspace => self.remove_previous(),
|
||||||
|
KeyCode::Delete => self.remove_next(),
|
||||||
|
KeyCode::Left => return self.cursor_left(),
|
||||||
|
KeyCode::Right => return self.cursor_right(),
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.input_position = 0;
|
||||||
|
self.search_input.clear();
|
||||||
|
return SearchAction::Exit;
|
||||||
|
}
|
||||||
|
KeyCode::Enter => return SearchAction::Exit,
|
||||||
|
_ => return SearchAction::None,
|
||||||
|
};
|
||||||
|
SearchAction::Update
|
||||||
|
}
|
||||||
|
fn cursor_left(&mut self) -> SearchAction {
|
||||||
|
self.input_position = self.input_position.saturating_sub(1);
|
||||||
|
SearchAction::None
|
||||||
|
}
|
||||||
|
fn cursor_right(&mut self) -> SearchAction {
|
||||||
|
if self.input_position < self.search_input.len() {
|
||||||
|
self.input_position += 1;
|
||||||
|
}
|
||||||
|
SearchAction::None
|
||||||
|
}
|
||||||
|
fn insert_char(&mut self, input: char) {
|
||||||
|
self.search_input.insert(self.input_position, input);
|
||||||
|
self.cursor_right();
|
||||||
|
}
|
||||||
|
fn remove_previous(&mut self) {
|
||||||
|
let current = self.input_position;
|
||||||
|
if current > 0 {
|
||||||
|
self.search_input.remove(current - 1);
|
||||||
|
self.cursor_left();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn remove_next(&mut self) {
|
||||||
|
let current = self.input_position;
|
||||||
|
if current < self.search_input.len() {
|
||||||
|
self.search_input.remove(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod filter;
|
||||||
mod float;
|
mod float;
|
||||||
mod floating_text;
|
mod floating_text;
|
||||||
mod running_command;
|
mod running_command;
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
|
||||||
use ratatui::{
|
|
||||||
layout::Rect,
|
|
||||||
style::Style,
|
|
||||||
text::Span,
|
|
||||||
widgets::{Block, Borders, Paragraph},
|
|
||||||
Frame,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::state::AppState;
|
|
||||||
|
|
||||||
pub struct SearchBar {
|
|
||||||
search_input: String,
|
|
||||||
in_search_mode: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchBar {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
SearchBar {
|
|
||||||
search_input: String::new(),
|
|
||||||
in_search_mode: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn activate_search(&mut self) {
|
|
||||||
self.in_search_mode = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deactivate_search(&mut self) {
|
|
||||||
self.in_search_mode = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_search_active(&self) -> bool {
|
|
||||||
self.in_search_mode
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw(&self, frame: &mut Frame, area: Rect, state: &AppState) {
|
|
||||||
//Set the search bar text (If empty use the placeholder)
|
|
||||||
let display_text = if !self.in_search_mode && self.search_input.is_empty() {
|
|
||||||
Span::raw("Press / to search")
|
|
||||||
} else {
|
|
||||||
Span::raw(&self.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(state.theme.unfocused_color));
|
|
||||||
|
|
||||||
//Change the color if in search mode
|
|
||||||
if self.in_search_mode {
|
|
||||||
search_bar = search_bar
|
|
||||||
.clone()
|
|
||||||
.style(Style::default().fg(state.theme.focused_color));
|
|
||||||
}
|
|
||||||
|
|
||||||
//Render the search bar (First chunk of the screen)
|
|
||||||
frame.render_widget(search_bar, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_key(&mut self, event: KeyEvent) -> String {
|
|
||||||
//Insert user input into the search bar
|
|
||||||
match event.code {
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
self.search_input.push(c);
|
|
||||||
}
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
self.search_input.pop();
|
|
||||||
}
|
|
||||||
KeyCode::Esc => {
|
|
||||||
self.search_input = String::new();
|
|
||||||
self.in_search_mode = false;
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
self.in_search_mode = false;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
self.search_input.clone()
|
|
||||||
}
|
|
||||||
}
|
|
109
src/state.rs
109
src/state.rs
|
@ -1,4 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
|
filter::{FilterInstance, SearchAction},
|
||||||
float::{Float, FloatContent},
|
float::{Float, FloatContent},
|
||||||
floating_text::FloatingText,
|
floating_text::FloatingText,
|
||||||
running_command::{Command, RunningCommand},
|
running_command::{Command, RunningCommand},
|
||||||
|
@ -9,9 +10,9 @@ use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||||
use ego_tree::NodeId;
|
use ego_tree::NodeId;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
style::{Color, Style, Stylize},
|
style::{Style, Stylize},
|
||||||
text::{Line, Span},
|
text::Line,
|
||||||
widgets::{Block, Borders, List, ListState, Paragraph},
|
widgets::{Block, Borders, List, ListState},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -25,16 +26,13 @@ pub struct AppState {
|
||||||
tabs: Vec<Tab>,
|
tabs: Vec<Tab>,
|
||||||
/// Current tab
|
/// Current tab
|
||||||
current_tab: ListState,
|
current_tab: ListState,
|
||||||
/// Current search query
|
|
||||||
search_query: String,
|
|
||||||
/// Current items
|
|
||||||
items: Vec<ListEntry>,
|
|
||||||
/// This stack keeps track of our "current dirrectory". You can think of it as `pwd`. but not
|
/// This stack keeps track of our "current dirrectory". You can think of it as `pwd`. but not
|
||||||
/// just the current directory, all paths that took us here, so we can "cd .."
|
/// just the current directory, all paths that took us here, so we can "cd .."
|
||||||
visit_stack: Vec<NodeId>,
|
visit_stack: Vec<NodeId>,
|
||||||
/// This is the state asociated with the list widget, used to display the selection in the
|
/// This is the state asociated with the list widget, used to display the selection in the
|
||||||
/// widget
|
/// widget
|
||||||
selection: ListState,
|
selection: ListState,
|
||||||
|
filter: FilterInstance,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Focus {
|
pub enum Focus {
|
||||||
|
@ -44,10 +42,10 @@ pub enum Focus {
|
||||||
FloatingWindow(Float),
|
FloatingWindow(Float),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ListEntry {
|
pub struct ListEntry {
|
||||||
node: ListNode,
|
pub node: ListNode,
|
||||||
id: NodeId,
|
pub id: NodeId,
|
||||||
has_children: bool,
|
pub has_children: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
@ -59,10 +57,9 @@ impl AppState {
|
||||||
focus: Focus::List,
|
focus: Focus::List,
|
||||||
tabs,
|
tabs,
|
||||||
current_tab: ListState::default().with_selected(Some(0)),
|
current_tab: ListState::default().with_selected(Some(0)),
|
||||||
search_query: String::new(),
|
|
||||||
items: vec![],
|
|
||||||
visit_stack: vec![root_id],
|
visit_stack: vec![root_id],
|
||||||
selection: ListState::default().with_selected(Some(0)),
|
selection: ListState::default().with_selected(Some(0)),
|
||||||
|
filter: FilterInstance::new(),
|
||||||
};
|
};
|
||||||
state.update_items();
|
state.update_items();
|
||||||
state
|
state
|
||||||
|
@ -110,20 +107,7 @@ impl AppState {
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
|
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
|
||||||
.split(horizontal[1]);
|
.split(horizontal[1]);
|
||||||
|
|
||||||
// Render search bar
|
self.filter.draw(frame, chunks[0], &self.theme);
|
||||||
let search_text = match self.focus {
|
|
||||||
Focus::Search => Span::raw(&self.search_query),
|
|
||||||
_ if !self.search_query.is_empty() => Span::raw(&self.search_query),
|
|
||||||
_ => Span::raw("Press / to search"),
|
|
||||||
};
|
|
||||||
let search_bar = Paragraph::new(search_text)
|
|
||||||
.block(Block::default().borders(Borders::ALL))
|
|
||||||
.style(Style::default().fg(if let Focus::Search = self.focus {
|
|
||||||
Color::Blue
|
|
||||||
} else {
|
|
||||||
Color::DarkGray
|
|
||||||
}));
|
|
||||||
frame.render_widget(search_bar, chunks[0]);
|
|
||||||
|
|
||||||
let mut items: Vec<Line> = Vec::new();
|
let mut items: Vec<Line> = Vec::new();
|
||||||
if !self.at_root() {
|
if !self.at_root() {
|
||||||
|
@ -132,7 +116,7 @@ impl AppState {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
items.extend(self.items.iter().map(
|
items.extend(self.filter.item_list().iter().map(
|
||||||
|ListEntry {
|
|ListEntry {
|
||||||
node, has_children, ..
|
node, has_children, ..
|
||||||
}| {
|
}| {
|
||||||
|
@ -171,21 +155,11 @@ impl AppState {
|
||||||
self.focus = Focus::List;
|
self.focus = Focus::List;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Focus::Search => {
|
Focus::Search => match self.filter.handle_key(key) {
|
||||||
match key.code {
|
SearchAction::Exit => self.exit_search(),
|
||||||
KeyCode::Char(c) => self.search_query.push(c),
|
SearchAction::Update => self.update_items(),
|
||||||
KeyCode::Backspace => {
|
_ => {}
|
||||||
self.search_query.pop();
|
},
|
||||||
}
|
|
||||||
KeyCode::Esc => {
|
|
||||||
self.search_query = String::new();
|
|
||||||
self.exit_search();
|
|
||||||
}
|
|
||||||
KeyCode::Enter => self.exit_search(),
|
|
||||||
_ => return true,
|
|
||||||
}
|
|
||||||
self.update_items();
|
|
||||||
}
|
|
||||||
_ if key.code == KeyCode::Char('q') => return false,
|
_ if key.code == KeyCode::Char('q') => return false,
|
||||||
Focus::TabList => match key.code {
|
Focus::TabList => match key.code {
|
||||||
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => {
|
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => {
|
||||||
|
@ -228,45 +202,12 @@ impl AppState {
|
||||||
};
|
};
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
pub fn update_items(&mut self) {
|
fn update_items(&mut self) {
|
||||||
if self.search_query.is_empty() {
|
self.filter.update_items(
|
||||||
let curr = self.tabs[self.current_tab.selected().unwrap()]
|
&self.tabs,
|
||||||
.tree
|
self.current_tab.selected().unwrap(),
|
||||||
.get(*self.visit_stack.last().unwrap())
|
*self.visit_stack.last().unwrap(),
|
||||||
.unwrap();
|
);
|
||||||
|
|
||||||
self.items = curr
|
|
||||||
.children()
|
|
||||||
.map(|node| ListEntry {
|
|
||||||
node: node.value().clone(),
|
|
||||||
id: node.id(),
|
|
||||||
has_children: node.has_children(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
} else {
|
|
||||||
self.items.clear();
|
|
||||||
|
|
||||||
let query_lower = self.search_query.to_lowercase();
|
|
||||||
for tab in self.tabs.iter() {
|
|
||||||
let mut stack = vec![tab.tree.root().id()];
|
|
||||||
while let Some(node_id) = stack.pop() {
|
|
||||||
let node = tab.tree.get(node_id).unwrap();
|
|
||||||
|
|
||||||
if node.value().name.to_lowercase().contains(&query_lower)
|
|
||||||
&& !node.has_children()
|
|
||||||
{
|
|
||||||
self.items.push(ListEntry {
|
|
||||||
node: node.value().clone(),
|
|
||||||
id: node.id(),
|
|
||||||
has_children: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
stack.extend(node.children().map(|child| child.id()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/// Checks ehther the current tree node is the root node (can we go up the tree or no)
|
/// Checks ehther the current tree node is the root node (can we go up the tree or no)
|
||||||
/// Returns `true` if we can't go up the tree (we are at the tree root)
|
/// Returns `true` if we can't go up the tree (we are at the tree root)
|
||||||
|
@ -292,7 +233,7 @@ impl AppState {
|
||||||
selected_index = selected_index.saturating_sub(1);
|
selected_index = selected_index.saturating_sub(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(item) = self.items.get(selected_index) {
|
if let Some(item) = self.filter.item_list().get(selected_index) {
|
||||||
if !item.has_children {
|
if !item.has_children {
|
||||||
return Some(item.node.command.clone());
|
return Some(item.node.command.clone());
|
||||||
} else if change_directory {
|
} else if change_directory {
|
||||||
|
@ -321,11 +262,13 @@ impl AppState {
|
||||||
}
|
}
|
||||||
fn enter_search(&mut self) {
|
fn enter_search(&mut self) {
|
||||||
self.focus = Focus::Search;
|
self.focus = Focus::Search;
|
||||||
|
self.filter.activate_search();
|
||||||
self.selection.select(None);
|
self.selection.select(None);
|
||||||
}
|
}
|
||||||
fn exit_search(&mut self) {
|
fn exit_search(&mut self) {
|
||||||
self.selection.select(Some(0));
|
self.selection.select(Some(0));
|
||||||
self.focus = Focus::List;
|
self.focus = Focus::List;
|
||||||
|
self.filter.deactivate_search();
|
||||||
self.update_items();
|
self.update_items();
|
||||||
}
|
}
|
||||||
fn refresh_tab(&mut self) {
|
fn refresh_tab(&mut self) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user