diff --git a/Cargo.lock b/Cargo.lock index c082257..e17cd3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1133,7 +1133,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "xplr" -version = "0.1.8" +version = "0.1.9" dependencies = [ "criterion", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 4935b8a..1637e8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xplr" -version = "0.1.8" # Update app.rs +version = "0.1.9" # Update app.rs authors = ["Arijit Basu "] edition = "2018" description = "An experimental, minimal, configurable TUI file explorer, stealing ideas from nnn and fzf." diff --git a/benches/navigation.rs b/benches/navigation.rs index 8fd5b3b..33a9ad2 100644 --- a/benches/navigation.rs +++ b/benches/navigation.rs @@ -3,43 +3,30 @@ use std::fs; use xplr::*; fn criterion_benchmark(c: &mut Criterion) { - let app = app::create() - .expect("failed to create app") - .change_directory(&"/tmp/xplr_bench".to_string()) - .unwrap(); - fs::create_dir_all("/tmp/xplr_bench").unwrap(); (1..10000).for_each(|i| { fs::File::create(format!("/tmp/xplr_bench/{}", i)).unwrap(); }); + let app = app::create() + .expect("failed to create app") + .change_directory(&"/tmp/xplr_bench".to_string()) + .unwrap(); + c.bench_function("focus next item", |b| { - b.iter(|| { - app.clone() - .handle(&config::Action::Global(config::GlobalAction::FocusNext)) - }) + b.iter(|| app.clone().handle(&config::Action::FocusNext)) }); c.bench_function("focus previous item", |b| { - b.iter(|| { - app.clone().handle(&config::Action::Global( - config::GlobalAction::FocusPrevious, - )) - }) + b.iter(|| app.clone().handle(&config::Action::FocusPrevious)) }); c.bench_function("focus first item", |b| { - b.iter(|| { - app.clone() - .handle(&config::Action::Global(config::GlobalAction::FocusFirst)) - }) + b.iter(|| app.clone().handle(&config::Action::FocusFirst)) }); c.bench_function("focus last item", |b| { - b.iter(|| { - app.clone() - .handle(&config::Action::Global(config::GlobalAction::FocusLast)) - }) + b.iter(|| app.clone().handle(&config::Action::FocusLast)) }); } criterion_group!(benches, criterion_benchmark); diff --git a/src/app.rs b/src/app.rs index f767d2c..1feeee8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,4 @@ -use crate::config::{ - Action, CommandConfig, Config, ExploreModeAction, GlobalAction, Mode, SelectModeAction, -}; +use crate::config::{Action, CommandConfig, Config, Mode}; use crate::error::Error; use crate::input::Key; use dirs; @@ -142,8 +140,8 @@ impl DirectoryBuffer { &config.general.normal_ui }; - let is_first_item = net_idx == 0; - let is_last_item = net_idx == total.max(1) - 1; + let is_first = net_idx == 0; + let is_last = net_idx == total.max(1) - 1; let tree = config .general @@ -151,9 +149,9 @@ impl DirectoryBuffer { .tree .clone() .map(|t| { - if is_last_item { + if is_last { t.2.format.clone() - } else if is_first_item { + } else if is_first { t.0.format.clone() } else { t.1.format.clone() @@ -191,8 +189,8 @@ impl DirectoryBuffer { suffix: ui.suffix.clone(), tree: tree.into(), is_symlink, - is_first_item, - is_last_item, + is_first, + is_last, is_dir, is_file, is_readonly, @@ -201,7 +199,7 @@ impl DirectoryBuffer { index: net_idx + 1, focus_relative_index, buffer_relative_index: rel_idx + 1, - total_items: total, + total: total, }; (abs.to_owned(), m) }) @@ -223,7 +221,7 @@ impl DirectoryBuffer { }) } - pub fn focused_item(&self) -> Option<(PathBuf, DirectoryItemMetadata)> { + pub fn focused(&self) -> Option<(PathBuf, DirectoryItemMetadata)> { self.focus.and_then(|f| { self.items .get(Self::relative_focus(f)) @@ -242,8 +240,8 @@ pub struct DirectoryItemMetadata { pub prefix: String, pub suffix: String, pub tree: String, - pub is_first_item: bool, - pub is_last_item: bool, + pub is_first: bool, + pub is_last: bool, pub is_symlink: bool, pub is_dir: bool, pub is_file: bool, @@ -253,7 +251,7 @@ pub struct DirectoryItemMetadata { pub index: usize, pub focus_relative_index: String, pub buffer_relative_index: usize, - pub total_items: usize, + pub total: usize, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -264,6 +262,7 @@ pub struct App { pub saved_buffers: HashMap>, pub selected_paths: HashSet, pub mode: Mode, + pub parsed_key_bindings: HashMap)>, pub show_hidden: bool, pub call: Option, pub result: Option, @@ -288,6 +287,8 @@ impl App { directory_buffer.focus.clone(), ); + let parsed_key_bindings = config.key_bindings.clone().filtered(&mode); + Ok(Self { version: VERSION.into(), config: config.to_owned(), @@ -295,6 +296,7 @@ impl App { saved_buffers, selected_paths: selected_paths.to_owned(), mode, + parsed_key_bindings, show_hidden, result: None, call: None, @@ -342,7 +344,7 @@ impl App { ) } - pub fn focus_first_item(self) -> Result { + pub fn focus_first(self) -> Result { let focus = if self.directory_buffer.total == 0 { None } else { @@ -360,7 +362,7 @@ impl App { ) } - pub fn focus_last_item(self) -> Result { + pub fn focus_last(self) -> Result { let focus = if self.directory_buffer.total == 0 { None } else { @@ -387,7 +389,7 @@ impl App { Ok(self) } - pub fn focus_next_item(self) -> Result { + pub fn focus_next(self) -> Result { let len = self.directory_buffer.total; let focus = self .directory_buffer @@ -406,7 +408,7 @@ impl App { ) } - pub fn focus_previous_item(self) -> Result { + pub fn focus_previous(self) -> Result { let len = self.directory_buffer.total; let focus = if len == 0 { None @@ -499,7 +501,7 @@ impl App { pub fn enter(self) -> Result { let pwd = self .directory_buffer - .focused_item() + .focused() .map(|(p, _)| p) .map(|p| { if p.is_dir() { @@ -531,7 +533,7 @@ impl App { pub fn select(self) -> Result { let selected_paths = self .directory_buffer - .focused_item() + .focused() .map(|(p, _)| { let mut selected_paths = self.selected_paths.clone(); selected_paths.insert(p); @@ -548,7 +550,7 @@ impl App { pub fn toggle_selection(self) -> Result { let selected_paths = self .directory_buffer - .focused_item() + .focused() .map(|(p, _)| { let mut selected_paths = self.selected_paths.clone(); if selected_paths.contains(&p) { @@ -582,7 +584,7 @@ impl App { let mut app = self; app.result = app .directory_buffer - .focused_item() + .focused() .and_then(|(p, _)| p.to_str().map(|s| s.to_string())); Ok(app) } @@ -623,148 +625,34 @@ impl App { Err(Error::Terminated) } - pub fn actions_from_key(&self, key: Key) -> Option> { - self.config - .key_bindings - .global - .get(&key) - .map(|m| { - m.actions - .iter() - .map(|a| Action::Global(a.clone())) - .collect() - }) - .or_else(|| match &self.mode { - Mode::Explore => self.config.key_bindings.explore_mode.get(&key).map(|m| { - m.actions - .iter() - .map(|a| Action::ExploreMode(a.clone())) - .collect() - }), - - Mode::Select => self.config.key_bindings.select_mode.get(&key).map(|m| { - m.actions - .iter() - .map(|a| Action::SelectMode(a.clone())) - .collect() - }), - - Mode::ExploreSubmode(sub) => self - .config - .key_bindings - .explore_submodes - .get(sub) - .and_then(|kb| { - kb.get(&key).map(|m| { - m.actions - .iter() - .map(|a| Action::ExploreMode(a.clone())) - .collect() - }) - }), - - Mode::SelectSubmode(sub) => self - .config - .key_bindings - .select_submodes - .get(sub) - .and_then(|kb| { - kb.get(&key).map(|m| { - m.actions - .iter() - .map(|a| Action::SelectMode(a.clone())) - .collect() - }) - }), - }) + pub fn actions_from_key(&self, key: &Key) -> Option> { + self.parsed_key_bindings.get(key).map(|(_, a)| a.to_owned()) } pub fn handle(self, action: &Action) -> Result { match action { - // Global actions - Action::Global(GlobalAction::ToggleShowHidden) => self.toggle_hidden(), - Action::Global(GlobalAction::Back) => self.back(), - Action::Global(GlobalAction::Enter) => self.enter(), - Action::Global(GlobalAction::FocusNext) => self.focus_next_item(), - Action::Global(GlobalAction::FocusPrevious) => self.focus_previous_item(), - Action::Global(GlobalAction::FocusFirst) => self.focus_first_item(), - Action::Global(GlobalAction::FocusLast) => self.focus_last_item(), - Action::Global(GlobalAction::FocusPath(path)) => self.focus_path(&path.into()), - Action::Global(GlobalAction::FocusPathByIndex(n)) => self.focus_by_index(n), - Action::Global(GlobalAction::FocusPathByBufferRelativeIndex(n)) => { - self.focus_by_buffer_relative_index(&n) - } - Action::Global(GlobalAction::FocusPathByFocusRelativeIndex(n)) => { - self.focus_by_focus_relative_index(&n) - } - Action::Global(GlobalAction::ChangeDirectory(dir)) => self.change_directory(&dir), - Action::Global(GlobalAction::Call(cmd)) => self.call(&cmd), - Action::Global(GlobalAction::PrintFocused) => self.print_focused(), - Action::Global(GlobalAction::PrintPwd) => self.print_pwd(), - Action::Global(GlobalAction::PrintAppState) => self.print_app_state(), - Action::Global(GlobalAction::Quit) => self.quit(), - Action::Global(GlobalAction::Terminate) => self.terminate(), - - // Explore mode - Action::ExploreMode(ExploreModeAction::ToggleShowHidden) => self.toggle_hidden(), - Action::ExploreMode(ExploreModeAction::Back) => self.back(), - Action::ExploreMode(ExploreModeAction::Enter) => self.enter(), - Action::ExploreMode(ExploreModeAction::FocusNext) => self.focus_next_item(), - Action::ExploreMode(ExploreModeAction::FocusPrevious) => self.focus_previous_item(), - Action::ExploreMode(ExploreModeAction::FocusFirst) => self.focus_first_item(), - Action::ExploreMode(ExploreModeAction::FocusLast) => self.focus_last_item(), - Action::ExploreMode(ExploreModeAction::FocusPath(path)) => { - self.focus_path(&path.into()) - } - Action::ExploreMode(ExploreModeAction::FocusPathByIndex(n)) => self.focus_by_index(n), - Action::ExploreMode(ExploreModeAction::FocusPathByBufferRelativeIndex(n)) => { - self.focus_by_buffer_relative_index(&n) - } - Action::ExploreMode(ExploreModeAction::FocusPathByFocusRelativeIndex(n)) => { - self.focus_by_focus_relative_index(&n) - } - Action::ExploreMode(ExploreModeAction::ChangeDirectory(dir)) => { - self.change_directory(&dir) - } - Action::ExploreMode(ExploreModeAction::Call(cmd)) => self.call(&cmd), - Action::ExploreMode(ExploreModeAction::Select) => self.select(), - Action::ExploreMode(ExploreModeAction::EnterSubmode(submode)) => { - self.enter_submode(submode) - } - Action::ExploreMode(ExploreModeAction::ExitSubmode) => self.exit_submode(), - Action::ExploreMode(ExploreModeAction::PrintFocused) => self.print_focused(), - Action::ExploreMode(ExploreModeAction::PrintPwd) => self.print_pwd(), - Action::ExploreMode(ExploreModeAction::PrintAppState) => self.print_app_state(), - Action::ExploreMode(ExploreModeAction::Quit) => self.quit(), - - // Select mode - Action::SelectMode(SelectModeAction::ToggleShowHidden) => self.toggle_hidden(), - Action::SelectMode(SelectModeAction::Back) => self.back(), - Action::SelectMode(SelectModeAction::Enter) => self.enter(), - Action::SelectMode(SelectModeAction::FocusNext) => self.focus_next_item(), - Action::SelectMode(SelectModeAction::FocusPrevious) => self.focus_previous_item(), - Action::SelectMode(SelectModeAction::FocusFirst) => self.focus_first_item(), - Action::SelectMode(SelectModeAction::FocusLast) => self.focus_last_item(), - Action::SelectMode(SelectModeAction::FocusPath(path)) => self.focus_path(&path.into()), - Action::SelectMode(SelectModeAction::FocusPathByIndex(n)) => self.focus_by_index(n), - Action::SelectMode(SelectModeAction::FocusPathByBufferRelativeIndex(n)) => { - self.focus_by_buffer_relative_index(&n) - } - Action::SelectMode(SelectModeAction::FocusPathByFocusRelativeIndex(n)) => { - self.focus_by_focus_relative_index(&n) - } - Action::SelectMode(SelectModeAction::ChangeDirectory(dir)) => { - self.change_directory(&dir) - } - Action::SelectMode(SelectModeAction::Call(cmd)) => self.call(&cmd), - Action::SelectMode(SelectModeAction::ToggleSelection) => self.toggle_selection(), - Action::SelectMode(SelectModeAction::EnterSubmode(submode)) => { - self.enter_submode(submode) - } - Action::SelectMode(SelectModeAction::ExitSubmode) => self.exit_submode(), - Action::SelectMode(SelectModeAction::PrintSelected) => self.print_selected(), - Action::SelectMode(SelectModeAction::PrintAppState) => self.print_app_state(), - Action::SelectMode(SelectModeAction::Quit) => self.quit(), + 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(), } } } diff --git a/src/config.rs b/src/config.rs index 3277004..e29d03c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,69 @@ pub enum Mode { SelectSubmode(String), } +impl Mode { + pub fn does_support(self, action: &Action) -> bool { + match (self, action) { + // Special + (_, Action::Terminate) => 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, @@ -39,33 +102,7 @@ pub struct CommandConfig { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum GlobalAction { - // Common actions - ToggleShowHidden, - Back, - Enter, - FocusPrevious, - FocusNext, - FocusFirst, - FocusLast, - FocusPath(String), - FocusPathByIndex(usize), - FocusPathByBufferRelativeIndex(usize), - FocusPathByFocusRelativeIndex(isize), - ChangeDirectory(String), - Call(CommandConfig), - - // Quit options - PrintFocused, - PrintPwd, - PrintAppState, - Quit, - Terminate, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum ExploreModeAction { - // Common actions +pub enum Action { ToggleShowHidden, Back, Enter, @@ -79,99 +116,80 @@ pub enum ExploreModeAction { FocusPath(String), ChangeDirectory(String), Call(CommandConfig), - - // Explore mode exclusive options EnterSubmode(String), ExitSubmode, Select, // Unselect, // SelectAll, // SelectAllRecursive, - - // Quit options - PrintFocused, - PrintPwd, - PrintAppState, - Quit, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum SelectModeAction { - // Common actions - ToggleShowHidden, - Back, - Enter, - FocusPrevious, - FocusNext, - FocusFirst, - FocusLast, - FocusPathByIndex(usize), - FocusPathByBufferRelativeIndex(usize), - FocusPathByFocusRelativeIndex(isize), - FocusPath(String), - ChangeDirectory(String), - Call(CommandConfig), - - // Select mode exclusive options - EnterSubmode(String), - ExitSubmode, - // Select, - // Unselect, - // SelectAll, - // SelectAllRecursive, // UnselectAll, // UnSelectAllRecursive, ToggleSelection, // ClearSelectedPaths, // Quit options + PrintFocused, PrintSelected, PrintAppState, Quit, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum Action { - Global(GlobalAction), - ExploreMode(ExploreModeAction), - SelectMode(SelectModeAction), -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct GlobalActionMenu { - #[serde(default)] - pub help: String, - pub actions: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ExploreModeActionMenu { - #[serde(default)] - pub help: String, - pub actions: Vec, + Terminate, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct SelectModeActionMenu { +pub struct ActionMenu { #[serde(default)] pub help: String, - pub actions: Vec, + pub actions: Vec, } -pub type ExploreSubmodeActionMenu = HashMap; -pub type SelectSubmodeActionMenu = HashMap; +pub type SubmodeActionMenu = HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KeyBindings { - pub global: HashMap, + pub global: HashMap, #[serde(default)] - pub explore_mode: HashMap, + pub explore_mode: HashMap, #[serde(default)] - pub explore_submodes: HashMap, + pub explore_submodes: HashMap, #[serde(default)] - pub select_mode: HashMap, + pub select_mode: HashMap, #[serde(default)] - pub select_submodes: HashMap, + 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 { diff --git a/src/main.rs b/src/main.rs index 2044dbe..c9ac7b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,7 @@ fn main() -> Result<(), Error> { let mut result = Ok(()); 'outer: for key in keys { - if let Some(actions) = app.actions_from_key(key) { + if let Some(actions) = app.actions_from_key(&key) { for action in actions.iter() { app = match app.handle(action) { Ok(mut a) => { @@ -71,7 +71,7 @@ fn main() -> Result<(), Error> { if let Some(cmd) = a.call.clone() { term::disable_raw_mode().unwrap(); std::mem::drop(terminal); - if let Some((_, meta)) = a.directory_buffer.focused_item() { + if let Some((_, meta)) = a.directory_buffer.focused() { let _ = std::process::Command::new(cmd.command.clone()) .current_dir(&a.directory_buffer.pwd) .args( diff --git a/tests/test_input.rs b/tests/test_input.rs index f0e648a..8923056 100644 --- a/tests/test_input.rs +++ b/tests/test_input.rs @@ -6,7 +6,7 @@ fn test_key_down() { assert_eq!(app.directory_buffer.focus, Some(0)); - let actions = app.actions_from_key(input::Key::Down).unwrap(); + let actions = app.actions_from_key(&input::Key::Down).unwrap(); for action in actions { app = app.handle(&action).unwrap()