chore: First commit

Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb 2024-12-22 20:10:53 +00:00
parent c99f4704a1
commit 36a979d234
Signed by: awiteb
GPG key ID: 3F6B55640AA6682F
64 changed files with 3591 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

538
Cargo.lock generated Normal file
View file

@ -0,0 +1,538 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cc"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "exec"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "fern"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29"
dependencies = [
"log",
]
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "js-sys"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "numtoa"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f"
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_termios"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb"
[[package]]
name = "serde"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "syn"
version = "2.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termion"
version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eaa98560e51a2cf4f0bb884d8b2098a9ea11ecf3b7078e9c68242c74cc923a7"
dependencies = [
"libc",
"libredox",
"numtoa",
"redox_termios",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "twilight-tree"
version = "0.1.0"
dependencies = [
"chrono",
"exec",
"fern",
"log",
"serde",
"signal-hook",
"termion",
"toml",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "wasm-bindgen"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]

18
Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
authors = ["Awiteb <a@4rs.nl>"]
description = "Embeddable TUI files tree"
edition = "2021"
license = "MIT"
name = "twilight-tree"
version = "0.1.0"
rust-version = "1.66.1"
[dependencies]
chrono = "0.4.39"
exec = "0.3.1"
fern = "0.7"
log = "0.4"
serde = { version = "1.0.216", features = ["derive"] }
signal-hook = "0.3.17"
termion = "4.0.3"
toml = "0.8.19"

85
README.md Normal file
View file

@ -0,0 +1,85 @@
> This is a fork of [twilight-commander](https://github.com/golmman/twilight-commander)
# twilight-tree
A simple console tree file explorer for linux, similiar to NERDTree but independent of vim.
Developed and tested on Ubuntu 18.04 with xterm derivatives.
## Build and install
```sh
git clone https://git.4rs.nl/awiteb/twilight-tree
cd twilight-tree
cargo install --path . # this will install the binary in $HOME/.cargo/bin
```
## Implemented features
### Configuration
The configuration is loaded as follows
1. load values from `$XDG_CONFIG_HOME/twilight-tree.toml`
2. else load values from `$HOME/.config/twilight-tree/twilight-tree.toml`
2. fill missing values with app defaults
3. overwrite values with defines from the command line options
For a config file with the default values, see [twilight-tree.toml](./twilight-tree.toml).
The command line options are derived from the values defined inside the twilight-tree.toml .
E.g.
```
[debug]
enabled = true
```
is set with the option `--debug.enabled=true`.
### Configurable key bindings
The key bindings are configurable. For the set of configurable keys and key combinations consult the [event.rs](./src/model/event.rs).
|default key|default configuration|action|
|---|---|---|
|up arrow|`--keybinding.entry_up=up`|move an entry up|
|down arrow|`--keybinding.entry_down=down`|move an entry down|
|left arrow|`--keybinding.collapse_dir=left`|collapse an entry directory or jump to parent if not collapsable|
|right arrow|`--keybinding.expand_dir=left`|expand an entry directory|
|r|`--keybinding.reload=r`|collapse all directories and reload root directory|
|return|`--keybinding.file_action=return`|perform configured file action|
|q|`--keybinding.quit=q`|quit|
### Directory entry management
#### File Action
The command line option / config value `--behavior.file_action` defines the action taken when the return key is pressed
on a file. The action is interpreted by `bash` and any occurence of `%s` will be replaced by the selected filename.
E.g. when enter is pressed on the file `.bashrc` in a twilight-tree process created with
```
twilight-tree "--behavior.file_action=xterm -e 'cat %s; echo opened file: %s; bash'"
```
then
```
bash -c "xterm -e 'cat /home/user/.bashrc; echo opened file: /home/user/.bashrc; bash'"
```
is executed, i.e.:
* a new xterm window is opened
* where the selected file (`.bashrc`) is printed to stdout
* then `opened file: ~/.bashrc` is printed
* `bash` prevents the window from closing.
`--behavior.file_action` defaults to [true](https://en.wikipedia.org/wiki/True_and_false_(commands)), which does
(almost) nothing.
### Scrolling modes
Specified with the option `--behaviour.scrolling` (default = `center`)
* `center`: move the cursor until it is in the center, then move the text instead
* `editor`: move the cursor until it hits the top/bottom boundaries set by the `debug.paddin_top/bot` limits
### Utf-8 support
In case your terminal does not support utf-8 you can disable it with `--composition.use_utf8=false`.
### Logs
Logs are written to
1. `$XDG_CONFIG_HOME/tc.log` if XDG_CONFIG_HOME is defined
2. else they are placed in `$HOME/.config/twilight-tree/tc.log`
## License
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.

View file

@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::model::event::Event;
use crate::model::event::Key;
use std::io::stdin;
use std::sync::mpsc::SyncSender;
use std::sync::mpsc::{self, TryRecvError};
use termion::input::TermRead;
pub struct KeyEventHandler {}
impl KeyEventHandler {
pub fn handle(sync_sender: SyncSender<Event>, rx: mpsc::Receiver<()>) {
let stdin = stdin();
for termion_event in stdin.events() {
if let Ok(termion_event) = termion_event {
let _ = sync_sender.send(Event::Key(Key::from(termion_event)));
}
match rx.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => {
// println!("Terminating.");
break;
}
Err(TryRecvError::Empty) => {}
}
}
}
}

View file

@ -0,0 +1,119 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::controller::EventQueue;
use crate::model::tree_index::TreeIndex;
use std::io::Write;
impl<W: Write> EventQueue<W> {
pub fn do_collapse_dir(&mut self) -> Option<()> {
let tree_index = self
.path_node_root
.flat_index_to_tree_index(self.pager.cursor_row as usize);
let cursor_delta = self.get_parent_dir_cursor_delta(&tree_index);
if cursor_delta == 0 {
self.path_node_root.collapse_dir(&tree_index);
}
self.text_entries = self.composer.compose_path_node(&self.path_node_root);
self.update_pager(cursor_delta);
Some(())
}
fn get_parent_dir_cursor_delta(&mut self, tree_index: &TreeIndex) -> i32 {
let child_path_node = self.path_node_root.get_child_path_node(tree_index);
if child_path_node.is_dir && child_path_node.is_expanded {
return 0;
}
let parent_path_node_tree_index = tree_index.get_parent();
if parent_path_node_tree_index == TreeIndex::new() {
return 0;
}
let parent_flat_index =
self.path_node_root
.tree_index_to_flat_index(&parent_path_node_tree_index) as i32;
parent_flat_index - self.pager.cursor_row
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::config::Config;
use crate::model::path_node::PathNode;
use crate::model::tree_index::TreeIndex;
use crate::view::composer::Composer;
use crate::view::Pager;
// TODO: duplicate code, create test utils?
fn get_expanded_path_node() -> PathNode {
let mut path_node = PathNode::from("./tests/test_dirs");
path_node.expand_dir(&TreeIndex::new(), PathNode::compare_dirs_top_simple);
path_node.expand_dir(&TreeIndex::from(vec![0]), PathNode::compare_dirs_top_simple);
path_node.expand_dir(
&TreeIndex::from(vec![0, 0]),
PathNode::compare_dirs_top_simple,
);
path_node.expand_dir(&TreeIndex::from(vec![1]), PathNode::compare_dirs_top_simple);
path_node.expand_dir(
&TreeIndex::from(vec![1, 0]),
PathNode::compare_dirs_top_simple,
);
path_node.expand_dir(
&TreeIndex::from(vec![1, 0, 2]),
PathNode::compare_dirs_top_simple,
);
path_node
}
fn prepare_event_queue() -> EventQueue<Vec<u8>> {
let config = Config::default();
let composer = Composer::from(config.clone());
let pager = Pager::new(config.clone(), Vec::new());
let path_node = PathNode::from(config.setup.working_dir.clone());
let mut event_queue = EventQueue::new(config, composer, pager, path_node);
event_queue.path_node_root = get_expanded_path_node();
event_queue
}
mod get_parent_dir_cursor_delta_tests {
use super::*;
#[test]
fn expanded() {
let mut event_queue = prepare_event_queue();
let delta = event_queue.get_parent_dir_cursor_delta(&TreeIndex::from(vec![0]));
assert_eq!(0, delta);
}
#[test]
fn empty_tree_index() {
let mut event_queue = prepare_event_queue();
let delta = event_queue.get_parent_dir_cursor_delta(&TreeIndex::new());
assert_eq!(0, delta);
}
#[test]
fn jump() {
let mut event_queue = prepare_event_queue();
let delta = event_queue.get_parent_dir_cursor_delta(&TreeIndex::from(vec![1, 0, 4]));
assert_eq!(7, delta);
}
}
}

View file

@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::controller::EventQueue;
use std::io::Write;
impl<W: Write> EventQueue<W> {
pub fn do_entry_down(&mut self) -> Option<()> {
self.update_pager(1);
Some(())
}
}

View file

@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::controller::EventQueue;
use std::io::Write;
impl<W: Write> EventQueue<W> {
pub fn do_entry_up(&mut self) -> Option<()> {
self.update_pager(-1);
Some(())
}
}

View file

@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::controller::EventQueue;
use std::io::Write;
impl<W: Write> EventQueue<W> {
pub fn do_expand_dir(&mut self) -> Option<()> {
let tree_index = self
.path_node_root
.flat_index_to_tree_index(self.pager.cursor_row as usize);
self.path_node_root
.expand_dir(&tree_index, self.path_node_compare);
self.text_entries = self.composer.compose_path_node(&self.path_node_root);
self.update_pager(0);
Some(())
}
}

View file

@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::controller::EventQueue;
use log::info;
use std::io::Write;
impl<W: Write> EventQueue<W> {
pub fn do_file_action(&mut self) -> Option<()> {
let tree_index = self
.path_node_root
.flat_index_to_tree_index(self.pager.cursor_row as usize);
let child_node = self.path_node_root.get_child_path_node(&tree_index);
if !child_node.is_dir {
let file_path = &child_node.get_absolute_path();
let file_action_replaced = self.config.behavior.file_action.replace("%s", file_path);
info!("executing file action:\n{}", file_action_replaced);
if self.config.behavior.quit_on_action {
self.command_to_run_on_exit = Some(file_action_replaced);
None
} else {
std::process::Command::new("bash")
.arg("-c")
.arg(file_action_replaced)
.spawn()
.unwrap()
.wait()
.unwrap();
Some(())
}
} else {
Some(())
}
}
}

View file

@ -0,0 +1,140 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::controller::EventQueue;
use crate::model::event::Key;
use std::io::Write;
mod collapse_dir;
mod entry_down;
mod entry_up;
mod expand_dir;
mod file_action;
mod quit;
mod reload;
impl<W: Write> EventQueue<W> {
#[rustfmt::skip]
pub fn match_key_event(&mut self, key: Key) -> Option<()> {
let ck = self.config.keybinding.clone();
if key == Key::from(ck.collapse_dir) { self.do_collapse_dir() }
else if key == Key::from(ck.entry_down) { self.do_entry_down() }
else if key == Key::from(ck.entry_up) { self.do_entry_up() }
else if key == Key::from(ck.expand_dir) { self.do_expand_dir() }
else if key == Key::from(ck.file_action) { self.do_file_action() }
else if key == Key::from(ck.quit) { self.do_quit() }
else if key == Key::from(ck.reload) { self.do_reload() }
else { Some(()) }
}
fn update_pager(&mut self, cursor_delta: i32) {
self.pager.update(
cursor_delta,
&self.text_entries,
self.path_node_root.get_absolute_path(),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::config::Config;
use crate::model::path_node::PathNode;
use crate::view::composer::Composer;
use crate::view::Pager;
fn prepare_event_queue() -> EventQueue<Vec<u8>> {
let config = Config::default();
let composer = Composer::from(config.clone());
let pager = Pager::new(config.clone(), Vec::new());
let path_node = PathNode::from(config.setup.working_dir.clone());
EventQueue::new(config, composer, pager, path_node)
}
#[test]
fn match_key_event_default_test() {
let result = {
let mut event_queue = prepare_event_queue();
event_queue.match_key_event(Key::from("nonsense"))
};
assert!(result.is_some());
}
#[test]
fn match_key_event_quit_test() {
let result = {
let mut event_queue = prepare_event_queue();
event_queue.match_key_event(Key::from(event_queue.config.keybinding.quit.clone()))
};
assert!(result.is_none());
}
#[test]
fn match_key_event_reload_test() {
let result = {
let mut event_queue = prepare_event_queue();
event_queue.match_key_event(Key::from(event_queue.config.keybinding.reload.clone()))
};
assert!(result.is_some());
}
#[test]
fn match_key_event_file_action_test() {
let result = {
let mut event_queue = prepare_event_queue();
event_queue
.match_key_event(Key::from(event_queue.config.keybinding.file_action.clone()))
};
assert!(result.is_some());
}
#[test]
fn match_key_event_entry_up_test() {
let result = {
let mut event_queue = prepare_event_queue();
event_queue.match_key_event(Key::from(event_queue.config.keybinding.entry_up.clone()))
};
assert!(result.is_some());
}
#[test]
fn match_key_event_entry_down_test() {
let result = {
let mut event_queue = prepare_event_queue();
event_queue.match_key_event(Key::from(event_queue.config.keybinding.entry_down.clone()))
};
assert!(result.is_some());
}
#[test]
fn match_key_event_collapse_dir_test() {
let result = {
let mut event_queue = prepare_event_queue();
event_queue.match_key_event(Key::from(
event_queue.config.keybinding.collapse_dir.clone(),
))
};
assert!(result.is_some());
}
#[test]
fn match_key_event_expand_dir_test() {
let result = {
let mut event_queue = prepare_event_queue();
event_queue.match_key_event(Key::from(event_queue.config.keybinding.expand_dir.clone()))
};
assert!(result.is_some());
}
}

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::controller::EventQueue;
use std::io::Write;
impl<W: Write> EventQueue<W> {
pub fn do_quit(&mut self) -> Option<()> {
None
}
}

View file

@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
// Copyright (c) 2024 Awiteb <a@4rs.nl>
use crate::controller::EventQueue;
use crate::model::path_node::PathNode;
use crate::model::tree_index::TreeIndex;
use std::io::Write;
impl<W: Write> EventQueue<W> {
pub fn do_reload(&mut self) -> Option<()> {
self.reload_openend_dirs();
self.text_entries = self.composer.compose_path_node(&self.path_node_root);
self.update_pager(0);
Some(())
}
fn reload_openend_dirs(&mut self) {
// backup the old path node structure
let old_path_node_root = self.path_node_root.clone();
// reset the root path node
self.path_node_root = PathNode::from(self.config.setup.working_dir.clone());
self.path_node_root
.expand_dir(&TreeIndex::from(Vec::new()), self.path_node_compare);
// restore the old path nodes structure for the root path node
self.restore_expansions(&old_path_node_root, &mut TreeIndex::new());
}
fn restore_expansions(&mut self, path_node: &PathNode, tree_index: &mut TreeIndex) {
for (c, child) in path_node.children.iter().enumerate() {
if child.is_expanded {
tree_index.index.push(c);
self.path_node_root
.expand_dir(tree_index, self.path_node_compare);
self.restore_expansions(child, tree_index);
tree_index.index.pop();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::config::Config;
use crate::model::path_node::PathNode;
use crate::model::tree_index::TreeIndex;
use crate::view::composer::Composer;
use crate::view::Pager;
fn get_expanded_path_node(working_dir: &str) -> PathNode {
let mut path_node = PathNode::from(working_dir);
path_node.expand_dir(&TreeIndex::new(), PathNode::compare_dirs_top_simple);
path_node.expand_dir(&TreeIndex::from(vec![0]), PathNode::compare_dirs_top_simple);
path_node.expand_dir(
&TreeIndex::from(vec![0, 0]),
PathNode::compare_dirs_top_simple,
);
path_node.expand_dir(&TreeIndex::from(vec![1]), PathNode::compare_dirs_top_simple);
path_node.expand_dir(
&TreeIndex::from(vec![1, 0]),
PathNode::compare_dirs_top_simple,
);
path_node.expand_dir(
&TreeIndex::from(vec![1, 0, 2]),
PathNode::compare_dirs_top_simple,
);
path_node
}
fn prepare_event_queue(working_dir: &str) -> EventQueue<Vec<u8>> {
let mut config = Config::default();
config.setup.working_dir = String::from(working_dir);
let composer = Composer::from(config.clone());
let pager = Pager::new(config.clone(), Vec::new());
let path_node = PathNode::from(config.setup.working_dir.clone());
let mut event_queue = EventQueue::new(config, composer, pager, path_node);
event_queue.path_node_root = get_expanded_path_node(working_dir);
event_queue
}
#[test]
fn do_reload() {
// TODO: implement proper test
let mut event_queue = prepare_event_queue("./tests/test_dirs");
event_queue.do_reload();
}
}

98
src/controller/mod.rs Normal file
View file

@ -0,0 +1,98 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::controller::key_event_handler::KeyEventHandler;
use crate::controller::resize_event_handler::ResizeEventHandler;
use crate::model::compare_functions::PathNodeCompare;
use crate::model::config::Config;
use crate::model::event::Event;
use crate::model::path_node::PathNode;
use crate::view::composer::Composer;
use crate::view::Pager;
use log::info;
use std::io::Write;
use std::sync::mpsc::sync_channel;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::SyncSender;
use std::thread;
mod key_event_handler;
mod key_event_matcher;
mod resize_event_handler;
pub struct EventQueue<W: Write> {
config: Config,
composer: Composer,
pager: Pager<W>,
path_node_root: PathNode,
path_node_compare: PathNodeCompare,
queue_receiver: Receiver<Event>,
queue_sender: SyncSender<Event>,
// TODO: should be part of the view?
text_entries: Vec<String>,
command_to_run_on_exit: Option<String>,
}
impl<W: Write> EventQueue<W> {
pub fn new(
config: Config,
composer: Composer,
mut pager: Pager<W>,
path_node_root: PathNode,
) -> Self {
info!("initializing event queue");
let (queue_sender, queue_receiver): (SyncSender<Event>, Receiver<Event>) =
sync_channel(1024);
let path_node_compare = PathNode::get_path_node_compare(&config);
let text_entries = composer.compose_path_node(&path_node_root);
pager.update(0, &text_entries, path_node_root.get_absolute_path());
let command_to_run_on_exit = None;
Self {
config,
composer,
pager,
path_node_root,
path_node_compare,
queue_receiver,
queue_sender,
text_entries,
command_to_run_on_exit,
}
}
pub fn handle_messages(&mut self) -> Option<String> {
let (tx1, rx1) = std::sync::mpsc::channel();
let (tx2, rx2) = std::sync::mpsc::channel();
let sender1 = self.queue_sender.clone();
let sender2 = self.queue_sender.clone();
thread::spawn(move || KeyEventHandler::handle(sender1, rx1));
thread::spawn(move || ResizeEventHandler::handle(sender2, rx2));
while self
.match_event(self.queue_receiver.recv().unwrap())
.is_some()
{}
let _ = tx1.send(());
let _ = tx2.send(());
self.command_to_run_on_exit.clone()
}
fn match_event(&mut self, event: Event) -> Option<()> {
match event {
Event::Key(key) => self.match_key_event(key),
Event::Resize => {
self.pager.update(
0,
&self.text_entries,
self.path_node_root.get_absolute_path(),
);
Some(())
}
}
}
}

View file

@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
// Copyright (c) 2024 Awiteb <a@4rs.nl>
use crate::model::event::Event;
use signal_hook::consts::SIGWINCH;
use signal_hook::low_level::{register, unregister};
use std::sync::mpsc;
use std::sync::mpsc::SyncSender;
pub struct ResizeEventHandler {}
impl ResizeEventHandler {
pub fn handle(sync_sender: SyncSender<Event>, rx: mpsc::Receiver<()>) {
let hook_id = unsafe {
register(SIGWINCH, move || {
sync_sender.send(Event::Resize).unwrap();
})
};
let _ = rx.recv();
unregister(hook_id.unwrap());
}
}

45
src/main.rs Normal file
View file

@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
extern crate chrono;
extern crate termion;
extern crate toml;
use controller::EventQueue;
use exec::execvp;
use log::info;
use model::config::Config;
use model::path_node::PathNode;
use std::io::stdout;
use termion::raw::IntoRawMode;
use utils::setup_logger;
use view::composer::Composer;
use view::Pager;
mod controller;
mod model;
mod utils;
mod view;
fn main() {
let command_to_run_on_exit = {
let _ = setup_logger();
let config = Config::new();
let composer = Composer::from(config.clone());
let pager = Pager::new(config.clone(), stdout().into_raw_mode().unwrap());
let path_node_root = PathNode::new_expanded(config.clone());
let mut event_queue = EventQueue::new(config, composer, pager, path_node_root);
event_queue.handle_messages()
};
if let Some(cmd) = command_to_run_on_exit {
let _ = execvp("bash", &["bash", "-c", &cmd]);
};
info!("clean exit");
}

View file

@ -0,0 +1,131 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::model::config::Config;
use crate::model::path_node::PathNode;
use std::cmp::Ordering;
pub type PathNodeCompare = fn(&PathNode, &PathNode) -> Ordering;
impl PathNode {
pub fn compare_dirs_bot_simple(a: &PathNode, b: &PathNode) -> Ordering {
if a.is_dir && !b.is_dir {
return std::cmp::Ordering::Greater;
} else if !a.is_dir && b.is_dir {
return std::cmp::Ordering::Less;
}
a.display_text.cmp(&b.display_text)
}
pub fn compare_dirs_top_simple(a: &PathNode, b: &PathNode) -> Ordering {
if a.is_dir && !b.is_dir {
return std::cmp::Ordering::Less;
} else if !a.is_dir && b.is_dir {
return std::cmp::Ordering::Greater;
}
a.display_text.cmp(&b.display_text)
}
pub fn get_path_node_compare(config: &Config) -> PathNodeCompare {
let path_node_compare: fn(&PathNode, &PathNode) -> Ordering =
match config.behavior.path_node_sort.as_str() {
"dirs_bot_simple" => PathNode::compare_dirs_bot_simple,
"dirs_top_simple" => PathNode::compare_dirs_top_simple,
"none" => |_, _| Ordering::Equal,
_ => |_, _| Ordering::Equal,
};
path_node_compare
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cmp::Ordering::Greater;
use std::cmp::Ordering::Less;
mod compare_dirs_bot_simple_tests {
use super::*;
#[test]
fn dir_to_dir() {
let dir_a = get_dir("dir_a");
let dir_b = get_dir("dir_b");
let order = PathNode::compare_dirs_bot_simple(&dir_a, &dir_b);
assert_eq!(Less, order);
}
#[test]
fn dir_to_file() {
let dir = get_dir("something");
let file = get_file("something");
let order = PathNode::compare_dirs_bot_simple(&dir, &file);
assert_eq!(Greater, order);
}
#[test]
fn file_to_file() {
let file_a = get_file("file_a");
let file_b = get_file("file_b");
let order = PathNode::compare_dirs_bot_simple(&file_a, &file_b);
assert_eq!(Less, order);
}
}
mod compare_dirs_top_simple_tests {
use super::*;
#[test]
fn dir_to_dir() {
let dir_a = get_dir("dir_a");
let dir_b = get_dir("dir_b");
let order = PathNode::compare_dirs_top_simple(&dir_a, &dir_b);
assert_eq!(Less, order);
}
#[test]
fn dir_to_file() {
let dir = get_dir("something");
let file = get_file("something");
let order = PathNode::compare_dirs_top_simple(&dir, &file);
assert_eq!(Less, order);
}
#[test]
fn file_to_file() {
let file_a = get_file("file_a");
let file_b = get_file("file_b");
let order = PathNode::compare_dirs_top_simple(&file_a, &file_b);
assert_eq!(Less, order);
}
}
fn get_dir(name: &str) -> PathNode {
let mut path_node = PathNode::from(".");
path_node.is_dir = true;
path_node.display_text = String::from(name);
path_node
}
fn get_file(name: &str) -> PathNode {
let mut path_node = PathNode::from(".");
path_node.is_dir = false;
path_node.display_text = String::from(name);
path_node
}
}

View file

@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
pub struct Behavior {
#[serde(default = "Behavior::default_file_action")]
pub file_action: String,
#[serde(default = "Behavior::default_path_node_sort")]
pub path_node_sort: String,
#[serde(default = "Behavior::default_scrolling")]
pub scrolling: String,
#[serde(default = "Behavior::default_quit_on_action")]
pub quit_on_action: bool,
}
impl Default for Behavior {
fn default() -> Behavior {
Behavior {
file_action: Self::default_file_action(),
path_node_sort: Self::default_path_node_sort(),
scrolling: Self::default_scrolling(),
quit_on_action: Self::default_quit_on_action(),
}
}
}
impl Behavior {
fn default_file_action() -> String {
String::from("true") // do nothing!
}
fn default_path_node_sort() -> String {
String::from("dirs_top_simple")
}
fn default_scrolling() -> String {
String::from("center")
}
fn default_quit_on_action() -> bool {
false
}
}

32
src/model/config/color.rs Normal file
View file

@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
pub struct Color {
#[serde(default = "Color::default_background")]
pub background: String,
#[serde(default = "Color::default_foreground")]
pub foreground: String,
}
impl Default for Color {
fn default() -> Self {
Color {
background: Self::default_background(),
foreground: Self::default_foreground(),
}
}
}
impl Color {
fn default_background() -> String {
String::from("000000")
}
fn default_foreground() -> String {
String::from("FFFFFF")
}
}

View file

@ -0,0 +1,40 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
pub struct Composition {
#[serde(default = "Composition::default_indent")]
pub indent: i32,
#[serde(default = "Composition::default_show_indent")]
pub show_indent: bool,
#[serde(default = "Composition::default_use_utf8")]
pub use_utf8: bool,
}
impl Default for Composition {
fn default() -> Composition {
Composition {
indent: Self::default_indent(),
show_indent: Self::default_show_indent(),
use_utf8: Self::default_use_utf8(),
}
}
}
impl Composition {
fn default_indent() -> i32 {
2
}
fn default_show_indent() -> bool {
false
}
fn default_use_utf8() -> bool {
true
}
}

56
src/model/config/debug.rs Normal file
View file

@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
pub struct Debug {
#[serde(default = "Debug::default_enabled")]
pub enabled: bool,
#[serde(default = "Debug::default_padding_bot")]
pub padding_bot: i32,
#[serde(default = "Debug::default_padding_top")]
pub padding_top: i32,
#[serde(default = "Debug::default_spacing_bot")]
pub spacing_bot: i32,
#[serde(default = "Debug::default_spacing_top")]
pub spacing_top: i32,
}
impl Default for Debug {
fn default() -> Self {
Self {
enabled: Self::default_enabled(),
padding_bot: Self::default_padding_bot(),
padding_top: Self::default_padding_top(),
spacing_bot: Self::default_spacing_bot(),
spacing_top: Self::default_spacing_top(),
}
}
}
impl Debug {
fn default_enabled() -> bool {
false
}
fn default_padding_bot() -> i32 {
3
}
fn default_padding_top() -> i32 {
3
}
fn default_spacing_bot() -> i32 {
2
}
fn default_spacing_top() -> i32 {
2
}
}

View file

@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
pub struct Keybinding {
#[serde(default = "Keybinding::default_quit")]
pub quit: String,
#[serde(default = "Keybinding::default_entry_up")]
pub entry_up: String,
#[serde(default = "Keybinding::default_entry_down")]
pub entry_down: String,
#[serde(default = "Keybinding::default_expand_dir")]
pub expand_dir: String,
#[serde(default = "Keybinding::default_collapse_dir")]
pub collapse_dir: String,
#[serde(default = "Keybinding::default_file_action")]
pub file_action: String,
#[serde(default = "Keybinding::default_reload")]
pub reload: String,
}
impl Default for Keybinding {
fn default() -> Self {
Self {
quit: Self::default_quit(),
entry_up: Self::default_entry_up(),
entry_down: Self::default_entry_down(),
expand_dir: Self::default_expand_dir(),
collapse_dir: Self::default_collapse_dir(),
file_action: Self::default_file_action(),
reload: Self::default_reload(),
}
}
}
impl Keybinding {
fn default_quit() -> String {
String::from("q")
}
fn default_entry_up() -> String {
String::from("up")
}
fn default_entry_down() -> String {
String::from("down")
}
fn default_expand_dir() -> String {
String::from("right")
}
fn default_collapse_dir() -> String {
String::from("left")
}
fn default_file_action() -> String {
String::from("return")
}
fn default_reload() -> String {
String::from("r")
}
}

229
src/model/config/mod.rs Normal file
View file

@ -0,0 +1,229 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
// Copyright (c) 2024 Awiteb <a@4rs.nl>
use crate::model::config::behavior::Behavior;
use crate::model::config::color::Color;
use crate::model::config::composition::Composition;
use crate::model::config::debug::Debug;
use crate::model::config::keybinding::Keybinding;
use crate::model::config::setup::Setup;
use crate::utils::get_config_dir;
use crate::utils::print_help;
use crate::utils::read_file;
use log::{info, warn};
use serde::Deserialize;
use std::env::args;
use std::process::exit;
mod behavior;
mod color;
mod composition;
mod debug;
mod keybinding;
mod setup;
#[derive(Clone, Debug, Default, Deserialize)]
pub struct Config {
#[serde(default)]
pub behavior: Behavior,
#[serde(default)]
pub color: Color,
#[serde(default)]
pub composition: Composition,
#[serde(default)]
pub debug: Debug,
#[serde(default)]
pub keybinding: Keybinding,
#[serde(default)]
pub setup: Setup,
}
impl Config {
pub fn new() -> Self {
info!("initializing config");
let config = Self::read_config_file().unwrap_or_default();
Self::parse_args(config, args().skip(1))
}
#[rustfmt::skip]
fn parse_args<T>(mut config: Self, args: T) -> Self
where
T: IntoIterator<Item = String>,
{
for arg in args {
let (key, value) = Self::split_arg(arg);
match key.as_str() {
"--behavior.file_action" => config.behavior.file_action = Self::parse_value((key, value)),
"--behavior.quit_on_action" => config.behavior.quit_on_action = Self::parse_value((key, value)),
"--behavior.path_node_sort" => config.behavior.path_node_sort = Self::parse_value((key, value)),
"--behavior.scrolling" => config.behavior.scrolling = Self::parse_value((key, value)),
"--color.background" => config.color.background = Self::parse_value((key, value)),
"--color.foreground" => config.color.foreground = Self::parse_value((key, value)),
"--composition.indent" => config.composition.indent = Self::parse_value((key, value)),
"--composition.show_indent" => config.composition.show_indent = Self::parse_value((key, value)),
"--composition.use_utf8" => config.composition.use_utf8 = Self::parse_value((key, value)),
"--debug.enabled" => config.debug.enabled = Self::parse_value((key, value)),
"--debug.padding_bot" => config.debug.padding_bot = Self::parse_value((key, value)),
"--debug.padding_top" => config.debug.padding_top = Self::parse_value((key, value)),
"--debug.spacing_bot" => config.debug.spacing_bot = Self::parse_value((key, value)),
"--debug.spacing_top" => config.debug.spacing_top = Self::parse_value((key, value)),
"--keybinding.collapse_dir" => config.keybinding.collapse_dir = Self::parse_value((key, value)),
"--keybinding.entry_down" => config.keybinding.entry_down = Self::parse_value((key, value)),
"--keybinding.entry_up" => config.keybinding.entry_up = Self::parse_value((key, value)),
"--keybinding.expand_dir" => config.keybinding.expand_dir = Self::parse_value((key, value)),
"--keybinding.file_action" => config.keybinding.file_action = Self::parse_value((key, value)),
"--keybinding.quit" => config.keybinding.quit = Self::parse_value((key, value)),
"--keybinding.reload" => config.keybinding.reload = Self::parse_value((key, value)),
"--setup.working_dir" => config.setup.working_dir = Self::parse_value((key, value)),
"--help" | "--version" => print_help(),
"--" => break,
_ => {
warn!("unknown option {}", key);
}
}
}
info!("config loaded as:\n{:?}", config);
config
}
fn split_arg(arg: String) -> (String, String) {
println!("{}", arg);
if let Some(equal_sign_index) = arg.find('=') {
let before_split = arg.split_at(equal_sign_index);
let after_split = arg.split_at(equal_sign_index + 1);
return (String::from(before_split.0), String::from(after_split.1));
}
(arg, String::from(""))
}
fn parse_value<F>((key, value): (String, String)) -> F
where
F: std::str::FromStr,
{
value.parse().unwrap_or_else(|_| {
println!("option '{}={}' was not parsable", key, value);
exit(1);
})
}
fn read_config_file() -> std::io::Result<Self> {
let config_dir = get_config_dir()?;
let config_file = format!("{}/twilight-commander.toml", config_dir);
let config_file_content = read_file(&config_file)?;
toml::from_str(&config_file_content).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"could not read the config file",
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_args() {
let default_config = Config::default();
let args_vec = vec![
String::from("--behavior.file_action=file_action_test"),
String::from("--behavior.path_node_sort=path_node_sort_test"),
String::from("--behavior.scrolling=scrolling_test"),
String::from("--color.background=background_test"),
String::from("--color.foreground=foreground_test"),
String::from("--debug.enabled=true"),
String::from("--debug.padding_bot=111"),
String::from("--debug.padding_top=222"),
String::from("--debug.spacing_bot=333"),
String::from("--debug.spacing_top=444"),
String::from("--setup.working_dir=working_dir_test"),
];
let config = Config::parse_args(default_config, args_vec);
assert_eq!(
config.behavior.file_action,
String::from("file_action_test")
);
assert_eq!(
config.behavior.path_node_sort,
String::from("path_node_sort_test")
);
assert_eq!(config.behavior.scrolling, String::from("scrolling_test"));
assert_eq!(config.color.background, String::from("background_test"));
assert_eq!(config.color.foreground, String::from("foreground_test"));
assert!(config.debug.enabled);
assert_eq!(config.debug.padding_bot, 111);
assert_eq!(config.debug.padding_top, 222);
assert_eq!(config.debug.spacing_bot, 333);
assert_eq!(config.debug.spacing_top, 444);
assert_eq!(config.setup.working_dir, String::from("working_dir_test"));
}
#[test]
fn test_parse_args_with_stopper() {
let default_config = Config::default();
let args_vec = vec![
String::from("--behavior.file_action=file_action_test"),
String::from("--behavior.path_node_sort=path_node_sort_test"),
String::from("--behavior.scrolling=scrolling_test"),
String::from("--color.background=background_test"),
String::from("--color.foreground=foreground_test"),
String::from("--"),
String::from("--debug.enabled=true"),
String::from("--debug.padding_bot=111"),
String::from("--debug.padding_top=222"),
String::from("--debug.spacing_bot=333"),
String::from("--debug.spacing_top=444"),
String::from("--setup.working_dir=working_dir_test"),
];
let config = Config::parse_args(default_config, args_vec);
let def_conf = Config::default();
assert_eq!(
config.behavior.file_action,
String::from("file_action_test")
);
assert_eq!(
config.behavior.path_node_sort,
String::from("path_node_sort_test")
);
assert_eq!(config.behavior.scrolling, String::from("scrolling_test"));
assert_eq!(config.color.background, String::from("background_test"));
assert_eq!(config.color.foreground, String::from("foreground_test"));
assert_eq!(config.debug.enabled, def_conf.debug.enabled);
assert_eq!(config.debug.padding_bot, def_conf.debug.padding_bot);
assert_eq!(config.debug.padding_top, def_conf.debug.padding_top);
assert_eq!(config.debug.spacing_bot, def_conf.debug.spacing_bot);
assert_eq!(config.debug.spacing_top, def_conf.debug.spacing_top);
assert_eq!(config.setup.working_dir, def_conf.setup.working_dir);
}
#[test]
fn test_parse_args_with_multiple_equals() {
let default_config = Config::default();
let args_vec = vec![String::from("--behavior.file_action=(x=1; y=2; echo $x$y)")];
let config = Config::parse_args(default_config, args_vec);
assert_eq!(
config.behavior.file_action,
String::from("(x=1; y=2; echo $x$y)")
);
}
}

24
src/model/config/setup.rs Normal file
View file

@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
pub struct Setup {
#[serde(default = "Setup::default_working_dir")]
pub working_dir: String,
}
impl Default for Setup {
fn default() -> Self {
Setup {
working_dir: Self::default_working_dir(),
}
}
}
impl Setup {
fn default_working_dir() -> String {
String::from(".")
}
}

197
src/model/event.rs Normal file
View file

@ -0,0 +1,197 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
type TEvent = termion::event::Event;
type TKey = termion::event::Key;
#[derive(Clone, Debug, PartialEq)]
pub struct Key {
inner: termion::event::Event,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
Resize,
Key(Key),
}
impl From<TEvent> for Key {
fn from(t_event: TEvent) -> Key {
Key { inner: t_event }
}
}
impl From<&str> for Key {
fn from(s: &str) -> Key {
Key::from(convert_str_to_termion_event(s))
}
}
impl From<String> for Key {
fn from(s: String) -> Key {
Key::from(convert_str_to_termion_event(&s))
}
}
fn convert_str_to_termion_event(s: &str) -> TEvent {
if s.chars().count() == 1 {
return TEvent::Key(TKey::Char(s.chars().last().unwrap()));
}
if s.starts_with("alt+") && s.len() == 5 {
return TEvent::Key(TKey::Alt(s.chars().last().unwrap()));
}
if s.starts_with("ctrl+") && s.len() == 6 {
return TEvent::Key(TKey::Ctrl(s.chars().last().unwrap()));
}
match s {
// f keys
"f1" => TEvent::Key(TKey::F(1)),
"f2" => TEvent::Key(TKey::F(2)),
"f3" => TEvent::Key(TKey::F(3)),
"f4" => TEvent::Key(TKey::F(4)),
"f5" => TEvent::Key(TKey::F(5)),
"f6" => TEvent::Key(TKey::F(6)),
"f7" => TEvent::Key(TKey::F(7)),
"f8" => TEvent::Key(TKey::F(8)),
"f9" => TEvent::Key(TKey::F(9)),
"f10" => TEvent::Key(TKey::F(10)),
"f11" => TEvent::Key(TKey::F(11)),
"f12" => TEvent::Key(TKey::F(12)),
// special keys
"backspace" => TEvent::Key(TKey::Backspace),
"left" => TEvent::Key(TKey::Left),
"right" => TEvent::Key(TKey::Right),
"up" => TEvent::Key(TKey::Up),
"down" => TEvent::Key(TKey::Down),
"home" => TEvent::Key(TKey::Home),
"end" => TEvent::Key(TKey::End),
"page_up" => TEvent::Key(TKey::PageUp),
"page_down" => TEvent::Key(TKey::PageDown),
"delete" => TEvent::Key(TKey::Delete),
"insert" => TEvent::Key(TKey::Insert),
"esc" => TEvent::Key(TKey::Esc),
"return" => TEvent::Key(TKey::Char('\n')),
"tab" => TEvent::Key(TKey::Char('\t')),
// special key combinations
// arrow keys
"ctrl+left" => TEvent::Unsupported(vec![27, 91, 49, 59, 53, 68]),
"ctrl+right" => TEvent::Unsupported(vec![27, 91, 49, 59, 53, 67]),
"ctrl+up" => TEvent::Unsupported(vec![27, 91, 49, 59, 53, 65]),
"ctrl+down" => TEvent::Unsupported(vec![27, 91, 49, 59, 53, 66]),
"shift+left" => TEvent::Unsupported(vec![27, 91, 49, 59, 50, 68]),
"shift+right" => TEvent::Unsupported(vec![27, 91, 49, 59, 50, 67]),
"shift+up" => TEvent::Unsupported(vec![27, 91, 49, 59, 50, 65]),
"shift+down" => TEvent::Unsupported(vec![27, 91, 49, 59, 50, 66]),
"alt+shift+left" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 68]),
"alt+shift+right" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 67]),
"alt+shift+up" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 65]),
"alt+shift+down" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 66]),
"shift+alt+left" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 68]),
"shift+alt+right" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 67]),
"shift+alt+up" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 65]),
"shift+alt+down" => TEvent::Unsupported(vec![27, 91, 49, 59, 52, 66]),
// default
_ => TEvent::Unsupported(Vec::new()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_from_termion_event_test() {
assert_eq!(
Key::from(TEvent::Unsupported(vec![27, 91, 49, 59, 52, 66])),
Key {
inner: TEvent::Unsupported(vec![27, 91, 49, 59, 52, 66])
}
);
}
#[test]
fn key_from_str_test() {
assert_eq!(
Key::from("shift+alt+down"),
Key {
inner: TEvent::Unsupported(vec![27, 91, 49, 59, 52, 66])
}
);
}
#[test]
fn key_from_string_test() {
assert_eq!(
Key::from(String::from("shift+alt+up")),
Key {
inner: TEvent::Unsupported(vec![27, 91, 49, 59, 52, 65])
}
);
}
mod convert_str_to_termion_event_tests {
use super::super::*;
#[test]
fn nonsense() {
assert_eq!(
TEvent::Unsupported(Vec::new()),
convert_str_to_termion_event("x1")
);
assert_eq!(
TEvent::Unsupported(Vec::new()),
convert_str_to_termion_event("alt+x1")
);
assert_eq!(
TEvent::Unsupported(Vec::new()),
convert_str_to_termion_event("ctrl+x1")
);
}
#[test]
fn single_digit() {
assert_eq!(
TEvent::Key(TKey::Char('x')),
convert_str_to_termion_event("x")
);
}
#[test]
fn alt_digit() {
assert_eq!(
TEvent::Key(TKey::Alt('x')),
convert_str_to_termion_event("alt+x")
);
}
#[test]
fn ctrl_digit() {
assert_eq!(
TEvent::Key(TKey::Ctrl('x')),
convert_str_to_termion_event("ctrl+x")
);
}
#[test]
fn f_key() {
assert_eq!(TEvent::Key(TKey::F(5)), convert_str_to_termion_event("f5"));
}
#[test]
fn special_key() {
assert_eq!(
TEvent::Key(TKey::PageDown),
convert_str_to_termion_event("page_down")
);
}
#[test]
fn special_key_comination() {
assert_eq!(
TEvent::Unsupported(vec![27, 91, 49, 59, 52, 65]),
convert_str_to_termion_event("alt+shift+up")
);
}
}
}

86
src/model/mod.rs Normal file
View file

@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
pub mod compare_functions;
pub mod config;
pub mod event;
pub mod path_node;
pub mod tree_index;
#[cfg(test)]
mod tests {
use crate::model::config::Config;
use crate::model::path_node::PathNode;
use crate::model::tree_index::TreeIndex;
use crate::view::composer::Composer;
#[test]
fn test_integration_with_path_node_sort_dirs_top_simple() {
let mut config = Config::default();
config.setup.working_dir = String::from("./tests/test_dirs");
let composer = Composer::from(config.clone());
let mut path_node = PathNode::from(config.setup.working_dir);
let path_node_compare = PathNode::compare_dirs_top_simple;
assert_eq!(0, composer.compose_path_node(&path_node).len());
// expand_dir
path_node.expand_dir(&TreeIndex::from(Vec::new()), path_node_compare);
assert_eq!(
13,
composer.compose_path_node(&path_node).len(),
"expanding the root directory"
);
path_node.expand_dir(&TreeIndex::from(vec![3]), path_node_compare);
assert_eq!(
13,
composer.compose_path_node(&path_node).len(),
"expanding a file does nothing"
);
path_node.expand_dir(&TreeIndex::from(vec![1]), path_node_compare);
assert_eq!(17, composer.compose_path_node(&path_node).len());
path_node.expand_dir(&TreeIndex::from(vec![1, 0]), path_node_compare);
assert_eq!(23, composer.compose_path_node(&path_node).len());
path_node.expand_dir(&TreeIndex::from(vec![1, 0, 2]), path_node_compare);
assert_eq!(26, composer.compose_path_node(&path_node).len());
path_node.expand_dir(&TreeIndex::from(vec![1, 0, 2, 1]), path_node_compare);
assert_eq!(29, composer.compose_path_node(&path_node).len());
// tree_index_to_flat_index
let flat_index = TreeIndex::from(vec![7, 2, 4, 0, 0]).to_flat_index();
assert_eq!(17, flat_index);
// flat_index_to_tree_index
let tree_index = path_node.flat_index_to_tree_index(9);
assert_eq!(vec![1, 0, 2, 1, 1], tree_index.index);
let tree_index = path_node.flat_index_to_tree_index(10);
assert_eq!(vec![1, 0, 2, 1, 2], tree_index.index);
let tree_index = path_node.flat_index_to_tree_index(11);
assert_eq!(vec![1, 0, 2, 2], tree_index.index);
let tree_index = path_node.flat_index_to_tree_index(15);
assert_eq!(vec![1, 1], tree_index.index);
// collapse_dir
path_node.collapse_dir(&TreeIndex::from(vec![1, 0, 2, 1]));
assert_eq!(
26,
composer.compose_path_node(&path_node).len(),
"reducing the last opened dir"
);
path_node.collapse_dir(&TreeIndex::from(vec![1, 0]));
assert_eq!(
17,
composer.compose_path_node(&path_node).len(),
"reducing lots of sub dirs"
);
}
}

View file

@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::model::config::Config;
use crate::model::path_node::PathNode;
use crate::view::composer::Composer;
use std::fmt::Debug;
use std::fmt::Formatter;
use std::fmt::Result;
impl Debug for PathNode {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let composer = Composer::from(Config::new());
let entries = composer.compose_path_node(self);
for (index, entry) in entries.iter().enumerate() {
writeln!(f, "{:4}|{}", index, entry)?;
}
Ok(())
}
}

293
src/model/path_node/mod.rs Normal file
View file

@ -0,0 +1,293 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::model::compare_functions::PathNodeCompare;
use crate::model::config::Config;
use crate::model::tree_index::TreeIndex;
use log::info;
use std::fs::canonicalize;
use std::path::PathBuf;
mod debug;
#[derive(Clone)]
pub struct PathNode {
pub children: Vec<PathNode>,
pub display_text: String,
pub is_dir: bool,
pub is_err: bool,
pub is_expanded: bool,
pub path: PathBuf,
}
impl From<&str> for PathNode {
fn from(working_dir: &str) -> Self {
Self {
children: Vec::new(),
display_text: String::from(working_dir),
is_dir: true,
is_err: false,
is_expanded: false,
path: PathBuf::from(working_dir),
}
}
}
impl From<String> for PathNode {
fn from(working_dir: String) -> Self {
Self {
children: Vec::new(),
display_text: working_dir.clone(),
is_dir: true,
is_err: false,
is_expanded: false,
path: PathBuf::from(working_dir),
}
}
}
impl PathNode {
pub fn new_expanded(config: Config) -> Self {
info!("initializing path node");
let mut path_node = Self::from(config.setup.working_dir.clone());
let path_node_compare = Self::get_path_node_compare(&config);
path_node.expand_dir(&TreeIndex::new(), path_node_compare);
path_node
}
pub fn get_absolute_path(&self) -> String {
let canonicalized_path = canonicalize(self.path.as_path()).unwrap();
canonicalized_path.to_str().unwrap().to_string()
}
fn list_path_node_children(&mut self, compare: PathNodeCompare) -> Vec<PathNode> {
let dirs = self.path.read_dir();
if dirs.is_err() {
self.is_err = true;
return Vec::new();
}
let mut path_nodes = dirs
.unwrap()
.map(|dir_entry| {
let dir_entry = dir_entry.unwrap();
PathNode {
children: Vec::new(),
display_text: dir_entry.file_name().into_string().unwrap(),
is_dir: dir_entry.path().is_dir(),
is_err: false,
is_expanded: false,
path: dir_entry.path(),
}
})
.collect::<Vec<PathNode>>();
path_nodes.sort_unstable_by(compare);
path_nodes
}
pub fn expand_dir(&mut self, tree_index: &TreeIndex, compare: PathNodeCompare) {
let mut path_node = self;
for i in &tree_index.index {
if path_node.children.len() > *i {
path_node = &mut path_node.children[*i];
}
}
if !path_node.path.is_dir() {
return;
}
path_node.is_expanded = true;
path_node.children = path_node.list_path_node_children(compare);
}
pub fn collapse_dir(&mut self, tree_index: &TreeIndex) {
let mut path_node = self;
for i in &tree_index.index {
path_node = &mut path_node.children[*i];
}
path_node.is_expanded = false;
path_node.children = Vec::new();
}
fn flat_index_to_tree_index_rec(
&self,
flat_index: &mut usize,
tree_index: &mut TreeIndex,
) -> bool {
if *flat_index == 0 {
return true;
}
for (c, child) in self.children.iter().enumerate() {
*flat_index -= 1;
tree_index.index.push(c);
if child.flat_index_to_tree_index_rec(flat_index, tree_index) {
return true;
}
tree_index.index.pop();
}
false
}
pub fn flat_index_to_tree_index(&self, flat_index: usize) -> TreeIndex {
let mut tree_index = TreeIndex::from(Vec::new());
self.flat_index_to_tree_index_rec(&mut (flat_index + 1), &mut tree_index);
tree_index
}
pub fn tree_index_to_flat_index_rec(
&self,
target_tree_index: &TreeIndex,
current_tree_index: &TreeIndex,
) -> usize {
if current_tree_index >= target_tree_index {
return 0;
}
if self.children.is_empty() {
return 1;
}
let mut sum = 1;
for (index, child) in self.children.iter().enumerate() {
let mut new_current_tree_index = current_tree_index.clone();
new_current_tree_index.index.push(index);
sum += child.tree_index_to_flat_index_rec(target_tree_index, &new_current_tree_index);
}
sum
}
pub fn tree_index_to_flat_index(&self, tree_index: &TreeIndex) -> usize {
// We count the root directory, hence we have to subtract 1 to get the
// proper index.
self.tree_index_to_flat_index_rec(tree_index, &TreeIndex::new()) - 1
}
pub fn get_child_path_node(&self, tree_index: &TreeIndex) -> &Self {
let mut child_node = self;
for i in &tree_index.index {
child_node = &child_node.children[*i];
}
child_node
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get_expanded_path_node() -> PathNode {
let mut path_node = PathNode::from("./tests/test_dirs");
path_node.expand_dir(&TreeIndex::new(), PathNode::compare_dirs_top_simple);
path_node.expand_dir(&TreeIndex::from(vec![0]), PathNode::compare_dirs_top_simple);
path_node.expand_dir(
&TreeIndex::from(vec![0, 0]),
PathNode::compare_dirs_top_simple,
);
path_node.expand_dir(&TreeIndex::from(vec![1]), PathNode::compare_dirs_top_simple);
path_node.expand_dir(
&TreeIndex::from(vec![1, 0]),
PathNode::compare_dirs_top_simple,
);
path_node.expand_dir(
&TreeIndex::from(vec![1, 0, 2]),
PathNode::compare_dirs_top_simple,
);
path_node
}
mod get_child_path_node_tests {
use super::*;
#[test]
fn first_dirs() {
let path_node = {
let mut path_node = PathNode::from("./tests/test_dirs");
path_node.expand_dir(&TreeIndex::new(), PathNode::compare_dirs_top_simple);
path_node.expand_dir(&TreeIndex::from(vec![0]), PathNode::compare_dirs_top_simple);
path_node.expand_dir(
&TreeIndex::from(vec![0, 0]),
PathNode::compare_dirs_top_simple,
);
path_node
};
let child_path_node = path_node.get_child_path_node(&TreeIndex::from(vec![0, 0, 0]));
assert_eq!("file4", child_path_node.display_text);
}
#[test]
fn complex_dirs() {
let path_node = get_expanded_path_node();
let child_path_node = path_node.get_child_path_node(&TreeIndex::from(vec![1, 0, 2, 2]));
assert_eq!("file12", child_path_node.display_text);
}
}
mod tree_index_to_flat_index_tests {
use super::*;
#[test]
fn complex_dirs() {
let path_node = get_expanded_path_node();
let flat_index = path_node.tree_index_to_flat_index(&TreeIndex::from(vec![4]));
assert_eq!(22, flat_index);
}
#[test]
fn complex_dirs2() {
let path_node = get_expanded_path_node();
let flat_index = path_node.tree_index_to_flat_index(&TreeIndex::from(vec![5]));
assert_eq!(23, flat_index);
}
#[test]
fn complex_dirs3() {
let path_node = get_expanded_path_node();
let flat_index = path_node.tree_index_to_flat_index(&TreeIndex::from(vec![1, 0, 4]));
assert_eq!(15, flat_index);
}
#[test]
fn total_count() {
let path_node = get_expanded_path_node();
let flat_index = path_node.tree_index_to_flat_index(&TreeIndex::from(vec![100_000]));
assert_eq!(31, flat_index);
}
#[test]
fn zero() {
let path_node = get_expanded_path_node();
let flat_index = path_node.tree_index_to_flat_index(&TreeIndex::from(vec![0]));
assert_eq!(0, flat_index);
}
}
}

113
src/model/tree_index.rs Normal file
View file

@ -0,0 +1,113 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct TreeIndex {
pub index: Vec<usize>,
}
impl From<Vec<usize>> for TreeIndex {
fn from(index: Vec<usize>) -> Self {
Self { index }
}
}
impl TreeIndex {
pub fn new() -> Self {
Self { index: vec![] }
}
pub fn get_parent(&self) -> Self {
if self.index.is_empty() {
return Self { index: vec![] };
}
let mut index = self.index.clone();
index.pop().unwrap();
Self { index }
}
#[allow(dead_code)] // TODO: remove?
pub fn to_flat_index(&self) -> usize {
if self.index.is_empty() {
return 0;
}
let mut flat_index = 0;
for i in &self.index {
flat_index += i + 1;
}
flat_index - 1
}
}
#[cfg(test)]
mod tests {
use super::*;
mod get_parent_tests {
use super::*;
#[test]
fn empty() {
let tree_index = TreeIndex::new();
let parent = tree_index.get_parent();
assert_eq!(TreeIndex::new(), parent);
}
#[test]
fn minimal() {
let tree_index = TreeIndex::from(vec![0]);
let parent = tree_index.get_parent();
assert_eq!(TreeIndex::new(), parent);
}
#[test]
fn zeroes() {
let tree_index = TreeIndex::from(vec![0, 0, 0, 0, 0]);
let parent = tree_index.get_parent();
assert_eq!(TreeIndex::from(vec![0, 0, 0, 0]), parent);
}
#[test]
fn complex() {
let tree_index = TreeIndex::from(vec![3, 4, 6, 7, 1]);
let parent = tree_index.get_parent();
assert_eq!(TreeIndex::from(vec![3, 4, 6, 7]), parent);
}
}
mod to_flat_index_tests {
use super::*;
#[test]
fn empty() {
let tree_index = TreeIndex::new();
let flat_index = tree_index.to_flat_index();
assert_eq!(0, flat_index);
}
#[test]
fn minimal() {
let tree_index = TreeIndex::from(vec![0]);
let flat_index = tree_index.to_flat_index();
assert_eq!(0, flat_index);
}
#[test]
fn zeroes() {
let tree_index = TreeIndex::from(vec![0, 0, 0, 0, 0]);
let flat_index = tree_index.to_flat_index();
assert_eq!(4, flat_index);
}
#[test]
fn complex() {
let tree_index = TreeIndex::from(vec![3, 4, 6, 7, 1]);
let flat_index = tree_index.to_flat_index();
assert_eq!(25, flat_index);
}
}
}

79
src/utils.rs Normal file
View file

@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use log::info;
use std::fs::File;
use std::io::Read;
use std::panic::set_hook;
use std::process::exit;
pub fn read_file(file_name: &str) -> std::io::Result<String> {
let mut file = File::open(file_name)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
pub fn print_help() {
println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
println!(r#"usage: twilight-tree [--key1=value1 --key2=value2 ...]"#);
exit(0);
}
pub fn setup_logger() -> Result<(), fern::InitError> {
let log_file_path = format!("{}/{}", get_config_dir()?, "tc.log");
fern::Dispatch::new()
.format(|out, message, record| {
let target = record.target();
let target_split_at = 0.max(target.len() as i32 - 20) as usize;
let target_short = target.split_at(target_split_at);
out.finish(format_args!(
"[{}][{:05}][{:>20}] {}",
chrono::Local::now().to_rfc3339_opts(::chrono::SecondsFormat::Millis, true),
record.level(),
target_short.1,
message
))
})
.level(log::LevelFilter::Debug)
.chain(fern::log_file(log_file_path)?)
.apply()?;
set_hook(Box::new(|panic_info| {
if let Some(p) = panic_info.payload().downcast_ref::<String>() {
info!("{:?}, \npayload: {}", panic_info, p,);
} else if let Some(p) = panic_info.payload().downcast_ref::<&str>() {
info!("{:?}, \npayload: {}", panic_info, p,);
} else {
info!("{:?}", panic_info);
}
}));
info!(
r#"starting...
_|_ o|o _ |__|_ _ _ ._ _ ._ _ _.._ _| _ ._
|_\/\/|||(_|| ||_ (_(_)| | || | |(_|| |(_|(/_|
_|
"#
);
info!("logger initialized");
Ok(())
}
pub fn get_config_dir() -> std::io::Result<String> {
if let Ok(xdg_config_home) = std::env::var("XDG_CONFIG_HOME") {
Ok(xdg_config_home)
} else if let Ok(home) = std::env::var("HOME") {
Ok(format!("{}/.config/twilight-tree", home))
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no HOME or XDG_CONFIG_HOME variable is defined",
))
}
}

126
src/view/composer.rs Normal file
View file

@ -0,0 +1,126 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::model::config::Config;
use crate::model::path_node::PathNode;
use log::info;
pub struct Composer {
config: Config,
}
impl From<Config> for Composer {
fn from(config: Config) -> Self {
info!("initializing composer");
Self { config }
}
}
impl Composer {
pub fn truncate_string(string: &str, desired_char_count: usize) -> String {
if desired_char_count < 1 {
return String::new();
}
if desired_char_count >= string.chars().count() {
return String::from(string);
}
let truncated = match string.char_indices().nth(desired_char_count - 1) {
None => string,
Some((idx, _)) => &string[..idx],
};
format!("{}~", truncated)
}
pub fn compose_path_node(&self, path_node: &PathNode) -> Vec<String> {
let mut result = Vec::new();
self.compose_path_node_recursive(path_node, &mut result, 0);
result
}
fn compose_path_node_recursive(
&self,
path_node: &PathNode,
texts: &mut Vec<String>,
depth: usize,
) {
for child in &path_node.children {
let dir_prefix = self.get_dir_prefix(child);
let dir_suffix = self.get_dir_suffix(child);
let indent = self.get_indent(depth);
let text = format!(
"{}{}{}{}",
indent,
dir_prefix,
child.display_text.clone(),
dir_suffix,
);
texts.push(text);
self.compose_path_node_recursive(child, texts, depth + 1);
}
}
fn get_dir_prefix(&self, path_node: &PathNode) -> String {
let (err_char, expanded_char, reduced_char) = if self.config.composition.use_utf8 {
('', '▼', '▶')
} else {
('x', 'v', '>')
};
let expanded_indicator = if path_node.is_err {
err_char
} else if path_node.is_expanded {
expanded_char
} else {
reduced_char
};
if path_node.is_dir {
format!("{} ", expanded_indicator)
} else {
String::from(" ")
}
}
fn get_dir_suffix(&self, path_node: &PathNode) -> String {
if path_node.is_dir {
String::from("/")
} else {
String::from("")
}
}
fn get_indent(&self, depth: usize) -> String {
let indent_char = if !self.config.composition.show_indent {
' '
} else if self.config.composition.use_utf8 {
'·'
} else {
'-'
};
let indent = " ".repeat(self.config.composition.indent as usize - 1);
format!("{}{}", indent_char, indent).repeat(depth)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_string_test() {
let tc = Composer::truncate_string;
assert_eq!(tc("hello world", 5), "hell~");
assert_eq!(tc("hello world", 1), "~");
assert_eq!(tc("hello world", 0), "");
assert_eq!(tc("aaa▶bbb▶ccc", 8), "aaa▶bbb~");
assert_eq!(tc("aaa▶bbb▶ccc", 6), "aaa▶b~");
assert_eq!(tc("aaa▶bbb▶ccc", 4), "aaa~");
}
}

68
src/view/mod.rs Normal file
View file

@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::model::config::Config;
use crate::view::composer::Composer;
use log::info;
use std::io::Write;
pub mod composer;
mod print;
mod scroll;
mod update;
pub struct Pager<W: Write> {
config: Config,
pub cursor_row: i32,
out: W,
terminal_cols: i32,
terminal_rows: i32,
text_row: i32,
}
impl<W: Write> Pager<W> {
pub fn new(config: Config, mut out: W) -> Self {
info!("initializing pager");
write!(
out,
"{}{}{}",
termion::cursor::Hide,
termion::cursor::Goto(1, 1),
termion::clear::All,
)
.unwrap();
Self {
config,
cursor_row: 0,
out,
terminal_cols: 0,
terminal_rows: 0,
text_row: 0,
}
}
}
impl<W: Write> Drop for Pager<W> {
fn drop(&mut self) {
write!(
self,
"{}{}{}",
termion::clear::All,
termion::cursor::Goto(1, 1),
termion::cursor::Show,
)
.unwrap();
}
}
impl<W: Write> Write for Pager<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.out.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.out.flush()
}
}

247
src/view/print.rs Normal file
View file

@ -0,0 +1,247 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
// Copyright (c) 2024 Awiteb <a@4rs.nl>
use crate::view::Composer;
use crate::view::Pager;
use std::io::Write;
use termion::{color, style};
impl<W: Write> Pager<W> {
pub fn print_clear(&mut self) {
write!(self, "{}", termion::clear::All).unwrap();
}
pub fn print_text_entry(&mut self, text_entry: &str, row: i32) {
write!(
self,
"{}{}{}",
termion::cursor::Goto(1, row as u16),
Composer::truncate_string(text_entry, self.terminal_cols as usize),
style::Reset
)
.unwrap();
}
pub fn print_text_entry_emphasized(&mut self, text_entry: &str, row: i32) {
write!(
self,
"{}{}{}{}",
termion::cursor::Goto(1, row as u16),
color::Bg(color::Blue),
Composer::truncate_string(text_entry, self.terminal_cols as usize),
style::Reset
)
.unwrap();
}
pub fn print_header(&mut self, text: &str) {
write!(
self,
"{}{}",
termion::cursor::Goto(1, 1),
Composer::truncate_string(text, self.terminal_cols as usize),
)
.unwrap();
}
pub fn print_footer(&mut self, text: &str) {
write!(
self,
"{}{}",
termion::cursor::Goto(1, 1 + self.terminal_rows as u16),
Composer::truncate_string(text, self.terminal_cols as usize),
)
.unwrap();
}
pub fn print_debug_info(&mut self) {
if !self.config.debug.enabled {
return;
}
let padding_bot = self.config.debug.padding_bot;
let padding_top = self.config.debug.padding_top;
let spacing_bot = self.config.debug.spacing_bot;
let spacing_top = self.config.debug.spacing_top;
// line numbers
for i in 0..self.terminal_rows {
write!(self, "{} L{i}", termion::cursor::Goto(50, 1 + i as u16)).unwrap();
}
// padding_top debug
for i in 0..padding_bot {
write!(
self,
"{}~~~ padding_bot",
termion::cursor::Goto(30, (self.terminal_rows - (spacing_bot + i)) as u16)
)
.unwrap();
}
for i in 0..padding_top {
write!(
self,
"{}~~~ padding_top",
termion::cursor::Goto(30, (1 + spacing_top + i) as u16)
)
.unwrap();
}
// spacing_top debug
for i in 0..spacing_bot {
write!(
self,
"{}--- spacing_bot",
termion::cursor::Goto(30, (self.terminal_rows - i) as u16)
)
.unwrap();
}
for i in 0..spacing_top {
write!(
self,
"{}--- spacing_top",
termion::cursor::Goto(30, 1 + i as u16)
)
.unwrap();
}
// debug info
let terminal_rows = self.terminal_rows;
let terminal_cols = self.terminal_cols;
let cursor_row = self.cursor_row;
let text_row = self.text_row;
write!(
self,
"{}rows: {}, cols: {}",
termion::cursor::Goto(1, (self.terminal_rows - 1) as u16),
terminal_rows,
terminal_cols
)
.unwrap();
write!(
self,
"{}cursor_row: {}, text_row: {}",
termion::cursor::Goto(1, self.terminal_rows as u16),
cursor_row,
text_row
)
.unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::config::Config;
fn prepare_pager() -> Pager<Vec<u8>> {
let mut config = Config::default();
config.debug.enabled = true;
config.debug.padding_bot = 1;
config.debug.padding_top = 1;
config.debug.spacing_bot = 1;
config.debug.spacing_top = 1;
let out: Vec<u8> = Vec::new();
let mut pager = Pager::new(config, out);
pager.terminal_cols = 100;
pager.terminal_rows = 10;
pager
}
fn get_result(pager: Pager<Vec<u8>>) -> Option<String> {
let pager_out = pager.out.clone();
Some(String::from(std::str::from_utf8(&pager_out).unwrap()))
}
#[test]
fn print_clear_test() {
let result = {
let mut pager = prepare_pager();
pager.print_clear();
get_result(pager)
};
assert_eq!("\u{1b}[?25l\u{1b}[1;1H\u{1b}[2J\u{1b}[2J", result.unwrap());
}
#[test]
fn print_text_entry_test() {
let result = {
let mut pager = prepare_pager();
pager.print_text_entry("--- test 123 ---", 42);
get_result(pager)
};
assert_eq!(
"\u{1b}[?25l\u{1b}[1;1H\u{1b}[2J\u{1b}[42;1H--- test 123 ---\u{1b}[m",
result.unwrap(),
);
}
#[test]
fn print_text_entry_emphasized_test() {
let result = {
let mut pager = prepare_pager();
pager.print_text_entry_emphasized("--- test 123 ---", 42);
get_result(pager)
};
assert_eq!(
"\u{1b}[?25l\u{1b}[1;1H\u{1b}[2J\u{1b}[42;1H\u{1b}[48;5;4m--- test 123 ---\u{1b}[m",
result.unwrap(),
);
}
#[test]
fn print_header_test() {
let result = {
let mut pager = prepare_pager();
pager.print_header("--- test 123 ---");
get_result(pager)
};
assert_eq!(
"\u{1b}[?25l\u{1b}[1;1H\u{1b}[2J\u{1b}[1;1H--- test 123 ---",
result.unwrap(),
);
}
#[test]
fn print_footer_test() {
let result = {
let mut pager = prepare_pager();
pager.print_footer("--- test 123 ---");
get_result(pager)
};
assert_eq!(
"\u{1b}[?25l\u{1b}[1;1H\u{1b}[2J\u{1b}[11;1H--- test 123 ---",
result.unwrap(),
);
}
#[test]
fn print_debug_info_test() {
let result = {
let mut pager = prepare_pager();
pager.print_debug_info();
get_result(pager)
}
.unwrap();
assert!(result.contains("~~~ padding_bot"));
assert!(result.contains("~~~ padding_top"));
assert!(result.contains("--- spacing_bot"));
assert!(result.contains("--- spacing_top"));
assert!(result.contains("cols: 100"));
assert!(result.contains("rows: 10"));
assert!(result.contains("cursor_row: 0"));
assert!(result.contains("text_row: 0"));
}
}

309
src/view/scroll.rs Normal file
View file

@ -0,0 +1,309 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::view::Pager;
use std::io::Write;
impl<W: Write> Pager<W> {
fn get_index_overshoot(index_under_test: i32, index_now: i32, index_delta: i32) -> Option<i32> {
let index_before = index_now - index_delta;
// cross from below
if index_before <= index_under_test && index_now >= index_under_test {
return Some(index_now - index_under_test);
}
// cross from above
if index_before >= index_under_test && index_now <= index_under_test {
return Some(index_now - index_under_test);
}
None
}
pub fn scroll_like_center(&self, cursor_row_delta: i32, text_entries_len: i32) -> i32 {
let spacing_bot = self.config.debug.spacing_bot;
let spacing_top = self.config.debug.spacing_top;
let center_text_row =
spacing_top - self.text_row + (self.terminal_rows - (spacing_bot + spacing_top)) / 2;
let last_text_row = self.terminal_rows - (self.text_row + spacing_bot);
// re-center a cursor row that is below the center (last text entry was
// visible) in the case that a subdirectory is opened
// in such a way that the bottom is not visible anymore
if cursor_row_delta == 0
&& self.cursor_row - center_text_row > 0
&& self.cursor_row - center_text_row <= text_entries_len - last_text_row
{
return self.text_row - (self.cursor_row - center_text_row);
}
// cursor row is moved over the center
if let Some(overshoot) =
Self::get_index_overshoot(center_text_row, self.cursor_row, cursor_row_delta)
{
// no need to keep it centered when we reach the top or bottom
if self.text_row >= spacing_top && cursor_row_delta < 0 {
return self.text_row;
}
if self.text_row + text_entries_len <= self.terminal_rows - spacing_bot
&& cursor_row_delta > 0
{
return self.text_row;
}
// Prevent violating the spacing when centering on the cursor row.
// E.g. when jumping to the parent directory and it is the topmost
// entry do not want it centered.
if self.text_row - overshoot > spacing_top {
return spacing_top;
}
// keep it centered
return self.text_row - overshoot;
}
// cursor row is beyond vision -> move the text row the minimal amount
// to correct that
if self.text_row + self.cursor_row < spacing_top {
return spacing_top - self.cursor_row;
} else if self.text_row + self.cursor_row > self.terminal_rows - (1 + spacing_bot) {
return self.terminal_rows - (1 + spacing_bot + self.cursor_row);
}
self.text_row
}
pub fn scroll_like_editor(&self) -> i32 {
let padding_bot = self.config.debug.padding_bot;
let padding_top = self.config.debug.padding_top;
let spacing_bot = self.config.debug.spacing_bot;
let spacing_top = self.config.debug.spacing_top;
if self.text_row + self.cursor_row < spacing_top + padding_top {
return spacing_top + padding_top - self.cursor_row;
} else if self.text_row + self.cursor_row
> self.terminal_rows - (1 + spacing_bot + padding_bot)
{
return self.terminal_rows - (1 + spacing_bot + padding_bot + self.cursor_row);
}
self.text_row
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::config::Config;
fn prepare_pager() -> Pager<Vec<u8>> {
let mut config = Config::default();
config.debug.enabled = true;
config.debug.padding_bot = 1;
config.debug.padding_top = 1;
config.debug.spacing_bot = 1;
config.debug.spacing_top = 1;
let out: Vec<u8> = Vec::new();
let mut pager = Pager::new(config, out);
pager.terminal_cols = 100;
pager.terminal_rows = 10;
pager
}
mod get_index_overshoot_tests {
use super::*;
#[test]
fn overshoot_from_below() {
let overshoot = Pager::<Vec<u8>>::get_index_overshoot(10, 11, 3);
assert_eq!(Some(1), overshoot);
}
#[test]
fn overshoot_from_above() {
let overshoot = Pager::<Vec<u8>>::get_index_overshoot(10, 7, -4);
assert_eq!(Some(-3), overshoot);
}
#[test]
fn no_overshoot_from_below() {
let overshoot = Pager::<Vec<u8>>::get_index_overshoot(10, 7, 2);
assert_eq!(None, overshoot);
}
#[test]
fn no_overshoot_from_above() {
let overshoot = Pager::<Vec<u8>>::get_index_overshoot(10, 14, -3);
assert_eq!(None, overshoot);
}
}
mod scroll_like_center_tests {
use super::*;
#[test]
fn scroll_like_center_cursor_top_test() {
let text_row = {
let pager = prepare_pager();
pager.scroll_like_center(1, 17)
};
assert_eq!(1, text_row);
}
#[test]
fn scroll_like_center_text_moves_up1_test() {
let text_row = {
let mut pager = prepare_pager();
pager.cursor_row = 5;
pager.scroll_like_center(1, 17)
};
assert_eq!(0, text_row);
}
#[test]
fn scroll_like_center_text_moves_up2_test() {
let text_row = {
let mut pager = prepare_pager();
pager.cursor_row = 6;
pager.scroll_like_center(1, 17)
};
assert_eq!(-1, text_row);
}
#[test]
fn scroll_like_center_text_moves_down_test() {
let text_row = {
let mut pager = prepare_pager();
pager.cursor_row = 6;
pager.scroll_like_center(-1, 17)
};
assert_eq!(0, text_row);
}
#[test]
fn scroll_like_center_cursor_bot_test() {
let text_row = {
let mut pager = prepare_pager();
pager.cursor_row = 9;
pager.scroll_like_center(-1, 17)
};
assert_eq!(-1, text_row);
}
#[test]
fn scroll_like_center_cursor_bot_no_delta_test() {
let text_row = {
let mut pager = prepare_pager();
pager.cursor_row = 9;
pager.scroll_like_center(0, 17)
};
assert_eq!(-4, text_row);
}
#[test]
fn scroll_like_center_cursor_with_overshoot() {
let text_row = {
let mut pager = prepare_pager();
println!("{}", pager.text_row);
pager.cursor_row = 13;
pager.text_row = -10;
pager.scroll_like_center(-3, 123)
};
assert_eq!(-8, text_row);
}
#[test]
fn scroll_like_center_cursor_top_most_overshoot() {
let text_row = {
let mut pager = prepare_pager();
println!("{}", pager.text_row);
pager.cursor_row = 0;
pager.text_row = -10;
pager.scroll_like_center(-16, 123)
};
assert_eq!(1, text_row);
}
}
mod scroll_like_editor_tests {
use super::*;
#[test]
fn scroll_like_editor_cursor_top_test() {
let text_row = {
let pager = prepare_pager();
pager.scroll_like_editor()
};
assert_eq!(2, text_row);
}
#[test]
fn scroll_like_editor_text_moves_up1_test() {
let text_row = {
let mut pager = prepare_pager();
pager.cursor_row = 5;
pager.scroll_like_editor()
};
assert_eq!(0, text_row);
}
#[test]
fn scroll_like_editor_text_moves_up2_test() {
let text_row = {
let mut pager = prepare_pager();
pager.cursor_row = 6;
pager.scroll_like_editor()
};
assert_eq!(0, text_row);
}
#[test]
fn scroll_like_editor_text_moves_down_test() {
let text_row = {
let mut pager = prepare_pager();
pager.cursor_row = 6;
pager.scroll_like_editor()
};
assert_eq!(0, text_row);
}
#[test]
fn scroll_like_editor_cursor_bot_test() {
let text_row = {
let mut pager = prepare_pager();
pager.cursor_row = 9;
pager.scroll_like_editor()
};
assert_eq!(-2, text_row);
}
#[test]
fn scroll_like_editor_cursor_bot_no_delta_test() {
let text_row = {
let mut pager = prepare_pager();
pager.cursor_row = 9;
pager.scroll_like_editor()
};
assert_eq!(-2, text_row);
}
}
}

72
src/view/update.rs Normal file
View file

@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2019-2022 golmman
use crate::view::Pager;
use std::io::Write;
use termion::terminal_size;
impl<W: Write> Pager<W> {
fn update_terminal_size(&mut self) {
let (terminal_cols_raw, terminal_rows_raw) = terminal_size().unwrap();
self.terminal_cols = i32::from(terminal_cols_raw);
self.terminal_rows = i32::from(terminal_rows_raw);
}
fn update_cursor_row(&mut self, cursor_row_delta: i32, text_entries_len: i32) {
self.cursor_row += cursor_row_delta;
if self.cursor_row < 0 {
self.cursor_row = text_entries_len - 1;
}
if self.cursor_row >= text_entries_len {
self.cursor_row = 0;
}
}
pub fn update(&mut self, cursor_row_delta: i32, text_entries: &[String], header_text: String) {
self.update_terminal_size();
let spacing_bot = self.config.debug.spacing_bot;
let spacing_top = self.config.debug.spacing_top;
let text_entries_len = text_entries.len() as i32;
self.update_cursor_row(cursor_row_delta, text_entries_len);
self.text_row = match self.config.behavior.scrolling.as_str() {
"center" => self.scroll_like_center(cursor_row_delta, text_entries_len),
"editor" => self.scroll_like_editor(),
_ => 0,
};
let displayable_rows = self.terminal_rows - (spacing_bot + spacing_top);
let first_index = spacing_top - self.text_row;
// clear screen
self.print_clear();
// print rows
for i in 0..displayable_rows {
let index = first_index + i;
if index >= 0 && index < text_entries.len() as i32 {
let text_entry = &text_entries[index as usize];
if index == self.cursor_row {
self.print_text_entry_emphasized(text_entry, 1 + spacing_top + i)
} else {
self.print_text_entry(text_entry, 1 + spacing_top + i);
}
}
}
let footer_text = format!("[{}/{}]", self.cursor_row + 1, text_entries_len);
self.print_header(&header_text);
self.print_footer(&footer_text);
self.print_debug_info();
self.flush().unwrap();
}
}

View file

View file

View file

View file

View file

View file

View file

View file

View file

View file

View file

View file

View file

View file

0
tests/test_dirs/file0 Normal file
View file

0
tests/test_dirs/file1 Normal file
View file

0
tests/test_dirs/file2 Normal file
View file

0
tests/test_dirs/file21 Normal file
View file

0
tests/test_dirs/file22 Normal file
View file

0
tests/test_dirs/file23 Normal file
View file

0
tests/test_dirs/file24 Normal file
View file

0
tests/test_dirs/file25 Normal file
View file

0
tests/test_dirs/file26 Normal file
View file

0
tests/test_dirs/file27 Normal file
View file

56
twilight-tree.toml Normal file
View file

@ -0,0 +1,56 @@
[behavior]
# command interpreted by bash when pressing the file_action key
file_action = "true"
# when true the program will quit after first action
quit_on_action = false
# determines the compare function used for sorting entries
# enum: none, dirs_top_simple, dirs_bot_simple
# TODO: rename to entry_sort
path_node_sort = "dirs_top_simple"
# the scrollung algorithm used
# enum: center, editor
scrolling = "center"
# the amount of entries skipped when the skip keys are pressed
skip_amount = 5
[composition]
# indention used for subentries
indent = 2
# when true shows visual markers for indention whitespaces
show_indent = false
# when true uses utf8 characters
use_utf8 = true
[debug]
# enables debug mode
enabled = false
# the minimum distance of the highlighted entry to the spacing
padding_bot = 3
padding_top = 3
# the number of lines not uses for entries
spacing_bot = 2
spacing_top = 2
[keybinding]
collapse_dir = "left"
entry_down = "down"
entry_up = "up"
expand_dir = "right"
file_action = "return"
quit = "q"
reload = "r"
skip_up = "ctrl+up"
skip_down = "ctrl+down"
[setup]
# the working directory used when starting
working_dir = "."