Add sorting support

Also improve filtering.

Closes: https://github.com/sayanarijit/xplr/issues/58
pull/76/head
Arijit Basu 3 years ago committed by Arijit Basu
parent 1dc25c4998
commit a8896740c8

270
Cargo.lock generated

@ -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",

@ -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 <sayanarijit@gmail.com>"]
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"

@ -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<SymlinkNode>,
pub canonical: Option<ResolvedNode>,
pub symlink: Option<ResolvedNode>,
}
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<H: std::hash::Hasher>(&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<NodeFilterApplicable>,
filters: IndexSet<NodeFilterApplicable>,
sorters: IndexSet<NodeSorterApplicable>,
}
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<NodeFilterApplicable> {
&self.filters
}
/// Get a reference to the explorer config's sorters.
pub fn sorters(&self) -> &IndexSet<NodeSorterApplicable> {
&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<Self> {
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<Self> {
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<Self> {
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<Self> {
@ -1410,41 +1694,28 @@ impl App {
}
fn add_node_filter(mut self, filter: NodeFilterApplicable) -> Result<Self> {
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<Self> {
fn add_node_filter_from_input(mut self, filter: NodeFilter) -> Result<Self> {
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> {
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<Self> {
fn remove_node_filter_from_input(mut self, filter: NodeFilter) -> Result<Self> {
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> {
self.explorer_config.filters.clear();
Ok(self)
}
fn add_node_sorter(mut self, sorter: NodeSorterApplicable) -> Result<Self> {
self.explorer_config.sorters.replace(sorter);
Ok(self)
}
fn remove_node_sorter(mut self, sorter: NodeSorter) -> Result<Self> {
self.explorer_config.sorters.retain(|s| s.sorter != sorter);
Ok(self)
}
fn reverse_node_sorter(mut self, sorter: NodeSorter) -> Result<Self> {
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<Self> {
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> {
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> {
self.explorer_config.sorters = self
.config
.general
.initial_sorting
.to_owned()
.unwrap_or_default();
Ok(self)
}
fn clear_node_sorters(mut self) -> Result<Self> {
self.explorer_config.sorters.clear();
Ok(self)
}
fn log_info(mut self, message: String) -> Result<Self> {
self.logs.push(Log::new(LogLevel::Info, message));

@ -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<NodeSorter, UiElement>,
#[serde(default)]
pub filter_identifiers: HashMap<NodeFilter, UiElement>,
}
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<IndexSet<NodeSorterApplicable>>,
}
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<bool> {
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<Option<&str>> {
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) */
}
}

@ -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: {}

@ -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::<Vec<Node>>()
.map(|name| Node::new(parent.clone(), name))
.filter(|n| config.filter(n))
.collect::<Vec<Node>>();
nodes.sort_by(|a, b| config.sort(a, b));
nodes
})
.map(|nodes| {
let focus_index = if let Some(focus) = focused_path {

@ -11,4 +11,5 @@ pub mod explorer;
pub mod input;
pub mod pipe_reader;
pub mod pwd_watcher;
pub mod runner;
pub mod ui;

@ -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<ExitStatus> {
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<Option<String>> {
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<Option<String>> {
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::<Vec<String>>()
.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::<Vec<String>>()
.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);
}
}
}

@ -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<ExitStatus> {
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<String>) -> Result<Option<String>> {
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::<Vec<String>>()
.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::<Vec<String>>()
.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
}

@ -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<TuiStyle> 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<SymlinkNode> for SymlinkNodeUiMetadata {
fn from(node: SymlinkNode) -> Self {
impl From<ResolvedNode> 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<SymlinkNodeUiMetadata>,
pub canonical: Option<ResolvedNodeUiMetadata>,
pub symlink: Option<ResolvedNodeUiMetadata>,
// 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<B: Backend>(f: &mut Frame<B>, rect: Rect, app: &app::App, _
f.render_widget(input_buf, rect);
}
fn draw_sort_n_filter_by<B: Backend>(f: &mut Frame<B>, 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::<Vec<Span>>();
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<B: Backend>(f: &mut Frame<B>, rect: Rect, app: &app::App, _: &Handlebars) {
let config = app.config().general.logs.clone();
let logs = app
@ -452,19 +522,21 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, 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()

@ -0,0 +1,2 @@
mod bdd;
mod unit;

@ -0,0 +1,2 @@
mod config;
mod ui;
Loading…
Cancel
Save