Merge branch 'main' into dwm-fixes

This commit is contained in:
Chris Titus 2024-09-03 17:29:33 -05:00 committed by GitHub
commit 43ee314e33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 219 additions and 178 deletions

1
Cargo.lock generated
View File

@ -882,6 +882,7 @@ dependencies = [
"tempdir", "tempdir",
"toml", "toml",
"tui-term", "tui-term",
"unicode-width",
"which", "which",
] ]

View File

@ -16,6 +16,10 @@ 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.3" which = "6.0.3"
unicode-width = "0.1.13"
[build-dependencies]
chrono = "0.4.33"
[[bin]] [[bin]]
name = "linutil" name = "linutil"

View File

@ -1,4 +1,9 @@
fn main() { fn main() {
// Rebuild program if any file in commands directory changes. // Rebuild program if any file in commands directory changes.
println!("cargo:rerun-if-changed=src/commands"); println!("cargo:rerun-if-changed=src/commands");
// Add current date as a variable to be displayed in the 'Linux Toolbox' text.
println!(
"cargo:rustc-env=BUILD_DATE={}",
chrono::Local::now().format("%Y-%m-%d")
);
} }

Binary file not shown.

160
src/filter.rs Normal file
View 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 Filter {
search_input: Vec<char>,
in_search_mode: bool,
input_position: usize,
items: Vec<ListEntry>,
}
impl Filter {
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_searchbar(&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);
}
}
}

View File

@ -68,8 +68,18 @@ impl FloatContent for FloatingText {
.text .text
.iter() .iter()
.skip(self.scroll) .skip(self.scroll)
.flat_map(|line| {
if line.is_empty() {
return vec![String::new()];
}
line.chars()
.collect::<Vec<char>>()
.chunks(inner_area.width as usize)
.map(|chunk| chunk.iter().collect())
.collect::<Vec<String>>()
})
.take(inner_area.height as usize) .take(inner_area.height as usize)
.map(|line| Line::from(line.as_str())) .map(Line::from)
.collect(); .collect();
// Create list widget // Create list widget

View File

@ -1,3 +1,4 @@
mod filter;
mod float; mod float;
mod floating_text; mod floating_text;
mod running_command; mod running_command;

View File

@ -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()
}
}

View File

@ -1,4 +1,5 @@
use crate::{ use crate::{
filter::{Filter, 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: Filter,
} }
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: Filter::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_searchbar(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, ..
}| { }| {
@ -156,7 +140,7 @@ impl AppState {
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(format!("Linux Toolbox")), .title(format!("Linux Toolbox - {}", env!("BUILD_DATE"))),
) )
.scroll_padding(1); .scroll_padding(1);
frame.render_stateful_widget(list, chunks[1], &mut self.selection); frame.render_stateful_widget(list, chunks[1], &mut self.selection);
@ -172,21 +156,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 => {
@ -203,8 +177,8 @@ impl AppState {
self.refresh_tab(); self.refresh_tab();
} }
KeyCode::Char('/') => self.enter_search(), KeyCode::Char('/') => self.enter_search(),
KeyCode::Char('t') => self.theme = self.theme.next(), KeyCode::Char('t') => self.theme.next(),
KeyCode::Char('T') => self.theme = self.theme.prev(), KeyCode::Char('T') => self.theme.prev(),
_ => {} _ => {}
}, },
Focus::List if key.kind != KeyEventKind::Release => match key.code { Focus::List if key.kind != KeyEventKind::Release => match key.code {
@ -221,53 +195,20 @@ impl AppState {
} }
KeyCode::Char('/') => self.enter_search(), KeyCode::Char('/') => self.enter_search(),
KeyCode::Tab => self.focus = Focus::TabList, KeyCode::Tab => self.focus = Focus::TabList,
KeyCode::Char('t') => self.theme = self.theme.next(), KeyCode::Char('t') => self.theme.next(),
KeyCode::Char('T') => self.theme = self.theme.prev(), KeyCode::Char('T') => self.theme.prev(),
_ => {} _ => {}
}, },
_ => {} _ => {}
}; };
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)
@ -293,7 +234,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 {
@ -322,11 +263,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) {

View File

@ -86,17 +86,15 @@ impl Theme {
} }
impl Theme { impl Theme {
#[allow(unused)] pub fn next(&mut self) {
pub fn next(self) -> Self { let position = *self as usize;
let position = self as usize;
let types = Theme::value_variants(); let types = Theme::value_variants();
types[(position + 1) % types.len()].into() *self = types[(position + 1) % types.len()];
} }
#[allow(unused)] pub fn prev(&mut self) {
pub fn prev(self) -> Self { let position = *self as usize;
let position = self as usize;
let types = Theme::value_variants(); let types = Theme::value_variants();
types[(position + types.len() - 1) % types.len()].into() *self = types[(position + types.len() - 1) % types.len()];
} }
} }