diff --git a/Cargo.lock b/Cargo.lock index 6879f844..93eddc3b 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" @@ -665,6 +794,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + [[package]] name = "net2" version = "0.2.37" @@ -846,6 +981,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 +1095,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 +1230,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 +1268,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 +1375,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 +1624,7 @@ dependencies = [ [[package]] name = "xplr" -version = "0.4.4" +version = "0.5.0" dependencies = [ "anyhow", "chrono", @@ -1370,10 +1632,14 @@ dependencies = [ "crossterm", "dirs", "handlebars", + "indexmap", "lazy_static", "mime_guess", + "natord", "notify", + "rspec", "serde", + "serde_json", "serde_yaml", "termion", "tui", diff --git a/Cargo.toml b/Cargo.toml index 813e9771..4b29e1c3 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.5.0" # 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,13 @@ 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"] } +natord = "1.0.9" [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..d6ef89e7 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; @@ -84,7 +84,7 @@ impl Pipe { } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct SymlinkNode { +pub struct ResolvedNode { pub absolute_path: String, pub extension: String, pub is_dir: bool, @@ -93,7 +93,7 @@ pub struct SymlinkNode { pub mime_essence: String, } -impl SymlinkNode { +impl ResolvedNode { pub fn from(path: PathBuf) -> Self { let extension = path .extension() @@ -138,7 +138,8 @@ pub struct Node { pub is_broken: bool, pub is_readonly: bool, pub mime_essence: String, - pub symlink: Option, + pub canonical: Option, + pub symlink: Option, } impl Node { @@ -162,13 +163,10 @@ impl Node { .map(|m| m.file_type().is_symlink()) .unwrap_or(false); - let (is_broken, maybe_symlink_meta) = if is_symlink { - path.canonicalize() - .map(|p| (false, Some(SymlinkNode::from(p)))) - .unwrap_or_else(|_| (true, None)) - } else { - (false, None) - }; + let (is_broken, maybe_canonical_meta) = path + .canonicalize() + .map(|p| (false, Some(ResolvedNode::from(p)))) + .unwrap_or_else(|_| (true, None)); let is_dir = maybe_metadata.clone().map(|m| m.is_dir()).unwrap_or(false); @@ -194,7 +192,12 @@ impl Node { is_broken, is_readonly, mime_essence, - symlink: maybe_symlink_meta, + canonical: maybe_canonical_meta.clone(), + symlink: if is_symlink { + maybe_canonical_meta + } else { + None + }, } } } @@ -243,194 +246,441 @@ pub enum InternalMsg { HandleKey(Key), } +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum NodeSorter { + ByRelativePath, + ByIRelativePath, + ByExtension, + ByIsDir, + ByIsFile, + ByIsSymlink, + ByIsBroken, + ByIsReadonly, + ByMimeEssence, + + ByCanonicalAbsolutePath, + ByICanonicalAbsolutePath, + ByCanonicalExtension, + ByCanonicalIsDir, + ByCanonicalIsFile, + ByCanonicalIsReadonly, + ByCanonicalMimeEssence, + + BySymlinkAbsolutePath, + ByISymlinkAbsolutePath, + BySymlinkExtension, + BySymlinkIsDir, + BySymlinkIsFile, + BySymlinkIsReadonly, + BySymlinkMimeEssence, +} + +#[derive(Debug, Clone, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct NodeSorterApplicable { + pub sorter: NodeSorter, + #[serde(default)] + pub reverse: bool, +} + +impl PartialEq for NodeSorterApplicable { + fn eq(&self, other: &NodeSorterApplicable) -> bool { + self.sorter == other.sorter + } +} + +impl std::hash::Hash for NodeSorterApplicable { + fn hash(&self, state: &mut H) { + self.sorter.hash(state); + } +} + +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, false) => { + natord::compare(&a.relative_path, &b.relative_path) + } + (NodeSorter::ByIRelativePath, false) => { + natord::compare_ignore_case(&a.relative_path, &b.relative_path) + } + (NodeSorter::ByRelativePath, true) => { + natord::compare(&b.relative_path, &a.relative_path) + } + (NodeSorter::ByIRelativePath, true) => { + natord::compare_ignore_case(&b.relative_path, &a.relative_path) + } + (NodeSorter::ByExtension, false) => a.extension.cmp(&b.extension), + (NodeSorter::ByExtension, true) => b.extension.cmp(&a.extension), + (NodeSorter::ByIsDir, false) => a.is_dir.cmp(&b.is_dir), + (NodeSorter::ByIsDir, true) => b.is_dir.cmp(&a.is_dir), + (NodeSorter::ByIsFile, false) => a.is_file.cmp(&b.is_file), + (NodeSorter::ByIsFile, true) => b.is_file.cmp(&a.is_file), + (NodeSorter::ByIsSymlink, false) => a.is_symlink.cmp(&b.is_symlink), + (NodeSorter::ByIsSymlink, true) => b.is_symlink.cmp(&a.is_symlink), + (NodeSorter::ByIsBroken, false) => a.is_broken.cmp(&b.is_broken), + (NodeSorter::ByIsBroken, true) => b.is_broken.cmp(&a.is_broken), + (NodeSorter::ByIsReadonly, false) => a.is_readonly.cmp(&b.is_readonly), + (NodeSorter::ByIsReadonly, true) => b.is_readonly.cmp(&a.is_readonly), + (NodeSorter::ByMimeEssence, false) => a.mime_essence.cmp(&b.mime_essence), + (NodeSorter::ByMimeEssence, true) => b.mime_essence.cmp(&a.mime_essence), + + (NodeSorter::ByCanonicalAbsolutePath, false) => natord::compare( + &a.canonical + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + &b.canonical + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + ), + + (NodeSorter::ByICanonicalAbsolutePath, false) => natord::compare_ignore_case( + &a.canonical + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + &b.canonical + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + ), + + (NodeSorter::ByCanonicalAbsolutePath, true) => natord::compare( + &b.canonical + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + &a.canonical + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + ), + + (NodeSorter::ByICanonicalAbsolutePath, true) => natord::compare_ignore_case( + &b.canonical + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + &a.canonical + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + ), + + (NodeSorter::ByCanonicalExtension, false) => a + .canonical + .as_ref() + .map(|s| &s.extension) + .cmp(&b.canonical.as_ref().map(|s| &s.extension)), + + (NodeSorter::ByCanonicalExtension, true) => b + .canonical + .as_ref() + .map(|s| &s.extension) + .cmp(&a.canonical.as_ref().map(|s| &s.extension)), + + (NodeSorter::ByCanonicalIsDir, false) => a + .canonical + .as_ref() + .map(|s| &s.is_dir) + .cmp(&b.canonical.as_ref().map(|s| &s.is_dir)), + + (NodeSorter::ByCanonicalIsFile, true) => b + .canonical + .as_ref() + .map(|s| &s.is_file) + .cmp(&a.canonical.as_ref().map(|s| &s.is_file)), + + (NodeSorter::ByCanonicalIsDir, true) => b + .canonical + .as_ref() + .map(|s| &s.is_dir) + .cmp(&a.canonical.as_ref().map(|s| &s.is_dir)), + + (NodeSorter::ByCanonicalIsReadonly, true) => b + .canonical + .as_ref() + .map(|s| &s.is_readonly) + .cmp(&a.canonical.as_ref().map(|s| &s.is_readonly)), + + (NodeSorter::ByCanonicalIsFile, false) => a + .canonical + .as_ref() + .map(|s| &s.is_file) + .cmp(&b.canonical.as_ref().map(|s| &s.is_file)), + + (NodeSorter::ByCanonicalMimeEssence, true) => b + .canonical + .as_ref() + .map(|s| &s.mime_essence) + .cmp(&a.canonical.as_ref().map(|s| &s.mime_essence)), + + (NodeSorter::ByCanonicalIsReadonly, false) => a + .canonical + .as_ref() + .map(|s| &s.is_readonly) + .cmp(&b.canonical.as_ref().map(|s| &s.is_readonly)), + + (NodeSorter::ByCanonicalMimeEssence, false) => a + .canonical + .as_ref() + .map(|s| &s.mime_essence) + .cmp(&b.canonical.as_ref().map(|s| &s.mime_essence)), + + (NodeSorter::BySymlinkAbsolutePath, false) => natord::compare( + &a.symlink + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + &b.symlink + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + ), + + (NodeSorter::ByISymlinkAbsolutePath, false) => natord::compare_ignore_case( + &a.symlink + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + &b.symlink + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + ), + + (NodeSorter::BySymlinkAbsolutePath, true) => natord::compare( + &b.symlink + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + &a.symlink + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + ), + + (NodeSorter::ByISymlinkAbsolutePath, true) => natord::compare_ignore_case( + &b.symlink + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + &a.symlink + .as_ref() + .map(|s| s.absolute_path.clone()) + .unwrap_or_default(), + ), + + (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 { RelativePathIs, RelativePathIsNot, + IRelativePathIs, + IRelativePathIsNot, + RelativePathDoesStartWith, RelativePathDoesNotStartWith, + IRelativePathDoesStartWith, + IRelativePathDoesNotStartWith, + RelativePathDoesContain, RelativePathDoesNotContain, + IRelativePathDoesContain, + IRelativePathDoesNotContain, + RelativePathDoesEndWith, RelativePathDoesNotEndWith, + IRelativePathDoesEndWith, + IRelativePathDoesNotEndWith, + AbsolutePathIs, AbsolutePathIsNot, + IAbsolutePathIs, + IAbsolutePathIsNot, + AbsolutePathDoesStartWith, AbsolutePathDoesNotStartWith, + IAbsolutePathDoesStartWith, + IAbsolutePathDoesNotStartWith, + AbsolutePathDoesContain, AbsolutePathDoesNotContain, + IAbsolutePathDoesContain, + IAbsolutePathDoesNotContain, + AbsolutePathDoesEndWith, AbsolutePathDoesNotEndWith, + + IAbsolutePathDoesEndWith, + IAbsolutePathDoesNotEndWith, } impl NodeFilter { - fn apply(&self, node: &Node, input: &str, case_sensitive: bool) -> bool { + fn apply(&self, node: &Node, input: &str) -> bool { match self { - Self::RelativePathIs => { - if case_sensitive { - node.relative_path == input - } else { - node.relative_path.to_lowercase() == input.to_lowercase() - } - } - - Self::RelativePathIsNot => { - if case_sensitive { - node.relative_path != input - } else { - node.relative_path.to_lowercase() != input.to_lowercase() - } - } - - Self::RelativePathDoesStartWith => { - if case_sensitive { - node.relative_path.starts_with(input) - } else { - node.relative_path - .to_lowercase() - .starts_with(&input.to_lowercase()) - } - } - - Self::RelativePathDoesNotStartWith => { - if case_sensitive { - !node.relative_path.starts_with(input) - } else { - !node - .relative_path - .to_lowercase() - .starts_with(&input.to_lowercase()) - } - } - - Self::RelativePathDoesContain => { - if case_sensitive { - node.relative_path.contains(input) - } else { - node.relative_path - .to_lowercase() - .contains(&input.to_lowercase()) - } - } - - Self::RelativePathDoesNotContain => { - if case_sensitive { - !node.relative_path.contains(input) - } else { - !node - .relative_path - .to_lowercase() - .contains(&input.to_lowercase()) - } - } - - Self::RelativePathDoesEndWith => { - if case_sensitive { - node.relative_path.ends_with(input) - } else { - node.relative_path - .to_lowercase() - .ends_with(&input.to_lowercase()) - } - } - - Self::RelativePathDoesNotEndWith => { - if case_sensitive { - !node.relative_path.ends_with(input) - } else { - !node - .relative_path - .to_lowercase() - .ends_with(&input.to_lowercase()) - } - } - - Self::AbsolutePathIs => { - if case_sensitive { - node.absolute_path == input - } else { - node.absolute_path.to_lowercase() == input.to_lowercase() - } - } - - Self::AbsolutePathIsNot => { - if case_sensitive { - node.absolute_path != input - } else { - node.absolute_path.to_lowercase() != input.to_lowercase() - } - } - - Self::AbsolutePathDoesStartWith => { - if case_sensitive { - node.absolute_path.starts_with(input) - } else { - node.absolute_path - .to_lowercase() - .starts_with(&input.to_lowercase()) - } - } - - Self::AbsolutePathDoesNotStartWith => { - if case_sensitive { - !node.absolute_path.starts_with(input) - } else { - !node - .absolute_path - .to_lowercase() - .starts_with(&input.to_lowercase()) - } - } - - Self::AbsolutePathDoesContain => { - if case_sensitive { - node.absolute_path.contains(input) - } else { - node.absolute_path - .to_lowercase() - .contains(&input.to_lowercase()) - } - } - - Self::AbsolutePathDoesNotContain => { - if case_sensitive { - !node.absolute_path.contains(input) - } else { - !node - .absolute_path - .to_lowercase() - .contains(&input.to_lowercase()) - } - } - - Self::AbsolutePathDoesEndWith => { - if case_sensitive { - node.absolute_path.ends_with(input) - } else { - node.absolute_path - .to_lowercase() - .ends_with(&input.to_lowercase()) - } - } - - Self::AbsolutePathDoesNotEndWith => { - if case_sensitive { - !node.absolute_path.ends_with(input) - } else { - !node - .absolute_path - .to_lowercase() - .ends_with(&input.to_lowercase()) - } - } + Self::RelativePathIs => node.relative_path.eq(input), + Self::IRelativePathIs => node.relative_path.eq_ignore_ascii_case(input), + + Self::RelativePathIsNot => !node.relative_path.eq(input), + Self::IRelativePathIsNot => !node.relative_path.eq_ignore_ascii_case(input), + + Self::RelativePathDoesStartWith => node.relative_path.starts_with(input), + Self::IRelativePathDoesStartWith => node + .relative_path + .to_lowercase() + .starts_with(&input.to_lowercase()), + + Self::RelativePathDoesNotStartWith => !node.relative_path.starts_with(input), + + Self::IRelativePathDoesNotStartWith => !node + .relative_path + .to_lowercase() + .starts_with(&input.to_lowercase()), + + Self::RelativePathDoesContain => node.relative_path.contains(input), + Self::IRelativePathDoesContain => node + .relative_path + .to_lowercase() + .contains(&input.to_lowercase()), + + Self::RelativePathDoesNotContain => !node.relative_path.contains(input), + Self::IRelativePathDoesNotContain => !node + .relative_path + .to_lowercase() + .contains(&input.to_lowercase()), + + Self::RelativePathDoesEndWith => node.relative_path.ends_with(input), + Self::IRelativePathDoesEndWith => node + .relative_path + .to_lowercase() + .ends_with(&input.to_lowercase()), + + Self::RelativePathDoesNotEndWith => !node.relative_path.ends_with(input), + Self::IRelativePathDoesNotEndWith => !node + .relative_path + .to_lowercase() + .ends_with(&input.to_lowercase()), + + Self::AbsolutePathIs => node.absolute_path.eq(input), + Self::IAbsolutePathIs => node.absolute_path.eq_ignore_ascii_case(input), + + Self::AbsolutePathIsNot => !node.absolute_path.eq(input), + Self::IAbsolutePathIsNot => !node.absolute_path.eq_ignore_ascii_case(input), + + Self::AbsolutePathDoesStartWith => node.absolute_path.starts_with(input), + Self::IAbsolutePathDoesStartWith => node + .absolute_path + .to_lowercase() + .starts_with(&input.to_lowercase()), + + Self::AbsolutePathDoesNotStartWith => !node.absolute_path.starts_with(input), + Self::IAbsolutePathDoesNotStartWith => !node + .absolute_path + .to_lowercase() + .starts_with(&input.to_lowercase()), + + Self::AbsolutePathDoesContain => node.absolute_path.contains(input), + Self::IAbsolutePathDoesContain => node + .absolute_path + .to_lowercase() + .contains(&input.to_lowercase()), + + Self::AbsolutePathDoesNotContain => !node.absolute_path.contains(input), + Self::IAbsolutePathDoesNotContain => !node + .absolute_path + .to_lowercase() + .contains(&input.to_lowercase()), + + Self::AbsolutePathDoesEndWith => node.absolute_path.ends_with(input), + Self::IAbsolutePathDoesEndWith => node + .absolute_path + .to_lowercase() + .ends_with(&input.to_lowercase()), + + Self::AbsolutePathDoesNotEndWith => !node.absolute_path.ends_with(input), + Self::IAbsolutePathDoesNotEndWith => !node + .absolute_path + .to_lowercase() + .ends_with(&input.to_lowercase()), } } } @@ -438,43 +688,48 @@ impl NodeFilter { #[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct NodeFilterApplicable { - filter: NodeFilter, - input: String, - #[serde(default)] - case_sensitive: bool, + pub filter: NodeFilter, + pub input: String, } impl NodeFilterApplicable { - pub fn new(filter: NodeFilter, input: String, case_sensitive: bool) -> Self { - Self { - filter, - input, - case_sensitive, - } + pub fn new(filter: NodeFilter, input: String) -> Self { + Self { filter, input } } fn apply(&self, node: &Node) -> bool { - self.filter.apply(node, &self.input, self.case_sensitive) + self.filter.apply(node, &self.input) } } -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct NodeFilterFromInput { - filter: NodeFilter, - #[serde(default)] - case_sensitive: bool, -} - #[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 +904,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), @@ -666,17 +921,49 @@ pub enum ExternalMsg { /// Add a node filter reading the input from the buffer. /// - /// Example: `AddNodeFilterFromInput: {filter: RelativePathDoesStartWith}` - AddNodeFilterFromInput(NodeFilterFromInput), + /// Example: `AddNodeFilterFromInput: RelativePathDoesStartWith` + AddNodeFilterFromInput(NodeFilter), /// Remove a node filter reading the input from the buffer. /// - /// Example: `RemoveNodeFilterFromInput: {filter: RelativePathDoesStartWith}` - RemoveNodeFilterFromInput(NodeFilterFromInput), + /// Example: `RemoveNodeFilterFromInput: RelativePathDoesStartWith` + RemoveNodeFilterFromInput(NodeFilter), /// Reset the node filters back to the default configuration. ResetNodeFilters, + /// Clear all the node filters. + ClearNodeFilters, + + /// 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, + + /// Clear all the node sorters. + ClearNodeSorters, + /// Log information message. /// /// Example: `LogInfo: launching satellite` @@ -888,13 +1175,16 @@ 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(), )); } + 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 +1303,14 @@ 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::ClearNodeFilters => self.clear_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::ClearNodeSorters => self.clear_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 +1640,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 +1679,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,41 +1694,28 @@ 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) } - fn add_node_filter_from_input(mut self, filter: NodeFilterFromInput) -> Result { + fn add_node_filter_from_input(mut self, filter: NodeFilter) -> Result { if let Some(input) = self.input_buffer() { self.explorer_config .filters - .insert(NodeFilterApplicable::new( - filter.filter, - input, - filter.case_sensitive, - )); - self.msg_out.push_back(MsgOut::Refresh); + .insert(NodeFilterApplicable::new(filter, input)); }; Ok(self) } fn remove_node_filter(mut self, filter: NodeFilterApplicable) -> Result { - self.explorer_config.filters.remove(&filter); - self.msg_out.push_back(MsgOut::Refresh); + self.explorer_config.filters.retain(|f| f != &filter); Ok(self) } - fn remove_node_filter_from_input(mut self, filter: NodeFilterFromInput) -> Result { + fn remove_node_filter_from_input(mut self, filter: NodeFilter) -> Result { if let Some(input) = self.input_buffer() { - self.explorer_config - .filters - .remove(&NodeFilterApplicable::new( - filter.filter, - input, - filter.case_sensitive, - )); - self.msg_out.push_back(MsgOut::Refresh); + let nfa = NodeFilterApplicable::new(filter, input); + self.explorer_config.filters.retain(|f| f != &nfa); }; Ok(self) } @@ -1464,13 +1735,68 @@ impl App { self.add_node_filter(NodeFilterApplicable::new( NodeFilter::RelativePathDoesNotStartWith, ".".into(), - Default::default(), )) } else { - self.msg_out.push_back(MsgOut::Refresh); Ok(self) } } + fn clear_node_filters(mut self) -> Result { + self.explorer_config.filters.clear(); + 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 = self + .config + .general + .initial_sorting + .to_owned() + .unwrap_or_default(); + Ok(self) + } + + fn clear_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)); diff --git a/src/config.rs b/src/config.rs index ca7332dc..49f6c758 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,12 @@ use crate::app::ExternalMsg; use crate::app::HelpMenuLine; +use crate::app::NodeFilter; +use crate::app::NodeSorter; +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; @@ -224,6 +228,52 @@ impl LogsConfig { } } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SortDirectionIdentifiersUi { + #[serde(default)] + pub forward: UiElement, + + #[serde(default)] + pub reverse: UiElement, +} + +impl SortDirectionIdentifiersUi { + pub fn extend(mut self, other: Self) -> Self { + self.forward = self.forward.extend(other.forward); + self.reverse = self.reverse.extend(other.reverse); + self + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SortAndFilterUi { + #[serde(default)] + pub separator: UiElement, + + #[serde(default)] + pub sort_direction_identifiers: SortDirectionIdentifiersUi, + + #[serde(default)] + pub sorter_identifiers: HashMap, + + #[serde(default)] + pub filter_identifiers: HashMap, +} + +impl SortAndFilterUi { + pub fn extend(mut self, other: Self) -> Self { + self.separator = self.separator.extend(other.separator); + self.sort_direction_identifiers = self + .sort_direction_identifiers + .extend(other.sort_direction_identifiers); + self.sorter_identifiers.extend(other.sorter_identifiers); + self.filter_identifiers.extend(other.filter_identifiers); + self + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct GeneralConfig { @@ -250,6 +300,12 @@ pub struct GeneralConfig { #[serde(default)] pub selection_ui: UiConfig, + + #[serde(default)] + pub sort_and_filter_ui: SortAndFilterUi, + + #[serde(default)] + pub initial_sorting: Option>, } impl GeneralConfig { @@ -262,6 +318,8 @@ 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.sort_and_filter_ui = self.sort_and_filter_ui.extend(other.sort_and_filter_ui); + self.initial_sorting = other.initial_sorting.or(self.initial_sorting); self } } @@ -419,6 +477,9 @@ pub struct BuiltinModesConfig { #[serde(default)] pub search: Mode, + + #[serde(default)] + pub sort: Mode, } impl BuiltinModesConfig { @@ -434,6 +495,7 @@ impl BuiltinModesConfig { self.number = self.number.extend(other.number); self.action = self.action.extend(other.action); self.search = self.search.extend(other.search); + self.sort = self.sort.extend(other.sort); self } @@ -454,6 +516,7 @@ impl BuiltinModesConfig { "delete" => Some(&self.delete), "action" => Some(&self.action), "search" => Some(&self.search), + "sort" => Some(&self.sort), _ => None, } } @@ -532,11 +595,7 @@ impl Config { pub fn is_compatible(&self) -> Result { let result = match self.parsed_version()? { - (0, 4, 4) => true, - (0, 4, 3) => true, - (0, 4, 2) => true, - (0, 4, 1) => true, - (0, 4, 0) => true, + (0, 5, 0) => true, (_, _, _) => false, }; @@ -544,11 +603,12 @@ 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"), + Ok(None) + /* let result = match self.parsed_version()? { + (0, 5, 0) => None, + (_, _, _) => Some("App version updated. New: added sorting support and some hacks: https://github.com/sayanarijit/xplr/wiki/Hacks"), }; - Ok(result) + Ok(result) */ } } diff --git a/src/config.yml b/src/config.yml index 5246fa4a..38b16a08 100644 --- a/src/config.yml +++ b/src/config.yml @@ -1,6 +1,11 @@ -version: v0.4.4 +version: v0.5.0 general: show_hidden: false + initial_sorting: + - sorter: ByCanonicalIsDir + reverse: true + - sorter: ByIRelativePath + reverse: false prompt: format: "> " cursor: @@ -159,6 +164,129 @@ general: sub_modifier: bits: 0 + sort_and_filter_ui: + separator: + format: " › " + sort_direction_identifiers: + forward: + format: "↓" + reverse: + format: "↑" + + sorter_identifiers: + ByRelativePath: + format: "rel" + ByIRelativePath: + format: "[i]rel" + ByExtension: + format: "ext" + ByIsDir: + format: "dir" + ByIsFile: + format: "file" + ByIsSymlink: + format: "sym" + ByIsBroken: + format: "⨯" + ByIsReadonly: + format: "ro" + ByMimeEssence: + format: "mime" + ByCanonicalAbsolutePath: + format: "[c]abs" + ByICanonicalAbsolutePath: + format: "[ci]abs" + ByCanonicalExtension: + format: "[c]ext" + ByCanonicalIsDir: + format: "[c]dir" + ByCanonicalIsFile: + format: "[c]file" + ByCanonicalIsReadonly: + format: "[c]ro" + ByCanonicalMimeEssence: + format: "[c]mime" + BySymlinkAbsolutePath: + format: "[s]abs" + ByISymlinkAbsolutePath: + format: "[si]abs" + BySymlinkExtension: + format: "[s]ext" + BySymlinkIsDir: + format: "[s]dir" + BySymlinkIsFile: + format: "[s]file" + BySymlinkIsReadonly: + format: "[s]ro" + BySymlinkMimeEssence: + format: "[s]mime" + + filter_identifiers: + RelativePathIs: + format: "rel==" + IRelativePathIs: + format: "[i]rel==" + RelativePathIsNot: + format: "rel!=" + IRelativePathIsNot: + format: "[i]rel!=" + RelativePathDoesStartWith: + format: "rel=^" + IRelativePathDoesStartWith: + format: "[i]rel=^" + RelativePathDoesNotStartWith: + format: "rel!^" + IRelativePathDoesNotStartWith: + format: "[i]rel!^" + RelativePathDoesContain: + format: "rel=~" + IRelativePathDoesContain: + format: "[i]rel=~" + RelativePathDoesNotContain: + format: "rel!~" + IRelativePathDoesNotContain: + format: "[i]rel!~" + RelativePathDoesEndWith: + format: "rel=$" + IRelativePathDoesEndWith: + format: "[i]rel=$" + RelativePathDoesNotEndWith: + format: "rel!$" + IRelativePathDoesNotEndWith: + format: "[i]rel!$" + AbsolutePathIs: + format: "abs==" + IAbsolutePathIs: + format: "[i]abs==" + AbsolutePathIsNot: + format: "abs!=" + IAbsolutePathIsNot: + format: "[i]abs!=" + AbsolutePathDoesStartWith: + format: "abs=^" + IAbsolutePathDoesStartWith: + format: "[i]abs=^" + AbsolutePathDoesNotStartWith: + format: "abs!^" + IAbsolutePathDoesNotStartWith: + format: "[i]abs!^" + AbsolutePathDoesContain: + format: "abs=~" + IAbsolutePathDoesContain: + format: "[i]abs=~" + AbsolutePathDoesNotContain: + format: "abs!~" + IAbsolutePathDoesNotContain: + format: "[i]abs!~" + AbsolutePathDoesEndWith: + format: "abs=$" + IAbsolutePathDoesEndWith: + format: "[i]abs=$" + AbsolutePathDoesNotEndWith: + format: "abs!$" + IAbsolutePathDoesNotEndWith: + format: "[i]abs!$" + node_types: directory: style: @@ -398,6 +526,102 @@ modes: messages: - BufferInputFromKey + sort: + name: sort + help: null + extra_help: null + key_bindings: + remaps: {} + on_key: + backspace: + help: clear + messages: + - ClearNodeSorters + - Explore + '!': + help: reverse all + messages: + - ReverseNodeSorters + - Explore + ctrl-r: + help: reset + messages: + - ResetNodeSorters + - Explore + r: + help: by relative path + messages: + - AddNodeSorter: + sorter: ByIRelativePath + - Explore + R: + help: by relative path reverse + messages: + - AddNodeSorter: + sorter: ByIRelativePath + reverse: true + - Explore + + e: + help: by canonical extension + messages: + - AddNodeSorter: + sorter: ByCanonicalExtension + - Explore + E: + help: by canonical extension reverse + messages: + - AddNodeSorter: + sorter: ByCanonicalExtension + reverse: true + - Explore + n: + help: by node type + messages: + - AddNodeSorter: + sorter: ByCanonicalIsDir + - AddNodeSorter: + sorter: ByCanonicalIsFile + - AddNodeSorter: + sorter: ByIsSymlink + - Explore + N: + help: by node type reverse + messages: + - AddNodeSorter: + sorter: ByCanonicalIsDir + reverse: true + - AddNodeSorter: + sorter: ByCanonicalIsFile + reverse: true + - AddNodeSorter: + sorter: ByIsSymlink + reverse: true + - Explore + + m: + help: by canonical mime essence + messages: + - AddNodeSorter: + sorter: ByCanonicalMimeEssence + - Explore + + M: + help: by canonical mime essence reverse + messages: + - AddNodeSorter: + sorter: ByCanonicalMimeEssence + reverse: true + - Explore + ctrl-c: + help: terminate + messages: + - Terminate + + default: + messages: + - SwitchMode: default + default: name: default help: null @@ -417,13 +641,16 @@ modes: help: null messages: - PrintAppStateAndQuit + s: + help: sort + messages: + - SwitchMode: sort .: help: show hidden messages: - ToggleNodeFilter: filter: RelativePathDoesNotStartWith input: . - case_sensitive: false - Explore ':': help: action @@ -718,9 +945,7 @@ modes: backspace: help: clear messages: - - RemoveNodeFilterFromInput: - filter: RelativePathDoesContain - case_sensitive: false + - RemoveNodeFilterFromInput: RelativePathDoesContain - SetInputBuffer: '' - Explore ctrl-c: @@ -734,34 +959,26 @@ modes: enter: help: focus messages: - - RemoveNodeFilterFromInput: - filter: RelativePathDoesContain - case_sensitive: false + - RemoveNodeFilterFromInput: RelativePathDoesContain - SwitchMode: default - Explore esc: help: cancel messages: - - RemoveNodeFilterFromInput: - filter: RelativePathDoesContain - case_sensitive: false + - RemoveNodeFilterFromInput: RelativePathDoesContain - SwitchMode: default - Explore left: help: back messages: - - RemoveNodeFilterFromInput: - filter: RelativePathDoesContain - case_sensitive: false + - RemoveNodeFilterFromInput: RelativePathDoesContain - Back - SetInputBuffer: '' - Explore right: help: enter messages: - - RemoveNodeFilterFromInput: - filter: RelativePathDoesContain - case_sensitive: false + - RemoveNodeFilterFromInput: RelativePathDoesContain - Enter - SetInputBuffer: '' - Explore @@ -775,13 +992,9 @@ modes: default: help: null messages: - - RemoveNodeFilterFromInput: - filter: RelativePathDoesContain - case_sensitive: false + - RemoveNodeFilterFromInput: RelativePathDoesContain - BufferInputFromKey - - AddNodeFilterFromInput: - filter: RelativePathDoesContain - case_sensitive: false + - AddNodeFilterFromInput: RelativePathDoesContain - Explore custom: {} 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..ca814a25 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,6 +1,6 @@ use crate::app; use crate::app::HelpMenuLine; -use crate::app::{Node, SymlinkNode}; +use crate::app::{Node, ResolvedNode}; use handlebars::Handlebars; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; @@ -67,7 +67,7 @@ impl Into for Style { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct SymlinkNodeUiMetadata { +pub struct ResolvedNodeUiMetadata { pub absolute_path: String, pub extension: String, pub is_dir: bool, @@ -76,8 +76,8 @@ pub struct SymlinkNodeUiMetadata { pub mime_essence: String, } -impl From for SymlinkNodeUiMetadata { - fn from(node: SymlinkNode) -> Self { +impl From for ResolvedNodeUiMetadata { + fn from(node: ResolvedNode) -> Self { Self { absolute_path: node.absolute_path.clone(), extension: node.extension.clone(), @@ -103,7 +103,8 @@ struct NodeUiMetadata { pub is_file: bool, pub is_readonly: bool, pub mime_essence: String, - pub symlink: Option, + pub canonical: Option, + pub symlink: Option, // Extra pub index: usize, @@ -145,6 +146,7 @@ impl NodeUiMetadata { is_file: node.is_file, is_readonly: node.is_readonly, mime_essence: node.mime_essence.clone(), + canonical: node.canonical.to_owned().map(|s| s.into()), symlink: node.symlink.to_owned().map(|s| s.into()), index, relative_index, @@ -395,6 +397,74 @@ 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 ui = app.config().general.sort_and_filter_ui.clone(); + let filter_by = app.explorer_config().filters(); + let sort_by = app.explorer_config().sorters(); + let forward = Span::styled( + ui.sort_direction_identifiers + .forward + .format + .to_owned() + .unwrap_or_default(), + ui.sort_direction_identifiers.forward.style.into(), + ); + + let reverse = Span::styled( + ui.sort_direction_identifiers + .reverse + .format + .to_owned() + .unwrap_or_default(), + ui.sort_direction_identifiers.reverse.style.into(), + ); + + let mut spans = filter_by + .iter() + .map(|f| { + ui.filter_identifiers + .get(&f.filter) + .map(|u| { + ( + Span::styled(u.format.to_owned().unwrap_or_default(), u.style.into()), + Span::raw(f.input.clone()), + ) + }) + .unwrap_or_else(|| (Span::raw("f"), Span::raw(""))) + }) + .chain(sort_by.iter().map(|s| { + let direction = if s.reverse { + reverse.clone() + } else { + forward.clone() + }; + + ui.sorter_identifiers + .get(&s.sorter) + .map(|u| { + ( + Span::styled(u.format.to_owned().unwrap_or_default(), u.style.into()), + direction.clone(), + ) + }) + .unwrap_or_else(|| (Span::raw("s"), direction.clone())) + })) + .zip(std::iter::repeat(Span::styled( + ui.separator.format.to_owned().unwrap_or_default(), + ui.separator.style.into(), + ))) + .map(|((a, b), c)| vec![a, b, c]) + .flatten() + .collect::>(); + spans.pop(); + + let p = Paragraph::new(Spans::from(spans)).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 +522,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