diff --git a/Cargo.lock b/Cargo.lock index 6879f844..57f68656 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + [[package]] name = "base64" version = "0.13.0" @@ -148,7 +154,7 @@ dependencies = [ "num-integer", "num-traits", "serde", - "time", + "time 0.1.44", "winapi 0.3.9", ] @@ -163,6 +169,23 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "const_fn" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076a6803b0dacd6a88cfe64deba628b01533ff5ef265687e6938280c1afd0a28" + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -297,6 +320,77 @@ dependencies = [ "memchr", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive-new" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +dependencies = [ + "darling", + "derive_builder_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.8.1" @@ -326,6 +420,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "dtoa" version = "0.4.8" @@ -356,6 +456,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "fsevent" version = "0.4.0" @@ -431,6 +537,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + [[package]] name = "hermit-abi" version = "0.1.18" @@ -440,6 +552,23 @@ dependencies = [ "libc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + [[package]] name = "inotify" version = "0.7.1" @@ -846,6 +975,12 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + [[package]] name = "proc-macro2" version = "1.0.26" @@ -954,6 +1089,19 @@ version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" +[[package]] +name = "rspec" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89389e7c690310e855df3d9b507985ca0d323e2e766b2fedf369b02671e70e0a" +dependencies = [ + "colored", + "derive-new", + "derive_builder", + "rayon", + "time 0.2.26", +] + [[package]] name = "rust-argon2" version = "0.8.3" @@ -1076,6 +1224,12 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + [[package]] name = "signal-hook" version = "0.1.17" @@ -1108,6 +1262,70 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "syn" version = "1.0.68" @@ -1151,6 +1369,44 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "time" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a8cbfbf47955132d0202d1662f49b2423ae35862aee471f3ba4b133358f372" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check", + "winapi 0.3.9", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -1362,7 +1618,7 @@ dependencies = [ [[package]] name = "xplr" -version = "0.4.4" +version = "0.4.5" dependencies = [ "anyhow", "chrono", @@ -1370,10 +1626,13 @@ dependencies = [ "crossterm", "dirs", "handlebars", + "indexmap", "lazy_static", "mime_guess", "notify", + "rspec", "serde", + "serde_json", "serde_yaml", "termion", "tui", diff --git a/Cargo.toml b/Cargo.toml index 813e9771..d38da0ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xplr" -version = "0.4.4" # Update config.yml, config.rs and default.nix +version = "0.4.5" # Update config.yml, config.rs and default.nix authors = ["Arijit Basu "] edition = "2018" description = "A hackable, minimal, fast TUI file explorer, stealing ideas from nnn and fzf" @@ -24,9 +24,12 @@ anyhow = "1.0" chrono = { version = "0.4", features = ["serde"] } notify = "4.0.12" lazy_static = "1.4.0" +indexmap = { version = "1.6.2", features = ["serde"] } [dev-dependencies] criterion = "0.3" +rspec = "1.0" +serde_json = "1.0" [[bench]] name = "navigation" diff --git a/src/app.rs b/src/app.rs index 3165cdcd..e9cf2f37 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,10 +3,10 @@ use crate::config::Mode; use crate::input::Key; use anyhow::{bail, Result}; use chrono::{DateTime, Local}; +use indexmap::set::IndexSet; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; -use std::collections::HashSet; use std::collections::VecDeque; use std::fs; use std::io; @@ -243,6 +243,169 @@ pub enum InternalMsg { HandleKey(Key), } +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum NodeSorter { + ByRelativePath, + ByExtension, + ByIsDir, + ByIsFile, + ByIsSymlink, + ByIsBroken, + ByIsReadonly, + ByMimeEssence, + BySymlinkAbsolutePath, + BySymlinkExtension, + BySymlinkIsDir, + BySymlinkIsFile, + BySymlinkIsReadonly, + BySymlinkMimeEssence, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct NodeSorterApplicable { + sorter: NodeSorter, + #[serde(default)] + reverse: bool, +} + +impl PartialEq for NodeSorterApplicable { + fn eq(&self, other: &NodeSorterApplicable) -> bool { + self.sorter == other.sorter + } +} + +impl Eq for NodeSorterApplicable {} + +impl std::hash::Hash for NodeSorterApplicable { + fn hash(&self, state: &mut H) { + self.sorter.hash(state); + } +} + +impl std::fmt::Display for NodeSorterApplicable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let suffix = if self.reverse { '↑' } else { '↓' }; + let acrn = match self.sorter { + NodeSorter::ByRelativePath => "rel", + NodeSorter::ByExtension => "ext", + NodeSorter::ByIsDir => "ð", + NodeSorter::ByIsFile => "ƒ", + NodeSorter::ByIsSymlink => "§", + NodeSorter::ByIsBroken => "§⨯", + NodeSorter::ByIsReadonly => "ro", + NodeSorter::ByMimeEssence => "mime", + NodeSorter::BySymlinkAbsolutePath => "§rel", + NodeSorter::BySymlinkExtension => "§ext", + NodeSorter::BySymlinkIsDir => "§ð", + NodeSorter::BySymlinkIsFile => "§ƒ", + NodeSorter::BySymlinkIsReadonly => "§ro", + NodeSorter::BySymlinkMimeEssence => "§mime", + }; + write!(f, "{}{}", acrn, suffix) + } +} + +impl NodeSorterApplicable { + fn reversed(mut self) -> Self { + self.reverse = !self.reverse; + self + } + + fn apply(&self, a: &Node, b: &Node) -> Ordering { + match (self.sorter, self.reverse) { + (NodeSorter::ByRelativePath, true) => b.relative_path.cmp(&a.relative_path), + (NodeSorter::ByRelativePath, false) => a.relative_path.cmp(&b.relative_path), + (NodeSorter::ByExtension, true) => b.extension.cmp(&a.extension), + (NodeSorter::ByExtension, false) => a.extension.cmp(&b.extension), + (NodeSorter::ByIsDir, true) => b.is_dir.cmp(&a.is_dir), + (NodeSorter::ByIsDir, false) => a.is_dir.cmp(&b.is_dir), + (NodeSorter::ByIsFile, true) => b.is_file.cmp(&a.is_file), + (NodeSorter::ByIsFile, false) => a.is_file.cmp(&b.is_file), + (NodeSorter::ByIsSymlink, true) => b.is_symlink.cmp(&a.is_symlink), + (NodeSorter::ByIsSymlink, false) => a.is_symlink.cmp(&b.is_symlink), + (NodeSorter::ByIsBroken, true) => b.is_broken.cmp(&a.is_broken), + (NodeSorter::ByIsBroken, false) => a.is_broken.cmp(&b.is_broken), + (NodeSorter::ByIsReadonly, true) => b.is_readonly.cmp(&a.is_readonly), + (NodeSorter::ByIsReadonly, false) => a.is_readonly.cmp(&b.is_readonly), + (NodeSorter::ByMimeEssence, true) => b.mime_essence.cmp(&a.mime_essence), + (NodeSorter::ByMimeEssence, false) => a.mime_essence.cmp(&b.mime_essence), + (NodeSorter::BySymlinkAbsolutePath, true) => b + .symlink + .as_ref() + .map(|s| &s.absolute_path) + .cmp(&a.symlink.as_ref().map(|s| &s.absolute_path)), + + (NodeSorter::BySymlinkAbsolutePath, false) => a + .symlink + .as_ref() + .map(|s| &s.absolute_path) + .cmp(&b.symlink.as_ref().map(|s| &s.absolute_path)), + + (NodeSorter::BySymlinkExtension, true) => b + .symlink + .as_ref() + .map(|s| &s.extension) + .cmp(&a.symlink.as_ref().map(|s| &s.extension)), + + (NodeSorter::BySymlinkExtension, false) => a + .symlink + .as_ref() + .map(|s| &s.extension) + .cmp(&b.symlink.as_ref().map(|s| &s.extension)), + + (NodeSorter::BySymlinkIsDir, true) => b + .symlink + .as_ref() + .map(|s| &s.is_dir) + .cmp(&a.symlink.as_ref().map(|s| &s.is_dir)), + + (NodeSorter::BySymlinkIsDir, false) => a + .symlink + .as_ref() + .map(|s| &s.is_dir) + .cmp(&b.symlink.as_ref().map(|s| &s.is_dir)), + + (NodeSorter::BySymlinkIsFile, true) => b + .symlink + .as_ref() + .map(|s| &s.is_file) + .cmp(&a.symlink.as_ref().map(|s| &s.is_file)), + + (NodeSorter::BySymlinkIsFile, false) => a + .symlink + .as_ref() + .map(|s| &s.is_file) + .cmp(&b.symlink.as_ref().map(|s| &s.is_file)), + + (NodeSorter::BySymlinkIsReadonly, true) => b + .symlink + .as_ref() + .map(|s| &s.is_readonly) + .cmp(&a.symlink.as_ref().map(|s| &s.is_readonly)), + + (NodeSorter::BySymlinkIsReadonly, false) => a + .symlink + .as_ref() + .map(|s| &s.is_readonly) + .cmp(&b.symlink.as_ref().map(|s| &s.is_readonly)), + + (NodeSorter::BySymlinkMimeEssence, true) => b + .symlink + .as_ref() + .map(|s| &s.mime_essence) + .cmp(&a.symlink.as_ref().map(|s| &s.mime_essence)), + + (NodeSorter::BySymlinkMimeEssence, false) => a + .symlink + .as_ref() + .map(|s| &s.mime_essence) + .cmp(&b.symlink.as_ref().map(|s| &s.mime_essence)), + } + } +} + #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub enum NodeFilter { @@ -444,6 +607,39 @@ pub struct NodeFilterApplicable { case_sensitive: bool, } +impl std::fmt::Display for NodeFilterApplicable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let csicon = if self.case_sensitive { "" } else { "[i]" }; + let acrn = match self.filter { + NodeFilter::RelativePathIs => "rel==", + NodeFilter::RelativePathIsNot => "rel!=", + + NodeFilter::RelativePathDoesStartWith => "rel=^", + NodeFilter::RelativePathDoesNotStartWith => "rel!^", + + NodeFilter::RelativePathDoesContain => "rel=~", + NodeFilter::RelativePathDoesNotContain => "rel!~", + + NodeFilter::RelativePathDoesEndWith => "rel=$", + NodeFilter::RelativePathDoesNotEndWith => "rel!$", + + NodeFilter::AbsolutePathIs => "abs==", + NodeFilter::AbsolutePathIsNot => "abs!=", + + NodeFilter::AbsolutePathDoesStartWith => "abs=^", + NodeFilter::AbsolutePathDoesNotStartWith => "abs!^", + + NodeFilter::AbsolutePathDoesContain => "abs=~", + NodeFilter::AbsolutePathDoesNotContain => "abs!~", + + NodeFilter::AbsolutePathDoesEndWith => "abs=$", + NodeFilter::AbsolutePathDoesNotEndWith => "abs!$", + }; + + write!(f, "{}{}{}", csicon, acrn, &self.input) + } +} + impl NodeFilterApplicable { pub fn new(filter: NodeFilter, input: String, case_sensitive: bool) -> Self { Self { @@ -468,13 +664,32 @@ pub struct NodeFilterFromInput { #[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct ExplorerConfig { - filters: HashSet, + filters: IndexSet, + sorters: IndexSet, } impl ExplorerConfig { pub fn filter(&self, node: &Node) -> bool { self.filters.iter().all(|f| f.apply(node)) } + + pub fn sort(&self, a: &Node, b: &Node) -> Ordering { + let mut ord = Ordering::Equal; + for s in self.sorters.iter() { + ord = ord.then(s.apply(a, b)); + } + ord + } + + /// Get a reference to the explorer config's filters. + pub fn filters(&self) -> &IndexSet { + &self.filters + } + + /// Get a reference to the explorer config's sorters. + pub fn sorters(&self) -> &IndexSet { + &self.sorters + } } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] @@ -649,7 +864,7 @@ pub enum ExternalMsg { /// Clear the selection. ClearSelection, - /// Add a filter to explude nodes while exploring directories. + /// Add a filter to exclude nodes while exploring directories. /// /// Example: `AddNodeFilter: {filter: RelativePathDoesStartWith, input: foo}` AddNodeFilter(NodeFilterApplicable), @@ -677,6 +892,32 @@ pub enum ExternalMsg { /// Reset the node filters back to the default configuration. ResetNodeFilters, + /// Add a sorter to sort nodes while exploring directories. + /// + /// Example: `AddNodeSorter: {sorter: ByRelativePath, reverse: false}` + AddNodeSorter(NodeSorterApplicable), + + /// Remove an existing sorter. + /// + /// Example: `RemoveNodeSorter: ByRelativePath` + RemoveNodeSorter(NodeSorter), + + /// Reverse a node sorter. + /// + /// Example: `ReverseNodeSorter: ByRelativePath` + ReverseNodeSorter(NodeSorter), + + /// Remove a sorter if it exists, else, add a it. + /// + /// Example: `ToggleSorterSorter: {sorter: ByRelativePath, reverse: false}` + ToggleNodeSorter(NodeSorterApplicable), + + /// Reverse the node sorters. + ReverseNodeSorters, + + /// Reset the node sorters back to the default configuration. + ResetNodeSorters, + /// Log information message. /// /// Example: `LogInfo: launching satellite` @@ -888,13 +1129,17 @@ impl App { let mut explorer_config = ExplorerConfig::default(); if !config.general.show_hidden.unwrap_or_default() { - explorer_config.filters.insert(NodeFilterApplicable::new( + explorer_config.filters.replace(NodeFilterApplicable::new( NodeFilter::RelativePathDoesNotStartWith, ".".into(), - Default::default(), + true, )); } + if let Some(sorters) = &config.general.initial_sorting { + explorer_config.sorters = sorters.clone(); + }; + let mut history = History::default(); history = history.push(pwd.to_string_lossy().to_string()); @@ -1013,6 +1258,12 @@ impl App { ExternalMsg::RemoveNodeFilterFromInput(f) => self.remove_node_filter_from_input(f), ExternalMsg::ToggleNodeFilter(f) => self.toggle_node_filter(f), ExternalMsg::ResetNodeFilters => self.reset_node_filters(), + ExternalMsg::AddNodeSorter(f) => self.add_node_sorter(f), + ExternalMsg::RemoveNodeSorter(f) => self.remove_node_sorter(f), + ExternalMsg::ReverseNodeSorter(f) => self.reverse_node_sorter(f), + ExternalMsg::ToggleNodeSorter(f) => self.toggle_node_sorter(f), + ExternalMsg::ReverseNodeSorters => self.reverse_node_sorters(), + ExternalMsg::ResetNodeSorters => self.reset_node_sorters(), ExternalMsg::LogInfo(l) => self.log_info(l), ExternalMsg::LogSuccess(l) => self.log_success(l), ExternalMsg::LogError(l) => self.log_error(l), @@ -1342,14 +1593,8 @@ impl App { } fn un_select_path(mut self, path: String) -> Result { - let path = PathBuf::from(path); - let parent = path.parent().map(|p| p.to_string_lossy().to_string()); - let filename = path.file_name().map(|p| p.to_string_lossy().to_string()); - if let (Some(p), Some(n)) = (parent, filename) { - let node = Node::new(p, n); - self.selection.retain(|n| n != &node); - self.msg_out.push_back(MsgOut::Refresh); - }; + self.selection.retain(|n| n.absolute_path != path); + self.msg_out.push_back(MsgOut::Refresh); Ok(self) } @@ -1387,20 +1632,12 @@ impl App { } } - fn toggle_selection_by_path(mut self, path: String) -> Result { - let path = PathBuf::from(path); - let parent = path.parent().map(|p| p.to_string_lossy().to_string()); - let filename = path.file_name().map(|p| p.to_string_lossy().to_string()); - if let (Some(p), Some(n)) = (parent, filename) { - let node = Node::new(p, n); - if self.selection.contains(&node) { - self.selection.retain(|n| n != &node); - } else { - self.selection.push(node); - } - self.msg_out.push_back(MsgOut::Refresh); - }; - Ok(self) + fn toggle_selection_by_path(self, path: String) -> Result { + if self.selection.iter().any(|n| n.absolute_path == path) { + self.select_path(path) + } else { + self.un_select_path(path) + } } fn clear_selection(mut self) -> Result { @@ -1410,8 +1647,7 @@ impl App { } fn add_node_filter(mut self, filter: NodeFilterApplicable) -> Result { - self.explorer_config.filters.insert(filter); - self.msg_out.push_back(MsgOut::Refresh); + self.explorer_config.filters.replace(filter); Ok(self) } @@ -1424,14 +1660,12 @@ impl App { input, filter.case_sensitive, )); - self.msg_out.push_back(MsgOut::Refresh); }; Ok(self) } fn remove_node_filter(mut self, filter: NodeFilterApplicable) -> Result { self.explorer_config.filters.remove(&filter); - self.msg_out.push_back(MsgOut::Refresh); Ok(self) } @@ -1444,7 +1678,6 @@ impl App { input, filter.case_sensitive, )); - self.msg_out.push_back(MsgOut::Refresh); }; Ok(self) } @@ -1464,14 +1697,56 @@ impl App { self.add_node_filter(NodeFilterApplicable::new( NodeFilter::RelativePathDoesNotStartWith, ".".into(), - Default::default(), + true, )) } else { - self.msg_out.push_back(MsgOut::Refresh); Ok(self) } } + fn add_node_sorter(mut self, sorter: NodeSorterApplicable) -> Result { + self.explorer_config.sorters.replace(sorter); + Ok(self) + } + + fn remove_node_sorter(mut self, sorter: NodeSorter) -> Result { + self.explorer_config.sorters.retain(|s| s.sorter != sorter); + Ok(self) + } + + fn reverse_node_sorter(mut self, sorter: NodeSorter) -> Result { + self.explorer_config.sorters = self + .explorer_config + .sorters + .into_iter() + .map(|s| if s.sorter == sorter { s.reversed() } else { s }) + .collect(); + Ok(self) + } + + fn toggle_node_sorter(self, sorter: NodeSorterApplicable) -> Result { + if self.explorer_config.sorters.contains(&sorter) { + self.remove_node_sorter(sorter.sorter) + } else { + self.add_node_sorter(sorter) + } + } + + fn reverse_node_sorters(mut self) -> Result { + self.explorer_config.sorters = self + .explorer_config + .sorters + .into_iter() + .map(|s| s.reversed()) + .collect(); + Ok(self) + } + + fn reset_node_sorters(mut self) -> Result { + self.explorer_config.sorters.clear(); + Ok(self) + } + fn log_info(mut self, message: String) -> Result { self.logs.push(Log::new(LogLevel::Info, message)); Ok(self) diff --git a/src/config.rs b/src/config.rs index ca7332dc..536bc18a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,10 @@ use crate::app::ExternalMsg; use crate::app::HelpMenuLine; +use crate::app::NodeSorterApplicable; use crate::default_config; use crate::ui::Style; use anyhow::Result; +use indexmap::IndexSet; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::collections::HashMap; @@ -250,6 +252,9 @@ pub struct GeneralConfig { #[serde(default)] pub selection_ui: UiConfig, + + #[serde(default)] + pub initial_sorting: Option>, } impl GeneralConfig { @@ -262,6 +267,7 @@ impl GeneralConfig { self.default_ui = self.default_ui.extend(other.default_ui); self.focus_ui = self.focus_ui.extend(other.focus_ui); self.selection_ui = self.selection_ui.extend(other.selection_ui); + self.initial_sorting = other.initial_sorting.or(self.initial_sorting); self } } @@ -419,6 +425,9 @@ pub struct BuiltinModesConfig { #[serde(default)] pub search: Mode, + + #[serde(default)] + pub sort: Mode, } impl BuiltinModesConfig { @@ -454,6 +463,7 @@ impl BuiltinModesConfig { "delete" => Some(&self.delete), "action" => Some(&self.action), "search" => Some(&self.search), + "sort" => Some(&self.sort), _ => None, } } @@ -532,6 +542,7 @@ impl Config { pub fn is_compatible(&self) -> Result { let result = match self.parsed_version()? { + (0, 4, 5) => true, (0, 4, 4) => true, (0, 4, 3) => true, (0, 4, 2) => true, @@ -545,8 +556,9 @@ impl Config { pub fn upgrade_notification(&self) -> Result> { let result = match self.parsed_version()? { - (0, 4, 4) => None, - (_, _, _) => Some("App version updated. New: check out some hacks: https://github.com/sayanarijit/xplr/wiki/Hacks"), + (0, 4, 5) => None, + (0, 4, 4) => Some("App version updated. New: added sorting support"), + (_, _, _) => Some("App version updated. New: added sorting support and some hacks: https://github.com/sayanarijit/xplr/wiki/Hacks"), }; Ok(result) diff --git a/src/config.yml b/src/config.yml index 5246fa4a..778c386d 100644 --- a/src/config.yml +++ b/src/config.yml @@ -1,6 +1,21 @@ -version: v0.4.4 +version: v0.4.5 general: show_hidden: false + initial_sorting: + - sorter: ByIsFile + reverse: false + - sorter: ByIsDir + reverse: false + - sorter: ByIsSymlink + reverse: false + - sorter: ByIsBroken + reverse: false + - sorter: BySymlinkIsFile + reverse: false + - sorter: BySymlinkIsDir + reverse: false + - sorter: ByRelativePath + reverse: false prompt: format: "> " cursor: @@ -398,6 +413,118 @@ modes: messages: - BufferInputFromKey + sort: + name: sort + help: null + extra_help: null + key_bindings: + remaps: {} + on_key: + backspace: + help: reset + messages: + - ResetNodeSorters + - Explore + '!': + help: reverse all + messages: + - ReverseNodeSorters + - Explore + r: + help: by relative path + messages: + - AddNodeSorter: + sorter: ByRelativePath + - Explore + R: + help: by relative path reverse + messages: + - AddNodeSorter: + sorter: ByRelativePath + reverse: true + - Explore + + e: + help: by extension + messages: + - AddNodeSorter: + sorter: ByExtension + - AddNodeSorter: + sorter: BySymlinkExtension + - Explore + E: + help: by extension reverse + messages: + - AddNodeSorter: + sorter: ByExtension + reverse: true + - AddNodeSorter: + sorter: BySymlinkExtension + reverse: true + - Explore + + n: + help: by node type + messages: + - AddNodeSorter: + sorter: ByIsFile + - AddNodeSorter: + sorter: ByIsDir + - AddNodeSorter: + sorter: ByIsSymlink + - AddNodeSorter: + sorter: ByIsBroken + - AddNodeSorter: + sorter: BySymlinkIsFile + - AddNodeSorter: + sorter: BySymlinkIsDir + - Explore + N: + help: by node type reverse + messages: + - AddNodeSorter: + sorter: ByIsFile + reverse: true + - AddNodeSorter: + sorter: ByIsDir + reverse: true + - AddNodeSorter: + sorter: ByIsSymlink + reverse: true + - AddNodeSorter: + sorter: ByIsBroken + reverse: true + - AddNodeSorter: + sorter: BySymlinkIsFile + reverse: true + - AddNodeSorter: + sorter: BySymlinkIsDir + reverse: true + - Explore + + m: + help: by mime essence + messages: + - AddNodeSorter: + sorter: ByMimeEssence + - Explore + + M: + help: by mime essence reverse + messages: + - AddNodeSorter: + sorter: ByMimeEssence + reverse: true + - Explore + ctrl-c: + help: terminate + messages: + - Terminate + + default: + messages: + - SwitchMode: default + default: name: default help: null @@ -417,13 +544,17 @@ modes: help: null messages: - PrintAppStateAndQuit + s: + help: sort + messages: + - SwitchMode: sort .: help: show hidden messages: - ToggleNodeFilter: filter: RelativePathDoesNotStartWith input: . - case_sensitive: false + case_sensitive: true - Explore ':': help: action diff --git a/src/explorer.rs b/src/explorer.rs index 97328907..5b982556 100644 --- a/src/explorer.rs +++ b/src/explorer.rs @@ -18,17 +18,20 @@ pub fn explore( thread::spawn(move || { fs::read_dir(&path) .map(|dirs| { - dirs.filter_map(|d| { - d.ok().map(|e| { - e.path() - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default() + let mut nodes = dirs + .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)) - .filter(|n| config.filter(n)) - .collect::>() + .map(|name| Node::new(parent.clone(), name)) + .filter(|n| config.filter(n)) + .collect::>(); + nodes.sort_by(|a, b| config.sort(a, b)); + nodes }) .map(|nodes| { let focus_index = if let Some(focus) = focused_path { diff --git a/src/lib.rs b/src/lib.rs index 169f511f..dadddcc7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,4 +11,5 @@ pub mod explorer; pub mod input; pub mod pipe_reader; pub mod pwd_watcher; +pub mod runner; pub mod ui; diff --git a/src/main.rs b/src/main.rs index 917b2ed8..6f5e04ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,83 +1,11 @@ #![allow(clippy::too_many_arguments)] -use anyhow::Result; -use crossterm::execute; -use crossterm::terminal as term; -use handlebars::Handlebars; use std::env; -use std::fs; -use std::io; -use std::io::prelude::*; use std::path::PathBuf; -use std::process::{Command, ExitStatus, Stdio}; -use std::sync::mpsc; -use termion::get_tty; -use tui::backend::CrosstermBackend; -use tui::Terminal; use xplr::app; -use xplr::auto_refresher; -use xplr::event_reader; -use xplr::explorer; -use xplr::pipe_reader; -use xplr::pwd_watcher; -use xplr::ui; - -fn call(app: &app::App, cmd: app::Command, silent: bool) -> io::Result { - let input_buffer = app.input_buffer().unwrap_or_default(); - - let focus_index = app - .directory_buffer() - .map(|d| d.focus) - .unwrap_or_default() - .to_string(); - - let pipe_msg_in = app.pipe().msg_in.clone(); - let pipe_mode_out = app.pipe().mode_out.clone(); - let pipe_focus_out = app.pipe().focus_out.clone(); - let pipe_selection_out = app.pipe().selection_out.clone(); - let pipe_result_out = app.pipe().result_out.clone(); - let pipe_directory_nodes_out = app.pipe().directory_nodes_out.clone(); - let pipe_global_help_menu_out = app.pipe().global_help_menu_out.clone(); - let pipe_logs_out = app.pipe().logs_out.clone(); - let pipe_history_out = app.pipe().history_out.clone(); - let session_path = app.session_path(); - - let (stdin, stdout, stderr) = if silent { - (Stdio::null(), Stdio::null(), Stdio::null()) - } else { - (Stdio::inherit(), Stdio::inherit(), Stdio::inherit()) - }; - - Command::new(cmd.command.clone()) - .current_dir(app.pwd()) - .env("XPLR_APP_VERSION", app.version()) - .env("XPLR_CONFIG_VERSION", &app.config().version) - .env("XPLR_PID", &app.pid().to_string()) - .env("XPLR_INPUT_BUFFER", input_buffer) - .env("XPLR_FOCUS_PATH", app.focused_node_str()) - .env("XPLR_FOCUS_INDEX", focus_index) - .env("XPLR_SESSION_PATH", session_path) - .env("XPLR_PIPE_MSG_IN", pipe_msg_in) - .env("XPLR_PIPE_SELECTION_OUT", pipe_selection_out) - .env("XPLR_PIPE_HISTORY_OUT", pipe_history_out) - .env("XPLR_PIPE_FOCUS_OUT", pipe_focus_out) - .env("XPLR_PIPE_MODE_OUT", pipe_mode_out) - .env("XPLR_PIPE_RESULT_OUT", pipe_result_out) - .env("XPLR_PIPE_GLOBAL_HELP_MENU_OUT", pipe_global_help_menu_out) - .env("XPLR_PIPE_DIRECTORY_NODES_OUT", pipe_directory_nodes_out) - .env("XPLR_PIPE_LOGS_OUT", pipe_logs_out) - .stdin(stdin) - .stdout(stdout) - .stderr(stderr) - .args(cmd.args) - .status() -} - -fn run() -> Result> { - let (tx_msg_in, rx_msg_in) = mpsc::channel(); - let (tx_event_reader, rx_event_reader) = mpsc::channel(); - let (tx_pwd_watcher, rx_pwd_watcher) = mpsc::channel(); +use xplr::runner; +fn main() { let mut pwd = PathBuf::from(env::args().nth(1).unwrap_or_else(|| ".".into())) .canonicalize() .unwrap_or_default(); @@ -93,226 +21,12 @@ fn run() -> Result> { pwd = pwd.parent().map(|p| p.into()).unwrap_or_default(); } - let mut app = app::App::create(pwd)?; - - fs::write(&app.pipe().global_help_menu_out, app.global_help_menu_str())?; - - explorer::explore( - app.explorer_config().clone(), - app.pwd().clone(), - focused_path, - tx_msg_in.clone(), - ); - - let mut hb = Handlebars::new(); - hb.register_template_string( - app::TEMPLATE_TABLE_ROW, - &app.config() - .general - .table - .row - .cols - .to_owned() - .unwrap_or_default() - .iter() - .map(|c| c.format.clone().unwrap_or_default()) - .collect::>() - .join("\t"), - )?; - - let mut result = Ok(None); - - term::enable_raw_mode()?; - let mut stdout = get_tty()?; - // let mut stdout = stdout.lock(); - execute!(stdout, term::EnterAlternateScreen)?; - // let stdout = MouseTerminal::from(stdout); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - terminal.hide_cursor()?; - - // Threads - auto_refresher::start_auto_refreshing(tx_msg_in.clone()); - pipe_reader::keep_reading(app.pipe().msg_in.clone(), tx_msg_in.clone()); - event_reader::keep_reading(tx_msg_in.clone(), rx_event_reader); - pwd_watcher::keep_watching(app.pwd(), tx_msg_in.clone(), rx_pwd_watcher)?; - - 'outer: for task in rx_msg_in { - let last_app = app.clone(); - - let (new_app, new_result) = match app.handle_task(task) { - Ok(a) => (a, Ok(None)), - Err(err) => (last_app.clone(), Err(err)), - }; - - app = new_app; - result = new_result; - - if result.is_err() { - break; - } - - while let Some(msg) = app.pop_msg_out() { - match msg { - app::MsgOut::Enque(task) => { - tx_msg_in.send(task)?; - } - - app::MsgOut::Quit => { - result = Ok(None); - break 'outer; - } - - app::MsgOut::PrintResultAndQuit => { - result = Ok(Some(app.result_str())); - break 'outer; - } - - app::MsgOut::PrintAppStateAndQuit => { - let out = serde_yaml::to_string(&app)?; - result = Ok(Some(out)); - break 'outer; - } - - app::MsgOut::Debug(path) => { - fs::write(&path, serde_yaml::to_string(&app)?)?; - } - - app::MsgOut::ClearScreen => { - terminal.clear()?; - } - - app::MsgOut::Explore => { - explorer::explore( - app.explorer_config().clone(), - app.pwd().clone(), - app.focused_node().map(|n| n.relative_path.clone()), - tx_msg_in.clone(), - ); - } - - app::MsgOut::Refresh => { - app = app.refresh_selection()?; - if app.pwd() != last_app.pwd() { - tx_pwd_watcher.send(app.pwd().clone())?; - explorer::explore( - app.explorer_config().clone(), - app.pwd().clone(), - app.focused_node().map(|n| n.relative_path.clone()), - tx_msg_in.clone(), - ); - }; - - // UI - terminal.draw(|f| ui::draw(f, &app, &hb))?; - } - - app::MsgOut::CallSilently(cmd) => { - tx_event_reader.send(true)?; - - let status = call(&app, cmd, false) - .map(|s| { - if s.success() { - Ok(()) - } else { - Err(format!("process exited with code {}", &s)) - } - }) - .unwrap_or_else(|e| Err(e.to_string())); - - if let Err(e) = status { - let msg = app::MsgIn::External(app::ExternalMsg::LogError(e)); - tx_msg_in.send(app::Task::new(msg, None))?; - }; - - tx_event_reader.send(false)?; - } - - app::MsgOut::Call(cmd) => { - tx_event_reader.send(true)?; - - terminal.clear()?; - terminal.set_cursor(0, 0)?; - term::disable_raw_mode()?; - terminal.show_cursor()?; - - let status = call(&app, cmd, false) - .map(|s| { - if s.success() { - Ok(()) - } else { - Err(format!("process exited with code {}", &s)) - } - }) - .unwrap_or_else(|e| Err(e.to_string())); - - if let Err(e) = status { - let msg = app::MsgIn::External(app::ExternalMsg::LogError(e)); - tx_msg_in.send(app::Task::new(msg, None))?; - }; - - terminal.clear()?; - term::enable_raw_mode()?; - terminal.hide_cursor()?; - tx_event_reader.send(false)?; - } - }; - } - - if app.focused_node() != last_app.focused_node() { - fs::write(&app.pipe().focus_out, app.focused_node_str())?; - }; - - if app.selection() != last_app.selection() { - fs::write(&app.pipe().selection_out, app.selection_str())?; - }; - - if app.history_str() != last_app.history_str() { - fs::write(&app.pipe().history_out, app.history_str())?; - }; - - if app.mode_str() != last_app.mode_str() { - fs::write(&app.pipe().mode_out, app.mode_str())?; - }; - - if app.directory_buffer() != last_app.directory_buffer() { - fs::write(&app.pipe().directory_nodes_out, app.directory_nodes_str())?; - }; - - if app.logs().len() != last_app.logs().len() { - let new_logs = app - .logs() - .iter() - .skip(last_app.logs().len()) - .map(|l| format!("{}\n", l)) - .collect::>() - .join(""); - - let mut file = fs::OpenOptions::new() - .append(true) - .open(&app.pipe().logs_out)?; - - file.write_all(new_logs.as_bytes())?; - }; - - if app.result() != last_app.result() { - fs::write(&app.pipe().result_out, app.result_str())?; - }; - } - - terminal.clear()?; - terminal.set_cursor(0, 0)?; - execute!(terminal.backend_mut(), term::LeaveAlternateScreen)?; - term::disable_raw_mode()?; - terminal.show_cursor()?; - - fs::remove_dir_all(app.session_path())?; + let app = app::App::create(pwd).unwrap_or_else(|e| { + eprintln!("error: {}", e); + std::process::exit(1); + }); - result -} - -fn main() { - match run() { + match runner::run(app, focused_path) { Ok(Some(out)) => print!("{}", out), Ok(None) => {} Err(err) => { @@ -320,7 +34,7 @@ fn main() { eprintln!("error: {}", err); }; - std::process::exit(1) + std::process::exit(1); } } } diff --git a/src/runner.rs b/src/runner.rs new file mode 100644 index 00000000..2d1a6a0a --- /dev/null +++ b/src/runner.rs @@ -0,0 +1,293 @@ +#![allow(clippy::too_many_arguments)] + +use crate::app; +use crate::auto_refresher; +use crate::event_reader; +use crate::explorer; +use crate::pipe_reader; +use crate::pwd_watcher; +use crate::ui; +use anyhow::Result; +use crossterm::execute; +use crossterm::terminal as term; +use handlebars::Handlebars; +use std::fs; +use std::io; +use std::io::prelude::*; +use std::process::{Command, ExitStatus, Stdio}; +use std::sync::mpsc; +use termion::get_tty; +use tui::backend::CrosstermBackend; +use tui::Terminal; + +fn call(app: &app::App, cmd: app::Command, silent: bool) -> io::Result { + let input_buffer = app.input_buffer().unwrap_or_default(); + + let focus_index = app + .directory_buffer() + .map(|d| d.focus) + .unwrap_or_default() + .to_string(); + + let pipe_msg_in = app.pipe().msg_in.clone(); + let pipe_mode_out = app.pipe().mode_out.clone(); + let pipe_focus_out = app.pipe().focus_out.clone(); + let pipe_selection_out = app.pipe().selection_out.clone(); + let pipe_result_out = app.pipe().result_out.clone(); + let pipe_directory_nodes_out = app.pipe().directory_nodes_out.clone(); + let pipe_global_help_menu_out = app.pipe().global_help_menu_out.clone(); + let pipe_logs_out = app.pipe().logs_out.clone(); + let pipe_history_out = app.pipe().history_out.clone(); + let session_path = app.session_path(); + + let (stdin, stdout, stderr) = if silent { + (Stdio::null(), Stdio::null(), Stdio::null()) + } else { + (Stdio::inherit(), Stdio::inherit(), Stdio::inherit()) + }; + + Command::new(cmd.command.clone()) + .current_dir(app.pwd()) + .env("XPLR_APP_VERSION", app.version()) + .env("XPLR_CONFIG_VERSION", &app.config().version) + .env("XPLR_PID", &app.pid().to_string()) + .env("XPLR_INPUT_BUFFER", input_buffer) + .env("XPLR_FOCUS_PATH", app.focused_node_str()) + .env("XPLR_FOCUS_INDEX", focus_index) + .env("XPLR_SESSION_PATH", session_path) + .env("XPLR_PIPE_MSG_IN", pipe_msg_in) + .env("XPLR_PIPE_SELECTION_OUT", pipe_selection_out) + .env("XPLR_PIPE_HISTORY_OUT", pipe_history_out) + .env("XPLR_PIPE_FOCUS_OUT", pipe_focus_out) + .env("XPLR_PIPE_MODE_OUT", pipe_mode_out) + .env("XPLR_PIPE_RESULT_OUT", pipe_result_out) + .env("XPLR_PIPE_GLOBAL_HELP_MENU_OUT", pipe_global_help_menu_out) + .env("XPLR_PIPE_DIRECTORY_NODES_OUT", pipe_directory_nodes_out) + .env("XPLR_PIPE_LOGS_OUT", pipe_logs_out) + .stdin(stdin) + .stdout(stdout) + .stderr(stderr) + .args(cmd.args) + .status() +} + +pub fn run(mut app: app::App, focused_path: Option) -> Result> { + let (tx_msg_in, rx_msg_in) = mpsc::channel(); + let (tx_event_reader, rx_event_reader) = mpsc::channel(); + let (tx_pwd_watcher, rx_pwd_watcher) = mpsc::channel(); + + fs::write(&app.pipe().global_help_menu_out, app.global_help_menu_str())?; + + explorer::explore( + app.explorer_config().clone(), + app.pwd().clone(), + focused_path, + tx_msg_in.clone(), + ); + + let mut hb = Handlebars::new(); + hb.register_template_string( + app::TEMPLATE_TABLE_ROW, + &app.config() + .general + .table + .row + .cols + .to_owned() + .unwrap_or_default() + .iter() + .map(|c| c.format.clone().unwrap_or_default()) + .collect::>() + .join("\t"), + )?; + + let mut result = Ok(None); + + term::enable_raw_mode()?; + let mut stdout = get_tty()?; + // let mut stdout = stdout.lock(); + execute!(stdout, term::EnterAlternateScreen)?; + // let stdout = MouseTerminal::from(stdout); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + terminal.hide_cursor()?; + + // Threads + auto_refresher::start_auto_refreshing(tx_msg_in.clone()); + pipe_reader::keep_reading(app.pipe().msg_in.clone(), tx_msg_in.clone()); + event_reader::keep_reading(tx_msg_in.clone(), rx_event_reader); + pwd_watcher::keep_watching(app.pwd(), tx_msg_in.clone(), rx_pwd_watcher)?; + + 'outer: for task in rx_msg_in { + let last_app = app.clone(); + + let (new_app, new_result) = match app.handle_task(task) { + Ok(a) => (a, Ok(None)), + Err(err) => (last_app.clone(), Err(err)), + }; + + app = new_app; + result = new_result; + + if result.is_err() { + break; + } + + while let Some(msg) = app.pop_msg_out() { + match msg { + app::MsgOut::Enque(task) => { + tx_msg_in.send(task)?; + } + + app::MsgOut::Quit => { + result = Ok(None); + break 'outer; + } + + app::MsgOut::PrintResultAndQuit => { + result = Ok(Some(app.result_str())); + break 'outer; + } + + app::MsgOut::PrintAppStateAndQuit => { + let out = serde_yaml::to_string(&app)?; + result = Ok(Some(out)); + break 'outer; + } + + app::MsgOut::Debug(path) => { + fs::write(&path, serde_yaml::to_string(&app)?)?; + } + + app::MsgOut::ClearScreen => { + terminal.clear()?; + } + + app::MsgOut::Explore => { + explorer::explore( + app.explorer_config().clone(), + app.pwd().clone(), + app.focused_node().map(|n| n.relative_path.clone()), + tx_msg_in.clone(), + ); + } + + app::MsgOut::Refresh => { + app = app.refresh_selection()?; + if app.pwd() != last_app.pwd() { + tx_pwd_watcher.send(app.pwd().clone())?; + explorer::explore( + app.explorer_config().clone(), + app.pwd().clone(), + app.focused_node().map(|n| n.relative_path.clone()), + tx_msg_in.clone(), + ); + }; + + // UI + terminal.draw(|f| ui::draw(f, &app, &hb))?; + } + + app::MsgOut::CallSilently(cmd) => { + tx_event_reader.send(true)?; + + let status = call(&app, cmd, false) + .map(|s| { + if s.success() { + Ok(()) + } else { + Err(format!("process exited with code {}", &s)) + } + }) + .unwrap_or_else(|e| Err(e.to_string())); + + if let Err(e) = status { + let msg = app::MsgIn::External(app::ExternalMsg::LogError(e)); + tx_msg_in.send(app::Task::new(msg, None))?; + }; + + tx_event_reader.send(false)?; + } + + app::MsgOut::Call(cmd) => { + tx_event_reader.send(true)?; + + terminal.clear()?; + terminal.set_cursor(0, 0)?; + term::disable_raw_mode()?; + terminal.show_cursor()?; + + let status = call(&app, cmd, false) + .map(|s| { + if s.success() { + Ok(()) + } else { + Err(format!("process exited with code {}", &s)) + } + }) + .unwrap_or_else(|e| Err(e.to_string())); + + if let Err(e) = status { + let msg = app::MsgIn::External(app::ExternalMsg::LogError(e)); + tx_msg_in.send(app::Task::new(msg, None))?; + }; + + terminal.clear()?; + term::enable_raw_mode()?; + terminal.hide_cursor()?; + tx_event_reader.send(false)?; + } + }; + } + + if app.focused_node() != last_app.focused_node() { + fs::write(&app.pipe().focus_out, app.focused_node_str())?; + }; + + if app.selection() != last_app.selection() { + fs::write(&app.pipe().selection_out, app.selection_str())?; + }; + + if app.history_str() != last_app.history_str() { + fs::write(&app.pipe().history_out, app.history_str())?; + }; + + if app.mode_str() != last_app.mode_str() { + fs::write(&app.pipe().mode_out, app.mode_str())?; + }; + + if app.directory_buffer() != last_app.directory_buffer() { + fs::write(&app.pipe().directory_nodes_out, app.directory_nodes_str())?; + }; + + if app.logs().len() != last_app.logs().len() { + let new_logs = app + .logs() + .iter() + .skip(last_app.logs().len()) + .map(|l| format!("{}\n", l)) + .collect::>() + .join(""); + + let mut file = fs::OpenOptions::new() + .append(true) + .open(&app.pipe().logs_out)?; + + file.write_all(new_logs.as_bytes())?; + }; + + if app.result() != last_app.result() { + fs::write(&app.pipe().result_out, app.result_str())?; + }; + } + + terminal.clear()?; + terminal.set_cursor(0, 0)?; + execute!(terminal.backend_mut(), term::LeaveAlternateScreen)?; + term::disable_raw_mode()?; + terminal.show_cursor()?; + + fs::remove_dir_all(app.session_path())?; + + result +} diff --git a/src/ui.rs b/src/ui.rs index 4e00dc31..45d428c5 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -395,6 +395,25 @@ fn draw_input_buffer(f: &mut Frame, rect: Rect, app: &app::App, _ f.render_widget(input_buf, rect); } +fn draw_sort_n_filter_by(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { + let filter_by = app.explorer_config().filters(); + let sort_by = app.explorer_config().sorters(); + let p = Paragraph::new( + filter_by + .iter() + .map(|s| s.to_string()) + .chain(sort_by.iter().map(|f| f.to_string())) + .collect::>() + .join(" › "), + ) + .block(Block::default().borders(Borders::ALL).title(format!( + " Sort & filter ({}) ", + filter_by.len() + sort_by.len() + ))); + + f.render_widget(p, rect); +} + fn draw_logs(f: &mut Frame, rect: Rect, app: &app::App, _: &Handlebars) { let config = app.config().general.logs.clone(); let logs = app @@ -452,19 +471,21 @@ pub fn draw(f: &mut Frame, app: &app::App, hb: &Handlebars) { .direction(Direction::Vertical) .constraints( [ - TuiConstraint::Length(rect.height - 3), + TuiConstraint::Length(3), + TuiConstraint::Length(rect.height - 6), TuiConstraint::Length(3), ] .as_ref(), ) .split(chunks[0]); - draw_table(f, left_chunks[0], app, hb); + draw_sort_n_filter_by(f, left_chunks[0], app, hb); + draw_table(f, left_chunks[1], app, hb); if app.input_buffer().is_some() { - draw_input_buffer(f, left_chunks[1], app, hb); + draw_input_buffer(f, left_chunks[2], app, hb); } else { - draw_logs(f, left_chunks[1], app, hb); + draw_logs(f, left_chunks[2], app, hb); }; let right_chunks = Layout::default() diff --git a/tests/bdd/mod.rs b/tests/bdd/mod.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/bdd/mod.rs @@ -0,0 +1 @@ + diff --git a/tests/mod.rs b/tests/mod.rs new file mode 100644 index 00000000..9841387d --- /dev/null +++ b/tests/mod.rs @@ -0,0 +1,2 @@ +mod bdd; +mod unit; diff --git a/tests/test_config.rs b/tests/unit/config.rs similarity index 100% rename from tests/test_config.rs rename to tests/unit/config.rs diff --git a/tests/unit/mod.rs b/tests/unit/mod.rs new file mode 100644 index 00000000..b9e069dc --- /dev/null +++ b/tests/unit/mod.rs @@ -0,0 +1,2 @@ +mod config; +mod ui; diff --git a/tests/test_ui.rs b/tests/unit/ui.rs similarity index 100% rename from tests/test_ui.rs rename to tests/unit/ui.rs