From 3572d26b95c06db41ade6acc67370b53a6341b63 Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Wed, 31 Mar 2021 17:20:37 +0530 Subject: [PATCH] Too big of a rewrite --- Cargo.lock | 37 +- Cargo.toml | 4 +- src/app.rs | 1222 +++++++++++++++++++---------------------------- src/config.rs | 624 +++++++++++------------- src/explorer.rs | 59 +++ src/input.rs | 707 ++++++++++++++++----------- src/lib.rs | 7 +- src/main.rs | 304 ++++++++---- src/ui.rs | 413 ++++++++++++---- 9 files changed, 1847 insertions(+), 1530 deletions(-) create mode 100644 src/explorer.rs diff --git a/Cargo.lock b/Cargo.lock index 700c3ea..9c9194a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aho-corasick" version = "0.7.15" @@ -473,6 +475,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "mio" version = "0.7.9" @@ -1001,6 +1019,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-segmentation" version = "1.7.1" @@ -1019,6 +1046,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + [[package]] name = "walkdir" version = "2.3.1" @@ -1133,13 +1166,15 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "xplr" -version = "0.1.13" +version = "0.2.0" dependencies = [ "criterion", "crossterm", "dirs", "handlebars", + "mime_guess", "serde", + "serde_json", "serde_yaml", "shellwords", "termion", diff --git a/Cargo.toml b/Cargo.toml index 3c94536..f43a8e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xplr" -version = "0.1.13" # Update app.rs +version = "0.2.0" # Update app.rs authors = ["Arijit Basu "] edition = "2018" description = "An experimental, minimal, configurable TUI file explorer, stealing ideas from nnn and fzf." @@ -17,9 +17,11 @@ termion = "1.5" crossterm = "0.18" dirs = "3.0.1" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" serde_yaml = "0.8" handlebars = "3.5.3" shellwords = "1.0.0" +mime_guess = "2.0.3" [dev-dependencies] criterion = "0.3" diff --git a/src/app.rs b/src/app.rs index 381449d..86df075 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,821 +1,593 @@ -use crate::config::{Action, CommandConfig, Config, Mode}; +use crate::config::Config; +use crate::config::Mode; use crate::error::Error; use crate::input::Key; -use dirs; +use mime_guess; use serde::{Deserialize, Serialize}; -use serde_yaml; +use std::cmp::Ordering; +use std::collections::BinaryHeap; use std::collections::HashMap; -use std::collections::HashSet; -use std::fs; -use std::fs::File; -use std::io::BufReader; -use std::path::Path; +use std::collections::VecDeque; use std::path::PathBuf; -pub const VERSION: &str = "v0.1.13"; // Update Cargo.toml -pub const UNSUPPORTED_STR: &str = "???"; -pub const TOTAL_ROWS: usize = 50; +pub const VERSION: &str = "v0.2.0"; // Update Cargo.toml pub const TEMPLATE_TABLE_ROW: &str = "TEMPLATE_TABLE_ROW"; -fn expand_tilde>(path_user_input: P) -> Option { - let p = path_user_input.as_ref(); - if !p.starts_with("~") { - return Some(p.to_path_buf()); +pub const UNSUPPORTED_STR: &str = "???"; + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Node { + pub parent: String, + pub relative_path: String, + pub absolute_path: String, + pub extension: String, + pub is_symlink: bool, + pub is_dir: bool, + pub is_file: bool, + pub is_readonly: bool, + pub mime_essence: String, +} + +impl Node { + pub fn new(parent: String, relative_path: String) -> Self { + let absolute_path = PathBuf::from(&parent) + .join(&relative_path) + .canonicalize() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + let path = PathBuf::from(&absolute_path); + + let extension = path + .extension() + .map(|e| e.to_string_lossy().to_string()) + .unwrap_or_default(); + + let maybe_metadata = path.metadata().ok(); + + let is_symlink = maybe_metadata + .clone() + .map(|m| m.file_type().is_symlink()) + .unwrap_or(false); + + let is_dir = maybe_metadata.clone().map(|m| m.is_dir()).unwrap_or(false); + + let is_file = maybe_metadata.clone().map(|m| m.is_file()).unwrap_or(false); + + let is_readonly = maybe_metadata + .map(|m| m.permissions().readonly()) + .unwrap_or(false); + + let mime_essence = mime_guess::from_path(&path) + .first() + .map(|m| m.essence_str().to_string()) + .unwrap_or_default(); + + Self { + parent, + relative_path, + absolute_path, + extension, + is_symlink, + is_dir, + is_file, + is_readonly, + mime_essence, + } } - if p == Path::new("~") { - return dirs::home_dir(); +} + +impl Ord for Node { + fn cmp(&self, other: &Self) -> Ordering { + // Notice that the we flip the ordering on costs. + // In case of a tie we compare positions - this step is necessary + // to make implementations of `PartialEq` and `Ord` consistent. + other.relative_path.cmp(&self.relative_path) + } +} +impl PartialOrd for Node { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } - dirs::home_dir().map(|mut h| { - if h == Path::new("/") { - // Corner case: `h` root directory; - // don't prepend extra `/`, just drop the tilde. - p.strip_prefix("~").unwrap().to_path_buf() - } else { - h.push(p.strip_prefix("~/").unwrap()); - h - } - }) } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct DirectoryBuffer { - pub pwd: PathBuf, - pub focus: Option, - pub items: Vec<(PathBuf, DirectoryItemMetadata)>, + pub parent: String, + pub nodes: Vec, pub total: usize, + pub focus: usize, } impl DirectoryBuffer { - pub fn relative_focus(focus: usize) -> usize { - focus.min(TOTAL_ROWS) - } - - pub fn explore( - path: &PathBuf, - show_hidden: bool, - ) -> Result<(usize, impl Iterator), Error> { - let hide_hidden = !show_hidden; - - let total = fs::read_dir(&path)? - .filter_map(|d| d.ok().map(|e| e.path())) - .filter_map(|p| p.canonicalize().ok()) - .filter_map(|abs_path| { - abs_path - .file_name() - .map(|rel_path| rel_path.to_str().unwrap_or(UNSUPPORTED_STR).to_string()) - }) - .filter(|rel_path| !(hide_hidden && rel_path.starts_with('.'))) - .count(); - - let items = fs::read_dir(&path)? - .filter_map(|d| d.ok().map(|e| e.path())) - .filter_map(|p| p.canonicalize().ok()) - .filter_map(|abs_path| { - abs_path.file_name().map(|rel_path| { - ( - abs_path.to_path_buf(), - rel_path.to_str().unwrap_or(UNSUPPORTED_STR).to_string(), - ) - }) - }) - .filter(move |(_, rel_path)| !(hide_hidden && rel_path.starts_with('.'))); - Ok((total, items)) - } - - pub fn load( - config: &Config, - focus: Option, - path: &PathBuf, - show_hidden: bool, - selected_paths: &HashSet, - ) -> Result { - let offset = focus - .map(|f| (f.max(TOTAL_ROWS) - TOTAL_ROWS, f.max(TOTAL_ROWS))) - .unwrap_or((0, TOTAL_ROWS)); - - let (total, items) = DirectoryBuffer::explore(&path, show_hidden)?; - let visible: Vec<(PathBuf, DirectoryItemMetadata)> = items - .enumerate() - .skip_while(|(i, _)| *i < offset.0) - .take_while(|(i, _)| *i <= offset.1) - .enumerate() - .map(|(rel_idx, (net_idx, (abs, rel)))| { - let ext = abs - .extension() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_string(); - (net_idx, rel_idx, abs, rel, ext) - }) - .map(|(net_idx, rel_idx, abs, rel, ext)| { - let absolute_path: String = - abs.as_os_str().to_str().unwrap_or(UNSUPPORTED_STR).into(); - let relative_path = rel.to_string(); - let extension = ext.to_string(); - let is_dir = abs.is_dir(); - let is_file = abs.is_file(); - - let maybe_meta = abs.metadata().ok(); - - let is_symlink = maybe_meta - .clone() - .map(|m| m.file_type().is_symlink()) - .unwrap_or(false); - - let is_readonly = maybe_meta - .clone() - .map(|m| m.permissions().readonly()) - .unwrap_or(false); - - let (focus_idx, is_focused) = - focus.map(|f| (f, net_idx == f)).unwrap_or((0, false)); - let is_selected = selected_paths.contains(&abs); - - let ui = if is_focused { - &config.general.focused_ui - } else if is_selected { - &config.general.selected_ui - } else { - &config.general.normal_ui - }; - - let is_first = net_idx == 0; - let is_last = net_idx == total.max(1) - 1; - - let tree = config - .general - .table - .tree - .clone() - .map(|t| { - if is_last { - t.2.format.clone() - } else if is_first { - t.0.format.clone() - } else { - t.1.format.clone() - } - }) - .unwrap_or_default(); - - let filetype = config - .filetypes - .special - .get(&relative_path) - .or_else(|| config.filetypes.extension.get(&extension)) - .unwrap_or_else(|| { - if is_symlink { - &config.filetypes.symlink - } else if is_dir { - &config.filetypes.directory - } else { - &config.filetypes.file - } - }); - - let focus_relative_index = if focus_idx <= net_idx { - format!(" {}", net_idx - focus_idx) - } else { - format!("-{}", focus_idx - net_idx) - }; - - let m = DirectoryItemMetadata { - absolute_path, - relative_path, - extension, - icon: filetype.icon.clone(), - prefix: ui.prefix.clone(), - suffix: ui.suffix.clone(), - tree: tree.into(), - is_symlink, - is_first, - is_last, - is_dir, - is_file, - is_readonly, - is_selected, - is_focused, - index: net_idx + 1, - focus_relative_index, - buffer_relative_index: rel_idx + 1, - total: total, - }; - (abs.to_owned(), m) - }) - .collect(); - - let focus = focus.map(|f| { - if Self::relative_focus(f) >= visible.len() { - visible.len().max(1) - 1 - } else { - f - } - }); - - Ok(Self { - pwd: path.into(), + pub fn new(parent: String, nodes: Vec, focus: usize) -> Self { + let total = nodes.len(); + Self { + parent, + nodes, total, - items: visible, focus, - }) + } } - pub fn focused(&self) -> Option<(PathBuf, DirectoryItemMetadata)> { - self.focus.and_then(|f| { - self.items - .get(Self::relative_focus(f)) - .map(|f| f.to_owned()) - }) + pub fn focused_node(&self) -> Option<&Node> { + self.nodes.get(self.focus) } } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DirectoryItemMetadata { - pub absolute_path: String, - pub relative_path: String, - pub extension: String, - pub icon: String, - pub prefix: String, - pub suffix: String, - pub tree: String, - pub is_first: bool, - pub is_last: bool, - pub is_symlink: bool, - pub is_dir: bool, - pub is_file: bool, - pub is_readonly: bool, - pub is_selected: bool, - pub is_focused: bool, - pub index: usize, - pub focus_relative_index: String, - pub buffer_relative_index: usize, - pub total: usize, +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub enum InternalMsg { + AddDirectory(String, DirectoryBuffer), + HandleKey(Key), } -pub fn parse_help_menu<'a>( - kb: impl Iterator))>, -) -> Vec<(String, String)> { - let mut m = kb - .map(|(k, a)| { - ( - a.0.clone(), - serde_yaml::to_string(k) - .unwrap() - .strip_prefix("---") - .unwrap_or_default() - .trim() - .to_string(), - ) - }) - .collect::>(); - m.sort(); - m +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub enum ExternalMsg { + Refresh, + FocusNext, + FocusNextByRelativeIndex(usize), + FocusNextByRelativeIndexFromInput, + FocusPrevious, + FocusPreviousByRelativeIndex(usize), + FocusPreviousByRelativeIndexFromInput, + FocusFirst, + FocusLast, + FocusPath(String), + FocusByIndex(usize), + FocusByIndexFromInput, + FocusByFileName(String), + ChangeDirectory(String), + Enter, + Back, + BufferString(String), + BufferStringFromKey, + ResetInputBuffer, + SwitchMode(String), + Call(Command), + ToggleSelection, + PrintResultAndQuit, + PrintAppStateAndQuit, + Debug(String), + Terminate, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Task { - NoOp, - Quit, - PrintAndQuit(String), - Call(CommandConfig), +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub enum MsgIn { + Internal(InternalMsg), + External(ExternalMsg), } -impl Default for Task { - fn default() -> Self { - Self::NoOp +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Command { + pub command: String, + + #[serde(default)] + pub args: Vec, +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub enum MsgOut { + Refresh, + PrintResultAndQuit, + PrintAppStateAndQuit, + Debug(String), + Call(Command), +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Task { + priority: usize, + msg: MsgIn, + key: Option, +} + +impl Task { + pub fn new(priority: usize, msg: MsgIn, key: Option) -> Self { + Self { priority, msg, key } + } +} + +impl Ord for Task { + fn cmp(&self, other: &Self) -> Ordering { + // Notice that the we flip the ordering on costs. + // In case of a tie we compare positions - this step is necessary + // to make implementations of `PartialEq` and `Ord` consistent. + other.priority.cmp(&self.priority) + } +} +impl PartialOrd for Task { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct App { - pub version: String, - pub config: Config, - pub directory_buffer: DirectoryBuffer, - pub saved_buffers: HashMap>, - pub selected_paths: HashSet, - pub mode: Mode, - pub parsed_key_bindings: HashMap)>, - pub parsed_help_menu: Vec<(String, String)>, - pub show_hidden: bool, - pub task: Task, - pub number_input: usize, + config: Config, + pwd: String, + directory_buffers: HashMap, + tasks: BinaryHeap, + selected: Vec, + msg_out: VecDeque, + mode: Mode, + input_buffer: Option, } impl App { - pub fn new( - config: &Config, - pwd: &PathBuf, - saved_buffers: &HashMap>, - selected_paths: &HashSet, - mode: Mode, - show_hidden: bool, - focus: Option, - number_input: usize, - ) -> Result { - let directory_buffer = - DirectoryBuffer::load(config, focus.or(Some(0)), &pwd, show_hidden, selected_paths)?; - - let mut saved_buffers = saved_buffers.clone(); - saved_buffers.insert( - directory_buffer.pwd.clone().into(), - directory_buffer.focus.clone(), - ); - - let parsed_key_bindings = config.key_bindings.clone().filtered(&mode); - - let parsed_help_menu = parse_help_menu(parsed_key_bindings.iter()); - - Ok(Self { - version: VERSION.into(), - config: config.to_owned(), - directory_buffer, - saved_buffers, - selected_paths: selected_paths.to_owned(), - mode, - parsed_key_bindings, - parsed_help_menu, - show_hidden, - task: Task::NoOp, - number_input: number_input, - }) - } - - pub fn refresh(self) -> Result { - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode, - self.show_hidden, - self.directory_buffer.focus, - 0, - ) - } - - pub fn exit_submode(self) -> Result { - let mode = match self.mode { - Mode::Explore => Mode::Explore, - Mode::ExploreSubmode(_) => Mode::Explore, - Mode::Select => Mode::Select, - Mode::SelectSubmode(_) => Mode::Select, - }; + pub fn new(pwd: String) -> Self { + let config = Config::default(); + let mode = config + .modes + .get(&"default".to_string()) + .map(|k| k.to_owned()) + .unwrap_or_default(); - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, + Self { + config, + pwd, + directory_buffers: Default::default(), + tasks: Default::default(), + selected: Default::default(), + msg_out: Default::default(), mode, - self.show_hidden, - self.directory_buffer.focus, - 0, - ) - } - - pub fn number_input(self, n: u8) -> Result { - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode, - self.show_hidden, - self.directory_buffer.focus, - self.number_input * 10 + n as usize, - ) - } - - pub fn toggle_hidden(self) -> Result { - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode, - !self.show_hidden, - self.directory_buffer.focus, - 0, - ) - } - - pub fn focus_first(self) -> Result { - let focus = if self.directory_buffer.total == 0 { - None + input_buffer: Default::default(), + } + } + + pub fn focused_node(&self) -> Option<&Node> { + self.directory_buffer().and_then(|d| d.focused_node()) + } + + pub fn enqueue(mut self, task: Task) -> Self { + self.tasks.push(task); + self + } + + pub fn possibly_mutate(mut self) -> Result { + if let Some(task) = self.tasks.pop() { + match task.msg { + MsgIn::Internal(msg) => self.handle_internal(msg), + MsgIn::External(msg) => self.handle_external(msg, task.key), + } } else { - Some(0) + Ok(self) + } + } + + fn handle_internal(self, msg: InternalMsg) -> Result { + match msg { + InternalMsg::AddDirectory(parent, dir) => self.add_directory(parent, dir), + InternalMsg::HandleKey(key) => self.handle_key(key), + } + } + + fn handle_external(self, msg: ExternalMsg, key: Option) -> Result { + match msg { + ExternalMsg::Refresh => self.refresh(), + ExternalMsg::FocusFirst => self.focus_first(), + ExternalMsg::FocusLast => self.focus_last(), + ExternalMsg::FocusPrevious => self.focus_previous(), + ExternalMsg::FocusPreviousByRelativeIndex(i) => { + self.focus_previous_by_relative_index(i) + } + + ExternalMsg::FocusPreviousByRelativeIndexFromInput => { + self.focus_previous_by_relative_index_from_input() + } + ExternalMsg::FocusNext => self.focus_next(), + ExternalMsg::FocusNextByRelativeIndex(i) => self.focus_next_by_relative_index(i), + ExternalMsg::FocusNextByRelativeIndexFromInput => { + self.focus_next_by_relative_index_from_input() + } + ExternalMsg::FocusPath(p) => self.focus_path(&p), + ExternalMsg::FocusByIndex(i) => self.focus_by_index(i), + ExternalMsg::FocusByIndexFromInput => self.focus_by_index_from_input(), + ExternalMsg::FocusByFileName(n) => self.focus_by_file_name(&n), + ExternalMsg::ChangeDirectory(dir) => self.change_directory(&dir), + ExternalMsg::Enter => self.enter(), + ExternalMsg::Back => self.back(), + ExternalMsg::BufferString(input) => self.buffer_string(&input), + ExternalMsg::BufferStringFromKey => self.buffer_string_from_key(key), + ExternalMsg::ResetInputBuffer => self.reset_input_buffer(), + ExternalMsg::SwitchMode(mode) => self.switch_mode(&mode), + ExternalMsg::Call(cmd) => self.call(cmd), + ExternalMsg::ToggleSelection => self.toggle_selection(), + ExternalMsg::PrintResultAndQuit => self.print_result_and_quit(), + ExternalMsg::PrintAppStateAndQuit => self.print_app_state_and_quit(), + ExternalMsg::Debug(path) => self.debug(&path), + ExternalMsg::Terminate => Err(Error::Terminated), + } + } + + fn handle_key(mut self, key: Key) -> Result { + let kb = self.mode.key_bindings.clone(); + let msgs = kb + .on_key + .get(&key.to_string()) + .map(|a| Some(a.messages.clone())) + .unwrap_or_else(|| { + if key.is_alphabet() { + kb.on_alphabet.map(|a| a.messages) + } else if key.is_number() { + kb.on_number.map(|a| a.messages) + } else if key.is_special_character() { + kb.on_special_character.map(|a| a.messages) + } else { + kb.default.map(|a| a.messages) + } + }); + + if let Some(msgs) = msgs.to_owned() { + for msg in msgs { + self = self.enqueue(Task::new(0, MsgIn::External(msg), Some(key))); + } }; - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode, - self.show_hidden, - focus, - 0, - ) + Ok(self) } - pub fn focus_last(self) -> Result { - let focus = if self.directory_buffer.total == 0 { - None - } else { - Some(self.directory_buffer.total - 1) + fn refresh(mut self) -> Result { + self.msg_out.push_back(MsgOut::Refresh); + Ok(self) + } + + fn focus_first(mut self) -> Result { + if let Some(dir) = self.directory_buffer_mut() { + dir.focus = 0; + self.msg_out.push_back(MsgOut::Refresh); }; + Ok(self) + } - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode, - self.show_hidden, - focus, - 0, - ) + fn focus_last(mut self) -> Result { + if let Some(dir) = self.directory_buffer_mut() { + dir.focus = dir.total.max(1) - 1; + self.msg_out.push_back(MsgOut::Refresh); + }; + Ok(self) } - pub fn change_directory(self, dir: &String) -> Result { - self.focus_path(&PathBuf::from(dir))?.enter() + fn focus_previous(mut self) -> Result { + if let Some(dir) = self.directory_buffer_mut() { + dir.focus = dir.focus.max(1) - 1; + self.msg_out.push_back(MsgOut::Refresh); + }; + Ok(self) } - pub fn call(mut self, cmd: &CommandConfig) -> Result { - self.task = Task::Call(cmd.clone()); + fn focus_previous_by_relative_index(mut self, index: usize) -> Result { + if let Some(dir) = self.directory_buffer_mut() { + dir.focus = dir.focus.max(index) - index; + self.msg_out.push_back(MsgOut::Refresh); + }; Ok(self) } - pub fn focus_next(self) -> Result { - let len = self.directory_buffer.total; - let step = self.number_input.max(1); - let focus = self - .directory_buffer - .focus - .map(|f| (len.max(1) - 1).min(f + step)) - .or(Some(0)); - - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode, - self.show_hidden, - focus, - 0, - ) + fn focus_previous_by_relative_index_from_input(self) -> Result { + if let Some(index) = self.input_buffer().and_then(|i| i.parse::().ok()) { + self.focus_previous_by_relative_index(index) + } else { + Ok(self) + } } - pub fn focus_previous(self) -> Result { - let len = self.directory_buffer.total; - let step = self.number_input.max(1); - let focus = if len == 0 { - None + fn focus_next(mut self) -> Result { + if let Some(dir) = self.directory_buffer_mut() { + dir.focus = (dir.focus + 1).min(dir.total.max(1) - 1); + self.msg_out.push_back(MsgOut::Refresh); + }; + Ok(self) + } + + fn focus_next_by_relative_index(mut self, index: usize) -> Result { + if let Some(dir) = self.directory_buffer_mut() { + dir.focus = (dir.focus + index).min(dir.total.max(1) - 1); + self.msg_out.push_back(MsgOut::Refresh); + }; + Ok(self) + } + + fn focus_next_by_relative_index_from_input(self) -> Result { + if let Some(index) = self.input_buffer().and_then(|i| i.parse::().ok()) { + self.focus_next_by_relative_index(index) } else { - self.directory_buffer - .focus - .map(|f| Some(step.max(f) - step)) - .unwrap_or(Some(step.max(len) - step)) + Ok(self) + } + } + + fn change_directory(mut self, dir: &String) -> Result { + if PathBuf::from(dir).is_dir() { + self.pwd = dir.to_owned(); + self.msg_out.push_back(MsgOut::Refresh); }; + Ok(self) + } - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode, - self.show_hidden, - focus, - 0, - ) + fn enter(self) -> Result { + self.focused_node() + .map(|n| n.absolute_path.clone()) + .map(|p| self.clone().change_directory(&p)) + .unwrap_or(Ok(self)) } - pub fn focus_path(self, path: &PathBuf) -> Result { - expand_tilde(path) - .unwrap_or(path.into()) + fn back(self) -> Result { + PathBuf::from(self.pwd()) .parent() - .map(|pwd| { - let (_, items) = DirectoryBuffer::explore(&pwd.into(), self.show_hidden)?; - let focus = items - .enumerate() - .find_map(|(i, (p, _))| { - if p.as_path() == path.as_path() { - Some(i) - } else { - None - } - }) - .or(Some(0)); - - Self::new( - &self.config, - &pwd.into(), - &self.saved_buffers, - &self.selected_paths, - self.mode.clone(), - self.show_hidden, - focus, - 0, - ) + .map(|p| { + self.clone() + .change_directory(&p.to_string_lossy().to_string()) }) - .unwrap_or_else(|| Ok(self.to_owned())) - } - - pub fn focus_by_index(self, idx: &usize) -> Result { - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode.clone(), - self.show_hidden, - Some(idx.clone()), - 0, - ) - } - - pub fn focus_by_buffer_relative_index(self, idx: &usize) -> Result { - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode.clone(), - self.show_hidden, - Some(DirectoryBuffer::relative_focus(idx.clone())), - 0, - ) - } - - pub fn focus_by_focus_relative_index(self, idx: &isize) -> Result { - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode.clone(), - self.show_hidden, - self.directory_buffer - .focus - .map(|f| ((f as isize) + idx).min(0) as usize), // TODO: make it safer - 0, - ) - } - - pub fn enter(mut self) -> Result { - let mut step = self.number_input.max(1); - while step > 0 { - let pwd = self - .directory_buffer - .focused() - .map(|(p, _)| p) - .map(|p| { - if p.is_dir() { - p - } else { - self.directory_buffer.pwd.clone() - } - }) - .unwrap_or_else(|| self.directory_buffer.pwd.clone()); - - let focus = self.saved_buffers.get(&pwd).unwrap_or(&None); - - self = Self::new( - &self.config, - &pwd, - &self.saved_buffers, - &self.selected_paths, - self.mode, - self.show_hidden, - focus.clone(), - 0, - )?; - step -= 1; - } + .unwrap_or(Ok(self)) + } + + fn buffer_string(mut self, input: &String) -> Result { + if let Some(buf) = self.input_buffer.as_mut() { + buf.extend(input.chars()); + } else { + self.input_buffer = Some(input.to_owned()); + }; + self.msg_out.push_back(MsgOut::Refresh); Ok(self) } - pub fn back(mut self) -> Result { - let mut step = self.number_input.max(1); - while step > 0 { - let pwd = self.directory_buffer.pwd.clone(); - self = self.focus_path(&pwd)?; - step -= 1; + fn buffer_string_from_key(self, key: Option) -> Result { + if let Some(c) = key.and_then(|k| k.to_char()) { + self.buffer_string(&c.to_string()) + } else { + Ok(self) } + } + + fn reset_input_buffer(mut self) -> Result { + self.input_buffer = None; + self.msg_out.push_back(MsgOut::Refresh); Ok(self) } - pub fn select(self) -> Result { - let selected_paths = self - .directory_buffer - .focused() - .map(|(p, _)| { - let mut selected_paths = self.selected_paths.clone(); - selected_paths.insert(p); - selected_paths - }) - .unwrap_or_else(|| self.selected_paths.clone()); - - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &selected_paths, - Mode::Select, - self.show_hidden, - self.directory_buffer.focus, - 0, - ) - } - - pub fn toggle_selection(self) -> Result { - let selected_paths = self - .directory_buffer - .focused() - .map(|(p, _)| { - let mut selected_paths = self.selected_paths.clone(); - if selected_paths.contains(&p) { - selected_paths.remove(&p); - } else { - selected_paths.insert(p); - } - selected_paths - }) - .unwrap_or_else(|| self.selected_paths.clone()); + fn focus_by_index(mut self, index: usize) -> Result { + if let Some(dir) = self.directory_buffer_mut() { + dir.focus = index.min(dir.total.max(1) - 1); + self.msg_out.push_back(MsgOut::Refresh); + }; + Ok(self) + } - let mode = if selected_paths.len() == 0 { - Mode::Explore + fn focus_by_index_from_input(self) -> Result { + if let Some(index) = self.input_buffer().and_then(|i| i.parse::().ok()) { + self.focus_by_index(index) } else { - Mode::Select + Ok(self) + } + } + + fn focus_by_file_name(mut self, name: &String) -> Result { + if let Some(dir_buf) = self.directory_buffer_mut() { + if let Some(focus) = dir_buf + .clone() + .nodes + .iter() + .enumerate() + .find(|(_, n)| &n.relative_path == name) + .map(|(i, _)| i) + { + dir_buf.focus = focus; + self.msg_out.push_back(MsgOut::Refresh); + }; }; + Ok(self) + } - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &selected_paths, - mode, - self.show_hidden, - self.directory_buffer.focus, - 0, - ) - } - - pub fn enter_submode(self, submode: &String) -> Result { - let mode = match self.mode { - Mode::Explore => Mode::ExploreSubmode(submode.clone()), - Mode::ExploreSubmode(_) => Mode::ExploreSubmode(submode.clone()), - Mode::Select => Mode::SelectSubmode(submode.clone()), - Mode::SelectSubmode(_) => Mode::SelectSubmode(submode.clone()), + fn focus_path(self, path: &String) -> Result { + let pathbuf = PathBuf::from(path); + if let Some(parent) = pathbuf.parent() { + if let Some(filename) = pathbuf.file_name() { + self.change_directory(&parent.to_string_lossy().to_string())? + .focus_by_file_name(&filename.to_string_lossy().to_string()) + } else { + Ok(self) + } + } else { + Ok(self) + } + } + + fn switch_mode(mut self, mode: &String) -> Result { + if let Some(mode) = self.config.modes.get(mode) { + self.mode = mode.to_owned(); + self.msg_out.push_back(MsgOut::Refresh); }; + Ok(self) + } - Self::new( - &self.config, - &self.directory_buffer.pwd, - &self.saved_buffers, - &self.selected_paths, - mode, - self.show_hidden, - self.directory_buffer.focus, - self.number_input, - ) - } - - pub fn print_focused(self) -> Result { - let mut app = self; - app.task = app - .directory_buffer - .focused() - .and_then(|(p, _)| p.to_str().map(|s| Task::PrintAndQuit(s.to_string()))) - .unwrap_or_default(); - Ok(app) + fn call(mut self, command: Command) -> Result { + self.msg_out.push_back(MsgOut::Call(command)); + Ok(self) } - pub fn print_pwd(self) -> Result { - let mut app = self; - app.task = app - .directory_buffer - .pwd - .to_str() - .map(|s| Task::PrintAndQuit(s.to_string())) - .unwrap_or_default(); - Ok(app) + fn add_directory(mut self, parent: String, dir: DirectoryBuffer) -> Result { + // TODO: Optimize + self.directory_buffers.insert(parent, dir); + self.msg_out.push_back(MsgOut::Refresh); + Ok(self) } - pub fn print_selected(self) -> Result { - let mut app = self; - app.task = Task::PrintAndQuit( - app.selected_paths - .clone() - .iter() - .filter_map(|p| p.to_str()) - .map(|s| s.to_string()) - .collect::>() - .join("\n"), - ); - Ok(app) + fn toggle_selection(mut self) -> Result { + self.clone() + .focused_node() + .map(|n| { + if self.selected().contains(n) { + self.selected = self + .clone() + .selected + .into_iter() + .filter(|s| s != n) + .collect(); + Ok(self.clone()) + } else { + self.selected.push(n.to_owned()); + Ok(self.clone()) + } + }) + .unwrap_or(Ok(self)) } - pub fn print_app_state(self) -> Result { - let state = serde_yaml::to_string(&self)?; - let mut app = self; - app.task = Task::PrintAndQuit(state); - Ok(app) + fn print_result_and_quit(mut self) -> Result { + self.msg_out.push_back(MsgOut::PrintResultAndQuit); + Ok(self) } - pub fn quit(mut self) -> Result { - self.task = Task::Quit; + fn print_app_state_and_quit(mut self) -> Result { + self.msg_out.push_back(MsgOut::PrintAppStateAndQuit); Ok(self) } - pub fn terminate(self) -> Result { - Err(Error::Terminated) + fn debug(mut self, path: &String) -> Result { + self.msg_out.push_back(MsgOut::Debug(path.to_owned())); + Ok(self) } - pub fn actions_from_key(&self, key: &Key) -> Option> { - match key { - Key::Number(n) => Some(vec![Action::NumberInput(*n)]), - key => self.parsed_key_bindings.get(key).map(|(_, a)| a.to_owned()), - } + fn directory_buffer_mut(&mut self) -> Option<&mut DirectoryBuffer> { + self.directory_buffers.get_mut(&self.pwd) } - pub fn handle(self, action: &Action) -> Result { - match action { - Action::NumberInput(n) => self.number_input(*n), - Action::ToggleShowHidden => self.toggle_hidden(), - Action::Back => self.back(), - Action::Enter => self.enter(), - Action::FocusPrevious => self.focus_previous(), - Action::FocusNext => self.focus_next(), - Action::FocusFirst => self.focus_first(), - Action::FocusLast => self.focus_last(), - Action::FocusPathByIndex(i) => self.focus_by_index(i), - Action::FocusPathByBufferRelativeIndex(i) => self.focus_by_buffer_relative_index(i), - Action::FocusPathByFocusRelativeIndex(i) => self.focus_by_focus_relative_index(i), - Action::FocusPath(p) => self.focus_path(&p.into()), - Action::ChangeDirectory(d) => self.change_directory(d.into()), - Action::Call(c) => self.call(c), - Action::EnterSubmode(s) => self.enter_submode(s), - Action::ExitSubmode => self.exit_submode(), - Action::Select => self.select(), - Action::ToggleSelection => self.toggle_selection(), - Action::PrintFocused => self.print_focused(), - Action::PrintSelected => self.print_selected(), - Action::PrintAppState => self.print_app_state(), - Action::Quit => self.quit(), - Action::Terminate => self.terminate(), - } + /// Get a reference to the app's pwd. + pub fn pwd(&self) -> &String { + &self.pwd + } + + /// Get a reference to the app's current directory buffer. + pub fn directory_buffer(&self) -> Option<&DirectoryBuffer> { + self.directory_buffers.get(&self.pwd) + } + + /// Get a reference to the app's config. + pub fn config(&self) -> &Config { + &self.config + } + + /// Get a reference to the app's selected. + pub fn selected(&self) -> &Vec { + &self.selected + } + + pub fn pop_msg_out(&mut self) -> Option { + self.msg_out.pop_front() + } + + /// Get a reference to the app's mode. + pub fn mode(&self) -> &Mode { + &self.mode + } + + /// Get a reference to the app's directory buffers. + pub fn directory_buffers(&self) -> &HashMap { + &self.directory_buffers } -} -pub fn create() -> Result { - let config_dir = dirs::config_dir() - .unwrap_or(PathBuf::from(".")) - .join("xplr"); - - let config_file = config_dir.join("config.yml"); - - let config: Config = if config_file.exists() { - serde_yaml::from_reader(BufReader::new(&File::open(&config_file)?))? - } else { - Config::default() - }; - - if !config.version.eq(VERSION) { - return Err(Error::IncompatibleVersion(format!( - "Config file {} is outdated", - config_file.to_string_lossy() - ))); - } - - let root = Path::new("/"); - let pwd = PathBuf::from(std::env::args().skip(1).next().unwrap_or("./".into())) - .canonicalize() - .unwrap_or(root.into()); - - let (pwd, file_to_focus) = if pwd.is_file() { - (pwd.parent().unwrap_or(root).into(), Some(pwd)) - } else { - (pwd, None) - }; - - let app = App::new( - &config, - &pwd, - &Default::default(), - &Default::default(), - Mode::Explore, - config.general.show_hidden, - None, - 0, - )?; - - if let Some(file) = file_to_focus { - app.focus_path(&file) - } else { - Ok(app) + /// Get a reference to the app's input buffer. + pub fn input_buffer(&self) -> Option<&String> { + self.input_buffer.as_ref() } } diff --git a/src/config.rs b/src/config.rs index 91a2acf..4fc98de 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,349 +1,23 @@ +use crate::app::ExternalMsg; use crate::app::VERSION; -use crate::input::Key; +use dirs; use serde::{Deserialize, Serialize}; +use serde_yaml; +use std::collections::BTreeMap; use std::collections::HashMap; -use std::fmt; +use std::fs; use tui::layout::Constraint as TUIConstraint; use tui::style::Color; use tui::style::Modifier; use tui::style::Style; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum Mode { - Explore, - ExploreSubmode(String), - Select, - SelectSubmode(String), -} - -impl fmt::Display for Mode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Explore => { - write!(f, "explore") - } - - Self::Select => { - write!(f, "select") - } - - Self::ExploreSubmode(s) => { - write!(f, "explore({})", &s) - } - - Self::SelectSubmode(s) => { - write!(f, "select({})", &s) - } - } - } -} - -impl Mode { - pub fn does_support(self, action: &Action) -> bool { - match (self, action) { - // Special - (_, Action::Terminate) => true, - (_, Action::NumberInput(_)) => true, - - // Explore mode - (Self::Explore, Action::Back) => true, - (Self::Explore, Action::Call(_)) => true, - (Self::Explore, Action::ChangeDirectory(_)) => true, - (Self::Explore, Action::Enter) => true, - (Self::Explore, Action::EnterSubmode(_)) => true, - (Self::Explore, Action::ExitSubmode) => false, - (Self::Explore, Action::FocusFirst) => true, - (Self::Explore, Action::FocusLast) => true, - (Self::Explore, Action::FocusNext) => true, - (Self::Explore, Action::FocusPath(_)) => true, - (Self::Explore, Action::FocusPathByBufferRelativeIndex(_)) => true, - (Self::Explore, Action::FocusPathByFocusRelativeIndex(_)) => true, - (Self::Explore, Action::FocusPathByIndex(_)) => true, - (Self::Explore, Action::FocusPrevious) => true, - (Self::Explore, Action::PrintAppState) => true, - (Self::Explore, Action::PrintFocused) => true, - (Self::Explore, Action::PrintSelected) => false, - (Self::Explore, Action::Quit) => true, - (Self::Explore, Action::Select) => true, - (Self::Explore, Action::ToggleSelection) => false, - (Self::Explore, Action::ToggleShowHidden) => true, - - // Explore submode - (Self::ExploreSubmode(_), Action::ExitSubmode) => true, - (Self::ExploreSubmode(_), a) => Self::does_support(Self::Explore, a), - - // Select mode - (Self::Select, Action::Back) => true, - (Self::Select, Action::Call(_)) => true, - (Self::Select, Action::ChangeDirectory(_)) => true, - (Self::Select, Action::Enter) => true, - (Self::Select, Action::EnterSubmode(_)) => true, - (Self::Select, Action::ExitSubmode) => true, - (Self::Select, Action::FocusFirst) => true, - (Self::Select, Action::FocusLast) => true, - (Self::Select, Action::FocusNext) => true, - (Self::Select, Action::FocusPath(_)) => true, - (Self::Select, Action::FocusPathByBufferRelativeIndex(_)) => true, - (Self::Select, Action::FocusPathByFocusRelativeIndex(_)) => true, - (Self::Select, Action::FocusPathByIndex(_)) => true, - (Self::Select, Action::FocusPrevious) => true, - (Self::Select, Action::PrintAppState) => true, - (Self::Select, Action::PrintFocused) => false, - (Self::Select, Action::PrintSelected) => true, - (Self::Select, Action::Quit) => true, - (Self::Select, Action::Select) => false, - (Self::Select, Action::ToggleSelection) => true, - (Self::Select, Action::ToggleShowHidden) => true, - - // Select submode - (Self::SelectSubmode(_), Action::ExitSubmode) => true, - (Self::SelectSubmode(_), a) => Self::does_support(Self::Select, a), - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum Format { - Line, - Pretty, - Yaml, - YamlPretty, - Template(String), -} - -impl Default for Format { - fn default() -> Self { - Self::Line - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct CommandConfig { - pub command: String, - +pub struct Action { #[serde(default)] - pub args: Vec, -} + pub help: Option, -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum Action { - NumberInput(u8), - ToggleShowHidden, - Back, - Enter, - FocusPrevious, - FocusNext, - FocusFirst, - FocusLast, - FocusPathByIndex(usize), - FocusPathByBufferRelativeIndex(usize), - FocusPathByFocusRelativeIndex(isize), - FocusPath(String), - ChangeDirectory(String), - Call(CommandConfig), - EnterSubmode(String), - ExitSubmode, - Select, - // Unselect, - // SelectAll, - // SelectAllRecursive, - // UnselectAll, - // UnSelectAllRecursive, - ToggleSelection, - // ClearSelectedPaths, - - // Quit options - PrintFocused, - PrintSelected, - PrintAppState, - Quit, - Terminate, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ActionMenu { #[serde(default)] - pub help: String, - pub actions: Vec, -} - -pub type SubmodeActionMenu = HashMap; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct KeyBindings { - pub global: HashMap, - #[serde(default)] - pub explore_mode: HashMap, - #[serde(default)] - pub explore_submodes: HashMap, - #[serde(default)] - pub select_mode: HashMap, - #[serde(default)] - pub select_submodes: HashMap, -} - -impl KeyBindings { - pub fn filtered(&self, mode: &Mode) -> HashMap)> { - let mode_bindings: Option> = match mode { - Mode::Explore => Some(self.explore_mode.clone()), - Mode::ExploreSubmode(s) => self.explore_submodes.clone().get(s).map(|a| a.to_owned()), - Mode::Select => Some(self.select_mode.clone()), - Mode::SelectSubmode(s) => self.select_submodes.clone().get(s).map(|a| a.to_owned()), - }; - - let kb = self.global.clone().into_iter(); - - let kb: HashMap = if let Some(modal_kb) = mode_bindings { - kb.chain(modal_kb.into_iter()).collect() - } else { - kb.collect() - }; - - kb.into_iter() - .map(|(k, am)| { - ( - k.clone(), - ( - am.help, - am.actions - .into_iter() - .filter(|a| mode.clone().does_support(a)) - .collect::>(), - ), - ) - }) - .filter(|(_, (_, actions))| !actions.is_empty()) - .collect() - } -} - -impl Default for KeyBindings { - fn default() -> Self { - let yaml = r###" - global: - ctrl-c: - help: quit - actions: - - Terminate - q: - help: quit - actions: - - Quit - pound: - help: print debug info - actions: - - PrintAppState - up: - help: up - actions: - - FocusPrevious - down: - help: down - actions: - - FocusNext - shift-g: - help: bottom - actions: - - FocusLast - tilde: - help: go home - actions: - - ChangeDirectory: "~" - dot: - help: toggle show hidden - actions: - - ToggleShowHidden - right: - help: enter - actions: - - Enter - left: - help: back - actions: - - Back - o: - help: open - actions: - - Call: - command: bash - args: - - "-c" - - FILE="{{relativePath}}" && xdg-open "${FILE:?}" &> /dev/null - e: - help: edit - actions: - - Call: - command: bash - args: - - -c - - FILE="{{relativePath}}" && "${EDITOR:-vim}" "${FILE:?}" - forward-slash: - help: search - actions: - - Call: - command: bash - args: - - "-c" - - FILE="$(ls -a | fzf)" && xplr "${FILE:?}" || xplr "${PWD:?}" - - Quit - - s: - help: shell - actions: - - Call: - command: bash - - esc: - help: quit - actions: - - Quit - - explore_mode: - g: - help: go to - actions: - - EnterSubmode: GoTo - return: - help: done - actions: - - PrintFocused - space: - help: select - actions: - - Select - - FocusNext - explore_submodes: - GoTo: - g: - help: top - actions: - - FocusFirst - - ExitSubmode - select_mode: - space: - help: toggle selection - actions: - - ToggleSelection - - FocusNext - g: - help: go to - actions: - - EnterSubmode: GoTo - return: - help: done - actions: - - PrintSelected - - Quit - select_submodes: - GoTo: - g: - help: top - actions: - - FocusFirst - - ExitSubmode - "###; - serde_yaml::from_str(yaml).unwrap() - } + pub messages: Vec, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -363,6 +37,8 @@ pub struct FileTypesConfig { #[serde(default)] pub symlink: FileTypeConfig, #[serde(default)] + pub mime_essence: HashMap, + #[serde(default)] pub extension: HashMap, #[serde(default)] pub special: HashMap, @@ -390,6 +66,7 @@ impl Default for FileTypesConfig { .fg(Color::Cyan), }, + mime_essence: Default::default(), extension: Default::default(), special: Default::default(), } @@ -472,12 +149,16 @@ pub struct TableConfig { pub struct GeneralConfig { #[serde(default)] pub show_hidden: bool, + #[serde(default)] pub table: TableConfig, + #[serde(default)] pub normal_ui: UIConfig, + #[serde(default)] pub focused_ui: UIConfig, + #[serde(default)] pub selected_ui: UIConfig, } @@ -489,9 +170,9 @@ impl Default for GeneralConfig { table: header: cols: - - format: "│ path" - - format: "is symlink" - - format: "index" + - format: "│ path" + - format: "type" + - format: " index" height: 1 style: add_modifier: @@ -500,9 +181,9 @@ impl Default for GeneralConfig { bits: 0 row: cols: - - format: "{{tree}}{{prefix}}{{icon}} {{relativePath}}{{#if isDir}}/{{/if}}{{suffix}}" - - format: "{{isSymlink}}" - - format: "{{focusRelativeIndex}}/{{bufferRelativeIndex}}/{{index}}/{{total}}" + - format: "{{{tree}}}{{{prefix}}}{{{icon}}} {{{relativePath}}}{{#if isDir}}/{{/if}}{{{suffix}}}" + - format: "{{{mimeEssence}}}" + - format: "{{#if isBeforeFocus}}-{{else}} {{/if}}{{{relativeIndex}}}/{{{index}}}/{{{total}}}" col_spacing: 3 col_widths: @@ -543,6 +224,203 @@ impl Default for GeneralConfig { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PipesConfig { + pub msg_in: String, + pub focus_out: String, + pub selected_out: String, + pub mode_out: String, +} + +impl Default for PipesConfig { + fn default() -> Self { + let pipesdir = dirs::runtime_dir() + .unwrap_or("/tmp".into()) + .join("xplr") + .join("session") + .join(std::process::id().to_string()) + .join("pipe"); + + fs::create_dir_all(&pipesdir).unwrap(); + + let msg_in = pipesdir.join("msg_in").to_string_lossy().to_string(); + + let focus_out = pipesdir.join("focus_out").to_string_lossy().to_string(); + + let selected_out = pipesdir.join("selected_out").to_string_lossy().to_string(); + + let mode_out = pipesdir.join("mode_out").to_string_lossy().to_string(); + + fs::write(&msg_in, "").unwrap(); + fs::write(&focus_out, "").unwrap(); + fs::write(&selected_out, "").unwrap(); + fs::write(&mode_out, "").unwrap(); + + Self { + msg_in, + focus_out, + selected_out, + mode_out, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeyBindings { + #[serde(default)] + pub on_key: BTreeMap, + + #[serde(default)] + pub on_alphabet: Option, + + #[serde(default)] + pub on_number: Option, + + #[serde(default)] + pub on_special_character: Option, + + #[serde(default)] + pub default: Option, +} + +impl Default for KeyBindings { + fn default() -> Self { + let on_key: BTreeMap = serde_yaml::from_str( + r###" + up: + help: up + messages: + - FocusPrevious + + down: + help: down + messages: + - FocusNext + + right: + help: enter + messages: + - Enter + + left: + help: back + messages: + - Back + + g: + help: go to + messages: + - SwitchMode: goto + + G: + help: bottom + messages: + - FocusLast + + s: + help: shell + messages: + - Call: + command: bash + args: [] + + /: + help: find + messages: + - Call: + command: bash + args: + - "-c" + - | + PTH="$(echo -e ${XPLR_DIRECTORY_NODES:?} | sed -s 's/,/\n/g' | fzf)" + if [ -d "$PTH" ]; then + echo "ChangeDirectory: ${PTH:?}" >> "${XPLR_PIPE_MSG_IN:?}" + elif [ -f "$PTH" ]; then + echo "FocusPath: ${PTH:?}" >> "${XPLR_PIPE_MSG_IN:?}" + fi + + space: + help: toggle selection + messages: + - ToggleSelection + - FocusNext + + d: + help: debug + messages: + - Debug: /tmp/xplr.yml + + enter: + help: quit with result + messages: + - PrintResultAndQuit + + "#": + help: quit with debug + messages: + - PrintAppStateAndQuit + + esc: + help: cancel & quit + messages: + - Terminate + + q: + help: cancel & quit + messages: + - Terminate + "###, + ) + .unwrap(); + + let default = Some(Action { + help: None, + messages: vec![ExternalMsg::SwitchMode("default".into())], + }); + + let on_number = Some(Action { + help: Some("input".to_string()), + messages: vec![ + ExternalMsg::BufferStringFromKey, + ExternalMsg::SwitchMode("number".into()), + ], + }); + + Self { + on_key, + on_alphabet: Default::default(), + on_number, + on_special_character: Default::default(), + default, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Mode { + pub name: String, + + #[serde(default)] + pub help: Option, + + #[serde(default)] + pub extra_help: Option, + + #[serde(default)] + pub key_bindings: KeyBindings, +} + +impl Default for Mode { + fn default() -> Self { + Self { + name: "default".into(), + help: Default::default(), + extra_help: Default::default(), + key_bindings: Default::default(), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub version: String, @@ -554,16 +432,78 @@ pub struct Config { pub filetypes: FileTypesConfig, #[serde(default)] - pub key_bindings: KeyBindings, + pub pipes: PipesConfig, + + #[serde(default)] + pub modes: HashMap, } impl Default for Config { fn default() -> Self { + let goto_mode: Mode = serde_yaml::from_str( + r###" + name: go to + key_bindings: + on_key: + g: + help: top + messages: + - FocusFirst + - SwitchMode: default + "###, + ) + .unwrap(); + + let number_mode: Mode = serde_yaml::from_str( + r###" + name: number + key_bindings: + on_key: + up: + help: go up + messages: + - FocusPreviousByRelativeIndexFromInput + - ResetInputBuffer + - SwitchMode: default + + down: + help: go down + messages: + - FocusNextByRelativeIndexFromInput + - ResetInputBuffer + - SwitchMode: default + + enter: + help: go down + messages: + - FocusByIndexFromInput + - ResetInputBuffer + - SwitchMode: default + + on_number: + help: input + messages: + - BufferStringFromKey + + default: + messages: + - ResetInputBuffer + - SwitchMode: default + "###, + ) + .unwrap(); + + let mut modes: HashMap = Default::default(); + modes.insert("default".into(), Mode::default()); + modes.insert("goto".into(), goto_mode); + modes.insert("number".into(), number_mode); + Self { version: VERSION.into(), general: Default::default(), filetypes: Default::default(), - key_bindings: Default::default(), + pipes: Default::default(), + modes, } } } diff --git a/src/explorer.rs b/src/explorer.rs new file mode 100644 index 0000000..f7b8882 --- /dev/null +++ b/src/explorer.rs @@ -0,0 +1,59 @@ +use crate::app::DirectoryBuffer; +use crate::app::Node; +use crate::app::Task; +use crate::app::{InternalMsg, MsgIn}; +use std::fs; +use std::path::PathBuf; +use std::sync::mpsc::Sender; +use std::thread; + +pub fn explore(parent: String, focused_path: Option, tx: Sender) { + let path = PathBuf::from(&parent); + let path_cloned = path.clone(); + let tx_cloned = tx.clone(); + + thread::spawn(move || { + let nodes: Vec = fs::read_dir(&path) + .unwrap() + .filter_map(|d| { + d.ok().map(|e| { + e.path() + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default() + }) + }) + .map(|name| Node::new(parent.clone(), name)) + .collect(); + + let focus_index = if let Some(focus) = focused_path { + nodes + .iter() + .enumerate() + .find(|(_, n)| n.relative_path == focus) + .map(|(i, _)| i) + .unwrap_or(0) + } else { + 0 + }; + + let dir = DirectoryBuffer::new(parent.clone(), nodes, focus_index); + + tx.send(Task::new( + 1, + MsgIn::Internal(InternalMsg::AddDirectory(parent, dir)), + None, + )) + .unwrap(); + }); + + if let Some(grand_parent) = path_cloned.parent() { + explore( + grand_parent.to_string_lossy().to_string(), + path_cloned + .file_name() + .map(|f| f.to_string_lossy().to_string()), + tx_cloned, + ); + } +} diff --git a/src/input.rs b/src/input.rs index d222ee8..3aeb102 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,10 +1,45 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use serde::{Deserialize, Serialize}; -use termion::event::Key as TermionKey; +use serde_yaml; +use std::cmp::Ordering; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Key { - Number(u8), + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + + Num0, + Num1, + Num2, + Num3, + Num4, + Num5, + Num6, + Num7, + Num8, + Num9, + + AltNum0, + AltNum1, + AltNum2, + AltNum3, + AltNum4, + AltNum5, + AltNum6, + AltNum7, + AltNum8, + AltNum9, Backspace, Left, @@ -18,7 +53,7 @@ pub enum Key { BackTab, Delete, Insert, - Return, + Enter, Space, Tab, Esc, @@ -131,297 +166,401 @@ pub enum Key { ShiftY, ShiftZ, - CtrlShiftA, - CtrlShiftB, - CtrlShiftC, - CtrlShiftD, - CtrlShiftE, - CtrlShiftF, - CtrlShiftG, - CtrlShiftH, - CtrlShiftI, - CtrlShiftJ, - CtrlShiftK, - CtrlShiftL, - CtrlShiftM, - CtrlShiftN, - CtrlShiftO, - CtrlShiftP, - CtrlShiftQ, - CtrlShiftR, - CtrlShiftS, - CtrlShiftT, - CtrlShiftU, - CtrlShiftV, - CtrlShiftW, - CtrlShiftX, - CtrlShiftY, - CtrlShiftZ, - - AltShiftA, - AltShiftB, - AltShiftC, - AltShiftD, - AltShiftE, - AltShiftF, - AltShiftG, - AltShiftH, - AltShiftI, - AltShiftJ, - AltShiftK, - AltShiftL, - AltShiftM, - AltShiftN, - AltShiftO, - AltShiftP, - AltShiftQ, - AltShiftR, - AltShiftS, - AltShiftT, - AltShiftU, - AltShiftV, - AltShiftW, - AltShiftX, - AltShiftY, - AltShiftZ, - - Plus, - Minus, - Backtick, - Tilde, - Underscore, - Equals, - Semicolon, - Colon, - SingleQuote, - DoubleQuote, - ForwardSlash, - BackSlash, - Dot, - Comma, - QuestionMark, - Pound, + Special(char), NotSupported, } impl std::fmt::Display for Key { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) + let key_str = self.to_char().map(|c| c.to_string()).unwrap_or_else(|| { + serde_yaml::to_value(self) + .ok() + .and_then(|v| v.as_str().map(|v| v.to_string())) + .unwrap_or_default() + }); + + write!(f, "{}", key_str) } } impl Key { - pub fn from_termion_event(key: TermionKey) -> Self { - match key { - TermionKey::Char('0') => Key::Number(0), - TermionKey::Char('1') => Key::Number(1), - TermionKey::Char('2') => Key::Number(2), - TermionKey::Char('3') => Key::Number(3), - TermionKey::Char('4') => Key::Number(4), - TermionKey::Char('5') => Key::Number(5), - TermionKey::Char('6') => Key::Number(6), - TermionKey::Char('7') => Key::Number(7), - TermionKey::Char('8') => Key::Number(8), - TermionKey::Char('9') => Key::Number(9), - - TermionKey::Backspace => Key::Backspace, - TermionKey::Left => Key::Left, - TermionKey::Right => Key::Right, - TermionKey::Up => Key::Up, - TermionKey::Down => Key::Down, - TermionKey::Home => Key::Home, - TermionKey::End => Key::End, - TermionKey::PageUp => Key::PageUp, - TermionKey::PageDown => Key::PageDown, - TermionKey::BackTab => Key::BackTab, - TermionKey::Delete => Key::Delete, - TermionKey::Insert => Key::Insert, - TermionKey::Char('\n') => Key::Return, - TermionKey::Char(' ') => Key::Space, - TermionKey::Char('\t') => Key::Tab, - TermionKey::Esc => Key::Esc, - - TermionKey::Char('a') => Key::A, - TermionKey::Char('b') => Key::B, - TermionKey::Char('c') => Key::C, - TermionKey::Char('d') => Key::D, - TermionKey::Char('e') => Key::E, - TermionKey::Char('f') => Key::F, - TermionKey::Char('g') => Key::G, - TermionKey::Char('h') => Key::H, - TermionKey::Char('i') => Key::I, - TermionKey::Char('j') => Key::J, - TermionKey::Char('k') => Key::K, - TermionKey::Char('l') => Key::L, - TermionKey::Char('m') => Key::M, - TermionKey::Char('n') => Key::N, - TermionKey::Char('o') => Key::O, - TermionKey::Char('p') => Key::P, - TermionKey::Char('q') => Key::Q, - TermionKey::Char('r') => Key::R, - TermionKey::Char('s') => Key::S, - TermionKey::Char('t') => Key::T, - TermionKey::Char('u') => Key::U, - TermionKey::Char('v') => Key::V, - TermionKey::Char('w') => Key::W, - TermionKey::Char('x') => Key::X, - TermionKey::Char('y') => Key::Y, - TermionKey::Char('z') => Key::Z, - - TermionKey::Ctrl('a') => Key::CtrlA, - TermionKey::Ctrl('b') => Key::CtrlB, - TermionKey::Ctrl('c') => Key::CtrlC, - TermionKey::Ctrl('d') => Key::CtrlD, - TermionKey::Ctrl('e') => Key::CtrlE, - TermionKey::Ctrl('f') => Key::CtrlF, - TermionKey::Ctrl('g') => Key::CtrlG, - TermionKey::Ctrl('h') => Key::CtrlH, - TermionKey::Ctrl('i') => Key::CtrlI, - TermionKey::Ctrl('j') => Key::CtrlJ, - TermionKey::Ctrl('k') => Key::CtrlK, - TermionKey::Ctrl('l') => Key::CtrlL, - TermionKey::Ctrl('m') => Key::CtrlM, - TermionKey::Ctrl('n') => Key::CtrlN, - TermionKey::Ctrl('o') => Key::CtrlO, - TermionKey::Ctrl('p') => Key::CtrlP, - TermionKey::Ctrl('q') => Key::CtrlQ, - TermionKey::Ctrl('r') => Key::CtrlR, - TermionKey::Ctrl('s') => Key::CtrlS, - TermionKey::Ctrl('t') => Key::CtrlT, - TermionKey::Ctrl('u') => Key::CtrlU, - TermionKey::Ctrl('v') => Key::CtrlV, - TermionKey::Ctrl('w') => Key::CtrlW, - TermionKey::Ctrl('x') => Key::CtrlX, - TermionKey::Ctrl('y') => Key::CtrlY, - TermionKey::Ctrl('z') => Key::CtrlZ, - - TermionKey::Alt('a') => Key::AltA, - TermionKey::Alt('b') => Key::AltB, - TermionKey::Alt('c') => Key::AltC, - TermionKey::Alt('d') => Key::AltD, - TermionKey::Alt('e') => Key::AltE, - TermionKey::Alt('f') => Key::AltF, - TermionKey::Alt('g') => Key::AltG, - TermionKey::Alt('h') => Key::AltH, - TermionKey::Alt('i') => Key::AltI, - TermionKey::Alt('j') => Key::AltJ, - TermionKey::Alt('k') => Key::AltK, - TermionKey::Alt('l') => Key::AltL, - TermionKey::Alt('m') => Key::AltM, - TermionKey::Alt('n') => Key::AltN, - TermionKey::Alt('o') => Key::AltO, - TermionKey::Alt('p') => Key::AltP, - TermionKey::Alt('q') => Key::AltQ, - TermionKey::Alt('r') => Key::AltR, - TermionKey::Alt('s') => Key::AltS, - TermionKey::Alt('t') => Key::AltT, - TermionKey::Alt('u') => Key::AltU, - TermionKey::Alt('v') => Key::AltV, - TermionKey::Alt('w') => Key::AltW, - TermionKey::Alt('x') => Key::AltX, - TermionKey::Alt('y') => Key::AltY, - TermionKey::Alt('z') => Key::AltZ, - - TermionKey::Char('A') => Key::ShiftA, - TermionKey::Char('B') => Key::ShiftB, - TermionKey::Char('C') => Key::ShiftC, - TermionKey::Char('D') => Key::ShiftD, - TermionKey::Char('E') => Key::ShiftE, - TermionKey::Char('F') => Key::ShiftF, - TermionKey::Char('G') => Key::ShiftG, - TermionKey::Char('H') => Key::ShiftH, - TermionKey::Char('I') => Key::ShiftI, - TermionKey::Char('J') => Key::ShiftJ, - TermionKey::Char('K') => Key::ShiftK, - TermionKey::Char('L') => Key::ShiftL, - TermionKey::Char('M') => Key::ShiftM, - TermionKey::Char('N') => Key::ShiftN, - TermionKey::Char('O') => Key::ShiftO, - TermionKey::Char('P') => Key::ShiftP, - TermionKey::Char('Q') => Key::ShiftQ, - TermionKey::Char('R') => Key::ShiftR, - TermionKey::Char('S') => Key::ShiftS, - TermionKey::Char('T') => Key::ShiftT, - TermionKey::Char('U') => Key::ShiftU, - TermionKey::Char('V') => Key::ShiftV, - TermionKey::Char('W') => Key::ShiftW, - TermionKey::Char('X') => Key::ShiftX, - TermionKey::Char('Y') => Key::ShiftY, - TermionKey::Char('Z') => Key::ShiftZ, - - TermionKey::Ctrl('A') => Key::CtrlShiftA, - TermionKey::Ctrl('B') => Key::CtrlShiftB, - TermionKey::Ctrl('C') => Key::CtrlShiftC, - TermionKey::Ctrl('D') => Key::CtrlShiftD, - TermionKey::Ctrl('E') => Key::CtrlShiftE, - TermionKey::Ctrl('F') => Key::CtrlShiftF, - TermionKey::Ctrl('G') => Key::CtrlShiftG, - TermionKey::Ctrl('H') => Key::CtrlShiftH, - TermionKey::Ctrl('I') => Key::CtrlShiftI, - TermionKey::Ctrl('J') => Key::CtrlShiftJ, - TermionKey::Ctrl('K') => Key::CtrlShiftK, - TermionKey::Ctrl('L') => Key::CtrlShiftL, - TermionKey::Ctrl('M') => Key::CtrlShiftM, - TermionKey::Ctrl('N') => Key::CtrlShiftN, - TermionKey::Ctrl('O') => Key::CtrlShiftO, - TermionKey::Ctrl('P') => Key::CtrlShiftP, - TermionKey::Ctrl('Q') => Key::CtrlShiftQ, - TermionKey::Ctrl('R') => Key::CtrlShiftR, - TermionKey::Ctrl('S') => Key::CtrlShiftS, - TermionKey::Ctrl('T') => Key::CtrlShiftT, - TermionKey::Ctrl('U') => Key::CtrlShiftU, - TermionKey::Ctrl('V') => Key::CtrlShiftV, - TermionKey::Ctrl('W') => Key::CtrlShiftW, - TermionKey::Ctrl('X') => Key::CtrlShiftX, - TermionKey::Ctrl('Y') => Key::CtrlShiftY, - TermionKey::Ctrl('Z') => Key::CtrlShiftZ, - - TermionKey::Alt('A') => Key::AltShiftA, - TermionKey::Alt('B') => Key::AltShiftB, - TermionKey::Alt('C') => Key::AltShiftC, - TermionKey::Alt('D') => Key::AltShiftD, - TermionKey::Alt('E') => Key::AltShiftE, - TermionKey::Alt('F') => Key::AltShiftF, - TermionKey::Alt('G') => Key::AltShiftG, - TermionKey::Alt('H') => Key::AltShiftH, - TermionKey::Alt('I') => Key::AltShiftI, - TermionKey::Alt('J') => Key::AltShiftJ, - TermionKey::Alt('K') => Key::AltShiftK, - TermionKey::Alt('L') => Key::AltShiftL, - TermionKey::Alt('M') => Key::AltShiftM, - TermionKey::Alt('N') => Key::AltShiftN, - TermionKey::Alt('O') => Key::AltShiftO, - TermionKey::Alt('P') => Key::AltShiftP, - TermionKey::Alt('Q') => Key::AltShiftQ, - TermionKey::Alt('R') => Key::AltShiftR, - TermionKey::Alt('S') => Key::AltShiftS, - TermionKey::Alt('T') => Key::AltShiftT, - TermionKey::Alt('U') => Key::AltShiftU, - TermionKey::Alt('V') => Key::AltShiftV, - TermionKey::Alt('W') => Key::AltShiftW, - TermionKey::Alt('X') => Key::AltShiftX, - TermionKey::Alt('Y') => Key::AltShiftY, - TermionKey::Alt('Z') => Key::AltShiftZ, - - TermionKey::Char('+') => Key::Plus, - TermionKey::Char('-') => Key::Minus, - TermionKey::Char('`') => Key::Backtick, - TermionKey::Char('~') => Key::Tilde, - TermionKey::Char('_') => Key::Underscore, - TermionKey::Char('=') => Key::Equals, - TermionKey::Char(';') => Key::Semicolon, - TermionKey::Char(':') => Key::Colon, - TermionKey::Char('\'') => Key::SingleQuote, - TermionKey::Char('"') => Key::DoubleQuote, - TermionKey::Char('/') => Key::ForwardSlash, - TermionKey::Char('\\') => Key::BackSlash, - TermionKey::Char('.') => Key::Dot, - TermionKey::Char(',') => Key::Comma, - TermionKey::Char('?') => Key::QuestionMark, - TermionKey::Char('#') => Key::Pound, - - _ => Key::NotSupported, + pub fn from_event(key: KeyEvent) -> Self { + match key.modifiers { + KeyModifiers::CONTROL => match key.code { + KeyCode::Char('a') => Key::CtrlA, + KeyCode::Char('b') => Key::CtrlB, + KeyCode::Char('c') => Key::CtrlC, + KeyCode::Char('d') => Key::CtrlD, + KeyCode::Char('e') => Key::CtrlE, + KeyCode::Char('f') => Key::CtrlF, + KeyCode::Char('g') => Key::CtrlG, + KeyCode::Char('h') => Key::CtrlH, + KeyCode::Char('i') => Key::CtrlI, + KeyCode::Char('j') => Key::CtrlJ, + KeyCode::Char('k') => Key::CtrlK, + KeyCode::Char('l') => Key::CtrlL, + KeyCode::Char('m') => Key::CtrlM, + KeyCode::Char('n') => Key::CtrlN, + KeyCode::Char('o') => Key::CtrlO, + KeyCode::Char('p') => Key::CtrlP, + KeyCode::Char('q') => Key::CtrlQ, + KeyCode::Char('r') => Key::CtrlR, + KeyCode::Char('s') => Key::CtrlS, + KeyCode::Char('t') => Key::CtrlT, + KeyCode::Char('u') => Key::CtrlU, + KeyCode::Char('v') => Key::CtrlV, + KeyCode::Char('w') => Key::CtrlW, + KeyCode::Char('x') => Key::CtrlX, + KeyCode::Char('y') => Key::CtrlY, + KeyCode::Char('z') => Key::CtrlZ, + + KeyCode::Char(c) => c.into(), + + _ => Key::NotSupported, + }, + + KeyModifiers::ALT => match key.code { + KeyCode::Char('0') => Key::AltNum0, + KeyCode::Char('1') => Key::AltNum1, + KeyCode::Char('2') => Key::AltNum2, + KeyCode::Char('3') => Key::AltNum3, + KeyCode::Char('4') => Key::AltNum4, + KeyCode::Char('5') => Key::AltNum5, + KeyCode::Char('6') => Key::AltNum6, + KeyCode::Char('7') => Key::AltNum7, + KeyCode::Char('8') => Key::AltNum8, + KeyCode::Char('9') => Key::AltNum9, + + KeyCode::Char('a') => Key::AltA, + KeyCode::Char('b') => Key::AltB, + KeyCode::Char('c') => Key::AltC, + KeyCode::Char('d') => Key::AltD, + KeyCode::Char('e') => Key::AltE, + KeyCode::Char('f') => Key::AltF, + KeyCode::Char('g') => Key::AltG, + KeyCode::Char('h') => Key::AltH, + KeyCode::Char('i') => Key::AltI, + KeyCode::Char('j') => Key::AltJ, + KeyCode::Char('k') => Key::AltK, + KeyCode::Char('l') => Key::AltL, + KeyCode::Char('m') => Key::AltM, + KeyCode::Char('n') => Key::AltN, + KeyCode::Char('o') => Key::AltO, + KeyCode::Char('p') => Key::AltP, + KeyCode::Char('q') => Key::AltQ, + KeyCode::Char('r') => Key::AltR, + KeyCode::Char('s') => Key::AltS, + KeyCode::Char('t') => Key::AltT, + KeyCode::Char('u') => Key::AltU, + KeyCode::Char('v') => Key::AltV, + KeyCode::Char('w') => Key::AltW, + KeyCode::Char('x') => Key::AltX, + KeyCode::Char('y') => Key::AltY, + KeyCode::Char('z') => Key::AltZ, + + KeyCode::Char(c) => c.into(), + + _ => Key::NotSupported, + }, + + _ => match key.code { + KeyCode::F(1) => Key::F1, + KeyCode::F(2) => Key::F2, + KeyCode::F(3) => Key::F3, + KeyCode::F(4) => Key::F4, + KeyCode::F(5) => Key::F5, + KeyCode::F(6) => Key::F6, + KeyCode::F(7) => Key::F7, + KeyCode::F(8) => Key::F8, + KeyCode::F(9) => Key::F9, + KeyCode::F(10) => Key::F10, + KeyCode::F(11) => Key::F11, + KeyCode::F(12) => Key::F12, + + KeyCode::Backspace => Key::Backspace, + KeyCode::Left => Key::Left, + KeyCode::Right => Key::Right, + KeyCode::Up => Key::Up, + KeyCode::Down => Key::Down, + KeyCode::Home => Key::Home, + KeyCode::End => Key::End, + KeyCode::PageUp => Key::PageUp, + KeyCode::PageDown => Key::PageDown, + KeyCode::BackTab => Key::BackTab, + KeyCode::Delete => Key::Delete, + KeyCode::Insert => Key::Insert, + KeyCode::Enter => Key::Enter, + KeyCode::Tab => Key::Tab, + KeyCode::Esc => Key::Esc, + + KeyCode::Char(c) => c.into(), + + _ => Key::NotSupported, + }, + } + } + + pub fn is_alphabet(&self) -> bool { + match self { + Self::A => true, + Self::B => true, + Self::C => true, + Self::D => true, + Self::E => true, + Self::F => true, + Self::G => true, + Self::H => true, + Self::I => true, + Self::J => true, + Self::K => true, + Self::L => true, + Self::M => true, + Self::N => true, + Self::O => true, + Self::P => true, + Self::Q => true, + Self::R => true, + Self::S => true, + Self::T => true, + Self::U => true, + Self::V => true, + Self::W => true, + Self::X => true, + Self::Y => true, + Self::Z => true, + + Self::ShiftA => true, + Self::ShiftB => true, + Self::ShiftC => true, + Self::ShiftD => true, + Self::ShiftE => true, + Self::ShiftF => true, + Self::ShiftG => true, + Self::ShiftH => true, + Self::ShiftI => true, + Self::ShiftJ => true, + Self::ShiftK => true, + Self::ShiftL => true, + Self::ShiftM => true, + Self::ShiftN => true, + Self::ShiftO => true, + Self::ShiftP => true, + Self::ShiftQ => true, + Self::ShiftR => true, + Self::ShiftS => true, + Self::ShiftT => true, + Self::ShiftU => true, + Self::ShiftV => true, + Self::ShiftW => true, + Self::ShiftX => true, + Self::ShiftY => true, + Self::ShiftZ => true, + + _ => false, + } + } + + pub fn is_number(&self) -> bool { + match self { + Self::Num0 => true, + Self::Num1 => true, + Self::Num2 => true, + Self::Num3 => true, + Self::Num4 => true, + Self::Num5 => true, + Self::Num6 => true, + Self::Num7 => true, + Self::Num8 => true, + Self::Num9 => true, + _ => false, + } + } + + pub fn is_special_character(&self) -> bool { + match self { + Self::Special(_) => true, + _ => false, + } + } + + pub fn to_char(&self) -> Option { + match self { + Self::Num0 => Some('0'), + Self::Num1 => Some('1'), + Self::Num2 => Some('2'), + Self::Num3 => Some('3'), + Self::Num4 => Some('4'), + Self::Num5 => Some('5'), + Self::Num6 => Some('6'), + Self::Num7 => Some('7'), + Self::Num8 => Some('8'), + Self::Num9 => Some('9'), + + Self::A => Some('a'), + Self::B => Some('b'), + Self::C => Some('c'), + Self::D => Some('d'), + Self::E => Some('e'), + Self::F => Some('f'), + Self::G => Some('g'), + Self::H => Some('h'), + Self::I => Some('i'), + Self::J => Some('j'), + Self::K => Some('k'), + Self::L => Some('l'), + Self::M => Some('m'), + Self::N => Some('n'), + Self::O => Some('o'), + Self::P => Some('p'), + Self::Q => Some('q'), + Self::R => Some('r'), + Self::S => Some('s'), + Self::T => Some('t'), + Self::U => Some('u'), + Self::V => Some('v'), + Self::W => Some('w'), + Self::X => Some('x'), + Self::Y => Some('y'), + Self::Z => Some('z'), + + Self::ShiftA => Some('A'), + Self::ShiftB => Some('B'), + Self::ShiftC => Some('C'), + Self::ShiftD => Some('D'), + Self::ShiftE => Some('E'), + Self::ShiftF => Some('F'), + Self::ShiftG => Some('G'), + Self::ShiftH => Some('H'), + Self::ShiftI => Some('I'), + Self::ShiftJ => Some('J'), + Self::ShiftK => Some('K'), + Self::ShiftL => Some('L'), + Self::ShiftM => Some('M'), + Self::ShiftN => Some('N'), + Self::ShiftO => Some('O'), + Self::ShiftP => Some('P'), + Self::ShiftQ => Some('Q'), + Self::ShiftR => Some('R'), + Self::ShiftS => Some('S'), + Self::ShiftT => Some('T'), + Self::ShiftU => Some('U'), + Self::ShiftV => Some('V'), + Self::ShiftW => Some('W'), + Self::ShiftX => Some('X'), + Self::ShiftY => Some('Y'), + Self::ShiftZ => Some('Z'), + + Self::Special(c) => Some(c.to_owned()), + + _ => None, } } } + +impl From for Key { + fn from(c: char) -> Self { + match c { + '0' => Key::Num0, + '1' => Key::Num1, + '2' => Key::Num2, + '3' => Key::Num3, + '4' => Key::Num4, + '5' => Key::Num5, + '6' => Key::Num6, + '7' => Key::Num7, + '8' => Key::Num8, + '9' => Key::Num9, + + 'a' => Key::A, + 'b' => Key::B, + 'c' => Key::C, + 'd' => Key::D, + 'e' => Key::E, + 'f' => Key::F, + 'g' => Key::G, + 'h' => Key::H, + 'i' => Key::I, + 'j' => Key::J, + 'k' => Key::K, + 'l' => Key::L, + 'm' => Key::M, + 'n' => Key::N, + 'o' => Key::O, + 'p' => Key::P, + 'q' => Key::Q, + 'r' => Key::R, + 's' => Key::S, + 't' => Key::T, + 'u' => Key::U, + 'v' => Key::V, + 'w' => Key::W, + 'x' => Key::X, + 'y' => Key::Y, + 'z' => Key::Z, + + 'A' => Key::ShiftA, + 'B' => Key::ShiftB, + 'C' => Key::ShiftC, + 'D' => Key::ShiftD, + 'E' => Key::ShiftE, + 'F' => Key::ShiftF, + 'G' => Key::ShiftG, + 'H' => Key::ShiftH, + 'I' => Key::ShiftI, + 'J' => Key::ShiftJ, + 'K' => Key::ShiftK, + 'L' => Key::ShiftL, + 'M' => Key::ShiftM, + 'N' => Key::ShiftN, + 'O' => Key::ShiftO, + 'P' => Key::ShiftP, + 'Q' => Key::ShiftQ, + 'R' => Key::ShiftR, + 'S' => Key::ShiftS, + 'T' => Key::ShiftT, + 'U' => Key::ShiftU, + 'V' => Key::ShiftV, + 'W' => Key::ShiftW, + 'X' => Key::ShiftX, + 'Y' => Key::ShiftY, + 'Z' => Key::ShiftZ, + + ' ' => Key::Space, + '\t' => Key::Tab, + '\n' => Key::Enter, + + c => Key::Special(c), + } + } +} + +impl From for Key { + fn from(string: String) -> Self { + string + .chars() + .next() + .map(|c| c.into()) + .unwrap_or(Key::NotSupported) + } +} + +impl From<&str> for Key { + fn from(string: &str) -> Self { + string.to_string().into() + } +} + +impl Ord for Key { + fn cmp(&self, other: &Self) -> Ordering { + // Notice that the we flip the ordering on costs. + // In case of a tie we compare positions - this step is necessary + // to make implementations of `PartialEq` and `Ord` consistent. + other.to_string().cmp(&self.to_string()) + } +} +impl PartialOrd for Key { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/src/lib.rs b/src/lib.rs index b891aa7..f20d14a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ -pub mod ui; -pub mod input; -pub mod config; pub mod app; +pub mod config; pub mod error; +pub mod explorer; +pub mod input; +pub mod ui; diff --git a/src/main.rs b/src/main.rs index 15af872..56e0ef2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,21 @@ +use crossterm::event::Event; use crossterm::terminal as term; +use crossterm::{event, execute}; use handlebars::{handlebars_helper, Handlebars}; use shellwords; +use std::env; use std::fs; -use std::io; +use std::io::prelude::*; +use std::path::PathBuf; +use std::sync::mpsc; +use std::thread; +use std::time::Duration; use termion::get_tty; -use termion::{input::TermRead, screen::AlternateScreen}; use tui::backend::CrosstermBackend; -use tui::widgets::{ListState, TableState}; use tui::Terminal; use xplr::app; -use xplr::app::Task; use xplr::error::Error; +use xplr::explorer; use xplr::input::Key; use xplr::ui; @@ -18,15 +23,29 @@ handlebars_helper!(shellescape: |v: str| format!("{}", shellwords::escape(v))); handlebars_helper!(readfile: |v: str| fs::read_to_string(v).unwrap_or_default()); fn main() -> Result<(), Error> { - let mut app = app::create()?; + let mut pwd = PathBuf::from(env::args().skip(1).next().unwrap_or(".".into())) + .canonicalize() + .unwrap_or_default(); + + let mut focused_path = None; + + if pwd.is_file() { + focused_path = pwd.file_name().map(|n| n.to_string_lossy().to_string()); + pwd = pwd.parent().map(|p| p.into()).unwrap_or_default(); + } + + let pwd = pwd.to_string_lossy().to_string(); + + let mut last_pwd = pwd.clone(); + + let mut app = app::App::new(pwd.clone()); let mut hb = Handlebars::new(); hb.register_helper("shellescape", Box::new(shellescape)); hb.register_helper("readfile", Box::new(readfile)); hb.register_template_string( app::TEMPLATE_TABLE_ROW, - &app.config - .clone() + &app.config() .general .table .row @@ -37,88 +56,211 @@ fn main() -> Result<(), Error> { .join("\t"), )?; - let stdin = io::stdin(); - let stdout = get_tty()?; - // let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let keys = stdin - .keys() - .map(|e| e.map_or(Key::NotSupported, |e| Key::from_termion_event(e))); + let mut result = Ok(()); + let mut output = None; - let mut table_state = TableState::default(); - let mut list_state = ListState::default(); + let (tx_key, rx) = mpsc::channel(); + let tx_init = tx_key.clone(); + let tx_pipe = tx_key.clone(); + let tx_explorer = tx_key.clone(); term::enable_raw_mode().unwrap(); - terminal.draw(|f| ui::draw(&app, &hb, f, &mut table_state, &mut list_state))?; + let mut stdout = get_tty().unwrap(); + // let mut stdout = stdout.lock(); + execute!(stdout, term::EnterAlternateScreen).unwrap(); + // let stdout = MouseTerminal::from(stdout); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend).unwrap(); + terminal.hide_cursor()?; - let mut result = Ok(()); - 'outer: for key in keys { - if let Some(actions) = app.actions_from_key(&key) { - for action in actions.iter() { - app = match app.handle(action) { - Ok(mut a) => { - terminal - .draw(|f| ui::draw(&a, &hb, f, &mut table_state, &mut list_state))?; - - match a.task.clone() { - Task::NoOp => {} - - Task::Quit => { - term::disable_raw_mode().unwrap(); - std::mem::drop(terminal); - break 'outer; - } - - Task::PrintAndQuit(txt) => { - term::disable_raw_mode().unwrap(); - std::mem::drop(terminal); - if !txt.is_empty() { - println!("{}", &txt); - }; - break 'outer; - } - - Task::Call(cmd) => { - term::disable_raw_mode().unwrap(); - std::mem::drop(terminal); - if let Some((_, meta)) = a.directory_buffer.focused() { - let _ = std::process::Command::new(cmd.command.clone()) - .current_dir(&a.directory_buffer.pwd) - .args( - cmd.args - .iter() - .map(|arg| hb.render_template(arg, &meta).unwrap()), - ) - .status(); - }; - - term::enable_raw_mode().unwrap(); - let stdout = get_tty()?; - let stdout = AlternateScreen::from(stdout); - let backend = CrosstermBackend::new(stdout); - terminal = Terminal::new(backend)?; - a = a.refresh()?; - terminal.draw(|f| { - ui::draw(&a, &hb, f, &mut table_state, &mut list_state) - })?; - } - }; - - a.task = Task::NoOp; - a - } - Err(e) => { - term::disable_raw_mode().unwrap(); - std::mem::drop(terminal); - result = Err(e); - break 'outer; + let (tx, rx_key) = mpsc::channel(); + thread::spawn(move || { + let mut is_paused = false; + loop { + if let Some(paused) = rx_key.try_recv().ok() { + is_paused = paused; + }; + + if !is_paused { + if event::poll(std::time::Duration::from_millis(1)).unwrap() { + if let Event::Key(key) = event::read().unwrap() { + let key = Key::from_event(key); + let msg = app::MsgIn::Internal(app::InternalMsg::HandleKey(key)); + tx_key.send(app::Task::new(0, msg, Some(key))).unwrap(); } } } + } + }); + + let pipe_msg_in = app.config().pipes.msg_in.clone(); + thread::spawn(move || loop { + let in_str = fs::read_to_string(&pipe_msg_in).unwrap_or_default(); + + if !in_str.is_empty() { + let msgs = in_str + .lines() + .filter_map(|s| serde_yaml::from_str::(s.trim()).ok()); + + msgs.for_each(|msg| { + tx_pipe + .send(app::Task::new(2, app::MsgIn::External(msg), None)) + .unwrap(); + }); + fs::write(&pipe_msg_in, "").unwrap(); }; + thread::sleep(Duration::from_millis(10)); + }); + + explorer::explore(pwd.clone(), focused_path, tx_init); + + 'outer: while result.is_ok() { + while let Some(msg) = app.pop_msg_out() { + match msg { + app::MsgOut::Debug(path) => { + fs::write(&path, serde_yaml::to_string(&app).unwrap_or_default())?; + } + + app::MsgOut::PrintResultAndQuit => { + let out = if app.selected().is_empty() { + app.focused_node() + .map(|n| n.absolute_path.clone()) + .unwrap_or_default() + } else { + app.selected() + .into_iter() + .map(|n| n.absolute_path.clone()) + .collect::>() + .join("\n") + }; + output = Some(out); + break 'outer; + } + + app::MsgOut::PrintAppStateAndQuit => { + let out = serde_yaml::to_string(&app)?; + output = Some(out); + break 'outer; + } + + app::MsgOut::Refresh => { + if app.pwd() != &last_pwd { + explorer::explore( + app.pwd().clone(), + app.focused_node().map(|n| n.relative_path.clone()), + tx_explorer.clone(), + ); + last_pwd = app.pwd().to_owned(); + }; + + // UI + terminal.draw(|f| ui::draw(f, &app, &hb)).unwrap(); + + // Pipes + let focused = app + .focused_node() + .map(|n| n.absolute_path.clone()) + .unwrap_or_default(); + + fs::write(&app.config().pipes.focus_out, focused).unwrap(); + + let selected = app + .selected() + .iter() + .map(|n| n.absolute_path.clone()) + .collect::>() + .join("\n"); + + fs::write(&app.config().pipes.selected_out, selected).unwrap(); + + fs::write(&app.config().pipes.mode_out, &app.mode().name).unwrap(); + } + + app::MsgOut::Call(cmd) => { + tx.send(true).unwrap(); + terminal.clear()?; + term::disable_raw_mode().unwrap(); + execute!(terminal.backend_mut(), term::LeaveAlternateScreen).unwrap(); + terminal.show_cursor()?; + + let focus_path = app + .focused_node() + .map(|n| n.absolute_path.clone()) + .unwrap_or_default(); + + let focus_index = app + .directory_buffer() + .map(|d| d.focus) + .unwrap_or_default() + .to_string(); + + let selected = app + .selected() + .iter() + .map(|n| n.absolute_path.clone()) + .collect::>() + .join(","); + + let directory_nodes = app + .directory_buffer() + .map(|d| { + d.nodes + .iter() + .map(|n| n.absolute_path.clone()) + .collect::>() + .join(",") + }) + .unwrap_or_default(); + + let pipe_msg_in = app.config().pipes.msg_in.clone(); + let pipe_focus_out = app.config().pipes.focus_out.clone(); + let pipe_selected_out = app.config().pipes.selected_out.clone(); + + let app_yaml = serde_yaml::to_string(&app).unwrap_or_default(); + + let _ = std::process::Command::new(cmd.command.clone()) + .current_dir(app.pwd()) + .env("XPLR_FOCUS_PATH", focus_path) + .env("XPLR_FOCUS_INDEX", focus_index) + .env("XPLR_SELECTED", selected) + .env("XPLR_PIPE_MSG_IN", pipe_msg_in) + .env("XPLR_PIPE_SELECTED_OUT", pipe_selected_out) + .env("XPLR_PIPE_FOCUS_OUT", pipe_focus_out) + .env("XPLR_APP_YAML", app_yaml) + .env("XPLR_DIRECTORY_NODES", directory_nodes) + .args(cmd.args.clone()) + .status(); + + terminal.hide_cursor()?; + execute!(terminal.backend_mut(), term::EnterAlternateScreen).unwrap(); + term::enable_raw_mode().unwrap(); + tx.send(false).unwrap(); + terminal.draw(|f| ui::draw(f, &app, &hb)).unwrap(); + } + }; + } + + for task in rx.try_iter() { + app = app.enqueue(task); + } + + let (new_app, new_result) = match app.clone().possibly_mutate() { + Ok(a) => (a, Ok(())), + Err(e) => (app, Err(e)), + }; + + app = new_app; + result = new_result; + + // thread::sleep(Duration::from_millis(10)); + } + + term::disable_raw_mode().unwrap(); + execute!(terminal.backend_mut(), term::LeaveAlternateScreen).unwrap(); + terminal.show_cursor()?; + + if let Some(out) = output { + println!("{}", out); } result diff --git a/src/ui.rs b/src/ui.rs index 3df6556..7cee041 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,66 +1,211 @@ use crate::app; +use crate::app::Node; use handlebars::Handlebars; +use serde::{Deserialize, Serialize}; use tui::backend::Backend; +use tui::layout::Rect; use tui::layout::{Constraint as TUIConstraint, Direction, Layout}; use tui::widgets::{ Block, Borders, Cell, List, ListItem, ListState, Paragraph, Row, Table, TableState, }; use tui::Frame; -pub fn draw( - app: &app::App, - hb: &Handlebars, - f: &mut Frame, - table_state: &mut TableState, - list_state: &mut ListState, -) { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .margin(1) - .constraints([TUIConstraint::Percentage(70), TUIConstraint::Percentage(30)].as_ref()) - .split(f.size()); +const TOTAL_ROWS: usize = 50; - let body = app - .directory_buffer - .items - .iter() - .map(|(_, m)| { - let txt = hb - .render(app::TEMPLATE_TABLE_ROW, &m) - .ok() - .unwrap_or_else(|| app::UNSUPPORTED_STR.into()) - .split("\t") - .map(|x| Cell::from(x.to_string())) - .collect::>(); - - let style = if m.is_focused { - app.config.general.focused_ui.style - } else if m.is_selected { - app.config.general.selected_ui.style - } else { - app.config - .filetypes - .special - .get(&m.relative_path) - .or_else(|| app.config.filetypes.extension.get(&m.extension)) - .unwrap_or_else(|| { - if m.is_symlink { - &app.config.filetypes.symlink - } else if m.is_dir { - &app.config.filetypes.directory - } else { - &app.config.filetypes.file - } - }) - .style - }; - (txt, style) +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct NodeUIMetadata { + // From Node + pub parent: String, + pub relative_path: String, + pub absolute_path: String, + pub extension: String, + pub is_symlink: bool, + pub is_dir: bool, + pub is_file: bool, + pub is_readonly: bool, + pub mime_essence: String, + + // Extra + pub index: usize, + pub relative_index: usize, + pub is_before_focus: bool, + pub is_after_focus: bool, + pub tree: String, + pub icon: String, + pub prefix: String, + pub suffix: String, + pub is_selected: bool, + pub is_focused: bool, + pub total: usize, +} + +impl NodeUIMetadata { + fn new( + node: &Node, + index: usize, + relative_index: usize, + is_before_focus: bool, + is_after_focus: bool, + tree: String, + icon: String, + prefix: String, + suffix: String, + is_selected: bool, + is_focused: bool, + total: usize, + ) -> Self { + Self { + parent: node.parent.clone(), + relative_path: node.relative_path.clone(), + absolute_path: node.absolute_path.clone(), + extension: node.extension.clone(), + is_symlink: node.is_symlink, + is_dir: node.is_dir, + is_file: node.is_file, + is_readonly: node.is_readonly, + mime_essence: node.mime_essence.clone(), + index, + relative_index, + is_before_focus, + is_after_focus, + tree, + icon, + prefix, + suffix, + is_selected, + is_focused, + total, + } + } +} + +fn draw_table(f: &mut Frame, rect: Rect, app: &app::App, hb: &Handlebars) { + let config = app.config().to_owned(); + + let rows = app + .directory_buffer() + .map(|dir| { + let offset = ( + dir.focus.max(TOTAL_ROWS) - TOTAL_ROWS, + dir.focus.max(TOTAL_ROWS), + ); + + dir.nodes + .iter() + .enumerate() + .skip_while(|(i, _)| *i < offset.0) + .take_while(|(i, _)| *i <= offset.1) + .map(|(index, node)| { + let is_focused = dir.focus == index; + + // TODO : Optimize + let is_selected = app.selected().contains(&node); + + let ui = if is_focused { + &config.general.focused_ui + } else if is_selected { + &config.general.selected_ui + } else { + &config.general.normal_ui + }; + + let is_first = index == 0; + let is_last = index == dir.total.max(1) - 1; + + let tree = config + .general + .table + .tree + .clone() + .map(|t| { + if is_last { + t.2.format.clone() + } else if is_first { + t.0.format.clone() + } else { + t.1.format.clone() + } + }) + .unwrap_or_default(); + + let filetype = config + .filetypes + .special + .get(&node.relative_path) + .or_else(|| config.filetypes.extension.get(&node.extension)) + .or_else(|| config.filetypes.mime_essence.get(&node.mime_essence)) + .unwrap_or_else(|| { + if node.is_symlink { + &config.filetypes.symlink + } else if node.is_dir { + &config.filetypes.directory + } else { + &config.filetypes.file + } + }); + + let (relative_index, is_before_focus, is_after_focus) = if dir.focus > index { + (dir.focus - index, true, false) + } else if dir.focus < index { + (index - dir.focus, false, true) + } else { + (0, false, false) + }; + + let meta = NodeUIMetadata::new( + &node, + index, + relative_index, + is_before_focus, + is_after_focus, + tree, + filetype.icon.clone(), + ui.prefix.clone(), + ui.suffix.clone(), + is_selected, + is_focused, + dir.total, + ); + + let cols = hb + .render(app::TEMPLATE_TABLE_ROW, &meta) + .ok() + .unwrap_or_else(|| app::UNSUPPORTED_STR.into()) + .split("\t") + .map(|x| Cell::from(x.to_string())) + .collect::>(); + + let style = if is_focused { + config.general.focused_ui.style + } else if is_selected { + config.general.selected_ui.style + } else { + config + .filetypes + .special + .get(&node.relative_path) + .or_else(|| config.filetypes.extension.get(&node.extension)) + .or_else(|| config.filetypes.mime_essence.get(&node.mime_essence)) + .unwrap_or_else(|| { + if node.is_symlink { + &config.filetypes.symlink + } else if node.is_dir { + &config.filetypes.directory + } else { + &config.filetypes.file + } + }) + .style + }; + + Row::new(cols).style(style) + }) + .collect::>() }) - .map(|(t, s)| Row::new(t).style(s)) - .collect::>(); + .unwrap_or_default(); - let table_constraints: Vec = app - .config + let table_constraints: Vec = config .general .table .col_widths @@ -69,18 +214,18 @@ pub fn draw( .map(|c| c.into()) .collect(); - let table = Table::new(body) + let table = Table::new(rows) .widths(&table_constraints) - .style(app.config.general.table.style) - .highlight_style(app.config.general.focused_ui.style) - .column_spacing(app.config.general.table.col_spacing) - .block(Block::default().borders(Borders::ALL).title(format!( - " {} ", - app.directory_buffer.pwd.to_str().unwrap_or("???") - ))); - - let table = app - .config + .style(config.general.table.style) + .highlight_style(config.general.focused_ui.style) + .column_spacing(config.general.table.col_spacing) + .block( + Block::default() + .borders(Borders::ALL) + .title(format!(" {} ", app.pwd())), + ); + + let table = config .general .table .header @@ -99,29 +244,17 @@ pub fn draw( }) .unwrap_or_else(|| table.clone()); - table_state.select( - app.directory_buffer - .focus - .map(app::DirectoryBuffer::relative_focus), - ); + let mut table_state = TableState::default(); + table_state.select(app.directory_buffer().map(|dir| dir.focus)); - let left_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - TUIConstraint::Percentage(40), - TUIConstraint::Percentage(50), - TUIConstraint::Min(1), - ] - .as_ref(), - ) - .split(chunks[1]); + f.render_stateful_widget(table, rect, &mut table_state); +} +fn draw_selected(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { let selected: Vec = app - .selected_paths + .selected() .iter() - .map(|p| p.to_str().unwrap_or(app::UNSUPPORTED_STR)) - .map(String::from) + .map(|n| n.absolute_path.clone()) .map(ListItem::new) .collect(); @@ -134,28 +267,122 @@ pub fn draw( .title(format!(" Selected ({}) ", selected_count)), ); + let mut list_state = ListState::default(); + if selected_count > 0 { + list_state.select(Some(selected_count.max(1) - 1)); + } + f.render_stateful_widget(selected_list, rect, &mut list_state); +} + +fn draw_help_menu(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { // Help menu - let help_menu_rows: Vec = app - .parsed_help_menu + let mode = app.mode(); + let extra_help_lines = mode + .extra_help .clone() - .iter() - .map(|(h, k)| Row::new(vec![Cell::from(h.to_string()), Cell::from(k.to_string())])) - .collect(); + .map(|e| e.lines().map(|l| l.to_string()).collect::>()); + + let help_menu_rows: Vec = mode + .help + .clone() + .map(|h| { + h.lines() + .map(|l| Row::new(vec![Cell::from(l.to_string())])) + .collect() + }) + .unwrap_or_else(|| { + extra_help_lines + .unwrap_or_default() + .into_iter() + .map(|l| Row::new(vec![Cell::from(l)])) + .chain(mode.key_bindings.on_key.iter().filter_map(|(k, a)| { + a.help.clone().map(|h| { + Row::new(vec![Cell::from(h.to_string()), Cell::from(k.to_string())]) + }) + })) + .chain( + mode.key_bindings + .on_alphabet + .iter() + .map(|a| ("a-Z", a.help.clone())) + .filter_map(|(k, mh)| { + mh.map(|h| { + Row::new(vec![Cell::from(h.to_string()), Cell::from(k.to_string())]) + }) + }), + ) + .chain( + mode.key_bindings + .on_number + .iter() + .map(|a| ("0-9", a.help.clone())) + .filter_map(|(k, mh)| { + mh.map(|h| { + Row::new(vec![Cell::from(h.to_string()), Cell::from(k.to_string())]) + }) + }), + ) + .chain( + mode.key_bindings + .on_special_character + .iter() + .map(|a| ("spcl chars", a.help.clone())) + .filter_map(|(k, mh)| { + mh.map(|h| { + Row::new(vec![Cell::from(h.to_string()), Cell::from(k.to_string())]) + }) + }), + ) + .chain( + mode.key_bindings + .default + .iter() + .map(|a| ("default", a.help.clone())) + .filter_map(|(k, mh)| { + mh.map(|h| { + Row::new(vec![Cell::from(k.to_string()), Cell::from(h.to_string())]) + }) + }), + ) + .collect::>() + }); let help_menu = Table::new(help_menu_rows) .block( Block::default() .borders(Borders::ALL) - .title(format!(" Help [{}] ", &app.mode)), + .title(format!(" Help [{}] ", &mode.name)), ) .widths(&[TUIConstraint::Percentage(40), TUIConstraint::Percentage(60)]); + f.render_widget(help_menu, rect); +} - // Input box - let input_box = Paragraph::new(format!("> {}", &app.number_input)) +fn draw_input_buffer(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { + let input_buf = Paragraph::new(format!("> {}", app.input_buffer().unwrap_or(&"".into()))) .block(Block::default().borders(Borders::ALL).title(" input ")); + f.render_widget(input_buf, rect); +} + +pub fn draw(f: &mut Frame, app: &app::App, hb: &Handlebars) { + let rect = f.size(); + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([TUIConstraint::Max(rect.height - 5), TUIConstraint::Max(3)].as_ref()) + .split(rect); + + let upper_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([TUIConstraint::Percentage(70), TUIConstraint::Percentage(30)].as_ref()) + .split(chunks[0]); + + let upper_left_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([TUIConstraint::Percentage(50), TUIConstraint::Percentage(50)].as_ref()) + .split(upper_chunks[1]); - f.render_stateful_widget(table, chunks[0], table_state); - f.render_stateful_widget(selected_list, left_chunks[0], list_state); - f.render_widget(help_menu, left_chunks[1]); - f.render_widget(input_box, left_chunks[2]); + draw_input_buffer(f, chunks[1], app, hb); + draw_table(f, upper_chunks[0], app, hb); + draw_selected(f, upper_left_chunks[0], app, hb); + draw_help_menu(f, upper_left_chunks[1], app, hb); }