chore: First commit
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
parent
c99f4704a1
commit
36a979d234
64 changed files with 3591 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
538
Cargo.lock
generated
Normal file
538
Cargo.lock
generated
Normal file
|
@ -0,0 +1,538 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
|
||||||
|
dependencies = [
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.2.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||||
|
dependencies = [
|
||||||
|
"errno-dragonfly",
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno-dragonfly"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exec"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fern"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.61"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.76"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.169"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numtoa"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_termios"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.216"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.216"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termion"
|
||||||
|
version = "4.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7eaa98560e51a2cf4f0bb884d8b2098a9ea11ecf3b7078e9c68242c74cc923a7"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
"numtoa",
|
||||||
|
"redox_termios",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "twilight-tree"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"exec",
|
||||||
|
"fern",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"signal-hook",
|
||||||
|
"termion",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.99"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.99"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.99"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.99"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.99"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.6.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
authors = ["Awiteb <a@4rs.nl>"]
|
||||||
|
description = "Embeddable TUI files tree"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
name = "twilight-tree"
|
||||||
|
version = "0.1.0"
|
||||||
|
rust-version = "1.66.1"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.39"
|
||||||
|
exec = "0.3.1"
|
||||||
|
fern = "0.7"
|
||||||
|
log = "0.4"
|
||||||
|
serde = { version = "1.0.216", features = ["derive"] }
|
||||||
|
signal-hook = "0.3.17"
|
||||||
|
termion = "4.0.3"
|
||||||
|
toml = "0.8.19"
|
85
README.md
Normal file
85
README.md
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
> This is a fork of [twilight-commander](https://github.com/golmman/twilight-commander)
|
||||||
|
|
||||||
|
# twilight-tree
|
||||||
|
|
||||||
|
A simple console tree file explorer for linux, similiar to NERDTree but independent of vim.
|
||||||
|
Developed and tested on Ubuntu 18.04 with xterm derivatives.
|
||||||
|
|
||||||
|
## Build and install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://git.4rs.nl/awiteb/twilight-tree
|
||||||
|
cd twilight-tree
|
||||||
|
cargo install --path . # this will install the binary in $HOME/.cargo/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implemented features
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
The configuration is loaded as follows
|
||||||
|
1. load values from `$XDG_CONFIG_HOME/twilight-tree.toml`
|
||||||
|
2. else load values from `$HOME/.config/twilight-tree/twilight-tree.toml`
|
||||||
|
2. fill missing values with app defaults
|
||||||
|
3. overwrite values with defines from the command line options
|
||||||
|
|
||||||
|
For a config file with the default values, see [twilight-tree.toml](./twilight-tree.toml).
|
||||||
|
The command line options are derived from the values defined inside the twilight-tree.toml .
|
||||||
|
E.g.
|
||||||
|
```
|
||||||
|
[debug]
|
||||||
|
enabled = true
|
||||||
|
```
|
||||||
|
is set with the option `--debug.enabled=true`.
|
||||||
|
|
||||||
|
### Configurable key bindings
|
||||||
|
|
||||||
|
The key bindings are configurable. For the set of configurable keys and key combinations consult the [event.rs](./src/model/event.rs).
|
||||||
|
|
||||||
|
|default key|default configuration|action|
|
||||||
|
|---|---|---|
|
||||||
|
|up arrow|`--keybinding.entry_up=up`|move an entry up|
|
||||||
|
|down arrow|`--keybinding.entry_down=down`|move an entry down|
|
||||||
|
|left arrow|`--keybinding.collapse_dir=left`|collapse an entry directory or jump to parent if not collapsable|
|
||||||
|
|right arrow|`--keybinding.expand_dir=left`|expand an entry directory|
|
||||||
|
|r|`--keybinding.reload=r`|collapse all directories and reload root directory|
|
||||||
|
|return|`--keybinding.file_action=return`|perform configured file action|
|
||||||
|
|q|`--keybinding.quit=q`|quit|
|
||||||
|
|
||||||
|
### Directory entry management
|
||||||
|
|
||||||
|
#### File Action
|
||||||
|
The command line option / config value `--behavior.file_action` defines the action taken when the return key is pressed
|
||||||
|
on a file. The action is interpreted by `bash` and any occurence of `%s` will be replaced by the selected filename.
|
||||||
|
E.g. when enter is pressed on the file `.bashrc` in a twilight-tree process created with
|
||||||
|
```
|
||||||
|
twilight-tree "--behavior.file_action=xterm -e 'cat %s; echo opened file: %s; bash'"
|
||||||
|
```
|
||||||
|
then
|
||||||
|
```
|
||||||
|
bash -c "xterm -e 'cat /home/user/.bashrc; echo opened file: /home/user/.bashrc; bash'"
|
||||||
|
```
|
||||||
|
is executed, i.e.:
|
||||||
|
* a new xterm window is opened
|
||||||
|
* where the selected file (`.bashrc`) is printed to stdout
|
||||||
|
* then `opened file: ~/.bashrc` is printed
|
||||||
|
* `bash` prevents the window from closing.
|
||||||
|
|
||||||
|
`--behavior.file_action` defaults to [true](https://en.wikipedia.org/wiki/True_and_false_(commands)), which does
|
||||||
|
(almost) nothing.
|
||||||
|
|
||||||
|
### Scrolling modes
|
||||||
|
Specified with the option `--behaviour.scrolling` (default = `center`)
|
||||||
|
|
||||||
|
* `center`: move the cursor until it is in the center, then move the text instead
|
||||||
|
* `editor`: move the cursor until it hits the top/bottom boundaries set by the `debug.paddin_top/bot` limits
|
||||||
|
|
||||||
|
### Utf-8 support
|
||||||
|
In case your terminal does not support utf-8 you can disable it with `--composition.use_utf8=false`.
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
Logs are written to
|
||||||
|
1. `$XDG_CONFIG_HOME/tc.log` if XDG_CONFIG_HOME is defined
|
||||||
|
2. else they are placed in `$HOME/.config/twilight-tree/tc.log`
|
||||||
|
|
||||||
|
## License
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
|
30
src/controller/key_event_handler.rs
Normal file
30
src/controller/key_event_handler.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::model::event::Event;
|
||||||
|
use crate::model::event::Key;
|
||||||
|
use std::io::stdin;
|
||||||
|
use std::sync::mpsc::SyncSender;
|
||||||
|
use std::sync::mpsc::{self, TryRecvError};
|
||||||
|
use termion::input::TermRead;
|
||||||
|
|
||||||
|
pub struct KeyEventHandler {}
|
||||||
|
|
||||||
|
impl KeyEventHandler {
|
||||||
|
pub fn handle(sync_sender: SyncSender<Event>, rx: mpsc::Receiver<()>) {
|
||||||
|
let stdin = stdin();
|
||||||
|
|
||||||
|
for termion_event in stdin.events() {
|
||||||
|
if let Ok(termion_event) = termion_event {
|
||||||
|
let _ = sync_sender.send(Event::Key(Key::from(termion_event)));
|
||||||
|
}
|
||||||
|
match rx.try_recv() {
|
||||||
|
Ok(_) | Err(TryRecvError::Disconnected) => {
|
||||||
|
// println!("Terminating.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(TryRecvError::Empty) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
119
src/controller/key_event_matcher/collapse_dir.rs
Normal file
119
src/controller/key_event_matcher/collapse_dir.rs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::controller::EventQueue;
|
||||||
|
use crate::model::tree_index::TreeIndex;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
pub fn do_collapse_dir(&mut self) -> Option<()> {
|
||||||
|
let tree_index = self
|
||||||
|
.path_node_root
|
||||||
|
.flat_index_to_tree_index(self.pager.cursor_row as usize);
|
||||||
|
|
||||||
|
let cursor_delta = self.get_parent_dir_cursor_delta(&tree_index);
|
||||||
|
|
||||||
|
if cursor_delta == 0 {
|
||||||
|
self.path_node_root.collapse_dir(&tree_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text_entries = self.composer.compose_path_node(&self.path_node_root);
|
||||||
|
|
||||||
|
self.update_pager(cursor_delta);
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_parent_dir_cursor_delta(&mut self, tree_index: &TreeIndex) -> i32 {
|
||||||
|
let child_path_node = self.path_node_root.get_child_path_node(tree_index);
|
||||||
|
if child_path_node.is_dir && child_path_node.is_expanded {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent_path_node_tree_index = tree_index.get_parent();
|
||||||
|
if parent_path_node_tree_index == TreeIndex::new() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent_flat_index =
|
||||||
|
self.path_node_root
|
||||||
|
.tree_index_to_flat_index(&parent_path_node_tree_index) as i32;
|
||||||
|
|
||||||
|
parent_flat_index - self.pager.cursor_row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::model::path_node::PathNode;
|
||||||
|
use crate::model::tree_index::TreeIndex;
|
||||||
|
use crate::view::composer::Composer;
|
||||||
|
use crate::view::Pager;
|
||||||
|
|
||||||
|
// TODO: duplicate code, create test utils?
|
||||||
|
fn get_expanded_path_node() -> PathNode {
|
||||||
|
let mut path_node = PathNode::from("./tests/test_dirs");
|
||||||
|
path_node.expand_dir(&TreeIndex::new(), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![0]), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(
|
||||||
|
&TreeIndex::from(vec![0, 0]),
|
||||||
|
PathNode::compare_dirs_top_simple,
|
||||||
|
);
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![1]), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(
|
||||||
|
&TreeIndex::from(vec![1, 0]),
|
||||||
|
PathNode::compare_dirs_top_simple,
|
||||||
|
);
|
||||||
|
path_node.expand_dir(
|
||||||
|
&TreeIndex::from(vec![1, 0, 2]),
|
||||||
|
PathNode::compare_dirs_top_simple,
|
||||||
|
);
|
||||||
|
path_node
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_event_queue() -> EventQueue<Vec<u8>> {
|
||||||
|
let config = Config::default();
|
||||||
|
|
||||||
|
let composer = Composer::from(config.clone());
|
||||||
|
let pager = Pager::new(config.clone(), Vec::new());
|
||||||
|
let path_node = PathNode::from(config.setup.working_dir.clone());
|
||||||
|
|
||||||
|
let mut event_queue = EventQueue::new(config, composer, pager, path_node);
|
||||||
|
|
||||||
|
event_queue.path_node_root = get_expanded_path_node();
|
||||||
|
|
||||||
|
event_queue
|
||||||
|
}
|
||||||
|
|
||||||
|
mod get_parent_dir_cursor_delta_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expanded() {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
|
||||||
|
let delta = event_queue.get_parent_dir_cursor_delta(&TreeIndex::from(vec![0]));
|
||||||
|
|
||||||
|
assert_eq!(0, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_tree_index() {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
|
||||||
|
let delta = event_queue.get_parent_dir_cursor_delta(&TreeIndex::new());
|
||||||
|
|
||||||
|
assert_eq!(0, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jump() {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
|
||||||
|
let delta = event_queue.get_parent_dir_cursor_delta(&TreeIndex::from(vec![1, 0, 4]));
|
||||||
|
|
||||||
|
assert_eq!(7, delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/controller/key_event_matcher/entry_down.rs
Normal file
12
src/controller/key_event_matcher/entry_down.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::controller::EventQueue;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
pub fn do_entry_down(&mut self) -> Option<()> {
|
||||||
|
self.update_pager(1);
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
}
|
12
src/controller/key_event_matcher/entry_up.rs
Normal file
12
src/controller/key_event_matcher/entry_up.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::controller::EventQueue;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
pub fn do_entry_up(&mut self) -> Option<()> {
|
||||||
|
self.update_pager(-1);
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
}
|
19
src/controller/key_event_matcher/expand_dir.rs
Normal file
19
src/controller/key_event_matcher/expand_dir.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::controller::EventQueue;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
pub fn do_expand_dir(&mut self) -> Option<()> {
|
||||||
|
let tree_index = self
|
||||||
|
.path_node_root
|
||||||
|
.flat_index_to_tree_index(self.pager.cursor_row as usize);
|
||||||
|
self.path_node_root
|
||||||
|
.expand_dir(&tree_index, self.path_node_compare);
|
||||||
|
self.text_entries = self.composer.compose_path_node(&self.path_node_root);
|
||||||
|
|
||||||
|
self.update_pager(0);
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
}
|
39
src/controller/key_event_matcher/file_action.rs
Normal file
39
src/controller/key_event_matcher/file_action.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::controller::EventQueue;
|
||||||
|
use log::info;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
pub fn do_file_action(&mut self) -> Option<()> {
|
||||||
|
let tree_index = self
|
||||||
|
.path_node_root
|
||||||
|
.flat_index_to_tree_index(self.pager.cursor_row as usize);
|
||||||
|
|
||||||
|
let child_node = self.path_node_root.get_child_path_node(&tree_index);
|
||||||
|
|
||||||
|
if !child_node.is_dir {
|
||||||
|
let file_path = &child_node.get_absolute_path();
|
||||||
|
let file_action_replaced = self.config.behavior.file_action.replace("%s", file_path);
|
||||||
|
|
||||||
|
info!("executing file action:\n{}", file_action_replaced);
|
||||||
|
|
||||||
|
if self.config.behavior.quit_on_action {
|
||||||
|
self.command_to_run_on_exit = Some(file_action_replaced);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
std::process::Command::new("bash")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(file_action_replaced)
|
||||||
|
.spawn()
|
||||||
|
.unwrap()
|
||||||
|
.wait()
|
||||||
|
.unwrap();
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
140
src/controller/key_event_matcher/mod.rs
Normal file
140
src/controller/key_event_matcher/mod.rs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::controller::EventQueue;
|
||||||
|
use crate::model::event::Key;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
mod collapse_dir;
|
||||||
|
mod entry_down;
|
||||||
|
mod entry_up;
|
||||||
|
mod expand_dir;
|
||||||
|
mod file_action;
|
||||||
|
mod quit;
|
||||||
|
mod reload;
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn match_key_event(&mut self, key: Key) -> Option<()> {
|
||||||
|
let ck = self.config.keybinding.clone();
|
||||||
|
|
||||||
|
if key == Key::from(ck.collapse_dir) { self.do_collapse_dir() }
|
||||||
|
else if key == Key::from(ck.entry_down) { self.do_entry_down() }
|
||||||
|
else if key == Key::from(ck.entry_up) { self.do_entry_up() }
|
||||||
|
else if key == Key::from(ck.expand_dir) { self.do_expand_dir() }
|
||||||
|
else if key == Key::from(ck.file_action) { self.do_file_action() }
|
||||||
|
else if key == Key::from(ck.quit) { self.do_quit() }
|
||||||
|
else if key == Key::from(ck.reload) { self.do_reload() }
|
||||||
|
else { Some(()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_pager(&mut self, cursor_delta: i32) {
|
||||||
|
self.pager.update(
|
||||||
|
cursor_delta,
|
||||||
|
&self.text_entries,
|
||||||
|
self.path_node_root.get_absolute_path(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::model::path_node::PathNode;
|
||||||
|
use crate::view::composer::Composer;
|
||||||
|
use crate::view::Pager;
|
||||||
|
|
||||||
|
fn prepare_event_queue() -> EventQueue<Vec<u8>> {
|
||||||
|
let config = Config::default();
|
||||||
|
|
||||||
|
let composer = Composer::from(config.clone());
|
||||||
|
let pager = Pager::new(config.clone(), Vec::new());
|
||||||
|
let path_node = PathNode::from(config.setup.working_dir.clone());
|
||||||
|
|
||||||
|
EventQueue::new(config, composer, pager, path_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_key_event_default_test() {
|
||||||
|
let result = {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
event_queue.match_key_event(Key::from("nonsense"))
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_key_event_quit_test() {
|
||||||
|
let result = {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
event_queue.match_key_event(Key::from(event_queue.config.keybinding.quit.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_key_event_reload_test() {
|
||||||
|
let result = {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
event_queue.match_key_event(Key::from(event_queue.config.keybinding.reload.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_key_event_file_action_test() {
|
||||||
|
let result = {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
event_queue
|
||||||
|
.match_key_event(Key::from(event_queue.config.keybinding.file_action.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_key_event_entry_up_test() {
|
||||||
|
let result = {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
event_queue.match_key_event(Key::from(event_queue.config.keybinding.entry_up.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_key_event_entry_down_test() {
|
||||||
|
let result = {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
event_queue.match_key_event(Key::from(event_queue.config.keybinding.entry_down.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_key_event_collapse_dir_test() {
|
||||||
|
let result = {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
event_queue.match_key_event(Key::from(
|
||||||
|
event_queue.config.keybinding.collapse_dir.clone(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_key_event_expand_dir_test() {
|
||||||
|
let result = {
|
||||||
|
let mut event_queue = prepare_event_queue();
|
||||||
|
event_queue.match_key_event(Key::from(event_queue.config.keybinding.expand_dir.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
}
|
11
src/controller/key_event_matcher/quit.rs
Normal file
11
src/controller/key_event_matcher/quit.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::controller::EventQueue;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
pub fn do_quit(&mut self) -> Option<()> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
100
src/controller/key_event_matcher/reload.rs
Normal file
100
src/controller/key_event_matcher/reload.rs
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
|
use crate::controller::EventQueue;
|
||||||
|
use crate::model::path_node::PathNode;
|
||||||
|
use crate::model::tree_index::TreeIndex;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
pub fn do_reload(&mut self) -> Option<()> {
|
||||||
|
self.reload_openend_dirs();
|
||||||
|
|
||||||
|
self.text_entries = self.composer.compose_path_node(&self.path_node_root);
|
||||||
|
|
||||||
|
self.update_pager(0);
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload_openend_dirs(&mut self) {
|
||||||
|
// backup the old path node structure
|
||||||
|
let old_path_node_root = self.path_node_root.clone();
|
||||||
|
|
||||||
|
// reset the root path node
|
||||||
|
self.path_node_root = PathNode::from(self.config.setup.working_dir.clone());
|
||||||
|
self.path_node_root
|
||||||
|
.expand_dir(&TreeIndex::from(Vec::new()), self.path_node_compare);
|
||||||
|
|
||||||
|
// restore the old path nodes structure for the root path node
|
||||||
|
self.restore_expansions(&old_path_node_root, &mut TreeIndex::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_expansions(&mut self, path_node: &PathNode, tree_index: &mut TreeIndex) {
|
||||||
|
for (c, child) in path_node.children.iter().enumerate() {
|
||||||
|
if child.is_expanded {
|
||||||
|
tree_index.index.push(c);
|
||||||
|
|
||||||
|
self.path_node_root
|
||||||
|
.expand_dir(tree_index, self.path_node_compare);
|
||||||
|
self.restore_expansions(child, tree_index);
|
||||||
|
|
||||||
|
tree_index.index.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::model::path_node::PathNode;
|
||||||
|
use crate::model::tree_index::TreeIndex;
|
||||||
|
use crate::view::composer::Composer;
|
||||||
|
use crate::view::Pager;
|
||||||
|
|
||||||
|
fn get_expanded_path_node(working_dir: &str) -> PathNode {
|
||||||
|
let mut path_node = PathNode::from(working_dir);
|
||||||
|
|
||||||
|
path_node.expand_dir(&TreeIndex::new(), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![0]), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(
|
||||||
|
&TreeIndex::from(vec![0, 0]),
|
||||||
|
PathNode::compare_dirs_top_simple,
|
||||||
|
);
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![1]), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(
|
||||||
|
&TreeIndex::from(vec![1, 0]),
|
||||||
|
PathNode::compare_dirs_top_simple,
|
||||||
|
);
|
||||||
|
path_node.expand_dir(
|
||||||
|
&TreeIndex::from(vec![1, 0, 2]),
|
||||||
|
PathNode::compare_dirs_top_simple,
|
||||||
|
);
|
||||||
|
path_node
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_event_queue(working_dir: &str) -> EventQueue<Vec<u8>> {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.setup.working_dir = String::from(working_dir);
|
||||||
|
|
||||||
|
let composer = Composer::from(config.clone());
|
||||||
|
let pager = Pager::new(config.clone(), Vec::new());
|
||||||
|
let path_node = PathNode::from(config.setup.working_dir.clone());
|
||||||
|
|
||||||
|
let mut event_queue = EventQueue::new(config, composer, pager, path_node);
|
||||||
|
|
||||||
|
event_queue.path_node_root = get_expanded_path_node(working_dir);
|
||||||
|
|
||||||
|
event_queue
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn do_reload() {
|
||||||
|
// TODO: implement proper test
|
||||||
|
let mut event_queue = prepare_event_queue("./tests/test_dirs");
|
||||||
|
event_queue.do_reload();
|
||||||
|
}
|
||||||
|
}
|
98
src/controller/mod.rs
Normal file
98
src/controller/mod.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::controller::key_event_handler::KeyEventHandler;
|
||||||
|
use crate::controller::resize_event_handler::ResizeEventHandler;
|
||||||
|
use crate::model::compare_functions::PathNodeCompare;
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::model::event::Event;
|
||||||
|
use crate::model::path_node::PathNode;
|
||||||
|
use crate::view::composer::Composer;
|
||||||
|
use crate::view::Pager;
|
||||||
|
use log::info;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::mpsc::sync_channel;
|
||||||
|
use std::sync::mpsc::Receiver;
|
||||||
|
use std::sync::mpsc::SyncSender;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
mod key_event_handler;
|
||||||
|
mod key_event_matcher;
|
||||||
|
mod resize_event_handler;
|
||||||
|
|
||||||
|
pub struct EventQueue<W: Write> {
|
||||||
|
config: Config,
|
||||||
|
composer: Composer,
|
||||||
|
pager: Pager<W>,
|
||||||
|
path_node_root: PathNode,
|
||||||
|
path_node_compare: PathNodeCompare,
|
||||||
|
queue_receiver: Receiver<Event>,
|
||||||
|
queue_sender: SyncSender<Event>,
|
||||||
|
|
||||||
|
// TODO: should be part of the view?
|
||||||
|
text_entries: Vec<String>,
|
||||||
|
command_to_run_on_exit: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> EventQueue<W> {
|
||||||
|
pub fn new(
|
||||||
|
config: Config,
|
||||||
|
composer: Composer,
|
||||||
|
mut pager: Pager<W>,
|
||||||
|
path_node_root: PathNode,
|
||||||
|
) -> Self {
|
||||||
|
info!("initializing event queue");
|
||||||
|
|
||||||
|
let (queue_sender, queue_receiver): (SyncSender<Event>, Receiver<Event>) =
|
||||||
|
sync_channel(1024);
|
||||||
|
|
||||||
|
let path_node_compare = PathNode::get_path_node_compare(&config);
|
||||||
|
|
||||||
|
let text_entries = composer.compose_path_node(&path_node_root);
|
||||||
|
pager.update(0, &text_entries, path_node_root.get_absolute_path());
|
||||||
|
let command_to_run_on_exit = None;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
composer,
|
||||||
|
pager,
|
||||||
|
path_node_root,
|
||||||
|
path_node_compare,
|
||||||
|
queue_receiver,
|
||||||
|
queue_sender,
|
||||||
|
text_entries,
|
||||||
|
command_to_run_on_exit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_messages(&mut self) -> Option<String> {
|
||||||
|
let (tx1, rx1) = std::sync::mpsc::channel();
|
||||||
|
let (tx2, rx2) = std::sync::mpsc::channel();
|
||||||
|
let sender1 = self.queue_sender.clone();
|
||||||
|
let sender2 = self.queue_sender.clone();
|
||||||
|
thread::spawn(move || KeyEventHandler::handle(sender1, rx1));
|
||||||
|
thread::spawn(move || ResizeEventHandler::handle(sender2, rx2));
|
||||||
|
|
||||||
|
while self
|
||||||
|
.match_event(self.queue_receiver.recv().unwrap())
|
||||||
|
.is_some()
|
||||||
|
{}
|
||||||
|
let _ = tx1.send(());
|
||||||
|
let _ = tx2.send(());
|
||||||
|
self.command_to_run_on_exit.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_event(&mut self, event: Event) -> Option<()> {
|
||||||
|
match event {
|
||||||
|
Event::Key(key) => self.match_key_event(key),
|
||||||
|
Event::Resize => {
|
||||||
|
self.pager.update(
|
||||||
|
0,
|
||||||
|
&self.text_entries,
|
||||||
|
self.path_node_root.get_absolute_path(),
|
||||||
|
);
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
src/controller/resize_event_handler.rs
Normal file
23
src/controller/resize_event_handler.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
|
use crate::model::event::Event;
|
||||||
|
use signal_hook::consts::SIGWINCH;
|
||||||
|
use signal_hook::low_level::{register, unregister};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::sync::mpsc::SyncSender;
|
||||||
|
|
||||||
|
pub struct ResizeEventHandler {}
|
||||||
|
|
||||||
|
impl ResizeEventHandler {
|
||||||
|
pub fn handle(sync_sender: SyncSender<Event>, rx: mpsc::Receiver<()>) {
|
||||||
|
let hook_id = unsafe {
|
||||||
|
register(SIGWINCH, move || {
|
||||||
|
sync_sender.send(Event::Resize).unwrap();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let _ = rx.recv();
|
||||||
|
unregister(hook_id.unwrap());
|
||||||
|
}
|
||||||
|
}
|
45
src/main.rs
Normal file
45
src/main.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
extern crate chrono;
|
||||||
|
extern crate termion;
|
||||||
|
extern crate toml;
|
||||||
|
use controller::EventQueue;
|
||||||
|
use exec::execvp;
|
||||||
|
use log::info;
|
||||||
|
use model::config::Config;
|
||||||
|
use model::path_node::PathNode;
|
||||||
|
use std::io::stdout;
|
||||||
|
use termion::raw::IntoRawMode;
|
||||||
|
use utils::setup_logger;
|
||||||
|
use view::composer::Composer;
|
||||||
|
use view::Pager;
|
||||||
|
|
||||||
|
mod controller;
|
||||||
|
mod model;
|
||||||
|
mod utils;
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let command_to_run_on_exit = {
|
||||||
|
let _ = setup_logger();
|
||||||
|
|
||||||
|
let config = Config::new();
|
||||||
|
|
||||||
|
let composer = Composer::from(config.clone());
|
||||||
|
|
||||||
|
let pager = Pager::new(config.clone(), stdout().into_raw_mode().unwrap());
|
||||||
|
|
||||||
|
let path_node_root = PathNode::new_expanded(config.clone());
|
||||||
|
|
||||||
|
let mut event_queue = EventQueue::new(config, composer, pager, path_node_root);
|
||||||
|
|
||||||
|
event_queue.handle_messages()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(cmd) = command_to_run_on_exit {
|
||||||
|
let _ = execvp("bash", &["bash", "-c", &cmd]);
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("clean exit");
|
||||||
|
}
|
131
src/model/compare_functions.rs
Normal file
131
src/model/compare_functions.rs
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::model::path_node::PathNode;
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
pub type PathNodeCompare = fn(&PathNode, &PathNode) -> Ordering;
|
||||||
|
|
||||||
|
impl PathNode {
|
||||||
|
pub fn compare_dirs_bot_simple(a: &PathNode, b: &PathNode) -> Ordering {
|
||||||
|
if a.is_dir && !b.is_dir {
|
||||||
|
return std::cmp::Ordering::Greater;
|
||||||
|
} else if !a.is_dir && b.is_dir {
|
||||||
|
return std::cmp::Ordering::Less;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.display_text.cmp(&b.display_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare_dirs_top_simple(a: &PathNode, b: &PathNode) -> Ordering {
|
||||||
|
if a.is_dir && !b.is_dir {
|
||||||
|
return std::cmp::Ordering::Less;
|
||||||
|
} else if !a.is_dir && b.is_dir {
|
||||||
|
return std::cmp::Ordering::Greater;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.display_text.cmp(&b.display_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_path_node_compare(config: &Config) -> PathNodeCompare {
|
||||||
|
let path_node_compare: fn(&PathNode, &PathNode) -> Ordering =
|
||||||
|
match config.behavior.path_node_sort.as_str() {
|
||||||
|
"dirs_bot_simple" => PathNode::compare_dirs_bot_simple,
|
||||||
|
"dirs_top_simple" => PathNode::compare_dirs_top_simple,
|
||||||
|
"none" => |_, _| Ordering::Equal,
|
||||||
|
_ => |_, _| Ordering::Equal,
|
||||||
|
};
|
||||||
|
|
||||||
|
path_node_compare
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::cmp::Ordering::Greater;
|
||||||
|
use std::cmp::Ordering::Less;
|
||||||
|
|
||||||
|
mod compare_dirs_bot_simple_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dir_to_dir() {
|
||||||
|
let dir_a = get_dir("dir_a");
|
||||||
|
let dir_b = get_dir("dir_b");
|
||||||
|
|
||||||
|
let order = PathNode::compare_dirs_bot_simple(&dir_a, &dir_b);
|
||||||
|
|
||||||
|
assert_eq!(Less, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dir_to_file() {
|
||||||
|
let dir = get_dir("something");
|
||||||
|
let file = get_file("something");
|
||||||
|
|
||||||
|
let order = PathNode::compare_dirs_bot_simple(&dir, &file);
|
||||||
|
|
||||||
|
assert_eq!(Greater, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_to_file() {
|
||||||
|
let file_a = get_file("file_a");
|
||||||
|
let file_b = get_file("file_b");
|
||||||
|
|
||||||
|
let order = PathNode::compare_dirs_bot_simple(&file_a, &file_b);
|
||||||
|
|
||||||
|
assert_eq!(Less, order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod compare_dirs_top_simple_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dir_to_dir() {
|
||||||
|
let dir_a = get_dir("dir_a");
|
||||||
|
let dir_b = get_dir("dir_b");
|
||||||
|
|
||||||
|
let order = PathNode::compare_dirs_top_simple(&dir_a, &dir_b);
|
||||||
|
|
||||||
|
assert_eq!(Less, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dir_to_file() {
|
||||||
|
let dir = get_dir("something");
|
||||||
|
let file = get_file("something");
|
||||||
|
|
||||||
|
let order = PathNode::compare_dirs_top_simple(&dir, &file);
|
||||||
|
|
||||||
|
assert_eq!(Less, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_to_file() {
|
||||||
|
let file_a = get_file("file_a");
|
||||||
|
let file_b = get_file("file_b");
|
||||||
|
|
||||||
|
let order = PathNode::compare_dirs_top_simple(&file_a, &file_b);
|
||||||
|
|
||||||
|
assert_eq!(Less, order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_dir(name: &str) -> PathNode {
|
||||||
|
let mut path_node = PathNode::from(".");
|
||||||
|
path_node.is_dir = true;
|
||||||
|
path_node.display_text = String::from(name);
|
||||||
|
path_node
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_file(name: &str) -> PathNode {
|
||||||
|
let mut path_node = PathNode::from(".");
|
||||||
|
path_node.is_dir = false;
|
||||||
|
path_node.display_text = String::from(name);
|
||||||
|
path_node
|
||||||
|
}
|
||||||
|
}
|
48
src/model/config/behavior.rs
Normal file
48
src/model/config/behavior.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Behavior {
|
||||||
|
#[serde(default = "Behavior::default_file_action")]
|
||||||
|
pub file_action: String,
|
||||||
|
|
||||||
|
#[serde(default = "Behavior::default_path_node_sort")]
|
||||||
|
pub path_node_sort: String,
|
||||||
|
|
||||||
|
#[serde(default = "Behavior::default_scrolling")]
|
||||||
|
pub scrolling: String,
|
||||||
|
|
||||||
|
#[serde(default = "Behavior::default_quit_on_action")]
|
||||||
|
pub quit_on_action: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Behavior {
|
||||||
|
fn default() -> Behavior {
|
||||||
|
Behavior {
|
||||||
|
file_action: Self::default_file_action(),
|
||||||
|
path_node_sort: Self::default_path_node_sort(),
|
||||||
|
scrolling: Self::default_scrolling(),
|
||||||
|
quit_on_action: Self::default_quit_on_action(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Behavior {
|
||||||
|
fn default_file_action() -> String {
|
||||||
|
String::from("true") // do nothing!
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_path_node_sort() -> String {
|
||||||
|
String::from("dirs_top_simple")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_scrolling() -> String {
|
||||||
|
String::from("center")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_quit_on_action() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
32
src/model/config/color.rs
Normal file
32
src/model/config/color.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Color {
|
||||||
|
#[serde(default = "Color::default_background")]
|
||||||
|
pub background: String,
|
||||||
|
|
||||||
|
#[serde(default = "Color::default_foreground")]
|
||||||
|
pub foreground: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Color {
|
||||||
|
fn default() -> Self {
|
||||||
|
Color {
|
||||||
|
background: Self::default_background(),
|
||||||
|
foreground: Self::default_foreground(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
fn default_background() -> String {
|
||||||
|
String::from("000000")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_foreground() -> String {
|
||||||
|
String::from("FFFFFF")
|
||||||
|
}
|
||||||
|
}
|
40
src/model/config/composition.rs
Normal file
40
src/model/config/composition.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Composition {
|
||||||
|
#[serde(default = "Composition::default_indent")]
|
||||||
|
pub indent: i32,
|
||||||
|
|
||||||
|
#[serde(default = "Composition::default_show_indent")]
|
||||||
|
pub show_indent: bool,
|
||||||
|
|
||||||
|
#[serde(default = "Composition::default_use_utf8")]
|
||||||
|
pub use_utf8: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Composition {
|
||||||
|
fn default() -> Composition {
|
||||||
|
Composition {
|
||||||
|
indent: Self::default_indent(),
|
||||||
|
show_indent: Self::default_show_indent(),
|
||||||
|
use_utf8: Self::default_use_utf8(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Composition {
|
||||||
|
fn default_indent() -> i32 {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_show_indent() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_use_utf8() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
56
src/model/config/debug.rs
Normal file
56
src/model/config/debug.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Debug {
|
||||||
|
#[serde(default = "Debug::default_enabled")]
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
#[serde(default = "Debug::default_padding_bot")]
|
||||||
|
pub padding_bot: i32,
|
||||||
|
|
||||||
|
#[serde(default = "Debug::default_padding_top")]
|
||||||
|
pub padding_top: i32,
|
||||||
|
|
||||||
|
#[serde(default = "Debug::default_spacing_bot")]
|
||||||
|
pub spacing_bot: i32,
|
||||||
|
|
||||||
|
#[serde(default = "Debug::default_spacing_top")]
|
||||||
|
pub spacing_top: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Debug {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: Self::default_enabled(),
|
||||||
|
padding_bot: Self::default_padding_bot(),
|
||||||
|
padding_top: Self::default_padding_top(),
|
||||||
|
spacing_bot: Self::default_spacing_bot(),
|
||||||
|
spacing_top: Self::default_spacing_top(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug {
|
||||||
|
fn default_enabled() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_padding_bot() -> i32 {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_padding_top() -> i32 {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_spacing_bot() -> i32 {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_spacing_top() -> i32 {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
}
|
72
src/model/config/keybinding.rs
Normal file
72
src/model/config/keybinding.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Keybinding {
|
||||||
|
#[serde(default = "Keybinding::default_quit")]
|
||||||
|
pub quit: String,
|
||||||
|
|
||||||
|
#[serde(default = "Keybinding::default_entry_up")]
|
||||||
|
pub entry_up: String,
|
||||||
|
|
||||||
|
#[serde(default = "Keybinding::default_entry_down")]
|
||||||
|
pub entry_down: String,
|
||||||
|
|
||||||
|
#[serde(default = "Keybinding::default_expand_dir")]
|
||||||
|
pub expand_dir: String,
|
||||||
|
|
||||||
|
#[serde(default = "Keybinding::default_collapse_dir")]
|
||||||
|
pub collapse_dir: String,
|
||||||
|
|
||||||
|
#[serde(default = "Keybinding::default_file_action")]
|
||||||
|
pub file_action: String,
|
||||||
|
|
||||||
|
#[serde(default = "Keybinding::default_reload")]
|
||||||
|
pub reload: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Keybinding {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
quit: Self::default_quit(),
|
||||||
|
entry_up: Self::default_entry_up(),
|
||||||
|
entry_down: Self::default_entry_down(),
|
||||||
|
expand_dir: Self::default_expand_dir(),
|
||||||
|
collapse_dir: Self::default_collapse_dir(),
|
||||||
|
file_action: Self::default_file_action(),
|
||||||
|
reload: Self::default_reload(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keybinding {
|
||||||
|
fn default_quit() -> String {
|
||||||
|
String::from("q")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_entry_up() -> String {
|
||||||
|
String::from("up")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_entry_down() -> String {
|
||||||
|
String::from("down")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_expand_dir() -> String {
|
||||||
|
String::from("right")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_collapse_dir() -> String {
|
||||||
|
String::from("left")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_file_action() -> String {
|
||||||
|
String::from("return")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_reload() -> String {
|
||||||
|
String::from("r")
|
||||||
|
}
|
||||||
|
}
|
229
src/model/config/mod.rs
Normal file
229
src/model/config/mod.rs
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
|
use crate::model::config::behavior::Behavior;
|
||||||
|
use crate::model::config::color::Color;
|
||||||
|
use crate::model::config::composition::Composition;
|
||||||
|
use crate::model::config::debug::Debug;
|
||||||
|
use crate::model::config::keybinding::Keybinding;
|
||||||
|
use crate::model::config::setup::Setup;
|
||||||
|
use crate::utils::get_config_dir;
|
||||||
|
use crate::utils::print_help;
|
||||||
|
use crate::utils::read_file;
|
||||||
|
use log::{info, warn};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::env::args;
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
mod behavior;
|
||||||
|
mod color;
|
||||||
|
mod composition;
|
||||||
|
mod debug;
|
||||||
|
mod keybinding;
|
||||||
|
mod setup;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
#[serde(default)]
|
||||||
|
pub behavior: Behavior,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub color: Color,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub composition: Composition,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub debug: Debug,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub keybinding: Keybinding,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub setup: Setup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
info!("initializing config");
|
||||||
|
|
||||||
|
let config = Self::read_config_file().unwrap_or_default();
|
||||||
|
|
||||||
|
Self::parse_args(config, args().skip(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn parse_args<T>(mut config: Self, args: T) -> Self
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = String>,
|
||||||
|
{
|
||||||
|
for arg in args {
|
||||||
|
let (key, value) = Self::split_arg(arg);
|
||||||
|
match key.as_str() {
|
||||||
|
"--behavior.file_action" => config.behavior.file_action = Self::parse_value((key, value)),
|
||||||
|
"--behavior.quit_on_action" => config.behavior.quit_on_action = Self::parse_value((key, value)),
|
||||||
|
"--behavior.path_node_sort" => config.behavior.path_node_sort = Self::parse_value((key, value)),
|
||||||
|
"--behavior.scrolling" => config.behavior.scrolling = Self::parse_value((key, value)),
|
||||||
|
"--color.background" => config.color.background = Self::parse_value((key, value)),
|
||||||
|
"--color.foreground" => config.color.foreground = Self::parse_value((key, value)),
|
||||||
|
"--composition.indent" => config.composition.indent = Self::parse_value((key, value)),
|
||||||
|
"--composition.show_indent" => config.composition.show_indent = Self::parse_value((key, value)),
|
||||||
|
"--composition.use_utf8" => config.composition.use_utf8 = Self::parse_value((key, value)),
|
||||||
|
"--debug.enabled" => config.debug.enabled = Self::parse_value((key, value)),
|
||||||
|
"--debug.padding_bot" => config.debug.padding_bot = Self::parse_value((key, value)),
|
||||||
|
"--debug.padding_top" => config.debug.padding_top = Self::parse_value((key, value)),
|
||||||
|
"--debug.spacing_bot" => config.debug.spacing_bot = Self::parse_value((key, value)),
|
||||||
|
"--debug.spacing_top" => config.debug.spacing_top = Self::parse_value((key, value)),
|
||||||
|
"--keybinding.collapse_dir" => config.keybinding.collapse_dir = Self::parse_value((key, value)),
|
||||||
|
"--keybinding.entry_down" => config.keybinding.entry_down = Self::parse_value((key, value)),
|
||||||
|
"--keybinding.entry_up" => config.keybinding.entry_up = Self::parse_value((key, value)),
|
||||||
|
"--keybinding.expand_dir" => config.keybinding.expand_dir = Self::parse_value((key, value)),
|
||||||
|
"--keybinding.file_action" => config.keybinding.file_action = Self::parse_value((key, value)),
|
||||||
|
"--keybinding.quit" => config.keybinding.quit = Self::parse_value((key, value)),
|
||||||
|
"--keybinding.reload" => config.keybinding.reload = Self::parse_value((key, value)),
|
||||||
|
"--setup.working_dir" => config.setup.working_dir = Self::parse_value((key, value)),
|
||||||
|
|
||||||
|
"--help" | "--version" => print_help(),
|
||||||
|
"--" => break,
|
||||||
|
_ => {
|
||||||
|
warn!("unknown option {}", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("config loaded as:\n{:?}", config);
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_arg(arg: String) -> (String, String) {
|
||||||
|
println!("{}", arg);
|
||||||
|
if let Some(equal_sign_index) = arg.find('=') {
|
||||||
|
let before_split = arg.split_at(equal_sign_index);
|
||||||
|
let after_split = arg.split_at(equal_sign_index + 1);
|
||||||
|
return (String::from(before_split.0), String::from(after_split.1));
|
||||||
|
}
|
||||||
|
(arg, String::from(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_value<F>((key, value): (String, String)) -> F
|
||||||
|
where
|
||||||
|
F: std::str::FromStr,
|
||||||
|
{
|
||||||
|
value.parse().unwrap_or_else(|_| {
|
||||||
|
println!("option '{}={}' was not parsable", key, value);
|
||||||
|
exit(1);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_config_file() -> std::io::Result<Self> {
|
||||||
|
let config_dir = get_config_dir()?;
|
||||||
|
|
||||||
|
let config_file = format!("{}/twilight-commander.toml", config_dir);
|
||||||
|
|
||||||
|
let config_file_content = read_file(&config_file)?;
|
||||||
|
|
||||||
|
toml::from_str(&config_file_content).map_err(|_| {
|
||||||
|
std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
"could not read the config file",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_args() {
|
||||||
|
let default_config = Config::default();
|
||||||
|
let args_vec = vec![
|
||||||
|
String::from("--behavior.file_action=file_action_test"),
|
||||||
|
String::from("--behavior.path_node_sort=path_node_sort_test"),
|
||||||
|
String::from("--behavior.scrolling=scrolling_test"),
|
||||||
|
String::from("--color.background=background_test"),
|
||||||
|
String::from("--color.foreground=foreground_test"),
|
||||||
|
String::from("--debug.enabled=true"),
|
||||||
|
String::from("--debug.padding_bot=111"),
|
||||||
|
String::from("--debug.padding_top=222"),
|
||||||
|
String::from("--debug.spacing_bot=333"),
|
||||||
|
String::from("--debug.spacing_top=444"),
|
||||||
|
String::from("--setup.working_dir=working_dir_test"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let config = Config::parse_args(default_config, args_vec);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.behavior.file_action,
|
||||||
|
String::from("file_action_test")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
config.behavior.path_node_sort,
|
||||||
|
String::from("path_node_sort_test")
|
||||||
|
);
|
||||||
|
assert_eq!(config.behavior.scrolling, String::from("scrolling_test"));
|
||||||
|
assert_eq!(config.color.background, String::from("background_test"));
|
||||||
|
assert_eq!(config.color.foreground, String::from("foreground_test"));
|
||||||
|
assert!(config.debug.enabled);
|
||||||
|
assert_eq!(config.debug.padding_bot, 111);
|
||||||
|
assert_eq!(config.debug.padding_top, 222);
|
||||||
|
assert_eq!(config.debug.spacing_bot, 333);
|
||||||
|
assert_eq!(config.debug.spacing_top, 444);
|
||||||
|
assert_eq!(config.setup.working_dir, String::from("working_dir_test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_args_with_stopper() {
|
||||||
|
let default_config = Config::default();
|
||||||
|
let args_vec = vec![
|
||||||
|
String::from("--behavior.file_action=file_action_test"),
|
||||||
|
String::from("--behavior.path_node_sort=path_node_sort_test"),
|
||||||
|
String::from("--behavior.scrolling=scrolling_test"),
|
||||||
|
String::from("--color.background=background_test"),
|
||||||
|
String::from("--color.foreground=foreground_test"),
|
||||||
|
String::from("--"),
|
||||||
|
String::from("--debug.enabled=true"),
|
||||||
|
String::from("--debug.padding_bot=111"),
|
||||||
|
String::from("--debug.padding_top=222"),
|
||||||
|
String::from("--debug.spacing_bot=333"),
|
||||||
|
String::from("--debug.spacing_top=444"),
|
||||||
|
String::from("--setup.working_dir=working_dir_test"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let config = Config::parse_args(default_config, args_vec);
|
||||||
|
let def_conf = Config::default();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.behavior.file_action,
|
||||||
|
String::from("file_action_test")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
config.behavior.path_node_sort,
|
||||||
|
String::from("path_node_sort_test")
|
||||||
|
);
|
||||||
|
assert_eq!(config.behavior.scrolling, String::from("scrolling_test"));
|
||||||
|
assert_eq!(config.color.background, String::from("background_test"));
|
||||||
|
assert_eq!(config.color.foreground, String::from("foreground_test"));
|
||||||
|
assert_eq!(config.debug.enabled, def_conf.debug.enabled);
|
||||||
|
assert_eq!(config.debug.padding_bot, def_conf.debug.padding_bot);
|
||||||
|
assert_eq!(config.debug.padding_top, def_conf.debug.padding_top);
|
||||||
|
assert_eq!(config.debug.spacing_bot, def_conf.debug.spacing_bot);
|
||||||
|
assert_eq!(config.debug.spacing_top, def_conf.debug.spacing_top);
|
||||||
|
assert_eq!(config.setup.working_dir, def_conf.setup.working_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_args_with_multiple_equals() {
|
||||||
|
let default_config = Config::default();
|
||||||
|
let args_vec = vec![String::from("--behavior.file_action=(x=1; y=2; echo $x$y)")];
|
||||||
|
|
||||||
|
let config = Config::parse_args(default_config, args_vec);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.behavior.file_action,
|
||||||
|
String::from("(x=1; y=2; echo $x$y)")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
24
src/model/config/setup.rs
Normal file
24
src/model/config/setup.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Setup {
|
||||||
|
#[serde(default = "Setup::default_working_dir")]
|
||||||
|
pub working_dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Setup {
|
||||||
|
fn default() -> Self {
|
||||||
|
Setup {
|
||||||
|
working_dir: Self::default_working_dir(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Setup {
|
||||||
|
fn default_working_dir() -> String {
|
||||||
|
String::from(".")
|
||||||
|
}
|
||||||
|
}
|
197
src/model/event.rs
Normal file
197
src/model/event.rs
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
type TEvent = termion::event::Event;
|
||||||
|
type TKey = termion::event::Key;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Key {
|
||||||
|
inner: termion::event::Event,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Event {
|
||||||
|
Resize,
|
||||||
|
Key(Key),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TEvent> for Key {
|
||||||
|
fn from(t_event: TEvent) -> Key {
|
||||||
|
Key { inner: t_event }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Key {
|
||||||
|
fn from(s: &str) -> Key {
|
||||||
|
Key::from(convert_str_to_termion_event(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Key {
|
||||||
|
fn from(s: String) -> Key {
|
||||||
|
Key::from(convert_str_to_termion_event(&s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_str_to_termion_event(s: &str) -> TEvent {
|
||||||
|
if s.chars().count() == 1 {
|
||||||
|
return TEvent::Key(TKey::Char(s.chars().last().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.starts_with("alt+") && s.len() == 5 {
|
||||||
|
return TEvent::Key(TKey::Alt(s.chars().last().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.starts_with("ctrl+") && s.len() == 6 {
|
||||||
|
return TEvent::Key(TKey::Ctrl(s.chars().last().unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
match s {
|
||||||
|
// f keys
|
||||||
|
"f1" => TEvent::Key(TKey::F(1)),
|
||||||
|
"f2" => TEvent::Key(TKey::F(2)),
|
||||||
|
"f3" => TEvent::Key(TKey::F(3)),
|
||||||
|
"f4" => TEvent::Key(TKey::F(4)),
|
||||||
|
"f5" => TEvent::Key(TKey::F(5)),
|
||||||
|
"f6" => TEvent::Key(TKey::F(6)),
|
||||||
|
"f7" => TEvent::Key(TKey::F(7)),
|
||||||
|
"f8" => TEvent::Key(TKey::F(8)),
|
||||||
|
"f9" => TEvent::Key(TKey::F(9)),
|
||||||
|
"f10" => TEvent::Key(TKey::F(10)),
|
||||||
|
"f11" => TEvent::Key(TKey::F(11)),
|
||||||
|
"f12" => TEvent::Key(TKey::F(12)),
|
||||||
|
|
||||||
|
// special keys
|
||||||
|
"backspace" => TEvent::Key(TKey::Backspace),
|
||||||
|
"left" => TEvent::Key(TKey::Left),
|
||||||
|
"right" => TEvent::Key(TKey::Right),
|
||||||
|
"up" => TEvent::Key(TKey::Up),
|
||||||
|
"down" => TEvent::Key(TKey::Down),
|
||||||
|
"home" => TEvent::Key(TKey::Home),
|
||||||
|
"end" => TEvent::Key(TKey::End),
|
||||||
|
"page_up" => TEvent::Key(TKey::PageUp),
|
||||||
|
"page_down" => TEvent::Key(TKey::PageDown),
|
||||||
|
"delete" => TEvent::Key(TKey::Delete),
|
||||||
|
"insert" => TEvent::Key(TKey::Insert),
|
||||||
|
"esc" => TEvent::Key(TKey::Esc),
|
||||||
|
"return" => TEvent::Key(TKey::Char('\n')),
|
||||||
|
"tab" => TEvent::Key(TKey::Char('\t')),
|
||||||
|
|
||||||
|
// special key combinations
|
||||||
|
|
||||||
|
// arrow keys
|
||||||
|
"ctrl+left" => TEvent::Unsupported(vec![27, 91, 49, 59, 53, 68]),
|
||||||
|
"ctrl+right" => TEvent::Unsupported(vec![27, 91, 49, 59, 53, 67]),
|
||||||
|
"ctrl+up" => TEvent::Unsupported(vec![27, 91, 49, 59, 53, 65]),
|
||||||
|
"ctrl+down" => TEvent::Unsupported(vec![27, 91, 49, 59, 53, 66]),
|
||||||
|
"shift+left" => TEvent::Unsupported(vec![27, 91, 49, 59, 50, 68]),
|
||||||
|
"shift+right" => TEvent::Unsupported(vec![27, 91, 49, 59, 50, 67]),
|
||||||
|
"shift+up" => TEvent::Unsupported(vec![27, 91, 49, 59, 50, 65]),
|
||||||
|
"shift+down" => TEvent::Unsupported(vec![27, 91, 49, 59, 50, 66]),
|
||||||
|
"alt+shift+left" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 68]),
|
||||||
|
"alt+shift+right" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 67]),
|
||||||
|
"alt+shift+up" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 65]),
|
||||||
|
"alt+shift+down" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 66]),
|
||||||
|
"shift+alt+left" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 68]),
|
||||||
|
"shift+alt+right" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 67]),
|
||||||
|
"shift+alt+up" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 65]),
|
||||||
|
"shift+alt+down" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 66]),
|
||||||
|
|
||||||
|
// default
|
||||||
|
_ => TEvent::Unsupported(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_from_termion_event_test() {
|
||||||
|
assert_eq!(
|
||||||
|
Key::from(TEvent::Unsupported(vec![27, 91, 49, 59, 52, 66])),
|
||||||
|
Key {
|
||||||
|
inner: TEvent::Unsupported(vec![27, 91, 49, 59, 52, 66])
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_from_str_test() {
|
||||||
|
assert_eq!(
|
||||||
|
Key::from("shift+alt+down"),
|
||||||
|
Key {
|
||||||
|
inner: TEvent::Unsupported(vec![27, 91, 49, 59, 52, 66])
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_from_string_test() {
|
||||||
|
assert_eq!(
|
||||||
|
Key::from(String::from("shift+alt+up")),
|
||||||
|
Key {
|
||||||
|
inner: TEvent::Unsupported(vec![27, 91, 49, 59, 52, 65])
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod convert_str_to_termion_event_tests {
|
||||||
|
use super::super::*;
|
||||||
|
#[test]
|
||||||
|
fn nonsense() {
|
||||||
|
assert_eq!(
|
||||||
|
TEvent::Unsupported(Vec::new()),
|
||||||
|
convert_str_to_termion_event("x1")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
TEvent::Unsupported(Vec::new()),
|
||||||
|
convert_str_to_termion_event("alt+x1")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
TEvent::Unsupported(Vec::new()),
|
||||||
|
convert_str_to_termion_event("ctrl+x1")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_digit() {
|
||||||
|
assert_eq!(
|
||||||
|
TEvent::Key(TKey::Char('x')),
|
||||||
|
convert_str_to_termion_event("x")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn alt_digit() {
|
||||||
|
assert_eq!(
|
||||||
|
TEvent::Key(TKey::Alt('x')),
|
||||||
|
convert_str_to_termion_event("alt+x")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn ctrl_digit() {
|
||||||
|
assert_eq!(
|
||||||
|
TEvent::Key(TKey::Ctrl('x')),
|
||||||
|
convert_str_to_termion_event("ctrl+x")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn f_key() {
|
||||||
|
assert_eq!(TEvent::Key(TKey::F(5)), convert_str_to_termion_event("f5"));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn special_key() {
|
||||||
|
assert_eq!(
|
||||||
|
TEvent::Key(TKey::PageDown),
|
||||||
|
convert_str_to_termion_event("page_down")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn special_key_comination() {
|
||||||
|
assert_eq!(
|
||||||
|
TEvent::Unsupported(vec![27, 91, 49, 59, 52, 65]),
|
||||||
|
convert_str_to_termion_event("alt+shift+up")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
src/model/mod.rs
Normal file
86
src/model/mod.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
pub mod compare_functions;
|
||||||
|
pub mod config;
|
||||||
|
pub mod event;
|
||||||
|
pub mod path_node;
|
||||||
|
pub mod tree_index;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::model::path_node::PathNode;
|
||||||
|
use crate::model::tree_index::TreeIndex;
|
||||||
|
use crate::view::composer::Composer;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_integration_with_path_node_sort_dirs_top_simple() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.setup.working_dir = String::from("./tests/test_dirs");
|
||||||
|
|
||||||
|
let composer = Composer::from(config.clone());
|
||||||
|
let mut path_node = PathNode::from(config.setup.working_dir);
|
||||||
|
let path_node_compare = PathNode::compare_dirs_top_simple;
|
||||||
|
assert_eq!(0, composer.compose_path_node(&path_node).len());
|
||||||
|
|
||||||
|
// expand_dir
|
||||||
|
path_node.expand_dir(&TreeIndex::from(Vec::new()), path_node_compare);
|
||||||
|
assert_eq!(
|
||||||
|
13,
|
||||||
|
composer.compose_path_node(&path_node).len(),
|
||||||
|
"expanding the root directory"
|
||||||
|
);
|
||||||
|
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![3]), path_node_compare);
|
||||||
|
assert_eq!(
|
||||||
|
13,
|
||||||
|
composer.compose_path_node(&path_node).len(),
|
||||||
|
"expanding a file does nothing"
|
||||||
|
);
|
||||||
|
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![1]), path_node_compare);
|
||||||
|
assert_eq!(17, composer.compose_path_node(&path_node).len());
|
||||||
|
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![1, 0]), path_node_compare);
|
||||||
|
assert_eq!(23, composer.compose_path_node(&path_node).len());
|
||||||
|
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![1, 0, 2]), path_node_compare);
|
||||||
|
assert_eq!(26, composer.compose_path_node(&path_node).len());
|
||||||
|
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![1, 0, 2, 1]), path_node_compare);
|
||||||
|
assert_eq!(29, composer.compose_path_node(&path_node).len());
|
||||||
|
|
||||||
|
// tree_index_to_flat_index
|
||||||
|
let flat_index = TreeIndex::from(vec![7, 2, 4, 0, 0]).to_flat_index();
|
||||||
|
assert_eq!(17, flat_index);
|
||||||
|
|
||||||
|
// flat_index_to_tree_index
|
||||||
|
let tree_index = path_node.flat_index_to_tree_index(9);
|
||||||
|
assert_eq!(vec![1, 0, 2, 1, 1], tree_index.index);
|
||||||
|
|
||||||
|
let tree_index = path_node.flat_index_to_tree_index(10);
|
||||||
|
assert_eq!(vec![1, 0, 2, 1, 2], tree_index.index);
|
||||||
|
|
||||||
|
let tree_index = path_node.flat_index_to_tree_index(11);
|
||||||
|
assert_eq!(vec![1, 0, 2, 2], tree_index.index);
|
||||||
|
|
||||||
|
let tree_index = path_node.flat_index_to_tree_index(15);
|
||||||
|
assert_eq!(vec![1, 1], tree_index.index);
|
||||||
|
|
||||||
|
// collapse_dir
|
||||||
|
path_node.collapse_dir(&TreeIndex::from(vec![1, 0, 2, 1]));
|
||||||
|
assert_eq!(
|
||||||
|
26,
|
||||||
|
composer.compose_path_node(&path_node).len(),
|
||||||
|
"reducing the last opened dir"
|
||||||
|
);
|
||||||
|
|
||||||
|
path_node.collapse_dir(&TreeIndex::from(vec![1, 0]));
|
||||||
|
assert_eq!(
|
||||||
|
17,
|
||||||
|
composer.compose_path_node(&path_node).len(),
|
||||||
|
"reducing lots of sub dirs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
23
src/model/path_node/debug.rs
Normal file
23
src/model/path_node/debug.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::model::path_node::PathNode;
|
||||||
|
use crate::view::composer::Composer;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::fmt::Formatter;
|
||||||
|
use std::fmt::Result;
|
||||||
|
|
||||||
|
impl Debug for PathNode {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||||
|
let composer = Composer::from(Config::new());
|
||||||
|
|
||||||
|
let entries = composer.compose_path_node(self);
|
||||||
|
|
||||||
|
for (index, entry) in entries.iter().enumerate() {
|
||||||
|
writeln!(f, "{:4}|{}", index, entry)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
293
src/model/path_node/mod.rs
Normal file
293
src/model/path_node/mod.rs
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::model::compare_functions::PathNodeCompare;
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::model::tree_index::TreeIndex;
|
||||||
|
use log::info;
|
||||||
|
use std::fs::canonicalize;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
mod debug;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PathNode {
|
||||||
|
pub children: Vec<PathNode>,
|
||||||
|
pub display_text: String,
|
||||||
|
pub is_dir: bool,
|
||||||
|
pub is_err: bool,
|
||||||
|
pub is_expanded: bool,
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for PathNode {
|
||||||
|
fn from(working_dir: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
children: Vec::new(),
|
||||||
|
display_text: String::from(working_dir),
|
||||||
|
is_dir: true,
|
||||||
|
is_err: false,
|
||||||
|
is_expanded: false,
|
||||||
|
path: PathBuf::from(working_dir),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for PathNode {
|
||||||
|
fn from(working_dir: String) -> Self {
|
||||||
|
Self {
|
||||||
|
children: Vec::new(),
|
||||||
|
display_text: working_dir.clone(),
|
||||||
|
is_dir: true,
|
||||||
|
is_err: false,
|
||||||
|
is_expanded: false,
|
||||||
|
path: PathBuf::from(working_dir),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathNode {
|
||||||
|
pub fn new_expanded(config: Config) -> Self {
|
||||||
|
info!("initializing path node");
|
||||||
|
|
||||||
|
let mut path_node = Self::from(config.setup.working_dir.clone());
|
||||||
|
let path_node_compare = Self::get_path_node_compare(&config);
|
||||||
|
path_node.expand_dir(&TreeIndex::new(), path_node_compare);
|
||||||
|
|
||||||
|
path_node
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_absolute_path(&self) -> String {
|
||||||
|
let canonicalized_path = canonicalize(self.path.as_path()).unwrap();
|
||||||
|
canonicalized_path.to_str().unwrap().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_path_node_children(&mut self, compare: PathNodeCompare) -> Vec<PathNode> {
|
||||||
|
let dirs = self.path.read_dir();
|
||||||
|
|
||||||
|
if dirs.is_err() {
|
||||||
|
self.is_err = true;
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut path_nodes = dirs
|
||||||
|
.unwrap()
|
||||||
|
.map(|dir_entry| {
|
||||||
|
let dir_entry = dir_entry.unwrap();
|
||||||
|
|
||||||
|
PathNode {
|
||||||
|
children: Vec::new(),
|
||||||
|
display_text: dir_entry.file_name().into_string().unwrap(),
|
||||||
|
is_dir: dir_entry.path().is_dir(),
|
||||||
|
is_err: false,
|
||||||
|
is_expanded: false,
|
||||||
|
path: dir_entry.path(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<PathNode>>();
|
||||||
|
|
||||||
|
path_nodes.sort_unstable_by(compare);
|
||||||
|
|
||||||
|
path_nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expand_dir(&mut self, tree_index: &TreeIndex, compare: PathNodeCompare) {
|
||||||
|
let mut path_node = self;
|
||||||
|
for i in &tree_index.index {
|
||||||
|
if path_node.children.len() > *i {
|
||||||
|
path_node = &mut path_node.children[*i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !path_node.path.is_dir() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
path_node.is_expanded = true;
|
||||||
|
path_node.children = path_node.list_path_node_children(compare);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collapse_dir(&mut self, tree_index: &TreeIndex) {
|
||||||
|
let mut path_node = self;
|
||||||
|
for i in &tree_index.index {
|
||||||
|
path_node = &mut path_node.children[*i];
|
||||||
|
}
|
||||||
|
|
||||||
|
path_node.is_expanded = false;
|
||||||
|
path_node.children = Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flat_index_to_tree_index_rec(
|
||||||
|
&self,
|
||||||
|
flat_index: &mut usize,
|
||||||
|
tree_index: &mut TreeIndex,
|
||||||
|
) -> bool {
|
||||||
|
if *flat_index == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (c, child) in self.children.iter().enumerate() {
|
||||||
|
*flat_index -= 1;
|
||||||
|
|
||||||
|
tree_index.index.push(c);
|
||||||
|
if child.flat_index_to_tree_index_rec(flat_index, tree_index) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
tree_index.index.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flat_index_to_tree_index(&self, flat_index: usize) -> TreeIndex {
|
||||||
|
let mut tree_index = TreeIndex::from(Vec::new());
|
||||||
|
self.flat_index_to_tree_index_rec(&mut (flat_index + 1), &mut tree_index);
|
||||||
|
|
||||||
|
tree_index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tree_index_to_flat_index_rec(
|
||||||
|
&self,
|
||||||
|
target_tree_index: &TreeIndex,
|
||||||
|
current_tree_index: &TreeIndex,
|
||||||
|
) -> usize {
|
||||||
|
if current_tree_index >= target_tree_index {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.children.is_empty() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sum = 1;
|
||||||
|
|
||||||
|
for (index, child) in self.children.iter().enumerate() {
|
||||||
|
let mut new_current_tree_index = current_tree_index.clone();
|
||||||
|
new_current_tree_index.index.push(index);
|
||||||
|
|
||||||
|
sum += child.tree_index_to_flat_index_rec(target_tree_index, &new_current_tree_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
sum
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tree_index_to_flat_index(&self, tree_index: &TreeIndex) -> usize {
|
||||||
|
// We count the root directory, hence we have to subtract 1 to get the
|
||||||
|
// proper index.
|
||||||
|
self.tree_index_to_flat_index_rec(tree_index, &TreeIndex::new()) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_child_path_node(&self, tree_index: &TreeIndex) -> &Self {
|
||||||
|
let mut child_node = self;
|
||||||
|
for i in &tree_index.index {
|
||||||
|
child_node = &child_node.children[*i];
|
||||||
|
}
|
||||||
|
|
||||||
|
child_node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn get_expanded_path_node() -> PathNode {
|
||||||
|
let mut path_node = PathNode::from("./tests/test_dirs");
|
||||||
|
path_node.expand_dir(&TreeIndex::new(), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![0]), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(
|
||||||
|
&TreeIndex::from(vec![0, 0]),
|
||||||
|
PathNode::compare_dirs_top_simple,
|
||||||
|
);
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![1]), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(
|
||||||
|
&TreeIndex::from(vec![1, 0]),
|
||||||
|
PathNode::compare_dirs_top_simple,
|
||||||
|
);
|
||||||
|
path_node.expand_dir(
|
||||||
|
&TreeIndex::from(vec![1, 0, 2]),
|
||||||
|
PathNode::compare_dirs_top_simple,
|
||||||
|
);
|
||||||
|
path_node
|
||||||
|
}
|
||||||
|
|
||||||
|
mod get_child_path_node_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_dirs() {
|
||||||
|
let path_node = {
|
||||||
|
let mut path_node = PathNode::from("./tests/test_dirs");
|
||||||
|
path_node.expand_dir(&TreeIndex::new(), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(&TreeIndex::from(vec![0]), PathNode::compare_dirs_top_simple);
|
||||||
|
path_node.expand_dir(
|
||||||
|
&TreeIndex::from(vec![0, 0]),
|
||||||
|
PathNode::compare_dirs_top_simple,
|
||||||
|
);
|
||||||
|
path_node
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_path_node = path_node.get_child_path_node(&TreeIndex::from(vec![0, 0, 0]));
|
||||||
|
|
||||||
|
assert_eq!("file4", child_path_node.display_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_dirs() {
|
||||||
|
let path_node = get_expanded_path_node();
|
||||||
|
|
||||||
|
let child_path_node = path_node.get_child_path_node(&TreeIndex::from(vec![1, 0, 2, 2]));
|
||||||
|
|
||||||
|
assert_eq!("file12", child_path_node.display_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod tree_index_to_flat_index_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_dirs() {
|
||||||
|
let path_node = get_expanded_path_node();
|
||||||
|
|
||||||
|
let flat_index = path_node.tree_index_to_flat_index(&TreeIndex::from(vec![4]));
|
||||||
|
|
||||||
|
assert_eq!(22, flat_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_dirs2() {
|
||||||
|
let path_node = get_expanded_path_node();
|
||||||
|
|
||||||
|
let flat_index = path_node.tree_index_to_flat_index(&TreeIndex::from(vec![5]));
|
||||||
|
|
||||||
|
assert_eq!(23, flat_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_dirs3() {
|
||||||
|
let path_node = get_expanded_path_node();
|
||||||
|
|
||||||
|
let flat_index = path_node.tree_index_to_flat_index(&TreeIndex::from(vec![1, 0, 4]));
|
||||||
|
|
||||||
|
assert_eq!(15, flat_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn total_count() {
|
||||||
|
let path_node = get_expanded_path_node();
|
||||||
|
|
||||||
|
let flat_index = path_node.tree_index_to_flat_index(&TreeIndex::from(vec![100_000]));
|
||||||
|
|
||||||
|
assert_eq!(31, flat_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero() {
|
||||||
|
let path_node = get_expanded_path_node();
|
||||||
|
|
||||||
|
let flat_index = path_node.tree_index_to_flat_index(&TreeIndex::from(vec![0]));
|
||||||
|
|
||||||
|
assert_eq!(0, flat_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
113
src/model/tree_index.rs
Normal file
113
src/model/tree_index.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||||
|
pub struct TreeIndex {
|
||||||
|
pub index: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<usize>> for TreeIndex {
|
||||||
|
fn from(index: Vec<usize>) -> Self {
|
||||||
|
Self { index }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TreeIndex {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { index: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_parent(&self) -> Self {
|
||||||
|
if self.index.is_empty() {
|
||||||
|
return Self { index: vec![] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut index = self.index.clone();
|
||||||
|
index.pop().unwrap();
|
||||||
|
|
||||||
|
Self { index }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)] // TODO: remove?
|
||||||
|
pub fn to_flat_index(&self) -> usize {
|
||||||
|
if self.index.is_empty() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut flat_index = 0;
|
||||||
|
for i in &self.index {
|
||||||
|
flat_index += i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
flat_index - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
mod get_parent_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
let tree_index = TreeIndex::new();
|
||||||
|
let parent = tree_index.get_parent();
|
||||||
|
assert_eq!(TreeIndex::new(), parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn minimal() {
|
||||||
|
let tree_index = TreeIndex::from(vec![0]);
|
||||||
|
let parent = tree_index.get_parent();
|
||||||
|
assert_eq!(TreeIndex::new(), parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zeroes() {
|
||||||
|
let tree_index = TreeIndex::from(vec![0, 0, 0, 0, 0]);
|
||||||
|
let parent = tree_index.get_parent();
|
||||||
|
assert_eq!(TreeIndex::from(vec![0, 0, 0, 0]), parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex() {
|
||||||
|
let tree_index = TreeIndex::from(vec![3, 4, 6, 7, 1]);
|
||||||
|
let parent = tree_index.get_parent();
|
||||||
|
assert_eq!(TreeIndex::from(vec![3, 4, 6, 7]), parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod to_flat_index_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
let tree_index = TreeIndex::new();
|
||||||
|
let flat_index = tree_index.to_flat_index();
|
||||||
|
assert_eq!(0, flat_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn minimal() {
|
||||||
|
let tree_index = TreeIndex::from(vec![0]);
|
||||||
|
let flat_index = tree_index.to_flat_index();
|
||||||
|
assert_eq!(0, flat_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zeroes() {
|
||||||
|
let tree_index = TreeIndex::from(vec![0, 0, 0, 0, 0]);
|
||||||
|
let flat_index = tree_index.to_flat_index();
|
||||||
|
assert_eq!(4, flat_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex() {
|
||||||
|
let tree_index = TreeIndex::from(vec![3, 4, 6, 7, 1]);
|
||||||
|
let flat_index = tree_index.to_flat_index();
|
||||||
|
assert_eq!(25, flat_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
src/utils.rs
Normal file
79
src/utils.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::panic::set_hook;
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
pub fn read_file(file_name: &str) -> std::io::Result<String> {
|
||||||
|
let mut file = File::open(file_name)?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)?;
|
||||||
|
Ok(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_help() {
|
||||||
|
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
||||||
|
println!(r#"usage: twilight-tree [--key1=value1 --key2=value2 ...]"#);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_logger() -> Result<(), fern::InitError> {
|
||||||
|
let log_file_path = format!("{}/{}", get_config_dir()?, "tc.log");
|
||||||
|
|
||||||
|
fern::Dispatch::new()
|
||||||
|
.format(|out, message, record| {
|
||||||
|
let target = record.target();
|
||||||
|
let target_split_at = 0.max(target.len() as i32 - 20) as usize;
|
||||||
|
let target_short = target.split_at(target_split_at);
|
||||||
|
|
||||||
|
out.finish(format_args!(
|
||||||
|
"[{}][{:05}][{:>20}] {}",
|
||||||
|
chrono::Local::now().to_rfc3339_opts(::chrono::SecondsFormat::Millis, true),
|
||||||
|
record.level(),
|
||||||
|
target_short.1,
|
||||||
|
message
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.level(log::LevelFilter::Debug)
|
||||||
|
.chain(fern::log_file(log_file_path)?)
|
||||||
|
.apply()?;
|
||||||
|
|
||||||
|
set_hook(Box::new(|panic_info| {
|
||||||
|
if let Some(p) = panic_info.payload().downcast_ref::<String>() {
|
||||||
|
info!("{:?}, \npayload: {}", panic_info, p,);
|
||||||
|
} else if let Some(p) = panic_info.payload().downcast_ref::<&str>() {
|
||||||
|
info!("{:?}, \npayload: {}", panic_info, p,);
|
||||||
|
} else {
|
||||||
|
info!("{:?}", panic_info);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
info!(
|
||||||
|
r#"starting...
|
||||||
|
|
||||||
|
_|_ o|o _ |__|_ _ _ ._ _ ._ _ _.._ _| _ ._
|
||||||
|
|_\/\/|||(_|| ||_ (_(_)| | || | |(_|| |(_|(/_|
|
||||||
|
_|
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("logger initialized");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config_dir() -> std::io::Result<String> {
|
||||||
|
if let Ok(xdg_config_home) = std::env::var("XDG_CONFIG_HOME") {
|
||||||
|
Ok(xdg_config_home)
|
||||||
|
} else if let Ok(home) = std::env::var("HOME") {
|
||||||
|
Ok(format!("{}/.config/twilight-tree", home))
|
||||||
|
} else {
|
||||||
|
Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
"no HOME or XDG_CONFIG_HOME variable is defined",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
126
src/view/composer.rs
Normal file
126
src/view/composer.rs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::model::path_node::PathNode;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
pub struct Composer {
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Config> for Composer {
|
||||||
|
fn from(config: Config) -> Self {
|
||||||
|
info!("initializing composer");
|
||||||
|
Self { config }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Composer {
|
||||||
|
pub fn truncate_string(string: &str, desired_char_count: usize) -> String {
|
||||||
|
if desired_char_count < 1 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
if desired_char_count >= string.chars().count() {
|
||||||
|
return String::from(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
let truncated = match string.char_indices().nth(desired_char_count - 1) {
|
||||||
|
None => string,
|
||||||
|
Some((idx, _)) => &string[..idx],
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{}~", truncated)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compose_path_node(&self, path_node: &PathNode) -> Vec<String> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
self.compose_path_node_recursive(path_node, &mut result, 0);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compose_path_node_recursive(
|
||||||
|
&self,
|
||||||
|
path_node: &PathNode,
|
||||||
|
texts: &mut Vec<String>,
|
||||||
|
depth: usize,
|
||||||
|
) {
|
||||||
|
for child in &path_node.children {
|
||||||
|
let dir_prefix = self.get_dir_prefix(child);
|
||||||
|
let dir_suffix = self.get_dir_suffix(child);
|
||||||
|
let indent = self.get_indent(depth);
|
||||||
|
|
||||||
|
let text = format!(
|
||||||
|
"{}{}{}{}",
|
||||||
|
indent,
|
||||||
|
dir_prefix,
|
||||||
|
child.display_text.clone(),
|
||||||
|
dir_suffix,
|
||||||
|
);
|
||||||
|
texts.push(text);
|
||||||
|
self.compose_path_node_recursive(child, texts, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_dir_prefix(&self, path_node: &PathNode) -> String {
|
||||||
|
let (err_char, expanded_char, reduced_char) = if self.config.composition.use_utf8 {
|
||||||
|
('⨯', '▼', '▶')
|
||||||
|
} else {
|
||||||
|
('x', 'v', '>')
|
||||||
|
};
|
||||||
|
|
||||||
|
let expanded_indicator = if path_node.is_err {
|
||||||
|
err_char
|
||||||
|
} else if path_node.is_expanded {
|
||||||
|
expanded_char
|
||||||
|
} else {
|
||||||
|
reduced_char
|
||||||
|
};
|
||||||
|
|
||||||
|
if path_node.is_dir {
|
||||||
|
format!("{} ", expanded_indicator)
|
||||||
|
} else {
|
||||||
|
String::from(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_dir_suffix(&self, path_node: &PathNode) -> String {
|
||||||
|
if path_node.is_dir {
|
||||||
|
String::from("/")
|
||||||
|
} else {
|
||||||
|
String::from("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_indent(&self, depth: usize) -> String {
|
||||||
|
let indent_char = if !self.config.composition.show_indent {
|
||||||
|
' '
|
||||||
|
} else if self.config.composition.use_utf8 {
|
||||||
|
'·'
|
||||||
|
} else {
|
||||||
|
'-'
|
||||||
|
};
|
||||||
|
let indent = " ".repeat(self.config.composition.indent as usize - 1);
|
||||||
|
|
||||||
|
format!("{}{}", indent_char, indent).repeat(depth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_string_test() {
|
||||||
|
let tc = Composer::truncate_string;
|
||||||
|
assert_eq!(tc("hello world", 5), "hell~");
|
||||||
|
assert_eq!(tc("hello world", 1), "~");
|
||||||
|
assert_eq!(tc("hello world", 0), "");
|
||||||
|
assert_eq!(tc("aaa▶bbb▶ccc", 8), "aaa▶bbb~");
|
||||||
|
assert_eq!(tc("aaa▶bbb▶ccc", 6), "aaa▶b~");
|
||||||
|
assert_eq!(tc("aaa▶bbb▶ccc", 4), "aaa~");
|
||||||
|
}
|
||||||
|
}
|
68
src/view/mod.rs
Normal file
68
src/view/mod.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::view::composer::Composer;
|
||||||
|
use log::info;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
pub mod composer;
|
||||||
|
mod print;
|
||||||
|
mod scroll;
|
||||||
|
mod update;
|
||||||
|
|
||||||
|
pub struct Pager<W: Write> {
|
||||||
|
config: Config,
|
||||||
|
pub cursor_row: i32,
|
||||||
|
out: W,
|
||||||
|
terminal_cols: i32,
|
||||||
|
terminal_rows: i32,
|
||||||
|
text_row: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Pager<W> {
|
||||||
|
pub fn new(config: Config, mut out: W) -> Self {
|
||||||
|
info!("initializing pager");
|
||||||
|
|
||||||
|
write!(
|
||||||
|
out,
|
||||||
|
"{}{}{}",
|
||||||
|
termion::cursor::Hide,
|
||||||
|
termion::cursor::Goto(1, 1),
|
||||||
|
termion::clear::All,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
cursor_row: 0,
|
||||||
|
out,
|
||||||
|
terminal_cols: 0,
|
||||||
|
terminal_rows: 0,
|
||||||
|
text_row: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Drop for Pager<W> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}{}{}",
|
||||||
|
termion::clear::All,
|
||||||
|
termion::cursor::Goto(1, 1),
|
||||||
|
termion::cursor::Show,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Write for Pager<W> {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
self.out.write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
self.out.flush()
|
||||||
|
}
|
||||||
|
}
|
247
src/view/print.rs
Normal file
247
src/view/print.rs
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
// Copyright (c) 2024 Awiteb <a@4rs.nl>
|
||||||
|
|
||||||
|
use crate::view::Composer;
|
||||||
|
use crate::view::Pager;
|
||||||
|
use std::io::Write;
|
||||||
|
use termion::{color, style};
|
||||||
|
|
||||||
|
impl<W: Write> Pager<W> {
|
||||||
|
pub fn print_clear(&mut self) {
|
||||||
|
write!(self, "{}", termion::clear::All).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_text_entry(&mut self, text_entry: &str, row: i32) {
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}{}{}",
|
||||||
|
termion::cursor::Goto(1, row as u16),
|
||||||
|
Composer::truncate_string(text_entry, self.terminal_cols as usize),
|
||||||
|
style::Reset
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_text_entry_emphasized(&mut self, text_entry: &str, row: i32) {
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}{}{}{}",
|
||||||
|
termion::cursor::Goto(1, row as u16),
|
||||||
|
color::Bg(color::Blue),
|
||||||
|
Composer::truncate_string(text_entry, self.terminal_cols as usize),
|
||||||
|
style::Reset
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_header(&mut self, text: &str) {
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}{}",
|
||||||
|
termion::cursor::Goto(1, 1),
|
||||||
|
Composer::truncate_string(text, self.terminal_cols as usize),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_footer(&mut self, text: &str) {
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}{}",
|
||||||
|
termion::cursor::Goto(1, 1 + self.terminal_rows as u16),
|
||||||
|
Composer::truncate_string(text, self.terminal_cols as usize),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_debug_info(&mut self) {
|
||||||
|
if !self.config.debug.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let padding_bot = self.config.debug.padding_bot;
|
||||||
|
let padding_top = self.config.debug.padding_top;
|
||||||
|
let spacing_bot = self.config.debug.spacing_bot;
|
||||||
|
let spacing_top = self.config.debug.spacing_top;
|
||||||
|
|
||||||
|
// line numbers
|
||||||
|
for i in 0..self.terminal_rows {
|
||||||
|
write!(self, "{} L{i}", termion::cursor::Goto(50, 1 + i as u16)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// padding_top debug
|
||||||
|
for i in 0..padding_bot {
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}~~~ padding_bot",
|
||||||
|
termion::cursor::Goto(30, (self.terminal_rows - (spacing_bot + i)) as u16)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..padding_top {
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}~~~ padding_top",
|
||||||
|
termion::cursor::Goto(30, (1 + spacing_top + i) as u16)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// spacing_top debug
|
||||||
|
for i in 0..spacing_bot {
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}--- spacing_bot",
|
||||||
|
termion::cursor::Goto(30, (self.terminal_rows - i) as u16)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
for i in 0..spacing_top {
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}--- spacing_top",
|
||||||
|
termion::cursor::Goto(30, 1 + i as u16)
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug info
|
||||||
|
let terminal_rows = self.terminal_rows;
|
||||||
|
let terminal_cols = self.terminal_cols;
|
||||||
|
let cursor_row = self.cursor_row;
|
||||||
|
let text_row = self.text_row;
|
||||||
|
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}rows: {}, cols: {}",
|
||||||
|
termion::cursor::Goto(1, (self.terminal_rows - 1) as u16),
|
||||||
|
terminal_rows,
|
||||||
|
terminal_cols
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
write!(
|
||||||
|
self,
|
||||||
|
"{}cursor_row: {}, text_row: {}",
|
||||||
|
termion::cursor::Goto(1, self.terminal_rows as u16),
|
||||||
|
cursor_row,
|
||||||
|
text_row
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::model::config::Config;
|
||||||
|
|
||||||
|
fn prepare_pager() -> Pager<Vec<u8>> {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.debug.enabled = true;
|
||||||
|
config.debug.padding_bot = 1;
|
||||||
|
config.debug.padding_top = 1;
|
||||||
|
config.debug.spacing_bot = 1;
|
||||||
|
config.debug.spacing_top = 1;
|
||||||
|
|
||||||
|
let out: Vec<u8> = Vec::new();
|
||||||
|
let mut pager = Pager::new(config, out);
|
||||||
|
|
||||||
|
pager.terminal_cols = 100;
|
||||||
|
pager.terminal_rows = 10;
|
||||||
|
|
||||||
|
pager
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_result(pager: Pager<Vec<u8>>) -> Option<String> {
|
||||||
|
let pager_out = pager.out.clone();
|
||||||
|
Some(String::from(std::str::from_utf8(&pager_out).unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn print_clear_test() {
|
||||||
|
let result = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.print_clear();
|
||||||
|
get_result(pager)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!("\u{1b}[?25l\u{1b}[1;1H\u{1b}[2J\u{1b}[2J", result.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn print_text_entry_test() {
|
||||||
|
let result = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.print_text_entry("--- test 123 ---", 42);
|
||||||
|
get_result(pager)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"\u{1b}[?25l\u{1b}[1;1H\u{1b}[2J\u{1b}[42;1H--- test 123 ---\u{1b}[m",
|
||||||
|
result.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn print_text_entry_emphasized_test() {
|
||||||
|
let result = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.print_text_entry_emphasized("--- test 123 ---", 42);
|
||||||
|
get_result(pager)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"\u{1b}[?25l\u{1b}[1;1H\u{1b}[2J\u{1b}[42;1H\u{1b}[48;5;4m--- test 123 ---\u{1b}[m",
|
||||||
|
result.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn print_header_test() {
|
||||||
|
let result = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.print_header("--- test 123 ---");
|
||||||
|
get_result(pager)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"\u{1b}[?25l\u{1b}[1;1H\u{1b}[2J\u{1b}[1;1H--- test 123 ---",
|
||||||
|
result.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn print_footer_test() {
|
||||||
|
let result = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.print_footer("--- test 123 ---");
|
||||||
|
get_result(pager)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"\u{1b}[?25l\u{1b}[1;1H\u{1b}[2J\u{1b}[11;1H--- test 123 ---",
|
||||||
|
result.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn print_debug_info_test() {
|
||||||
|
let result = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.print_debug_info();
|
||||||
|
get_result(pager)
|
||||||
|
}
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(result.contains("~~~ padding_bot"));
|
||||||
|
assert!(result.contains("~~~ padding_top"));
|
||||||
|
assert!(result.contains("--- spacing_bot"));
|
||||||
|
assert!(result.contains("--- spacing_top"));
|
||||||
|
assert!(result.contains("cols: 100"));
|
||||||
|
assert!(result.contains("rows: 10"));
|
||||||
|
assert!(result.contains("cursor_row: 0"));
|
||||||
|
assert!(result.contains("text_row: 0"));
|
||||||
|
}
|
||||||
|
}
|
309
src/view/scroll.rs
Normal file
309
src/view/scroll.rs
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::view::Pager;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
impl<W: Write> Pager<W> {
|
||||||
|
fn get_index_overshoot(index_under_test: i32, index_now: i32, index_delta: i32) -> Option<i32> {
|
||||||
|
let index_before = index_now - index_delta;
|
||||||
|
|
||||||
|
// cross from below
|
||||||
|
if index_before <= index_under_test && index_now >= index_under_test {
|
||||||
|
return Some(index_now - index_under_test);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cross from above
|
||||||
|
if index_before >= index_under_test && index_now <= index_under_test {
|
||||||
|
return Some(index_now - index_under_test);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_like_center(&self, cursor_row_delta: i32, text_entries_len: i32) -> i32 {
|
||||||
|
let spacing_bot = self.config.debug.spacing_bot;
|
||||||
|
let spacing_top = self.config.debug.spacing_top;
|
||||||
|
|
||||||
|
let center_text_row =
|
||||||
|
spacing_top - self.text_row + (self.terminal_rows - (spacing_bot + spacing_top)) / 2;
|
||||||
|
let last_text_row = self.terminal_rows - (self.text_row + spacing_bot);
|
||||||
|
|
||||||
|
// re-center a cursor row that is below the center (last text entry was
|
||||||
|
// visible) in the case that a subdirectory is opened
|
||||||
|
// in such a way that the bottom is not visible anymore
|
||||||
|
if cursor_row_delta == 0
|
||||||
|
&& self.cursor_row - center_text_row > 0
|
||||||
|
&& self.cursor_row - center_text_row <= text_entries_len - last_text_row
|
||||||
|
{
|
||||||
|
return self.text_row - (self.cursor_row - center_text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cursor row is moved over the center
|
||||||
|
if let Some(overshoot) =
|
||||||
|
Self::get_index_overshoot(center_text_row, self.cursor_row, cursor_row_delta)
|
||||||
|
{
|
||||||
|
// no need to keep it centered when we reach the top or bottom
|
||||||
|
if self.text_row >= spacing_top && cursor_row_delta < 0 {
|
||||||
|
return self.text_row;
|
||||||
|
}
|
||||||
|
if self.text_row + text_entries_len <= self.terminal_rows - spacing_bot
|
||||||
|
&& cursor_row_delta > 0
|
||||||
|
{
|
||||||
|
return self.text_row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent violating the spacing when centering on the cursor row.
|
||||||
|
// E.g. when jumping to the parent directory and it is the topmost
|
||||||
|
// entry do not want it centered.
|
||||||
|
if self.text_row - overshoot > spacing_top {
|
||||||
|
return spacing_top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep it centered
|
||||||
|
return self.text_row - overshoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cursor row is beyond vision -> move the text row the minimal amount
|
||||||
|
// to correct that
|
||||||
|
if self.text_row + self.cursor_row < spacing_top {
|
||||||
|
return spacing_top - self.cursor_row;
|
||||||
|
} else if self.text_row + self.cursor_row > self.terminal_rows - (1 + spacing_bot) {
|
||||||
|
return self.terminal_rows - (1 + spacing_bot + self.cursor_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text_row
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_like_editor(&self) -> i32 {
|
||||||
|
let padding_bot = self.config.debug.padding_bot;
|
||||||
|
let padding_top = self.config.debug.padding_top;
|
||||||
|
let spacing_bot = self.config.debug.spacing_bot;
|
||||||
|
let spacing_top = self.config.debug.spacing_top;
|
||||||
|
|
||||||
|
if self.text_row + self.cursor_row < spacing_top + padding_top {
|
||||||
|
return spacing_top + padding_top - self.cursor_row;
|
||||||
|
} else if self.text_row + self.cursor_row
|
||||||
|
> self.terminal_rows - (1 + spacing_bot + padding_bot)
|
||||||
|
{
|
||||||
|
return self.terminal_rows - (1 + spacing_bot + padding_bot + self.cursor_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text_row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::model::config::Config;
|
||||||
|
|
||||||
|
fn prepare_pager() -> Pager<Vec<u8>> {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.debug.enabled = true;
|
||||||
|
config.debug.padding_bot = 1;
|
||||||
|
config.debug.padding_top = 1;
|
||||||
|
config.debug.spacing_bot = 1;
|
||||||
|
config.debug.spacing_top = 1;
|
||||||
|
|
||||||
|
let out: Vec<u8> = Vec::new();
|
||||||
|
let mut pager = Pager::new(config, out);
|
||||||
|
|
||||||
|
pager.terminal_cols = 100;
|
||||||
|
pager.terminal_rows = 10;
|
||||||
|
|
||||||
|
pager
|
||||||
|
}
|
||||||
|
|
||||||
|
mod get_index_overshoot_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overshoot_from_below() {
|
||||||
|
let overshoot = Pager::<Vec<u8>>::get_index_overshoot(10, 11, 3);
|
||||||
|
assert_eq!(Some(1), overshoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overshoot_from_above() {
|
||||||
|
let overshoot = Pager::<Vec<u8>>::get_index_overshoot(10, 7, -4);
|
||||||
|
assert_eq!(Some(-3), overshoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_overshoot_from_below() {
|
||||||
|
let overshoot = Pager::<Vec<u8>>::get_index_overshoot(10, 7, 2);
|
||||||
|
assert_eq!(None, overshoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_overshoot_from_above() {
|
||||||
|
let overshoot = Pager::<Vec<u8>>::get_index_overshoot(10, 14, -3);
|
||||||
|
assert_eq!(None, overshoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod scroll_like_center_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_center_cursor_top_test() {
|
||||||
|
let text_row = {
|
||||||
|
let pager = prepare_pager();
|
||||||
|
pager.scroll_like_center(1, 17)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(1, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_center_text_moves_up1_test() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.cursor_row = 5;
|
||||||
|
pager.scroll_like_center(1, 17)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(0, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_center_text_moves_up2_test() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.cursor_row = 6;
|
||||||
|
pager.scroll_like_center(1, 17)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(-1, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_center_text_moves_down_test() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.cursor_row = 6;
|
||||||
|
pager.scroll_like_center(-1, 17)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(0, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_center_cursor_bot_test() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.cursor_row = 9;
|
||||||
|
pager.scroll_like_center(-1, 17)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(-1, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_center_cursor_bot_no_delta_test() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.cursor_row = 9;
|
||||||
|
pager.scroll_like_center(0, 17)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(-4, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_center_cursor_with_overshoot() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
println!("{}", pager.text_row);
|
||||||
|
pager.cursor_row = 13;
|
||||||
|
pager.text_row = -10;
|
||||||
|
pager.scroll_like_center(-3, 123)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(-8, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_center_cursor_top_most_overshoot() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
println!("{}", pager.text_row);
|
||||||
|
pager.cursor_row = 0;
|
||||||
|
pager.text_row = -10;
|
||||||
|
pager.scroll_like_center(-16, 123)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(1, text_row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod scroll_like_editor_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_editor_cursor_top_test() {
|
||||||
|
let text_row = {
|
||||||
|
let pager = prepare_pager();
|
||||||
|
pager.scroll_like_editor()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(2, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_editor_text_moves_up1_test() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.cursor_row = 5;
|
||||||
|
pager.scroll_like_editor()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(0, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_editor_text_moves_up2_test() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.cursor_row = 6;
|
||||||
|
pager.scroll_like_editor()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(0, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_editor_text_moves_down_test() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.cursor_row = 6;
|
||||||
|
pager.scroll_like_editor()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(0, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_editor_cursor_bot_test() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.cursor_row = 9;
|
||||||
|
pager.scroll_like_editor()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(-2, text_row);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scroll_like_editor_cursor_bot_no_delta_test() {
|
||||||
|
let text_row = {
|
||||||
|
let mut pager = prepare_pager();
|
||||||
|
pager.cursor_row = 9;
|
||||||
|
pager.scroll_like_editor()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(-2, text_row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
src/view/update.rs
Normal file
72
src/view/update.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2019-2022 golmman
|
||||||
|
|
||||||
|
use crate::view::Pager;
|
||||||
|
use std::io::Write;
|
||||||
|
use termion::terminal_size;
|
||||||
|
|
||||||
|
impl<W: Write> Pager<W> {
|
||||||
|
fn update_terminal_size(&mut self) {
|
||||||
|
let (terminal_cols_raw, terminal_rows_raw) = terminal_size().unwrap();
|
||||||
|
self.terminal_cols = i32::from(terminal_cols_raw);
|
||||||
|
self.terminal_rows = i32::from(terminal_rows_raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_cursor_row(&mut self, cursor_row_delta: i32, text_entries_len: i32) {
|
||||||
|
self.cursor_row += cursor_row_delta;
|
||||||
|
if self.cursor_row < 0 {
|
||||||
|
self.cursor_row = text_entries_len - 1;
|
||||||
|
}
|
||||||
|
if self.cursor_row >= text_entries_len {
|
||||||
|
self.cursor_row = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, cursor_row_delta: i32, text_entries: &[String], header_text: String) {
|
||||||
|
self.update_terminal_size();
|
||||||
|
|
||||||
|
let spacing_bot = self.config.debug.spacing_bot;
|
||||||
|
let spacing_top = self.config.debug.spacing_top;
|
||||||
|
|
||||||
|
let text_entries_len = text_entries.len() as i32;
|
||||||
|
|
||||||
|
self.update_cursor_row(cursor_row_delta, text_entries_len);
|
||||||
|
|
||||||
|
self.text_row = match self.config.behavior.scrolling.as_str() {
|
||||||
|
"center" => self.scroll_like_center(cursor_row_delta, text_entries_len),
|
||||||
|
"editor" => self.scroll_like_editor(),
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let displayable_rows = self.terminal_rows - (spacing_bot + spacing_top);
|
||||||
|
|
||||||
|
let first_index = spacing_top - self.text_row;
|
||||||
|
|
||||||
|
// clear screen
|
||||||
|
self.print_clear();
|
||||||
|
|
||||||
|
// print rows
|
||||||
|
for i in 0..displayable_rows {
|
||||||
|
let index = first_index + i;
|
||||||
|
|
||||||
|
if index >= 0 && index < text_entries.len() as i32 {
|
||||||
|
let text_entry = &text_entries[index as usize];
|
||||||
|
|
||||||
|
if index == self.cursor_row {
|
||||||
|
self.print_text_entry_emphasized(text_entry, 1 + spacing_top + i)
|
||||||
|
} else {
|
||||||
|
self.print_text_entry(text_entry, 1 + spacing_top + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let footer_text = format!("[{}/{}]", self.cursor_row + 1, text_entries_len);
|
||||||
|
|
||||||
|
self.print_header(&header_text);
|
||||||
|
self.print_footer(&footer_text);
|
||||||
|
|
||||||
|
self.print_debug_info();
|
||||||
|
|
||||||
|
self.flush().unwrap();
|
||||||
|
}
|
||||||
|
}
|
0
tests/test_dirs/dir0/dir3/file4
Normal file
0
tests/test_dirs/dir0/dir3/file4
Normal file
0
tests/test_dirs/dir0/dir3/file5
Normal file
0
tests/test_dirs/dir0/dir3/file5
Normal file
0
tests/test_dirs/dir0/dir4/file16
Normal file
0
tests/test_dirs/dir0/dir4/file16
Normal file
0
tests/test_dirs/dir0/dir5/file6
Normal file
0
tests/test_dirs/dir0/dir5/file6
Normal file
0
tests/test_dirs/dir1/dir6/dir10/file28
Normal file
0
tests/test_dirs/dir1/dir6/dir10/file28
Normal file
0
tests/test_dirs/dir1/dir6/dir8/file17
Normal file
0
tests/test_dirs/dir1/dir6/dir8/file17
Normal file
0
tests/test_dirs/dir1/dir6/dir9/dir11/file18
Normal file
0
tests/test_dirs/dir1/dir6/dir9/dir11/file18
Normal file
0
tests/test_dirs/dir1/dir6/dir9/dir12/file13
Normal file
0
tests/test_dirs/dir1/dir6/dir9/dir12/file13
Normal file
0
tests/test_dirs/dir1/dir6/dir9/dir12/file14
Normal file
0
tests/test_dirs/dir1/dir6/dir9/dir12/file14
Normal file
0
tests/test_dirs/dir1/dir6/dir9/dir12/file15
Normal file
0
tests/test_dirs/dir1/dir6/dir9/dir12/file15
Normal file
0
tests/test_dirs/dir1/dir6/dir9/file12
Normal file
0
tests/test_dirs/dir1/dir6/dir9/file12
Normal file
0
tests/test_dirs/dir1/dir6/file10
Normal file
0
tests/test_dirs/dir1/dir6/file10
Normal file
0
tests/test_dirs/dir1/dir6/file11
Normal file
0
tests/test_dirs/dir1/dir6/file11
Normal file
0
tests/test_dirs/dir1/dir6/file9
Normal file
0
tests/test_dirs/dir1/dir6/file9
Normal file
0
tests/test_dirs/dir1/dir7/file19
Normal file
0
tests/test_dirs/dir1/dir7/file19
Normal file
0
tests/test_dirs/dir1/file7
Normal file
0
tests/test_dirs/dir1/file7
Normal file
0
tests/test_dirs/dir1/file8
Normal file
0
tests/test_dirs/dir1/file8
Normal file
0
tests/test_dirs/dir2/file20
Normal file
0
tests/test_dirs/dir2/file20
Normal file
0
tests/test_dirs/file0
Normal file
0
tests/test_dirs/file0
Normal file
0
tests/test_dirs/file1
Normal file
0
tests/test_dirs/file1
Normal file
0
tests/test_dirs/file2
Normal file
0
tests/test_dirs/file2
Normal file
0
tests/test_dirs/file21
Normal file
0
tests/test_dirs/file21
Normal file
0
tests/test_dirs/file22
Normal file
0
tests/test_dirs/file22
Normal file
0
tests/test_dirs/file23
Normal file
0
tests/test_dirs/file23
Normal file
0
tests/test_dirs/file24
Normal file
0
tests/test_dirs/file24
Normal file
0
tests/test_dirs/file25
Normal file
0
tests/test_dirs/file25
Normal file
0
tests/test_dirs/file26
Normal file
0
tests/test_dirs/file26
Normal file
0
tests/test_dirs/file27
Normal file
0
tests/test_dirs/file27
Normal file
56
twilight-tree.toml
Normal file
56
twilight-tree.toml
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
[behavior]
|
||||||
|
# command interpreted by bash when pressing the file_action key
|
||||||
|
file_action = "true"
|
||||||
|
|
||||||
|
# when true the program will quit after first action
|
||||||
|
quit_on_action = false
|
||||||
|
|
||||||
|
# determines the compare function used for sorting entries
|
||||||
|
# enum: none, dirs_top_simple, dirs_bot_simple
|
||||||
|
# TODO: rename to entry_sort
|
||||||
|
path_node_sort = "dirs_top_simple"
|
||||||
|
|
||||||
|
# the scrollung algorithm used
|
||||||
|
# enum: center, editor
|
||||||
|
scrolling = "center"
|
||||||
|
|
||||||
|
# the amount of entries skipped when the skip keys are pressed
|
||||||
|
skip_amount = 5
|
||||||
|
|
||||||
|
|
||||||
|
[composition]
|
||||||
|
# indention used for subentries
|
||||||
|
indent = 2
|
||||||
|
|
||||||
|
# when true shows visual markers for indention whitespaces
|
||||||
|
show_indent = false
|
||||||
|
|
||||||
|
# when true uses utf8 characters
|
||||||
|
use_utf8 = true
|
||||||
|
|
||||||
|
[debug]
|
||||||
|
# enables debug mode
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
# the minimum distance of the highlighted entry to the spacing
|
||||||
|
padding_bot = 3
|
||||||
|
padding_top = 3
|
||||||
|
|
||||||
|
# the number of lines not uses for entries
|
||||||
|
spacing_bot = 2
|
||||||
|
spacing_top = 2
|
||||||
|
|
||||||
|
[keybinding]
|
||||||
|
collapse_dir = "left"
|
||||||
|
entry_down = "down"
|
||||||
|
entry_up = "up"
|
||||||
|
expand_dir = "right"
|
||||||
|
file_action = "return"
|
||||||
|
quit = "q"
|
||||||
|
reload = "r"
|
||||||
|
skip_up = "ctrl+up"
|
||||||
|
skip_down = "ctrl+down"
|
||||||
|
|
||||||
|
[setup]
|
||||||
|
# the working directory used when starting
|
||||||
|
working_dir = "."
|
Loading…
Reference in a new issue