2024-08-09 10:22:19 +01:00
|
|
|
use crate::{
|
|
|
|
float::{Float, FloatContent},
|
|
|
|
floating_text::FloatingText,
|
|
|
|
running_command::{Command, RunningCommand},
|
2024-08-11 03:52:49 +01:00
|
|
|
tabs::{ListNode, Tab},
|
2024-08-09 10:22:19 +01:00
|
|
|
theme::Theme,
|
|
|
|
};
|
|
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
|
|
|
use ego_tree::NodeId;
|
|
|
|
use ratatui::{
|
|
|
|
layout::{Constraint, Direction, Layout},
|
|
|
|
style::{Color, Style, Stylize},
|
|
|
|
text::{Line, Span},
|
|
|
|
widgets::{Block, Borders, List, ListState, Paragraph},
|
|
|
|
Frame,
|
|
|
|
};
|
2024-08-13 22:55:59 +01:00
|
|
|
use std::path::Path;
|
2024-07-28 16:17:06 +01:00
|
|
|
|
|
|
|
pub struct AppState {
|
|
|
|
/// Selected theme
|
2024-08-09 10:22:19 +01:00
|
|
|
theme: Theme,
|
|
|
|
/// Currently focused area
|
|
|
|
focus: Focus,
|
2024-08-11 03:52:49 +01:00
|
|
|
/// List of tabs
|
|
|
|
tabs: Vec<Tab>,
|
2024-08-09 10:22:19 +01:00
|
|
|
/// Current tab
|
|
|
|
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
|
|
|
|
/// just the current directory, all paths that took us here, so we can "cd .."
|
|
|
|
visit_stack: Vec<NodeId>,
|
|
|
|
/// This is the state asociated with the list widget, used to display the selection in the
|
|
|
|
/// widget
|
|
|
|
selection: ListState,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub enum Focus {
|
|
|
|
Search,
|
|
|
|
TabList,
|
|
|
|
List,
|
|
|
|
FloatingWindow(Float),
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ListEntry {
|
|
|
|
node: ListNode,
|
|
|
|
id: NodeId,
|
|
|
|
has_children: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AppState {
|
2024-08-13 22:55:59 +01:00
|
|
|
pub fn new(theme: Theme, temp_path: &Path, override_validation: bool) -> Self {
|
|
|
|
let tabs = crate::tabs::get_tabs(temp_path, !override_validation);
|
2024-08-11 03:52:49 +01:00
|
|
|
let root_id = tabs[0].tree.root().id();
|
2024-08-09 10:22:19 +01:00
|
|
|
let mut state = Self {
|
|
|
|
theme,
|
|
|
|
focus: Focus::List,
|
2024-08-11 03:52:49 +01:00
|
|
|
tabs,
|
2024-08-09 10:22:19 +01:00
|
|
|
current_tab: ListState::default().with_selected(Some(0)),
|
|
|
|
search_query: String::new(),
|
|
|
|
items: vec![],
|
|
|
|
visit_stack: vec![root_id],
|
|
|
|
selection: ListState::default().with_selected(Some(0)),
|
|
|
|
};
|
|
|
|
state.update_items();
|
|
|
|
state
|
|
|
|
}
|
|
|
|
pub fn draw(&mut self, frame: &mut Frame) {
|
2024-08-11 03:52:49 +01:00
|
|
|
let longest_tab_display_len = self
|
|
|
|
.tabs
|
2024-08-09 10:22:19 +01:00
|
|
|
.iter()
|
2024-08-15 23:13:47 +01:00
|
|
|
.map(|tab| tab.name.len() + self.theme.tab_icon().len())
|
2024-08-09 10:22:19 +01:00
|
|
|
.max()
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
|
|
let horizontal = Layout::default()
|
|
|
|
.direction(Direction::Horizontal)
|
|
|
|
.constraints([
|
|
|
|
Constraint::Min(longest_tab_display_len as u16 + 5),
|
|
|
|
Constraint::Percentage(100),
|
|
|
|
])
|
|
|
|
.split(frame.size());
|
|
|
|
let left_chunks = Layout::default()
|
|
|
|
.direction(Direction::Vertical)
|
|
|
|
.constraints([Constraint::Length(3), Constraint::Min(1)])
|
|
|
|
.split(horizontal[0]);
|
|
|
|
|
2024-08-11 03:52:49 +01:00
|
|
|
let tabs = self
|
|
|
|
.tabs
|
|
|
|
.iter()
|
|
|
|
.map(|tab| tab.name.as_str())
|
|
|
|
.collect::<Vec<_>>();
|
2024-08-09 10:22:19 +01:00
|
|
|
|
|
|
|
let tab_hl_style = if let Focus::TabList = self.focus {
|
2024-08-15 23:13:47 +01:00
|
|
|
Style::default().reversed().fg(self.theme.tab_color())
|
2024-08-09 10:22:19 +01:00
|
|
|
} else {
|
2024-08-15 23:13:47 +01:00
|
|
|
Style::new().fg(self.theme.tab_color())
|
2024-08-09 10:22:19 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
let list = List::new(tabs)
|
|
|
|
.block(Block::default().borders(Borders::ALL))
|
|
|
|
.highlight_style(tab_hl_style)
|
2024-08-15 23:13:47 +01:00
|
|
|
.highlight_symbol(self.theme.tab_icon());
|
2024-08-09 10:22:19 +01:00
|
|
|
frame.render_stateful_widget(list, left_chunks[1], &mut self.current_tab);
|
|
|
|
|
|
|
|
let chunks = Layout::default()
|
|
|
|
.direction(Direction::Vertical)
|
|
|
|
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
|
|
|
|
.split(horizontal[1]);
|
|
|
|
|
|
|
|
// Render search bar
|
|
|
|
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();
|
|
|
|
if !self.at_root() {
|
|
|
|
items.push(
|
2024-08-15 23:13:47 +01:00
|
|
|
Line::from(format!("{} ..", self.theme.dir_icon())).style(self.theme.dir_color()),
|
2024-08-09 10:22:19 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
items.extend(self.items.iter().map(
|
|
|
|
|ListEntry {
|
|
|
|
node, has_children, ..
|
|
|
|
}| {
|
|
|
|
if *has_children {
|
2024-08-15 23:13:47 +01:00
|
|
|
Line::from(format!("{} {}", self.theme.dir_icon(), node.name))
|
|
|
|
.style(self.theme.dir_color())
|
2024-08-09 10:22:19 +01:00
|
|
|
} else {
|
2024-08-15 23:13:47 +01:00
|
|
|
Line::from(format!("{} {}", self.theme.cmd_icon(), node.name))
|
|
|
|
.style(self.theme.cmd_color())
|
2024-08-09 10:22:19 +01:00
|
|
|
}
|
|
|
|
},
|
|
|
|
));
|
|
|
|
|
|
|
|
// Create the list widget with items
|
|
|
|
let list = List::new(items)
|
|
|
|
.highlight_style(if let Focus::List = self.focus {
|
|
|
|
Style::default().reversed()
|
|
|
|
} else {
|
|
|
|
Style::new()
|
|
|
|
})
|
|
|
|
.block(Block::default().borders(Borders::ALL).title(format!(
|
|
|
|
"Linux Toolbox - {}",
|
|
|
|
chrono::Local::now().format("%Y-%m-%d")
|
|
|
|
)))
|
|
|
|
.scroll_padding(1);
|
|
|
|
frame.render_stateful_widget(list, chunks[1], &mut self.selection);
|
|
|
|
|
|
|
|
if let Focus::FloatingWindow(float) = &mut self.focus {
|
|
|
|
float.draw(frame, chunks[1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
pub fn handle_key(&mut self, key: &KeyEvent) -> bool {
|
|
|
|
match &mut self.focus {
|
|
|
|
Focus::FloatingWindow(command) => {
|
|
|
|
if command.handle_key_event(key) {
|
|
|
|
self.focus = Focus::List;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Focus::Search => {
|
|
|
|
match key.code {
|
|
|
|
KeyCode::Char(c) => self.search_query.push(c),
|
|
|
|
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,
|
|
|
|
Focus::TabList => match key.code {
|
|
|
|
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => {
|
|
|
|
self.focus = Focus::List
|
|
|
|
}
|
|
|
|
KeyCode::Char('j') | KeyCode::Down
|
2024-08-11 03:52:49 +01:00
|
|
|
if self.current_tab.selected().unwrap() + 1 < self.tabs.len() =>
|
2024-08-09 10:22:19 +01:00
|
|
|
{
|
|
|
|
self.current_tab.select_next();
|
|
|
|
self.refresh_tab();
|
|
|
|
}
|
|
|
|
KeyCode::Char('k') | KeyCode::Up => {
|
|
|
|
self.current_tab.select_previous();
|
|
|
|
self.refresh_tab();
|
|
|
|
}
|
|
|
|
KeyCode::Char('/') => self.enter_search(),
|
2024-08-15 23:21:43 +01:00
|
|
|
KeyCode::Char('t') => self.theme = self.theme.next(),
|
|
|
|
KeyCode::Char('T') => self.theme = self.theme.prev(),
|
2024-08-09 10:22:19 +01:00
|
|
|
_ => {}
|
|
|
|
},
|
|
|
|
Focus::List if key.kind != KeyEventKind::Release => match key.code {
|
|
|
|
KeyCode::Char('j') | KeyCode::Down => self.selection.select_next(),
|
|
|
|
KeyCode::Char('k') | KeyCode::Up => self.selection.select_previous(),
|
|
|
|
KeyCode::Char('p') => self.enable_preview(),
|
|
|
|
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.handle_enter(),
|
|
|
|
KeyCode::Char('h') | KeyCode::Left => {
|
|
|
|
if self.at_root() {
|
|
|
|
self.focus = Focus::TabList;
|
|
|
|
} else {
|
|
|
|
self.enter_parent_directory();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
KeyCode::Char('/') => self.enter_search(),
|
|
|
|
KeyCode::Tab => self.focus = Focus::TabList,
|
2024-08-15 23:21:43 +01:00
|
|
|
KeyCode::Char('t') => self.theme = self.theme.next(),
|
|
|
|
KeyCode::Char('T') => self.theme = self.theme.prev(),
|
2024-08-09 10:22:19 +01:00
|
|
|
_ => {}
|
|
|
|
},
|
|
|
|
_ => {}
|
|
|
|
};
|
|
|
|
true
|
|
|
|
}
|
|
|
|
pub fn update_items(&mut self) {
|
|
|
|
if self.search_query.is_empty() {
|
2024-08-11 03:52:49 +01:00
|
|
|
let curr = self.tabs[self.current_tab.selected().unwrap()]
|
2024-08-09 10:22:19 +01:00
|
|
|
.tree
|
|
|
|
.get(*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();
|
2024-08-11 03:52:49 +01:00
|
|
|
for tab in self.tabs.iter() {
|
2024-08-09 10:22:19 +01:00
|
|
|
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()));
|
|
|
|
}
|
|
|
|
}
|
2024-08-11 03:52:49 +01:00
|
|
|
self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name));
|
2024-08-09 10:22:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
/// 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)
|
|
|
|
/// else returns `false`
|
|
|
|
fn at_root(&self) -> bool {
|
|
|
|
self.visit_stack.len() == 1
|
|
|
|
}
|
|
|
|
fn enter_parent_directory(&mut self) {
|
|
|
|
self.visit_stack.pop();
|
|
|
|
self.selection.select(Some(0));
|
|
|
|
self.update_items();
|
|
|
|
}
|
|
|
|
fn get_selected_command(&mut self, change_directory: bool) -> Option<Command> {
|
|
|
|
let mut selected_index = self.selection.selected().unwrap_or(0);
|
|
|
|
|
|
|
|
if !self.at_root() && selected_index == 0 {
|
|
|
|
if change_directory {
|
|
|
|
self.enter_parent_directory();
|
|
|
|
}
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
if !self.at_root() {
|
|
|
|
selected_index = selected_index.saturating_sub(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(item) = self.items.get(selected_index) {
|
|
|
|
if !item.has_children {
|
|
|
|
return Some(item.node.command.clone());
|
|
|
|
} else if change_directory {
|
|
|
|
self.visit_stack.push(item.id);
|
|
|
|
self.selection.select(Some(0));
|
|
|
|
self.update_items();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None
|
|
|
|
}
|
|
|
|
fn enable_preview(&mut self) {
|
|
|
|
if let Some(command) = self.get_selected_command(false) {
|
2024-08-13 22:55:59 +01:00
|
|
|
if let Some(preview) = FloatingText::from_command(&command) {
|
2024-08-09 10:22:19 +01:00
|
|
|
self.spawn_float(preview, 80, 80);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn handle_enter(&mut self) {
|
|
|
|
if let Some(cmd) = self.get_selected_command(true) {
|
2024-08-13 22:55:59 +01:00
|
|
|
let command = RunningCommand::new(cmd);
|
2024-08-09 10:22:19 +01:00
|
|
|
self.spawn_float(command, 80, 80);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn spawn_float<T: FloatContent + 'static>(&mut self, float: T, width: u16, height: u16) {
|
|
|
|
self.focus = Focus::FloatingWindow(Float::new(Box::new(float), width, height));
|
|
|
|
}
|
|
|
|
fn enter_search(&mut self) {
|
|
|
|
self.focus = Focus::Search;
|
|
|
|
self.selection.select(None);
|
|
|
|
}
|
|
|
|
fn exit_search(&mut self) {
|
|
|
|
self.selection.select(Some(0));
|
|
|
|
self.focus = Focus::List;
|
|
|
|
self.update_items();
|
|
|
|
}
|
|
|
|
fn refresh_tab(&mut self) {
|
2024-08-11 03:52:49 +01:00
|
|
|
self.visit_stack = vec![self.tabs[self.current_tab.selected().unwrap()]
|
|
|
|
.tree
|
|
|
|
.root()
|
|
|
|
.id()];
|
2024-08-09 10:22:19 +01:00
|
|
|
self.selection.select(Some(0));
|
|
|
|
self.update_items();
|
|
|
|
}
|
2024-07-28 16:17:06 +01:00
|
|
|
}
|