From 36a979d234dcf8c5b33fe6528a6d243ac7443688 Mon Sep 17 00:00:00 2001
From: Awiteb
Date: Sun, 22 Dec 2024 20:10:53 +0000
Subject: [PATCH] chore: First commit
Signed-off-by: Awiteb
---
.gitignore | 1 +
Cargo.lock | 538 ++++++++++++++++++
Cargo.toml | 18 +
README.md | 85 +++
src/controller/key_event_handler.rs | 30 +
.../key_event_matcher/collapse_dir.rs | 119 ++++
.../key_event_matcher/entry_down.rs | 12 +
src/controller/key_event_matcher/entry_up.rs | 12 +
.../key_event_matcher/expand_dir.rs | 19 +
.../key_event_matcher/file_action.rs | 39 ++
src/controller/key_event_matcher/mod.rs | 140 +++++
src/controller/key_event_matcher/quit.rs | 11 +
src/controller/key_event_matcher/reload.rs | 100 ++++
src/controller/mod.rs | 98 ++++
src/controller/resize_event_handler.rs | 23 +
src/main.rs | 45 ++
src/model/compare_functions.rs | 131 +++++
src/model/config/behavior.rs | 48 ++
src/model/config/color.rs | 32 ++
src/model/config/composition.rs | 40 ++
src/model/config/debug.rs | 56 ++
src/model/config/keybinding.rs | 72 +++
src/model/config/mod.rs | 229 ++++++++
src/model/config/setup.rs | 24 +
src/model/event.rs | 197 +++++++
src/model/mod.rs | 86 +++
src/model/path_node/debug.rs | 23 +
src/model/path_node/mod.rs | 293 ++++++++++
src/model/tree_index.rs | 113 ++++
src/utils.rs | 79 +++
src/view/composer.rs | 126 ++++
src/view/mod.rs | 68 +++
src/view/print.rs | 247 ++++++++
src/view/scroll.rs | 309 ++++++++++
src/view/update.rs | 72 +++
tests/test_dirs/dir0/dir3/file4 | 0
tests/test_dirs/dir0/dir3/file5 | 0
tests/test_dirs/dir0/dir4/file16 | 0
tests/test_dirs/dir0/dir5/file6 | 0
tests/test_dirs/dir1/dir6/dir10/file28 | 0
tests/test_dirs/dir1/dir6/dir8/file17 | 0
tests/test_dirs/dir1/dir6/dir9/dir11/file18 | 0
tests/test_dirs/dir1/dir6/dir9/dir12/file13 | 0
tests/test_dirs/dir1/dir6/dir9/dir12/file14 | 0
tests/test_dirs/dir1/dir6/dir9/dir12/file15 | 0
tests/test_dirs/dir1/dir6/dir9/file12 | 0
tests/test_dirs/dir1/dir6/file10 | 0
tests/test_dirs/dir1/dir6/file11 | 0
tests/test_dirs/dir1/dir6/file9 | 0
tests/test_dirs/dir1/dir7/file19 | 0
tests/test_dirs/dir1/file7 | 0
tests/test_dirs/dir1/file8 | 0
tests/test_dirs/dir2/file20 | 0
tests/test_dirs/file0 | 0
tests/test_dirs/file1 | 0
tests/test_dirs/file2 | 0
tests/test_dirs/file21 | 0
tests/test_dirs/file22 | 0
tests/test_dirs/file23 | 0
tests/test_dirs/file24 | 0
tests/test_dirs/file25 | 0
tests/test_dirs/file26 | 0
tests/test_dirs/file27 | 0
twilight-tree.toml | 56 ++
64 files changed, 3591 insertions(+)
create mode 100644 .gitignore
create mode 100644 Cargo.lock
create mode 100644 Cargo.toml
create mode 100644 README.md
create mode 100644 src/controller/key_event_handler.rs
create mode 100644 src/controller/key_event_matcher/collapse_dir.rs
create mode 100644 src/controller/key_event_matcher/entry_down.rs
create mode 100644 src/controller/key_event_matcher/entry_up.rs
create mode 100644 src/controller/key_event_matcher/expand_dir.rs
create mode 100644 src/controller/key_event_matcher/file_action.rs
create mode 100644 src/controller/key_event_matcher/mod.rs
create mode 100644 src/controller/key_event_matcher/quit.rs
create mode 100644 src/controller/key_event_matcher/reload.rs
create mode 100644 src/controller/mod.rs
create mode 100644 src/controller/resize_event_handler.rs
create mode 100644 src/main.rs
create mode 100644 src/model/compare_functions.rs
create mode 100644 src/model/config/behavior.rs
create mode 100644 src/model/config/color.rs
create mode 100644 src/model/config/composition.rs
create mode 100644 src/model/config/debug.rs
create mode 100644 src/model/config/keybinding.rs
create mode 100644 src/model/config/mod.rs
create mode 100644 src/model/config/setup.rs
create mode 100644 src/model/event.rs
create mode 100644 src/model/mod.rs
create mode 100644 src/model/path_node/debug.rs
create mode 100644 src/model/path_node/mod.rs
create mode 100644 src/model/tree_index.rs
create mode 100644 src/utils.rs
create mode 100644 src/view/composer.rs
create mode 100644 src/view/mod.rs
create mode 100644 src/view/print.rs
create mode 100644 src/view/scroll.rs
create mode 100644 src/view/update.rs
create mode 100644 tests/test_dirs/dir0/dir3/file4
create mode 100644 tests/test_dirs/dir0/dir3/file5
create mode 100644 tests/test_dirs/dir0/dir4/file16
create mode 100644 tests/test_dirs/dir0/dir5/file6
create mode 100644 tests/test_dirs/dir1/dir6/dir10/file28
create mode 100644 tests/test_dirs/dir1/dir6/dir8/file17
create mode 100644 tests/test_dirs/dir1/dir6/dir9/dir11/file18
create mode 100644 tests/test_dirs/dir1/dir6/dir9/dir12/file13
create mode 100644 tests/test_dirs/dir1/dir6/dir9/dir12/file14
create mode 100644 tests/test_dirs/dir1/dir6/dir9/dir12/file15
create mode 100644 tests/test_dirs/dir1/dir6/dir9/file12
create mode 100644 tests/test_dirs/dir1/dir6/file10
create mode 100644 tests/test_dirs/dir1/dir6/file11
create mode 100644 tests/test_dirs/dir1/dir6/file9
create mode 100644 tests/test_dirs/dir1/dir7/file19
create mode 100644 tests/test_dirs/dir1/file7
create mode 100644 tests/test_dirs/dir1/file8
create mode 100644 tests/test_dirs/dir2/file20
create mode 100644 tests/test_dirs/file0
create mode 100644 tests/test_dirs/file1
create mode 100644 tests/test_dirs/file2
create mode 100644 tests/test_dirs/file21
create mode 100644 tests/test_dirs/file22
create mode 100644 tests/test_dirs/file23
create mode 100644 tests/test_dirs/file24
create mode 100644 tests/test_dirs/file25
create mode 100644 tests/test_dirs/file26
create mode 100644 tests/test_dirs/file27
create mode 100644 twilight-tree.toml
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..d72573f
--- /dev/null
+++ b/Cargo.lock
@@ -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",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..4a1daca
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+authors = ["Awiteb "]
+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"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..651c691
--- /dev/null
+++ b/README.md
@@ -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.
diff --git a/src/controller/key_event_handler.rs b/src/controller/key_event_handler.rs
new file mode 100644
index 0000000..b9ea405
--- /dev/null
+++ b/src/controller/key_event_handler.rs
@@ -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, 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) => {}
+ }
+ }
+ }
+}
diff --git a/src/controller/key_event_matcher/collapse_dir.rs b/src/controller/key_event_matcher/collapse_dir.rs
new file mode 100644
index 0000000..6ca1996
--- /dev/null
+++ b/src/controller/key_event_matcher/collapse_dir.rs
@@ -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 EventQueue {
+ 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> {
+ 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);
+ }
+ }
+}
diff --git a/src/controller/key_event_matcher/entry_down.rs b/src/controller/key_event_matcher/entry_down.rs
new file mode 100644
index 0000000..ce201e6
--- /dev/null
+++ b/src/controller/key_event_matcher/entry_down.rs
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2019-2022 golmman
+
+use crate::controller::EventQueue;
+use std::io::Write;
+
+impl EventQueue {
+ pub fn do_entry_down(&mut self) -> Option<()> {
+ self.update_pager(1);
+ Some(())
+ }
+}
diff --git a/src/controller/key_event_matcher/entry_up.rs b/src/controller/key_event_matcher/entry_up.rs
new file mode 100644
index 0000000..2ff5d11
--- /dev/null
+++ b/src/controller/key_event_matcher/entry_up.rs
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2019-2022 golmman
+
+use crate::controller::EventQueue;
+use std::io::Write;
+
+impl EventQueue {
+ pub fn do_entry_up(&mut self) -> Option<()> {
+ self.update_pager(-1);
+ Some(())
+ }
+}
diff --git a/src/controller/key_event_matcher/expand_dir.rs b/src/controller/key_event_matcher/expand_dir.rs
new file mode 100644
index 0000000..ed8d1ac
--- /dev/null
+++ b/src/controller/key_event_matcher/expand_dir.rs
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2019-2022 golmman
+
+use crate::controller::EventQueue;
+use std::io::Write;
+
+impl EventQueue {
+ 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(())
+ }
+}
diff --git a/src/controller/key_event_matcher/file_action.rs b/src/controller/key_event_matcher/file_action.rs
new file mode 100644
index 0000000..35b77ae
--- /dev/null
+++ b/src/controller/key_event_matcher/file_action.rs
@@ -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 EventQueue {
+ 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(())
+ }
+ }
+}
diff --git a/src/controller/key_event_matcher/mod.rs b/src/controller/key_event_matcher/mod.rs
new file mode 100644
index 0000000..0c887e0
--- /dev/null
+++ b/src/controller/key_event_matcher/mod.rs
@@ -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 EventQueue {
+ #[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> {
+ 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());
+ }
+}
diff --git a/src/controller/key_event_matcher/quit.rs b/src/controller/key_event_matcher/quit.rs
new file mode 100644
index 0000000..9d2166c
--- /dev/null
+++ b/src/controller/key_event_matcher/quit.rs
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2019-2022 golmman
+
+use crate::controller::EventQueue;
+use std::io::Write;
+
+impl EventQueue {
+ pub fn do_quit(&mut self) -> Option<()> {
+ None
+ }
+}
diff --git a/src/controller/key_event_matcher/reload.rs b/src/controller/key_event_matcher/reload.rs
new file mode 100644
index 0000000..ed2ccc6
--- /dev/null
+++ b/src/controller/key_event_matcher/reload.rs
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2019-2022 golmman
+// Copyright (c) 2024 Awiteb
+
+use crate::controller::EventQueue;
+use crate::model::path_node::PathNode;
+use crate::model::tree_index::TreeIndex;
+use std::io::Write;
+
+impl EventQueue {
+ 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> {
+ 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();
+ }
+}
diff --git a/src/controller/mod.rs b/src/controller/mod.rs
new file mode 100644
index 0000000..a551025
--- /dev/null
+++ b/src/controller/mod.rs
@@ -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 {
+ config: Config,
+ composer: Composer,
+ pager: Pager,
+ path_node_root: PathNode,
+ path_node_compare: PathNodeCompare,
+ queue_receiver: Receiver,
+ queue_sender: SyncSender,
+
+ // TODO: should be part of the view?
+ text_entries: Vec,
+ command_to_run_on_exit: Option,
+}
+
+impl EventQueue {
+ pub fn new(
+ config: Config,
+ composer: Composer,
+ mut pager: Pager,
+ path_node_root: PathNode,
+ ) -> Self {
+ info!("initializing event queue");
+
+ let (queue_sender, queue_receiver): (SyncSender, Receiver) =
+ 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 {
+ 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(())
+ }
+ }
+ }
+}
diff --git a/src/controller/resize_event_handler.rs b/src/controller/resize_event_handler.rs
new file mode 100644
index 0000000..d5f863f
--- /dev/null
+++ b/src/controller/resize_event_handler.rs
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2019-2022 golmman
+// Copyright (c) 2024 Awiteb
+
+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, rx: mpsc::Receiver<()>) {
+ let hook_id = unsafe {
+ register(SIGWINCH, move || {
+ sync_sender.send(Event::Resize).unwrap();
+ })
+ };
+ let _ = rx.recv();
+ unregister(hook_id.unwrap());
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..00b9f88
--- /dev/null
+++ b/src/main.rs
@@ -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");
+}
diff --git a/src/model/compare_functions.rs b/src/model/compare_functions.rs
new file mode 100644
index 0000000..a72d8e7
--- /dev/null
+++ b/src/model/compare_functions.rs
@@ -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
+ }
+}
diff --git a/src/model/config/behavior.rs b/src/model/config/behavior.rs
new file mode 100644
index 0000000..7a61f00
--- /dev/null
+++ b/src/model/config/behavior.rs
@@ -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
+ }
+}
diff --git a/src/model/config/color.rs b/src/model/config/color.rs
new file mode 100644
index 0000000..8942253
--- /dev/null
+++ b/src/model/config/color.rs
@@ -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")
+ }
+}
diff --git a/src/model/config/composition.rs b/src/model/config/composition.rs
new file mode 100644
index 0000000..bda30a4
--- /dev/null
+++ b/src/model/config/composition.rs
@@ -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
+ }
+}
diff --git a/src/model/config/debug.rs b/src/model/config/debug.rs
new file mode 100644
index 0000000..e68cf5f
--- /dev/null
+++ b/src/model/config/debug.rs
@@ -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
+ }
+}
diff --git a/src/model/config/keybinding.rs b/src/model/config/keybinding.rs
new file mode 100644
index 0000000..bfbbff3
--- /dev/null
+++ b/src/model/config/keybinding.rs
@@ -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")
+ }
+}
diff --git a/src/model/config/mod.rs b/src/model/config/mod.rs
new file mode 100644
index 0000000..0ed014e
--- /dev/null
+++ b/src/model/config/mod.rs
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2019-2022 golmman
+// Copyright (c) 2024 Awiteb
+
+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(mut config: Self, args: T) -> Self
+ where
+ T: IntoIterator- ,
+ {
+ 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((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 {
+ 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)")
+ );
+ }
+}
diff --git a/src/model/config/setup.rs b/src/model/config/setup.rs
new file mode 100644
index 0000000..d12a150
--- /dev/null
+++ b/src/model/config/setup.rs
@@ -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(".")
+ }
+}
diff --git a/src/model/event.rs b/src/model/event.rs
new file mode 100644
index 0000000..c26c281
--- /dev/null
+++ b/src/model/event.rs
@@ -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 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 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")
+ );
+ }
+ }
+}
diff --git a/src/model/mod.rs b/src/model/mod.rs
new file mode 100644
index 0000000..73f3cce
--- /dev/null
+++ b/src/model/mod.rs
@@ -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"
+ );
+ }
+}
diff --git a/src/model/path_node/debug.rs b/src/model/path_node/debug.rs
new file mode 100644
index 0000000..c81ed9d
--- /dev/null
+++ b/src/model/path_node/debug.rs
@@ -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(())
+ }
+}
diff --git a/src/model/path_node/mod.rs b/src/model/path_node/mod.rs
new file mode 100644
index 0000000..5b7e701
--- /dev/null
+++ b/src/model/path_node/mod.rs
@@ -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,
+ 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 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 {
+ 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::>();
+
+ 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);
+ }
+ }
+}
diff --git a/src/model/tree_index.rs b/src/model/tree_index.rs
new file mode 100644
index 0000000..7249312
--- /dev/null
+++ b/src/model/tree_index.rs
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2019-2022 golmman
+
+#[derive(Clone, Debug, PartialEq, PartialOrd)]
+pub struct TreeIndex {
+ pub index: Vec,
+}
+
+impl From> for TreeIndex {
+ fn from(index: Vec) -> 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);
+ }
+ }
+}
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644
index 0000000..f9f0d8f
--- /dev/null
+++ b/src/utils.rs
@@ -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 {
+ 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::() {
+ 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 {
+ 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",
+ ))
+ }
+}
diff --git a/src/view/composer.rs b/src/view/composer.rs
new file mode 100644
index 0000000..134edcd
--- /dev/null
+++ b/src/view/composer.rs
@@ -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 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 {
+ 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,
+ 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~");
+ }
+}
diff --git a/src/view/mod.rs b/src/view/mod.rs
new file mode 100644
index 0000000..f5cd69b
--- /dev/null
+++ b/src/view/mod.rs
@@ -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 {
+ config: Config,
+ pub cursor_row: i32,
+ out: W,
+ terminal_cols: i32,
+ terminal_rows: i32,
+ text_row: i32,
+}
+
+impl Pager {
+ 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 Drop for Pager {
+ fn drop(&mut self) {
+ write!(
+ self,
+ "{}{}{}",
+ termion::clear::All,
+ termion::cursor::Goto(1, 1),
+ termion::cursor::Show,
+ )
+ .unwrap();
+ }
+}
+
+impl Write for Pager {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result {
+ self.out.write(buf)
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ self.out.flush()
+ }
+}
diff --git a/src/view/print.rs b/src/view/print.rs
new file mode 100644
index 0000000..c6ccf71
--- /dev/null
+++ b/src/view/print.rs
@@ -0,0 +1,247 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2019-2022 golmman
+// Copyright (c) 2024 Awiteb
+
+use crate::view::Composer;
+use crate::view::Pager;
+use std::io::Write;
+use termion::{color, style};
+
+impl Pager {
+ 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> {
+ 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 = Vec::new();
+ let mut pager = Pager::new(config, out);
+
+ pager.terminal_cols = 100;
+ pager.terminal_rows = 10;
+
+ pager
+ }
+
+ fn get_result(pager: Pager>) -> Option {
+ 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"));
+ }
+}
diff --git a/src/view/scroll.rs b/src/view/scroll.rs
new file mode 100644
index 0000000..3bf1382
--- /dev/null
+++ b/src/view/scroll.rs
@@ -0,0 +1,309 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2019-2022 golmman
+
+use crate::view::Pager;
+use std::io::Write;
+
+impl Pager {
+ fn get_index_overshoot(index_under_test: i32, index_now: i32, index_delta: i32) -> Option {
+ 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> {
+ 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 = 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::>::get_index_overshoot(10, 11, 3);
+ assert_eq!(Some(1), overshoot);
+ }
+
+ #[test]
+ fn overshoot_from_above() {
+ let overshoot = Pager::>::get_index_overshoot(10, 7, -4);
+ assert_eq!(Some(-3), overshoot);
+ }
+
+ #[test]
+ fn no_overshoot_from_below() {
+ let overshoot = Pager::>::get_index_overshoot(10, 7, 2);
+ assert_eq!(None, overshoot);
+ }
+
+ #[test]
+ fn no_overshoot_from_above() {
+ let overshoot = Pager::>::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);
+ }
+ }
+}
diff --git a/src/view/update.rs b/src/view/update.rs
new file mode 100644
index 0000000..c5718f9
--- /dev/null
+++ b/src/view/update.rs
@@ -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 Pager {
+ 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();
+ }
+}
diff --git a/tests/test_dirs/dir0/dir3/file4 b/tests/test_dirs/dir0/dir3/file4
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir0/dir3/file5 b/tests/test_dirs/dir0/dir3/file5
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir0/dir4/file16 b/tests/test_dirs/dir0/dir4/file16
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir0/dir5/file6 b/tests/test_dirs/dir0/dir5/file6
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir6/dir10/file28 b/tests/test_dirs/dir1/dir6/dir10/file28
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir6/dir8/file17 b/tests/test_dirs/dir1/dir6/dir8/file17
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir6/dir9/dir11/file18 b/tests/test_dirs/dir1/dir6/dir9/dir11/file18
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir6/dir9/dir12/file13 b/tests/test_dirs/dir1/dir6/dir9/dir12/file13
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir6/dir9/dir12/file14 b/tests/test_dirs/dir1/dir6/dir9/dir12/file14
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir6/dir9/dir12/file15 b/tests/test_dirs/dir1/dir6/dir9/dir12/file15
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir6/dir9/file12 b/tests/test_dirs/dir1/dir6/dir9/file12
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir6/file10 b/tests/test_dirs/dir1/dir6/file10
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir6/file11 b/tests/test_dirs/dir1/dir6/file11
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir6/file9 b/tests/test_dirs/dir1/dir6/file9
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/dir7/file19 b/tests/test_dirs/dir1/dir7/file19
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/file7 b/tests/test_dirs/dir1/file7
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir1/file8 b/tests/test_dirs/dir1/file8
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/dir2/file20 b/tests/test_dirs/dir2/file20
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/file0 b/tests/test_dirs/file0
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/file1 b/tests/test_dirs/file1
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/file2 b/tests/test_dirs/file2
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/file21 b/tests/test_dirs/file21
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/file22 b/tests/test_dirs/file22
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/file23 b/tests/test_dirs/file23
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/file24 b/tests/test_dirs/file24
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/file25 b/tests/test_dirs/file25
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/file26 b/tests/test_dirs/file26
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dirs/file27 b/tests/test_dirs/file27
new file mode 100644
index 0000000..e69de29
diff --git a/twilight-tree.toml b/twilight-tree.toml
new file mode 100644
index 0000000..db913f6
--- /dev/null
+++ b/twilight-tree.toml
@@ -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 = "."