├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── License ├── README.md ├── build ├── .gitignore ├── add_toolchain.sh └── build.sh ├── src ├── README.md ├── app.rs ├── config │ └── mod.rs ├── events │ ├── handler.rs │ └── mod.rs ├── fps_counter │ └── mod.rs ├── fs │ ├── folder.rs │ ├── folder_entry.rs │ ├── mod.rs │ └── store │ │ ├── ds_hashmap.rs │ │ └── mod.rs ├── lib.rs ├── logger │ └── mod.rs ├── main.rs ├── spinner │ └── mod.rs ├── task_manager │ ├── legacy.rs │ └── mod.rs ├── tui │ └── mod.rs └── ui │ ├── chart.rs │ ├── constants.rs │ ├── content.rs │ ├── footer.rs │ ├── mod.rs │ ├── title.rs │ └── utils.rs └── tests ├── common └── mod.rs ├── cursor.rs ├── delete.rs ├── file_tree.rs ├── handle_enter.rs ├── loading.rs └── test_files ├── .DS_Store ├── .gitignore ├── README.md └── view ├── a_folder ├── folder1_file1.txt └── folder1_file2.txt ├── a_root_file.txt ├── b_folder ├── extra_weight.txt ├── folder2_file1.txt ├── folder2_file2.txt └── folder2_file3.txt ├── c_folder ├── folder2_file1.txt ├── folder2_file2.txt └── folder2_file3.txt ├── d_root_file.txt └── z_root_file.txt /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Install Clippy 20 | run: rustup component add clippy 21 | - name: Build 22 | run: cargo build --verbose 23 | - name: Run tests 24 | run: cargo test 25 | - name: Run Clippy 26 | run: cargo clippy --all-targets -- -D warnings 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | .idea 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ikebastuz@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.7" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" 10 | dependencies = [ 11 | "cfg-if", 12 | "once_cell", 13 | "version_check", 14 | "zerocopy", 15 | ] 16 | 17 | [[package]] 18 | name = "allocator-api2" 19 | version = "0.2.16" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" 22 | 23 | [[package]] 24 | name = "android-tzdata" 25 | version = "0.1.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 28 | 29 | [[package]] 30 | name = "android_system_properties" 31 | version = "0.1.5" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 34 | dependencies = [ 35 | "libc", 36 | ] 37 | 38 | [[package]] 39 | name = "autocfg" 40 | version = "1.1.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 43 | 44 | [[package]] 45 | name = "bitflags" 46 | version = "1.3.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 49 | 50 | [[package]] 51 | name = "bitflags" 52 | version = "2.4.2" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 55 | 56 | [[package]] 57 | name = "bstr" 58 | version = "1.9.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" 61 | dependencies = [ 62 | "memchr", 63 | "regex-automata", 64 | "serde", 65 | ] 66 | 67 | [[package]] 68 | name = "bumpalo" 69 | version = "3.16.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 72 | 73 | [[package]] 74 | name = "cassowary" 75 | version = "0.3.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 78 | 79 | [[package]] 80 | name = "castaway" 81 | version = "0.2.2" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" 84 | dependencies = [ 85 | "rustversion", 86 | ] 87 | 88 | [[package]] 89 | name = "cc" 90 | version = "1.0.83" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 93 | dependencies = [ 94 | "libc", 95 | ] 96 | 97 | [[package]] 98 | name = "cfg-if" 99 | version = "1.0.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 102 | 103 | [[package]] 104 | name = "chrono" 105 | version = "0.4.38" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 108 | dependencies = [ 109 | "android-tzdata", 110 | "iana-time-zone", 111 | "num-traits", 112 | "windows-targets 0.52.5", 113 | ] 114 | 115 | [[package]] 116 | name = "compact_str" 117 | version = "0.7.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" 120 | dependencies = [ 121 | "castaway", 122 | "cfg-if", 123 | "itoa", 124 | "ryu", 125 | "static_assertions", 126 | ] 127 | 128 | [[package]] 129 | name = "core-foundation-sys" 130 | version = "0.8.6" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 133 | 134 | [[package]] 135 | name = "crossbeam" 136 | version = "0.8.4" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" 139 | dependencies = [ 140 | "crossbeam-channel", 141 | "crossbeam-deque", 142 | "crossbeam-epoch", 143 | "crossbeam-queue", 144 | "crossbeam-utils", 145 | ] 146 | 147 | [[package]] 148 | name = "crossbeam-channel" 149 | version = "0.5.12" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" 152 | dependencies = [ 153 | "crossbeam-utils", 154 | ] 155 | 156 | [[package]] 157 | name = "crossbeam-deque" 158 | version = "0.8.5" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 161 | dependencies = [ 162 | "crossbeam-epoch", 163 | "crossbeam-utils", 164 | ] 165 | 166 | [[package]] 167 | name = "crossbeam-epoch" 168 | version = "0.9.18" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 171 | dependencies = [ 172 | "crossbeam-utils", 173 | ] 174 | 175 | [[package]] 176 | name = "crossbeam-queue" 177 | version = "0.3.11" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" 180 | dependencies = [ 181 | "crossbeam-utils", 182 | ] 183 | 184 | [[package]] 185 | name = "crossbeam-utils" 186 | version = "0.8.19" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 189 | 190 | [[package]] 191 | name = "crossterm" 192 | version = "0.27.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 195 | dependencies = [ 196 | "bitflags 2.4.2", 197 | "crossterm_winapi", 198 | "futures-core", 199 | "libc", 200 | "mio", 201 | "parking_lot", 202 | "signal-hook", 203 | "signal-hook-mio", 204 | "winapi", 205 | ] 206 | 207 | [[package]] 208 | name = "crossterm_winapi" 209 | version = "0.9.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 212 | dependencies = [ 213 | "winapi", 214 | ] 215 | 216 | [[package]] 217 | name = "either" 218 | version = "1.9.0" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 221 | 222 | [[package]] 223 | name = "form_urlencoded" 224 | version = "1.2.1" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 227 | dependencies = [ 228 | "percent-encoding", 229 | ] 230 | 231 | [[package]] 232 | name = "futures" 233 | version = "0.3.30" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 236 | dependencies = [ 237 | "futures-channel", 238 | "futures-core", 239 | "futures-executor", 240 | "futures-io", 241 | "futures-sink", 242 | "futures-task", 243 | "futures-util", 244 | ] 245 | 246 | [[package]] 247 | name = "futures-channel" 248 | version = "0.3.30" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 251 | dependencies = [ 252 | "futures-core", 253 | "futures-sink", 254 | ] 255 | 256 | [[package]] 257 | name = "futures-core" 258 | version = "0.3.30" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 261 | 262 | [[package]] 263 | name = "futures-executor" 264 | version = "0.3.30" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 267 | dependencies = [ 268 | "futures-core", 269 | "futures-task", 270 | "futures-util", 271 | ] 272 | 273 | [[package]] 274 | name = "futures-io" 275 | version = "0.3.30" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 278 | 279 | [[package]] 280 | name = "futures-macro" 281 | version = "0.3.30" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 284 | dependencies = [ 285 | "proc-macro2", 286 | "quote", 287 | "syn 2.0.48", 288 | ] 289 | 290 | [[package]] 291 | name = "futures-sink" 292 | version = "0.3.30" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 295 | 296 | [[package]] 297 | name = "futures-task" 298 | version = "0.3.30" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 301 | 302 | [[package]] 303 | name = "futures-util" 304 | version = "0.3.30" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 307 | dependencies = [ 308 | "futures-channel", 309 | "futures-core", 310 | "futures-io", 311 | "futures-macro", 312 | "futures-sink", 313 | "futures-task", 314 | "memchr", 315 | "pin-project-lite", 316 | "pin-utils", 317 | "slab", 318 | ] 319 | 320 | [[package]] 321 | name = "hashbrown" 322 | version = "0.14.3" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 325 | dependencies = [ 326 | "ahash", 327 | "allocator-api2", 328 | ] 329 | 330 | [[package]] 331 | name = "heck" 332 | version = "0.4.1" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 335 | 336 | [[package]] 337 | name = "hermit-abi" 338 | version = "0.3.9" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 341 | 342 | [[package]] 343 | name = "iana-time-zone" 344 | version = "0.1.60" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 347 | dependencies = [ 348 | "android_system_properties", 349 | "core-foundation-sys", 350 | "iana-time-zone-haiku", 351 | "js-sys", 352 | "wasm-bindgen", 353 | "windows-core 0.52.0", 354 | ] 355 | 356 | [[package]] 357 | name = "iana-time-zone-haiku" 358 | version = "0.1.2" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 361 | dependencies = [ 362 | "cc", 363 | ] 364 | 365 | [[package]] 366 | name = "idna" 367 | version = "0.5.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 370 | dependencies = [ 371 | "unicode-bidi", 372 | "unicode-normalization", 373 | ] 374 | 375 | [[package]] 376 | name = "indoc" 377 | version = "2.0.4" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" 380 | 381 | [[package]] 382 | name = "is-docker" 383 | version = "0.2.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" 386 | dependencies = [ 387 | "once_cell", 388 | ] 389 | 390 | [[package]] 391 | name = "is-wsl" 392 | version = "0.4.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" 395 | dependencies = [ 396 | "is-docker", 397 | "once_cell", 398 | ] 399 | 400 | [[package]] 401 | name = "itertools" 402 | version = "0.12.1" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 405 | dependencies = [ 406 | "either", 407 | ] 408 | 409 | [[package]] 410 | name = "itoa" 411 | version = "1.0.10" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 414 | 415 | [[package]] 416 | name = "js-sys" 417 | version = "0.3.69" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 420 | dependencies = [ 421 | "wasm-bindgen", 422 | ] 423 | 424 | [[package]] 425 | name = "jwalk" 426 | version = "0.8.1" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" 429 | dependencies = [ 430 | "crossbeam", 431 | "rayon", 432 | ] 433 | 434 | [[package]] 435 | name = "libc" 436 | version = "0.2.153" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 439 | 440 | [[package]] 441 | name = "lock_api" 442 | version = "0.4.11" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 445 | dependencies = [ 446 | "autocfg", 447 | "scopeguard", 448 | ] 449 | 450 | [[package]] 451 | name = "log" 452 | version = "0.4.20" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 455 | 456 | [[package]] 457 | name = "lru" 458 | version = "0.12.2" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" 461 | dependencies = [ 462 | "hashbrown", 463 | ] 464 | 465 | [[package]] 466 | name = "malloc_buf" 467 | version = "0.0.6" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 470 | dependencies = [ 471 | "libc", 472 | ] 473 | 474 | [[package]] 475 | name = "memchr" 476 | version = "2.7.1" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 479 | 480 | [[package]] 481 | name = "mio" 482 | version = "0.8.10" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" 485 | dependencies = [ 486 | "libc", 487 | "log", 488 | "wasi", 489 | "windows-sys 0.48.0", 490 | ] 491 | 492 | [[package]] 493 | name = "normpath" 494 | version = "1.2.0" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "5831952a9476f2fed74b77d74182fa5ddc4d21c72ec45a333b250e3ed0272804" 497 | dependencies = [ 498 | "windows-sys 0.52.0", 499 | ] 500 | 501 | [[package]] 502 | name = "num-traits" 503 | version = "0.2.19" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 506 | dependencies = [ 507 | "autocfg", 508 | ] 509 | 510 | [[package]] 511 | name = "num_cpus" 512 | version = "1.16.0" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 515 | dependencies = [ 516 | "hermit-abi", 517 | "libc", 518 | ] 519 | 520 | [[package]] 521 | name = "objc" 522 | version = "0.2.7" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 525 | dependencies = [ 526 | "malloc_buf", 527 | ] 528 | 529 | [[package]] 530 | name = "once_cell" 531 | version = "1.19.0" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 534 | 535 | [[package]] 536 | name = "open" 537 | version = "5.1.3" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "2eb49fbd5616580e9974662cb96a3463da4476e649a7e4b258df0de065db0657" 540 | dependencies = [ 541 | "is-wsl", 542 | "libc", 543 | "pathdiff", 544 | ] 545 | 546 | [[package]] 547 | name = "opener" 548 | version = "0.7.0" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "f9901cb49d7fc923b256db329ee26ffed69130bf05d74b9efdd1875c92d6af01" 551 | dependencies = [ 552 | "bstr", 553 | "normpath", 554 | "windows-sys 0.52.0", 555 | ] 556 | 557 | [[package]] 558 | name = "parking_lot" 559 | version = "0.12.1" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 562 | dependencies = [ 563 | "lock_api", 564 | "parking_lot_core", 565 | ] 566 | 567 | [[package]] 568 | name = "parking_lot_core" 569 | version = "0.9.9" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 572 | dependencies = [ 573 | "cfg-if", 574 | "libc", 575 | "redox_syscall", 576 | "smallvec", 577 | "windows-targets 0.48.5", 578 | ] 579 | 580 | [[package]] 581 | name = "paste" 582 | version = "1.0.14" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 585 | 586 | [[package]] 587 | name = "pathdiff" 588 | version = "0.2.1" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" 591 | 592 | [[package]] 593 | name = "percent-encoding" 594 | version = "2.3.1" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 597 | 598 | [[package]] 599 | name = "pin-project-lite" 600 | version = "0.2.13" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 603 | 604 | [[package]] 605 | name = "pin-utils" 606 | version = "0.1.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 609 | 610 | [[package]] 611 | name = "proc-macro2" 612 | version = "1.0.78" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 615 | dependencies = [ 616 | "unicode-ident", 617 | ] 618 | 619 | [[package]] 620 | name = "quote" 621 | version = "1.0.35" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 624 | dependencies = [ 625 | "proc-macro2", 626 | ] 627 | 628 | [[package]] 629 | name = "ratatui" 630 | version = "0.26.0" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "154b85ef15a5d1719bcaa193c3c81fe645cd120c156874cd660fe49fd21d1373" 633 | dependencies = [ 634 | "bitflags 2.4.2", 635 | "cassowary", 636 | "compact_str", 637 | "crossterm", 638 | "indoc", 639 | "itertools", 640 | "lru", 641 | "paste", 642 | "stability", 643 | "strum", 644 | "unicode-segmentation", 645 | "unicode-width", 646 | ] 647 | 648 | [[package]] 649 | name = "rayon" 650 | version = "1.10.0" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 653 | dependencies = [ 654 | "either", 655 | "rayon-core", 656 | ] 657 | 658 | [[package]] 659 | name = "rayon-core" 660 | version = "1.12.1" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 663 | dependencies = [ 664 | "crossbeam-deque", 665 | "crossbeam-utils", 666 | ] 667 | 668 | [[package]] 669 | name = "redox_syscall" 670 | version = "0.4.1" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 673 | dependencies = [ 674 | "bitflags 1.3.2", 675 | ] 676 | 677 | [[package]] 678 | name = "regex-automata" 679 | version = "0.4.6" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 682 | 683 | [[package]] 684 | name = "rustversion" 685 | version = "1.0.14" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 688 | 689 | [[package]] 690 | name = "ryu" 691 | version = "1.0.16" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 694 | 695 | [[package]] 696 | name = "scopeguard" 697 | version = "1.2.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 700 | 701 | [[package]] 702 | name = "serde" 703 | version = "1.0.201" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" 706 | dependencies = [ 707 | "serde_derive", 708 | ] 709 | 710 | [[package]] 711 | name = "serde_derive" 712 | version = "1.0.201" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" 715 | dependencies = [ 716 | "proc-macro2", 717 | "quote", 718 | "syn 2.0.48", 719 | ] 720 | 721 | [[package]] 722 | name = "signal-hook" 723 | version = "0.3.17" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 726 | dependencies = [ 727 | "libc", 728 | "signal-hook-registry", 729 | ] 730 | 731 | [[package]] 732 | name = "signal-hook-mio" 733 | version = "0.2.3" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 736 | dependencies = [ 737 | "libc", 738 | "mio", 739 | "signal-hook", 740 | ] 741 | 742 | [[package]] 743 | name = "signal-hook-registry" 744 | version = "1.4.1" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 747 | dependencies = [ 748 | "libc", 749 | ] 750 | 751 | [[package]] 752 | name = "slab" 753 | version = "0.4.9" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 756 | dependencies = [ 757 | "autocfg", 758 | ] 759 | 760 | [[package]] 761 | name = "smallvec" 762 | version = "1.13.1" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 765 | 766 | [[package]] 767 | name = "stability" 768 | version = "0.1.1" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" 771 | dependencies = [ 772 | "quote", 773 | "syn 1.0.109", 774 | ] 775 | 776 | [[package]] 777 | name = "static_assertions" 778 | version = "1.1.0" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 781 | 782 | [[package]] 783 | name = "strum" 784 | version = "0.26.1" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" 787 | dependencies = [ 788 | "strum_macros", 789 | ] 790 | 791 | [[package]] 792 | name = "strum_macros" 793 | version = "0.26.1" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" 796 | dependencies = [ 797 | "heck", 798 | "proc-macro2", 799 | "quote", 800 | "rustversion", 801 | "syn 2.0.48", 802 | ] 803 | 804 | [[package]] 805 | name = "syn" 806 | version = "1.0.109" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 809 | dependencies = [ 810 | "proc-macro2", 811 | "quote", 812 | "unicode-ident", 813 | ] 814 | 815 | [[package]] 816 | name = "syn" 817 | version = "2.0.48" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 820 | dependencies = [ 821 | "proc-macro2", 822 | "quote", 823 | "unicode-ident", 824 | ] 825 | 826 | [[package]] 827 | name = "tinyvec" 828 | version = "1.6.0" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 831 | dependencies = [ 832 | "tinyvec_macros", 833 | ] 834 | 835 | [[package]] 836 | name = "tinyvec_macros" 837 | version = "0.1.1" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 840 | 841 | [[package]] 842 | name = "trash" 843 | version = "4.1.1" 844 | source = "registry+https://github.com/rust-lang/crates.io-index" 845 | checksum = "c254b119cf49bdde3dfef21b1dc492dc8026b75566ca24aa77993eccd7cbc1b5" 846 | dependencies = [ 847 | "chrono", 848 | "libc", 849 | "log", 850 | "objc", 851 | "once_cell", 852 | "scopeguard", 853 | "url", 854 | "windows", 855 | ] 856 | 857 | [[package]] 858 | name = "unicode-bidi" 859 | version = "0.3.15" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 862 | 863 | [[package]] 864 | name = "unicode-ident" 865 | version = "1.0.12" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 868 | 869 | [[package]] 870 | name = "unicode-normalization" 871 | version = "0.1.23" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 874 | dependencies = [ 875 | "tinyvec", 876 | ] 877 | 878 | [[package]] 879 | name = "unicode-segmentation" 880 | version = "1.10.1" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 883 | 884 | [[package]] 885 | name = "unicode-width" 886 | version = "0.1.11" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 889 | 890 | [[package]] 891 | name = "url" 892 | version = "2.5.0" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 895 | dependencies = [ 896 | "form_urlencoded", 897 | "idna", 898 | "percent-encoding", 899 | ] 900 | 901 | [[package]] 902 | name = "version_check" 903 | version = "0.9.4" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 906 | 907 | [[package]] 908 | name = "wasi" 909 | version = "0.11.0+wasi-snapshot-preview1" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 912 | 913 | [[package]] 914 | name = "wasm-bindgen" 915 | version = "0.2.92" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 918 | dependencies = [ 919 | "cfg-if", 920 | "wasm-bindgen-macro", 921 | ] 922 | 923 | [[package]] 924 | name = "wasm-bindgen-backend" 925 | version = "0.2.92" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 928 | dependencies = [ 929 | "bumpalo", 930 | "log", 931 | "once_cell", 932 | "proc-macro2", 933 | "quote", 934 | "syn 2.0.48", 935 | "wasm-bindgen-shared", 936 | ] 937 | 938 | [[package]] 939 | name = "wasm-bindgen-macro" 940 | version = "0.2.92" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 943 | dependencies = [ 944 | "quote", 945 | "wasm-bindgen-macro-support", 946 | ] 947 | 948 | [[package]] 949 | name = "wasm-bindgen-macro-support" 950 | version = "0.2.92" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 953 | dependencies = [ 954 | "proc-macro2", 955 | "quote", 956 | "syn 2.0.48", 957 | "wasm-bindgen-backend", 958 | "wasm-bindgen-shared", 959 | ] 960 | 961 | [[package]] 962 | name = "wasm-bindgen-shared" 963 | version = "0.2.92" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 966 | 967 | [[package]] 968 | name = "winapi" 969 | version = "0.3.9" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 972 | dependencies = [ 973 | "winapi-i686-pc-windows-gnu", 974 | "winapi-x86_64-pc-windows-gnu", 975 | ] 976 | 977 | [[package]] 978 | name = "winapi-i686-pc-windows-gnu" 979 | version = "0.4.0" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 982 | 983 | [[package]] 984 | name = "winapi-x86_64-pc-windows-gnu" 985 | version = "0.4.0" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 988 | 989 | [[package]] 990 | name = "windows" 991 | version = "0.56.0" 992 | source = "registry+https://github.com/rust-lang/crates.io-index" 993 | checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" 994 | dependencies = [ 995 | "windows-core 0.56.0", 996 | "windows-targets 0.52.5", 997 | ] 998 | 999 | [[package]] 1000 | name = "windows-core" 1001 | version = "0.52.0" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1004 | dependencies = [ 1005 | "windows-targets 0.52.5", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "windows-core" 1010 | version = "0.56.0" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" 1013 | dependencies = [ 1014 | "windows-implement", 1015 | "windows-interface", 1016 | "windows-result", 1017 | "windows-targets 0.52.5", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "windows-implement" 1022 | version = "0.56.0" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" 1025 | dependencies = [ 1026 | "proc-macro2", 1027 | "quote", 1028 | "syn 2.0.48", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "windows-interface" 1033 | version = "0.56.0" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" 1036 | dependencies = [ 1037 | "proc-macro2", 1038 | "quote", 1039 | "syn 2.0.48", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "windows-result" 1044 | version = "0.1.1" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" 1047 | dependencies = [ 1048 | "windows-targets 0.52.5", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "windows-sys" 1053 | version = "0.48.0" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1056 | dependencies = [ 1057 | "windows-targets 0.48.5", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "windows-sys" 1062 | version = "0.52.0" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1065 | dependencies = [ 1066 | "windows-targets 0.52.5", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "windows-targets" 1071 | version = "0.48.5" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1074 | dependencies = [ 1075 | "windows_aarch64_gnullvm 0.48.5", 1076 | "windows_aarch64_msvc 0.48.5", 1077 | "windows_i686_gnu 0.48.5", 1078 | "windows_i686_msvc 0.48.5", 1079 | "windows_x86_64_gnu 0.48.5", 1080 | "windows_x86_64_gnullvm 0.48.5", 1081 | "windows_x86_64_msvc 0.48.5", 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "windows-targets" 1086 | version = "0.52.5" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1089 | dependencies = [ 1090 | "windows_aarch64_gnullvm 0.52.5", 1091 | "windows_aarch64_msvc 0.52.5", 1092 | "windows_i686_gnu 0.52.5", 1093 | "windows_i686_gnullvm", 1094 | "windows_i686_msvc 0.52.5", 1095 | "windows_x86_64_gnu 0.52.5", 1096 | "windows_x86_64_gnullvm 0.52.5", 1097 | "windows_x86_64_msvc 0.52.5", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "windows_aarch64_gnullvm" 1102 | version = "0.48.5" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1105 | 1106 | [[package]] 1107 | name = "windows_aarch64_gnullvm" 1108 | version = "0.52.5" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1111 | 1112 | [[package]] 1113 | name = "windows_aarch64_msvc" 1114 | version = "0.48.5" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1117 | 1118 | [[package]] 1119 | name = "windows_aarch64_msvc" 1120 | version = "0.52.5" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1123 | 1124 | [[package]] 1125 | name = "windows_i686_gnu" 1126 | version = "0.48.5" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1129 | 1130 | [[package]] 1131 | name = "windows_i686_gnu" 1132 | version = "0.52.5" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1135 | 1136 | [[package]] 1137 | name = "windows_i686_gnullvm" 1138 | version = "0.52.5" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1141 | 1142 | [[package]] 1143 | name = "windows_i686_msvc" 1144 | version = "0.48.5" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1147 | 1148 | [[package]] 1149 | name = "windows_i686_msvc" 1150 | version = "0.52.5" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1153 | 1154 | [[package]] 1155 | name = "windows_x86_64_gnu" 1156 | version = "0.48.5" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1159 | 1160 | [[package]] 1161 | name = "windows_x86_64_gnu" 1162 | version = "0.52.5" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1165 | 1166 | [[package]] 1167 | name = "windows_x86_64_gnullvm" 1168 | version = "0.48.5" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1171 | 1172 | [[package]] 1173 | name = "windows_x86_64_gnullvm" 1174 | version = "0.52.5" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1177 | 1178 | [[package]] 1179 | name = "windows_x86_64_msvc" 1180 | version = "0.48.5" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1183 | 1184 | [[package]] 1185 | name = "windows_x86_64_msvc" 1186 | version = "0.52.5" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1189 | 1190 | [[package]] 1191 | name = "wiper" 1192 | version = "0.2.1" 1193 | dependencies = [ 1194 | "crossbeam", 1195 | "crossterm", 1196 | "futures", 1197 | "jwalk", 1198 | "num_cpus", 1199 | "open", 1200 | "opener", 1201 | "ratatui", 1202 | "trash", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "zerocopy" 1207 | version = "0.7.32" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 1210 | dependencies = [ 1211 | "zerocopy-derive", 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "zerocopy-derive" 1216 | version = "0.7.32" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 1219 | dependencies = [ 1220 | "proc-macro2", 1221 | "quote", 1222 | "syn 2.0.48", 1223 | ] 1224 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wiper" 3 | version = "0.2.1" 4 | authors = ["Alexandr Kobrin "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | crossbeam = "0.8.4" 10 | crossterm = { version = "0.27.0", features = ["event-stream"] } 11 | futures = "0.3.30" 12 | jwalk = "0.8.1" 13 | num_cpus = "1.16.0" 14 | open = "5.1.3" 15 | opener = { version = "0.7.0", default-features = false } 16 | ratatui = "0.26.0" 17 | trash = "4.1.1" 18 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alexandr Kobrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wiper - Disk cleanup tool 2 | 3 | Wiper is a handy command-line tool made with Rust. It's perfect for anyone looking to quickly spot which folders are eating up all the disk space. Super easy to use, it gives you a clear visual breakdown of directory sizes, so you can clean things up without a hassle. 4 | 5 | https://github.com/ikebastuz/wiper/assets/24222413/acf9384d-7f04-4f37-ac47-99b349e6ee29 6 | 7 | ## Features 8 | - Fast and Efficient: Quickly scans directories and subdirectories to provide size metrics. 9 | - Cross-Platform: Works on Linux, Windows, and macOS. 10 | - User-Friendly Output: Displays results in an easily understandable format. 11 | 12 | ## Usage 13 | #### Run in current dir 14 | `wiper` 15 | #### Run in specific dir 16 | `wiper [PATH]` 17 | 18 | ## Keybindings 19 | - `jk/↓↑` - Navigate up/down 20 | - `l/→/Enter` - Navigate into folder 21 | - `h/←/Backspace` - Navigate to parent 22 | - `d` - Delete file/folder. First hit - selects entry. Second hit - confirms deletion. 23 | - `s` - Toggle sorting (`Title` / `Size`) 24 | - `c` - Toggle coloring. When enabled - shows space usage with gradient 25 | - `t` - Toggle trash. When enabled - removed content goes to Trash bin. 26 | - `q` - Quit 27 | 28 | 29 | ## Installation 30 | 31 | ### MacOS 32 | #### Homebrew 33 | ``` 34 | brew tap ikebastuz/wiper 35 | brew install wiper 36 | ``` 37 | 38 | ### Linux 39 | #### AUR 40 | ``` 41 | paru -S wiper 42 | ``` 43 | 44 | ## Build from source 45 | ```bash 46 | git clone https://github.com/ikebastuz/wiper.git 47 | cd wiper 48 | cargo build --release 49 | ``` 50 | 51 | ## Contributing 52 | If you have any suggestions, improvements, or bug fixes, feel free to open an issue or submit a pull request. 53 | 54 | ## Why not [dua-cli](https://github.com/Byron/dua-cli)? 55 | I started this project as part of my journey to learn Rust. I always missed having such a tool but had never heard of dua-cli. From my understanding, there are some differences: 56 | 57 | #### Pros: 58 | - Wiper allows navigating to the parent directory at any time 59 | - Supports opening files with the default system app 60 | - Simpler deletion flow 61 | 62 | #### Cons: 63 | - ~~It is 10-15 times slower because of manually implemented file traversal~~. Not anymore, rewritten with `jwalk`. 64 | - Does not have filtering functionality 65 | - Not capable of marking multiple entries for deletion 66 | 67 | #### Subjective: 68 | - The UI is more "elegant" :) 69 | - Shows a "full" space-taken bar for the largest entry. So there will always be at least one entry with a full bar. If dua-cli has two large entries of similar size, they will be shown as approximately 50% bars. 70 | -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | /output 2 | -------------------------------------------------------------------------------- /build/add_toolchain.sh: -------------------------------------------------------------------------------- 1 | rustup target add x86_64-apple-darwin # for macOS Intel 2 | rustup target add aarch64-apple-darwin # for macOS ARM 3 | rustup target add x86_64-pc-windows-gnu # for Windows 4 | rustup target add x86_64-unknown-linux-gnu # for Linux 5 | # Alternatively for linux - https://github.com/messense/homebrew-macos-cross-toolchains 6 | 7 | 8 | -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build for each target 4 | targets=("x86_64-apple-darwin" "aarch64-apple-darwin" "x86_64-unknown-linux-gnu") 5 | 6 | for target in "${targets[@]}"; do 7 | cargo build --release --target $target 8 | done 9 | 10 | # Prepare output directory 11 | rm -rf ./build/output 12 | mkdir -p ./build/output 13 | 14 | # Package and generate shasums 15 | for target in "${targets[@]}"; do 16 | tar -czvf ./build/output/wiper-$target.tar.gz -C ./target/$target/release wiper 17 | shasum -a 256 ./build/output/wiper-$target.tar.gz > ./build/output/wiper-$target.sha256 18 | done 19 | 20 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | 3 | #### Features 4 | - [x] - Sort by size 5 | - [x] - Async subfolder calculation 6 | - [x] - Delete to Trash bin 7 | - [ ] - PageUp/PageDown / g/G navigation 8 | - [x] - Open file with system app 9 | - [x] - Debug slow parent navigation 10 | - [ ] - Check folder permissions 11 | - [x] - Show loading folder indicator if it is not calculated completely 12 | - [x] - File extension chart 13 | 14 | 15 | #### Non-functional 16 | - [x] - Lint with clippy 17 | - [x] - Colored first letters (keybindings) 18 | - [ ] - Better list scrolling (maybe auto-center cursor) 19 | - [ ] - Refactor unit tests for easier state awaiting 20 | 21 | #### Performance 22 | - [ ] - Indexing / caching / refreshing 23 | - [x] - Optimize folder sorting 24 | - [ ] - Prevent from locking main thread, always process inputs 25 | - [ ] - Review all variable clones, optimize 26 | 27 | ## Scripts 28 | #### Run 29 | ```bash 30 | cargo run 31 | ``` 32 | ```bash 33 | cargo run -- [PATH] 34 | ``` 35 | #### Lint 36 | ```bash 37 | cargo clippy --all-targets -- -D warnings 38 | ``` 39 | #### Test 40 | ```bash 41 | cargo test 42 | ``` 43 | #### Build 44 | ```bash 45 | RUSTFLAGS="-Z threads=8" cargo +nightly build --release 46 | ``` 47 | 48 | ## Install from homebrew 49 | ```bash 50 | brew tap ikebastuz/wiper 51 | brew install wiper 52 | ``` 53 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use opener; 2 | use std::error; 3 | 4 | use crate::fps_counter::FPSCounter; 5 | use crate::fs::{delete_file, delete_folder, DataStore, DataStoreKey, FolderEntryType, SortBy}; 6 | use crate::spinner::Spinner; 7 | use crate::task_manager::TaskManager; 8 | use std::path::{Path, PathBuf}; 9 | 10 | use crate::config::{InitConfig, UIConfig}; 11 | use std::env; 12 | 13 | use crate::logger::Logger; 14 | 15 | /// Application result type. 16 | pub type AppResult = std::result::Result>; 17 | 18 | enum DiffKind { 19 | Subtract, 20 | } 21 | 22 | /// Application. 23 | #[derive(Debug)] 24 | pub struct App> { 25 | /// Config to render UI 26 | pub ui_config: UIConfig, 27 | /// Is the application running? 28 | pub running: bool, 29 | /// Task manager for async jobs 30 | pub task_manager: TaskManager, 31 | /// Store for filesystem data 32 | pub store: S, 33 | /// Debug logger 34 | pub logger: Logger, 35 | /// FPS Counter 36 | pub fps_counter: FPSCounter, 37 | /// Spinner 38 | pub spinner: Spinner, 39 | } 40 | 41 | impl> App { 42 | /// Constructs a new instance of [`App`]. 43 | pub fn new(config: InitConfig) -> Self { 44 | let current_path = match config.file_path { 45 | Some(path) => { 46 | let path_buf = PathBuf::from(&path); 47 | if path_buf.is_absolute() { 48 | path_buf 49 | } else { 50 | let current_dir = env::current_dir().unwrap(); 51 | current_dir.join(&path_buf) 52 | } 53 | } 54 | None => env::current_dir().unwrap(), 55 | }; 56 | 57 | let mut app = App { 58 | running: true, 59 | ui_config: UIConfig { 60 | colored: false, 61 | confirming_deletion: false, 62 | sort_by: SortBy::Title, 63 | move_to_trash: true, 64 | open_file: true, 65 | debug_enabled: false, 66 | }, 67 | task_manager: TaskManager::::default(), 68 | store: S::new(), 69 | logger: Logger::default(), 70 | fps_counter: FPSCounter::default(), 71 | spinner: Spinner::default(), 72 | }; 73 | 74 | app.store.set_current_path(¤t_path); 75 | 76 | app 77 | } 78 | 79 | pub fn init(&mut self) { 80 | let path_buf = self.store.get_current_path().clone(); 81 | self.logger.log(path_buf.to_string_lossy().to_string()); 82 | 83 | self.task_manager.start(vec![path_buf], &mut self.logger); 84 | } 85 | 86 | pub fn reset(&mut self) { 87 | let current_path = self.store.get_current_path().clone(); 88 | self.store = S::new(); 89 | self.store.set_current_path(¤t_path); 90 | 91 | self.init(); 92 | } 93 | 94 | /// Handles the tick event of the terminal. 95 | pub fn tick(&mut self) { 96 | self.task_manager 97 | .process_results(&mut self.store, &mut self.logger); 98 | } 99 | 100 | /// Set running to false to quit the application. 101 | pub fn quit(&mut self) { 102 | self.running = false; 103 | } 104 | 105 | pub fn on_escape(&mut self) { 106 | self.ui_config.confirming_deletion = false; 107 | } 108 | 109 | pub fn on_toggle_coloring(&mut self) { 110 | self.ui_config.colored = !self.ui_config.colored; 111 | } 112 | 113 | pub fn on_toggle_sorting(&mut self) { 114 | match self.ui_config.sort_by { 115 | SortBy::Title => { 116 | self.ui_config.sort_by = SortBy::Size; 117 | } 118 | SortBy::Size => { 119 | self.ui_config.sort_by = SortBy::Title; 120 | } 121 | } 122 | } 123 | 124 | fn sort_current_folder(&mut self) { 125 | self.store 126 | .sort_current_folder(self.ui_config.sort_by.clone()); 127 | } 128 | 129 | pub fn on_toggle_move_to_trash(&mut self) { 130 | self.ui_config.move_to_trash = !self.ui_config.move_to_trash; 131 | } 132 | 133 | pub fn on_cursor_up(&mut self) { 134 | if let Some(folder) = self.store.get_current_folder_mut() { 135 | if folder.cursor_index > 0 { 136 | folder.cursor_index -= 1; 137 | } 138 | } 139 | self.ui_config.confirming_deletion = false; 140 | } 141 | 142 | pub fn on_cursor_down(&mut self) { 143 | if let Some(folder) = self.store.get_current_folder_mut() { 144 | if folder.cursor_index < folder.entries.len() - 1 { 145 | folder.cursor_index += 1; 146 | } 147 | } 148 | self.ui_config.confirming_deletion = false; 149 | } 150 | 151 | pub fn on_open_file_explorer(&mut self) { 152 | match open::that(self.store.get_current_path().to_string_lossy().to_string()) { 153 | Ok(_) => {} 154 | Err(_) => self.logger.log("Failed to open path".into()), 155 | } 156 | } 157 | 158 | fn navigate_to_parent(&mut self) { 159 | self.store.move_to_parent(); 160 | 161 | let updated_path = self.store.get_current_path().to_path_buf(); 162 | self.logger.log(updated_path.to_string_lossy().to_string()); 163 | 164 | let to_process = self 165 | .task_manager 166 | .process_path_sync(&mut self.store, &updated_path); 167 | 168 | self.sort_current_folder(); 169 | 170 | self.task_manager.start(to_process, &mut self.logger); 171 | } 172 | 173 | fn navigate_to_child(&mut self, title: &str) { 174 | self.store.move_to_child(title); 175 | 176 | self.logger 177 | .log(self.store.get_current_path().to_string_lossy().to_string()); 178 | 179 | match self.store.get_current_folder() { 180 | Some(_) => {} 181 | None => { 182 | self.task_manager.start( 183 | vec![self.store.get_current_path().clone()], 184 | &mut self.logger, 185 | ); 186 | } 187 | } 188 | } 189 | 190 | pub fn on_backspace(&mut self) { 191 | self.navigate_to_parent(); 192 | } 193 | 194 | pub fn on_enter(&mut self) { 195 | if let Some(folder) = self.store.get_current_folder().cloned() { 196 | let entry = folder.get_selected_entry(); 197 | 198 | match entry.kind { 199 | FolderEntryType::Parent => { 200 | self.navigate_to_parent(); 201 | } 202 | FolderEntryType::Folder => { 203 | self.navigate_to_child(&entry.title); 204 | } 205 | FolderEntryType::File => { 206 | if self.ui_config.open_file { 207 | let mut file_name = self.store.get_current_path().clone(); 208 | file_name.push(entry.title.clone()); 209 | let _ = opener::open(file_name); 210 | } 211 | } 212 | } 213 | } 214 | self.ui_config.confirming_deletion = false; 215 | } 216 | 217 | pub fn on_delete(&mut self) { 218 | if let Some(mut folder) = self.store.get_current_folder().cloned() { 219 | let entry = folder.get_selected_entry(); 220 | 221 | let mut to_delete_path = PathBuf::from(&self.store.get_current_path()); 222 | to_delete_path.push(&entry.title); 223 | 224 | match entry.kind { 225 | FolderEntryType::Parent => {} 226 | FolderEntryType::Folder => { 227 | if !self.ui_config.confirming_deletion { 228 | self.ui_config.confirming_deletion = true; 229 | } else if delete_folder(&to_delete_path, &self.ui_config).is_ok() { 230 | if let Some(subfolder_size) = entry.size { 231 | self.propagate_size_update_upwards( 232 | &to_delete_path, 233 | subfolder_size, 234 | DiffKind::Subtract, 235 | ); 236 | } 237 | folder.remove_selected(); 238 | self.store.remove_path(&to_delete_path); 239 | self.store.set_current_folder(folder.clone()); 240 | self.ui_config.confirming_deletion = false; 241 | } 242 | } 243 | FolderEntryType::File => { 244 | if !self.ui_config.confirming_deletion { 245 | self.ui_config.confirming_deletion = true; 246 | } else if delete_file(&to_delete_path, &self.ui_config).is_ok() { 247 | if let Some(subfile_size) = entry.size { 248 | let parent_folder = PathBuf::from(to_delete_path.parent().unwrap()); 249 | self.propagate_size_update_upwards( 250 | &parent_folder, 251 | subfile_size, 252 | DiffKind::Subtract, 253 | ); 254 | } 255 | folder.remove_selected(); 256 | self.store.set_current_folder(folder.clone()); 257 | self.ui_config.confirming_deletion = false; 258 | } 259 | } 260 | } 261 | } 262 | } 263 | 264 | /// Currently updates size after deletion 265 | fn propagate_size_update_upwards( 266 | &mut self, 267 | to_delete_path: &Path, 268 | entry_diff: u64, 269 | diff_kind: DiffKind, 270 | ) { 271 | let mut parent_path = to_delete_path.to_path_buf(); 272 | while let Some(parent) = parent_path.parent() { 273 | if let Some(parent_folder) = self.store.get_folder_mut(&parent_path) { 274 | if let Some(parent_folder_entry) = 275 | parent_folder.entries.get_mut(parent_folder.cursor_index) 276 | { 277 | if let Some(size) = parent_folder_entry.size.as_mut() { 278 | match diff_kind { 279 | DiffKind::Subtract => { 280 | *size = size.saturating_sub(entry_diff); 281 | } 282 | } 283 | } 284 | } 285 | parent_folder.sorted_by = None; 286 | parent_path = parent.to_path_buf(); 287 | } else { 288 | break; 289 | } 290 | } 291 | } 292 | 293 | pub fn toggle_debug(&mut self) { 294 | self.ui_config.debug_enabled = !self.ui_config.debug_enabled; 295 | } 296 | 297 | pub fn pre_render(&mut self) { 298 | self.sort_current_folder(); 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::fs::SortBy; 2 | 3 | pub struct InitConfig { 4 | pub file_path: Option, 5 | } 6 | 7 | impl InitConfig { 8 | pub fn build(mut args: impl Iterator) -> Result { 9 | args.next(); 10 | 11 | Ok(InitConfig { 12 | file_path: args.next(), 13 | }) 14 | } 15 | } 16 | 17 | #[derive(Debug)] 18 | pub struct UIConfig { 19 | pub colored: bool, 20 | pub confirming_deletion: bool, 21 | pub sort_by: SortBy, 22 | pub move_to_trash: bool, 23 | pub open_file: bool, 24 | pub debug_enabled: bool, 25 | } 26 | 27 | pub const EVENT_INTERVAL: u64 = 100; 28 | -------------------------------------------------------------------------------- /src/events/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{App, AppResult}; 2 | use crate::fs::{DataStore, DataStoreKey}; 3 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 4 | 5 | pub fn handle_key_events>( 6 | key_event: KeyEvent, 7 | app: &mut App, 8 | ) -> AppResult<()> { 9 | match key_event.code { 10 | KeyCode::Esc => { 11 | app.on_escape(); 12 | } 13 | KeyCode::Char('q') => { 14 | app.quit(); 15 | } 16 | KeyCode::Up | KeyCode::Char('k') => { 17 | app.on_cursor_up(); 18 | } 19 | KeyCode::Down | KeyCode::Char('j') => { 20 | app.on_cursor_down(); 21 | } 22 | KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => { 23 | app.on_enter(); 24 | } 25 | KeyCode::Backspace | KeyCode::Left | KeyCode::Char('h') => { 26 | app.on_backspace(); 27 | } 28 | // Exit application on `Ctrl-C` 29 | KeyCode::Char('c') | KeyCode::Char('C') => { 30 | if key_event.modifiers == KeyModifiers::CONTROL { 31 | app.quit(); 32 | } else { 33 | app.on_toggle_coloring(); 34 | } 35 | } 36 | KeyCode::Char('s') => { 37 | app.on_toggle_sorting(); 38 | } 39 | KeyCode::Char('e') => { 40 | app.on_open_file_explorer(); 41 | } 42 | KeyCode::Char('r') => { 43 | app.reset(); 44 | } 45 | KeyCode::Char('d') => { 46 | if key_event.modifiers == KeyModifiers::CONTROL { 47 | app.toggle_debug(); 48 | } else { 49 | app.on_delete(); 50 | } 51 | } 52 | KeyCode::Char('t') => { 53 | app.on_toggle_move_to_trash(); 54 | } 55 | _ => {} 56 | } 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /src/events/mod.rs: -------------------------------------------------------------------------------- 1 | mod handler; 2 | 3 | pub use handler::handle_key_events; 4 | 5 | use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent}; 6 | use std::sync::mpsc::{self, Receiver}; 7 | use std::thread; 8 | use std::time::{Duration, Instant}; 9 | 10 | /// Terminal events. 11 | #[derive(Clone, Copy, Debug)] 12 | pub enum Event { 13 | /// Terminal tick. 14 | Tick, 15 | /// Key press. 16 | Key(KeyEvent), 17 | /// Mouse click/scroll. 18 | Mouse(MouseEvent), 19 | /// Terminal resize. 20 | Resize(u16, u16), 21 | } 22 | 23 | #[derive(Debug)] 24 | pub struct EventHandler { 25 | receiver: Receiver, 26 | } 27 | 28 | impl EventHandler { 29 | pub fn new(tick_rate: u64) -> Self { 30 | let tick_rate = Duration::from_millis(tick_rate); 31 | let (sender, receiver) = mpsc::channel(); 32 | let sender_clone = sender.clone(); 33 | 34 | thread::spawn(move || { 35 | let mut last_tick = Instant::now(); 36 | loop { 37 | if last_tick.elapsed() >= tick_rate { 38 | if sender_clone.send(Event::Tick).is_err() { 39 | break; // Exit if the receiver has dropped 40 | } 41 | last_tick = Instant::now(); 42 | } 43 | if crossterm::event::poll(Duration::from_millis(1)).unwrap() { 44 | if let Ok(crossterm_event) = crossterm::event::read() { 45 | match crossterm_event { 46 | CrosstermEvent::Key(key) => { 47 | let _ = sender_clone.send(Event::Key(key)); 48 | } 49 | CrosstermEvent::Mouse(mouse) => { 50 | let _ = sender_clone.send(Event::Mouse(mouse)); 51 | } 52 | CrosstermEvent::Resize(x, y) => { 53 | let _ = sender_clone.send(Event::Resize(x, y)); 54 | } 55 | _ => {} 56 | } 57 | } 58 | } 59 | thread::sleep(Duration::from_millis(10)); 60 | } 61 | }); 62 | 63 | Self { receiver } 64 | } 65 | 66 | pub fn next(&self) -> Result { 67 | self.receiver.recv() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/fps_counter/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use crate::config::EVENT_INTERVAL; 4 | 5 | #[derive(Debug)] 6 | pub struct FPSCounter { 7 | pub frame_count: u64, 8 | pub last_frame_time: Instant, 9 | pub skipped_frames: f64, 10 | } 11 | 12 | impl Default for FPSCounter { 13 | fn default() -> Self { 14 | Self::new() 15 | } 16 | } 17 | 18 | impl FPSCounter { 19 | fn new() -> FPSCounter { 20 | FPSCounter { 21 | frame_count: 0, 22 | last_frame_time: Instant::now(), 23 | skipped_frames: 0.0, 24 | } 25 | } 26 | 27 | pub fn update(&mut self) -> f64 { 28 | self.frame_count += 1; 29 | let now = Instant::now(); 30 | let elapsed = now.duration_since(self.last_frame_time); 31 | let seconds = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 / 1_000_000_000.0; 32 | let fps = self.frame_count as f64 / seconds; 33 | let target_fps = (1000 / EVENT_INTERVAL) as f64; 34 | let diff = target_fps - fps; 35 | if diff >= 1.0 { 36 | self.skipped_frames += diff; 37 | } 38 | self.last_frame_time = now; 39 | self.frame_count = 0; 40 | fps 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/fs/folder.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::constants::TEXT_PARENT_DIR; 2 | 3 | use crate::fs::folder_entry::{FolderEntry, FolderEntryType}; 4 | use std::cmp::Ordering; 5 | use std::collections::HashMap; 6 | 7 | use super::SortBy; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Folder { 11 | pub title: String, 12 | pub cursor_index: usize, 13 | pub sorted_by: Option, 14 | pub entries: Vec, 15 | pub has_error: bool, 16 | pub file_type_map: HashMap, 17 | } 18 | 19 | impl Folder { 20 | pub fn new(title: String) -> Self { 21 | Folder { 22 | title, 23 | cursor_index: 0, 24 | sorted_by: None, 25 | entries: vec![FolderEntry { 26 | kind: FolderEntryType::Parent, 27 | title: String::from(TEXT_PARENT_DIR), 28 | size: None, 29 | is_loaded: true, 30 | }], 31 | has_error: false, 32 | file_type_map: HashMap::new(), 33 | } 34 | } 35 | 36 | pub fn get_size(&self) -> u64 { 37 | self.entries 38 | .iter() 39 | .fold(0, |acc, entry| acc + entry.size.unwrap_or(0)) 40 | } 41 | 42 | pub fn get_selected_entry_size(&self) -> u64 { 43 | self.get_selected_entry().size.unwrap_or(0) 44 | } 45 | 46 | pub fn remove_selected(&mut self) { 47 | self.entries.remove(self.cursor_index); 48 | self.cursor_index = self.cursor_index.min(self.entries.len() - 1); 49 | } 50 | 51 | pub fn get_selected_entry(&self) -> &FolderEntry { 52 | if let Some(entry) = self.entries.get(self.cursor_index) { 53 | entry 54 | } else { 55 | panic!("Cursor index out of bounds: {}", self.cursor_index); 56 | } 57 | } 58 | 59 | pub fn to_list(&self) -> Vec { 60 | vec![&self.entries] 61 | .into_iter() 62 | .flat_map(|v| v.iter().cloned()) 63 | .collect() 64 | } 65 | 66 | pub fn get_max_entry_size(&self) -> u64 { 67 | let mut max_entry_size = 0; 68 | 69 | for file in &self.entries { 70 | if let Some(size) = file.size { 71 | if size > max_entry_size { 72 | max_entry_size = size 73 | } 74 | } 75 | } 76 | 77 | max_entry_size 78 | } 79 | 80 | pub fn sort_by_title(&mut self) { 81 | self.entries.sort(); 82 | } 83 | 84 | pub fn sort_by_size(&mut self) { 85 | self.entries.sort_by(|a, b| { 86 | if a.kind == FolderEntryType::Parent || b.kind == FolderEntryType::Parent { 87 | // If either entry is a Parent, it should come before 88 | if a.kind == FolderEntryType::Parent && b.kind != FolderEntryType::Parent { 89 | Ordering::Less 90 | } else if a.kind != FolderEntryType::Parent && b.kind == FolderEntryType::Parent { 91 | Ordering::Greater 92 | } else { 93 | Ordering::Equal 94 | } 95 | } else if let (Some(size_a), Some(size_b)) = (a.size, b.size) { 96 | // Sort by size in descending order 97 | size_b.cmp(&size_a) 98 | } else if a.size.is_some() { 99 | // Entries with size come before those without 100 | Ordering::Greater 101 | } else if b.size.is_some() { 102 | // Entries without size come after those with 103 | Ordering::Less 104 | } else { 105 | // If both entries have no size, maintain their order 106 | Ordering::Equal 107 | } 108 | }); 109 | } 110 | 111 | pub fn append_file_type_size(&mut self, file_type: &String, size: u64) { 112 | let total_size = self.file_type_map.entry(file_type.to_owned()).or_insert(0); 113 | *total_size += size; 114 | } 115 | 116 | fn get_sorted_file_types_by_size(&self) -> Vec<(String, u64)> { 117 | let mut file_types: Vec<(String, u64)> = self 118 | .file_type_map 119 | .iter() 120 | .map(|(k, &v)| (k.clone(), v)) 121 | .collect(); 122 | file_types.sort_by(|a, b| b.1.cmp(&a.1)); 123 | file_types 124 | } 125 | 126 | pub fn get_chart_data(&self, threshold: f64, max_items: usize) -> Vec<(String, u64)> { 127 | let sorted_file_types = self.get_sorted_file_types_by_size(); 128 | let total_size: u64 = sorted_file_types.iter().map(|(_, size)| *size).sum(); 129 | let mut accumulated_size: u64 = 0; 130 | let mut chart_data: Vec<(String, u64)> = Vec::new(); 131 | let mut rest_size: u64 = 0; 132 | 133 | for (file_type, size) in sorted_file_types.into_iter() { 134 | if accumulated_size as f64 / total_size as f64 <= threshold 135 | && chart_data.len() < max_items - 1 136 | { 137 | chart_data.push((file_type, size)); 138 | accumulated_size += size; 139 | } else { 140 | rest_size += size; 141 | } 142 | } 143 | 144 | if rest_size > 0 { 145 | chart_data.push(("rest".to_string(), rest_size)); 146 | } 147 | 148 | chart_data 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/fs/folder_entry.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | #[derive(Debug, Clone, PartialEq, Eq)] 4 | pub enum FolderEntryType { 5 | Parent, 6 | File, 7 | Folder, 8 | } 9 | 10 | impl Ord for FolderEntryType { 11 | fn cmp(&self, other: &Self) -> Ordering { 12 | match (self, other) { 13 | (FolderEntryType::Parent, _) => Ordering::Less, 14 | (FolderEntryType::Folder, FolderEntryType::Parent) => Ordering::Greater, 15 | (FolderEntryType::Folder, FolderEntryType::Folder) => Ordering::Equal, 16 | (FolderEntryType::Folder, _) => Ordering::Less, 17 | (FolderEntryType::File, FolderEntryType::Parent) => Ordering::Greater, 18 | (FolderEntryType::File, FolderEntryType::Folder) => Ordering::Greater, 19 | (FolderEntryType::File, FolderEntryType::File) => Ordering::Equal, 20 | } 21 | } 22 | } 23 | 24 | impl PartialOrd for FolderEntryType { 25 | fn partial_cmp(&self, other: &Self) -> Option { 26 | Some(self.cmp(other)) 27 | } 28 | } 29 | 30 | #[derive(Debug, Clone, Eq, PartialEq)] 31 | pub struct FolderEntry { 32 | pub title: String, 33 | pub size: Option, 34 | pub kind: FolderEntryType, 35 | pub is_loaded: bool, 36 | } 37 | 38 | impl Ord for FolderEntry { 39 | fn cmp(&self, other: &Self) -> Ordering { 40 | let kind_ordering = self.kind.cmp(&other.kind); 41 | 42 | if kind_ordering != Ordering::Equal { 43 | return kind_ordering; 44 | } 45 | 46 | self.title.cmp(&other.title) 47 | } 48 | } 49 | 50 | impl PartialOrd for FolderEntry { 51 | fn partial_cmp(&self, other: &Self) -> Option { 52 | Some(self.cmp(other)) 53 | } 54 | } 55 | 56 | impl FolderEntry { 57 | pub fn increment_size(&mut self, addition: u64) { 58 | match self.size { 59 | Some(ref mut s) => { 60 | *s += addition; 61 | } 62 | None => self.size = Some(addition), 63 | } 64 | } 65 | 66 | pub fn sort_by_size(entries: &mut [FolderEntry]) { 67 | entries.sort_by(|a, b| { 68 | if let (Some(size_a), Some(size_b)) = (a.size, b.size) { 69 | size_a.cmp(&size_b) 70 | } else if a.size.is_some() { 71 | Ordering::Less 72 | } else if b.size.is_some() { 73 | Ordering::Greater 74 | } else { 75 | Ordering::Equal 76 | } 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/fs/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::config::UIConfig; 2 | use crate::ui::constants::TEXT_UNKNOWN; 3 | use std::fs::{read_dir, remove_dir_all, remove_file}; 4 | use std::path::PathBuf; 5 | use trash; 6 | 7 | mod folder; 8 | mod folder_entry; 9 | mod store; 10 | pub use folder::Folder; 11 | pub use folder_entry::{FolderEntry, FolderEntryType}; 12 | pub use store::{DSHashmap, DataStore, DataStoreKey, DataStoreType}; 13 | 14 | #[derive(Debug, Clone, PartialEq)] 15 | pub enum SortBy { 16 | Title, 17 | Size, 18 | } 19 | /// Returns new unsorted folder 20 | pub fn path_to_folder(path: PathBuf) -> Folder { 21 | let folder_name = path 22 | .file_name() 23 | .and_then(|name| name.to_str()) 24 | .unwrap_or(TEXT_UNKNOWN); 25 | let mut folder = Folder::new(folder_name.to_string()); 26 | 27 | match read_dir(path.clone()) { 28 | Ok(path) => { 29 | for entry in path.into_iter().flatten() { 30 | let file_name = entry.file_name(); 31 | if let Some(file_name) = file_name.to_str() { 32 | let mut folder_entry = FolderEntry { 33 | kind: FolderEntryType::File, 34 | title: file_name.to_owned(), 35 | size: None, 36 | is_loaded: true, 37 | }; 38 | if entry.path().is_dir() { 39 | folder_entry.kind = FolderEntryType::Folder; 40 | } else { 41 | match entry.metadata() { 42 | Ok(metadata) => { 43 | folder_entry.size = Some(metadata.len()); 44 | } 45 | Err(_) => { 46 | folder.has_error = true; 47 | } 48 | } 49 | } 50 | folder.entries.push(folder_entry); 51 | } 52 | } 53 | } 54 | Err(_) => { 55 | folder.has_error = true; 56 | } 57 | } 58 | 59 | folder 60 | } 61 | 62 | pub fn delete_folder(path: &PathBuf, config: &UIConfig) -> std::io::Result<()> { 63 | if config.move_to_trash { 64 | match trash::delete(path) { 65 | Ok(_) => Ok(()), 66 | Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)), 67 | } 68 | } else { 69 | remove_dir_all(path)?; 70 | Ok(()) 71 | } 72 | } 73 | 74 | pub fn delete_file(path: &PathBuf, config: &UIConfig) -> std::io::Result<()> { 75 | if config.move_to_trash { 76 | match trash::delete(path) { 77 | Ok(_) => Ok(()), 78 | Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)), 79 | } 80 | } else { 81 | remove_file(path)?; 82 | Ok(()) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/fs/store/ds_hashmap.rs: -------------------------------------------------------------------------------- 1 | use crate::fs::{DataStore, Folder, SortBy}; 2 | use std::collections::HashMap; 3 | use std::path::PathBuf; 4 | 5 | use super::DataStoreKey; 6 | 7 | pub type FileTreeMap = HashMap; 8 | 9 | pub struct DSHashmap { 10 | /// Current file path buffer 11 | pub current_path: PathBuf, 12 | /// Map for all file paths 13 | pub store: FileTreeMap, 14 | pub file_type_map: HashMap, 15 | } 16 | 17 | impl DataStore for DSHashmap { 18 | fn new() -> DSHashmap { 19 | DSHashmap { 20 | current_path: PathBuf::from("."), 21 | store: HashMap::new(), 22 | file_type_map: HashMap::new(), 23 | } 24 | } 25 | 26 | fn get_current_path(&mut self) -> &PathBuf { 27 | &self.current_path 28 | } 29 | 30 | fn set_current_path(&mut self, path: &PathBuf) { 31 | self.current_path.clone_from(path); 32 | } 33 | 34 | fn has_path(&self, path: &PathBuf) -> bool { 35 | self.store.contains_key(path) 36 | } 37 | 38 | fn get_current_folder(&self) -> Option<&Folder> { 39 | self.store.get(&self.current_path) 40 | } 41 | 42 | fn get_current_folder_mut(&mut self) -> Option<&mut Folder> { 43 | self.store.get_mut(&self.current_path) 44 | } 45 | 46 | fn set_folder(&mut self, path: &PathBuf, folder: Folder) { 47 | self.store.insert(path.clone(), folder); 48 | } 49 | 50 | fn get_folder_mut(&mut self, path: &PathBuf) -> Option<&mut Folder> { 51 | self.store.get_mut(path) 52 | } 53 | 54 | fn set_current_folder(&mut self, folder: Folder) { 55 | self.set_folder(&self.current_path.clone(), folder); 56 | } 57 | 58 | // TODO: refactor 59 | fn sort_current_folder(&mut self, sort_by: SortBy) { 60 | if let Some(folder) = self.get_current_folder_mut() { 61 | match &folder.sorted_by { 62 | None => match sort_by { 63 | SortBy::Title => folder.sort_by_title(), 64 | SortBy::Size => folder.sort_by_size(), 65 | }, 66 | Some(folder_sort_by) => { 67 | if folder_sort_by.clone() != sort_by { 68 | match sort_by { 69 | SortBy::Title => folder.sort_by_title(), 70 | SortBy::Size => folder.sort_by_size(), 71 | }; 72 | }; 73 | } 74 | } 75 | folder.sorted_by = Some(sort_by); 76 | } 77 | } 78 | 79 | fn move_to_parent(&mut self) { 80 | if let Some(parent) = &self.current_path.parent() { 81 | let parent_buf = parent.to_path_buf(); 82 | self.current_path.clone_from(&parent_buf); 83 | } 84 | } 85 | 86 | fn move_to_child(&mut self, title: &str) { 87 | let mut new_path = PathBuf::from(&self.current_path); 88 | new_path.push(title); 89 | self.current_path = new_path; 90 | } 91 | 92 | fn get_entry_size(&mut self, path: &PathBuf) -> Option { 93 | self.store.get(path).map(|entry| entry.get_size()) 94 | } 95 | 96 | fn remove_path(&mut self, path: &PathBuf) { 97 | self.store.remove(path); 98 | } 99 | 100 | fn get_nodes_len(&self) -> usize { 101 | self.store.keys().len() 102 | } 103 | 104 | fn get_keys(&mut self) -> Vec { 105 | self.store.keys().cloned().collect() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/fs/store/mod.rs: -------------------------------------------------------------------------------- 1 | mod ds_hashmap; 2 | pub use ds_hashmap::DSHashmap; 3 | 4 | use crate::fs::{Folder, SortBy}; 5 | use std::path::PathBuf; 6 | 7 | pub trait DataStore { 8 | fn new() -> Self; 9 | 10 | /// Get current active path 11 | fn get_current_path(&mut self) -> &T; 12 | 13 | /// Set current active path 14 | fn set_current_path(&mut self, path: &T); 15 | 16 | /// Check if store has provided path entry 17 | fn has_path(&self, path: &T) -> bool; 18 | 19 | /// Get optional current active Folder 20 | fn get_current_folder(&self) -> Option<&Folder>; 21 | 22 | /// Get optional current active mutable Folder 23 | fn get_current_folder_mut(&mut self) -> Option<&mut Folder>; 24 | 25 | /// Get optional mutable Folder for provided path 26 | fn get_folder_mut(&mut self, path: &T) -> Option<&mut Folder>; 27 | 28 | /// Update folder for provided path 29 | fn set_folder(&mut self, path: &T, folder: Folder); 30 | 31 | /// Update current active folder 32 | fn set_current_folder(&mut self, folder: Folder); 33 | 34 | /// Sort current active folder by provided order 35 | fn sort_current_folder(&mut self, sort_by: SortBy); 36 | 37 | /// Update current active path to its parent 38 | fn move_to_parent(&mut self); 39 | 40 | /// Update current active path to child folder by provided title 41 | fn move_to_child(&mut self, title: &str); 42 | 43 | /// Remove provided path record from store 44 | fn remove_path(&mut self, path: &T); 45 | 46 | /// Get total known size for provided path 47 | fn get_entry_size(&mut self, path: &T) -> Option; 48 | 49 | /// Get amount of processed file paths 50 | fn get_nodes_len(&self) -> usize; 51 | 52 | fn get_keys(&mut self) -> Vec; 53 | } 54 | 55 | pub type DataStoreKey = PathBuf; 56 | pub type DataStoreType = DSHashmap; 57 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Application. 2 | pub mod app; 3 | 4 | /// Terminal events handler. 5 | pub mod events; 6 | 7 | /// Widget renderer. 8 | pub mod ui; 9 | 10 | /// Terminal user interface. 11 | pub mod tui; 12 | 13 | /// Configs 14 | pub mod config; 15 | 16 | /// File systems utils 17 | pub mod fs; 18 | 19 | /// Task queue manager 20 | pub mod task_manager; 21 | 22 | /// Debug logger 23 | pub mod logger; 24 | 25 | /// FPS counter 26 | pub mod fps_counter; 27 | 28 | pub mod spinner; 29 | -------------------------------------------------------------------------------- /src/logger/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | use std::time::{SystemTime, UNIX_EPOCH}; 3 | 4 | #[derive(Debug)] 5 | pub enum MessageLevel { 6 | Info, 7 | Error, 8 | } 9 | 10 | #[derive(Debug)] 11 | pub struct Logger { 12 | pub messages: VecDeque<(u128, MessageLevel, String)>, 13 | timers: HashMap, 14 | } 15 | 16 | impl Default for Logger { 17 | fn default() -> Self { 18 | Self::new() 19 | } 20 | } 21 | 22 | impl Logger { 23 | fn new() -> Self { 24 | Logger { 25 | messages: VecDeque::new(), 26 | timers: HashMap::new(), 27 | } 28 | } 29 | 30 | pub fn log(&mut self, message: String) { 31 | let timestamp = SystemTime::now() 32 | .duration_since(UNIX_EPOCH) 33 | .expect("Time went backwards") 34 | .as_millis(); 35 | 36 | if self.messages.len() >= 30 { 37 | self.messages.pop_back(); 38 | } 39 | self.messages 40 | .push_front((timestamp, MessageLevel::Info, message)); 41 | } 42 | 43 | pub fn start_timer(&mut self, name: &str) { 44 | if !self.timers.contains_key(name) { 45 | let timestamp = SystemTime::now(); 46 | self.timers.insert(name.to_string(), timestamp); 47 | } 48 | } 49 | 50 | pub fn stop_timer(&mut self, name: &str) { 51 | if let Some(start_time) = self.timers.remove(name) { 52 | let diff = SystemTime::now() 53 | .duration_since(start_time) 54 | .expect("Time went backwards") 55 | .as_secs_f64(); 56 | self.log(format!("[{}]: {:.1}s", name, diff)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use ratatui::backend::CrosstermBackend; 2 | use ratatui::Terminal; 3 | use std::env; 4 | use std::io; 5 | use std::process; 6 | use wiper::app::{App, AppResult}; 7 | use wiper::config::InitConfig; 8 | use wiper::config::EVENT_INTERVAL; 9 | use wiper::events::{handle_key_events, Event, EventHandler}; 10 | use wiper::fs::DataStoreType; 11 | use wiper::tui::Tui; 12 | 13 | fn main() -> AppResult<()> { 14 | let config = InitConfig::build(env::args()).unwrap_or_else(|err| { 15 | eprintln!("Problem parsing arguments: {err}"); 16 | process::exit(1); 17 | }); 18 | 19 | let mut app: App = App::new(config); 20 | app.init(); 21 | 22 | let backend = CrosstermBackend::new(io::stderr()); 23 | let terminal = Terminal::new(backend)?; 24 | let events = EventHandler::new(EVENT_INTERVAL); 25 | let mut tui = Tui::new(terminal, events); 26 | tui.init()?; 27 | 28 | while app.running { 29 | tui.draw(&mut app)?; 30 | match tui.events.next()? { 31 | Event::Tick => app.tick(), 32 | Event::Key(key_event) => handle_key_events(key_event, &mut app)?, 33 | Event::Mouse(_) => {} 34 | Event::Resize(_, _) => {} 35 | } 36 | } 37 | 38 | tui.exit()?; 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /src/spinner/mod.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct Spinner { 3 | symbols: Vec, 4 | done: char, 5 | current: usize, 6 | } 7 | 8 | impl Spinner { 9 | fn new() -> Self { 10 | Spinner { 11 | symbols: vec!['⠁', '⠂', '⠄', '⡀', '⢀', '⠠', '⠐', '⠈'], 12 | done: '⣿', 13 | current: 0, 14 | } 15 | } 16 | 17 | fn move_position(&mut self, steps: isize) { 18 | let len = self.symbols.len() as isize; 19 | self.current = (((self.current as isize + steps) % len + len) % len) as usize; 20 | } 21 | 22 | pub fn get_icons(&mut self, is_loaded: bool) -> (char, char) { 23 | if is_loaded { 24 | return (self.done, self.done); 25 | } 26 | self.move_position(1); 27 | 28 | let right_symbol = self.symbols[self.current]; 29 | let left_symbol_index = (self.current + self.symbols.len() / 2) % self.symbols.len(); 30 | let left_symbol = self.symbols[left_symbol_index]; 31 | (left_symbol, right_symbol) 32 | } 33 | } 34 | 35 | impl Default for Spinner { 36 | fn default() -> Self { 37 | Self::new() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/task_manager/legacy.rs: -------------------------------------------------------------------------------- 1 | use crate::fs::{path_to_folder, DataStore, DataStoreKey, Folder, FolderEntryType}; 2 | use std::collections::VecDeque; 3 | use std::marker::PhantomData; 4 | use std::path::{Path, PathBuf}; 5 | use std::sync::mpsc::{self, Receiver, Sender}; 6 | use std::sync::{Arc, Mutex}; 7 | use std::thread; 8 | use std::time::SystemTime; 9 | 10 | use crate::logger::Logger; 11 | mod ng; 12 | 13 | pub use ng::TaskManagerNg; 14 | 15 | #[derive(Debug)] 16 | pub struct TaskTimer { 17 | pub start: Option, 18 | pub finish: Option, 19 | } 20 | 21 | #[derive(Debug)] 22 | pub struct TaskManagerLegacy> { 23 | /// Stack of file paths to process 24 | pub path_buf_stack: Arc>>, 25 | /// Single receiver to accept processed paths 26 | pub receiver: Receiver<(PathBuf, Folder)>, 27 | /// Sender associated with the single receiver 28 | pub sender: Sender<(PathBuf, Folder)>, 29 | /// Job execution timer 30 | pub task_timer: TaskTimer, 31 | pub running_tasks: Arc>, 32 | _store: PhantomData, 33 | } 34 | 35 | /// For debugging purposes 36 | fn _heavy_computation() { 37 | let mut _sum = 0.0; 38 | for i in 0..10_000_000 { 39 | _sum += (i as f64).sqrt(); 40 | } 41 | } 42 | 43 | impl> TaskManagerLegacy { 44 | fn new() -> Self { 45 | let (sender, receiver) = mpsc::channel(); 46 | let path_buf_stack = Arc::new(Mutex::new(VecDeque::::new())); 47 | let running_tasks = Arc::new(Mutex::new(0)); 48 | 49 | let worker_stack = Arc::clone(&path_buf_stack); 50 | let worker_sender = sender.clone(); 51 | let running_tasks_clone = Arc::clone(&running_tasks); 52 | thread::spawn(move || loop { 53 | let task = { 54 | let mut stack = worker_stack.lock().unwrap(); 55 | stack.pop_front() 56 | }; 57 | 58 | if let Some(path_buf) = task { 59 | let mut tasks = running_tasks_clone.lock().unwrap(); 60 | *tasks += 1; 61 | drop(tasks); 62 | 63 | let folder = path_to_folder(path_buf.clone()); 64 | 65 | let _ = worker_sender.send((path_buf, folder)); 66 | } else { 67 | thread::sleep(std::time::Duration::from_millis(100)); 68 | } 69 | }); 70 | 71 | TaskManagerLegacy { 72 | path_buf_stack, 73 | receiver, 74 | sender, 75 | task_timer: TaskTimer { 76 | start: None, 77 | finish: None, 78 | }, 79 | running_tasks, 80 | _store: PhantomData, 81 | } 82 | } 83 | 84 | pub fn add_task(&mut self, path_buf: &Path, logger: &mut Logger) { 85 | { 86 | let mut stack = self.path_buf_stack.lock().unwrap(); 87 | stack.push_back(path_buf.to_path_buf()); 88 | } // Lock is released here 89 | 90 | self.maybe_start_timer(logger); 91 | } 92 | 93 | pub fn is_done(&self) -> bool { 94 | let stack = self.path_buf_stack.lock().unwrap(); 95 | let running_tasks = self.running_tasks.lock().unwrap(); 96 | stack.is_empty() && *running_tasks == 0 97 | } 98 | 99 | pub fn maybe_add_task(&mut self, store: &S, path_buf: &PathBuf, logger: &mut Logger) -> bool { 100 | if !store.has_path(path_buf) { 101 | self.add_task(path_buf, logger); 102 | true 103 | } else { 104 | false 105 | } 106 | } 107 | 108 | pub fn handle_results(&mut self, store: &mut S, logger: &mut Logger) { 109 | let mut tasks_finished = 0; 110 | while let Ok((path_buf, folder)) = self.receiver.try_recv() { 111 | tasks_finished += 1; 112 | self.process_entry(store, &path_buf, folder, logger); 113 | } 114 | 115 | self.maybe_stop_timer(logger); 116 | 117 | let mut running_tasks = self.running_tasks.lock().unwrap(); 118 | *running_tasks -= tasks_finished; 119 | } 120 | 121 | pub fn process_entry( 122 | &mut self, 123 | store: &mut S, 124 | path_buf: &PathBuf, 125 | mut folder: Folder, 126 | logger: &mut Logger, 127 | ) { 128 | for child_entry in folder.entries.iter_mut() { 129 | if child_entry.kind == FolderEntryType::Folder { 130 | let mut subfolder_path = path_buf.clone(); 131 | subfolder_path.push(&child_entry.title); 132 | child_entry.size = store.get_entry_size(&subfolder_path); 133 | folder.sorted_by = None; 134 | 135 | let task_added = self.maybe_add_task(store, &subfolder_path, logger); 136 | if task_added { 137 | child_entry.is_loaded = false; 138 | } 139 | } 140 | } 141 | 142 | store.set_folder(path_buf, folder.clone()); 143 | 144 | let mut folder_traverse = folder.clone(); 145 | let mut path_traverse = path_buf.clone(); 146 | let mut is_loaded_traverse = folder.entries.iter().all(|entry| entry.is_loaded); 147 | 148 | while let Some(parent_buf) = path_traverse.parent() { 149 | if parent_buf == path_traverse { 150 | break; 151 | } 152 | if let Some(parent_folder) = store.get_folder_mut(&PathBuf::from(parent_buf)) { 153 | for entry in parent_folder.entries.iter_mut() { 154 | if entry.title == folder_traverse.title { 155 | entry.size = Some(folder_traverse.get_size()); 156 | entry.is_loaded = is_loaded_traverse; 157 | parent_folder.sorted_by = None; 158 | 159 | break; 160 | } 161 | } 162 | folder_traverse = parent_folder.clone(); 163 | path_traverse = parent_buf.to_path_buf(); 164 | is_loaded_traverse = parent_folder.entries.iter().all(|entry| entry.is_loaded); 165 | } else { 166 | break; 167 | } 168 | } 169 | } 170 | fn maybe_start_timer(&mut self, logger: &mut Logger) { 171 | logger.start_timer("TM-proc"); 172 | if let Ok(duration) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { 173 | if self.task_timer.start.is_none() { 174 | // Start is None - record start 175 | self.task_timer.start = Some(duration.as_millis()); 176 | } else { 177 | // Start is not None 178 | if self.task_timer.finish.is_some() { 179 | // Finish is not None - restart 180 | self.task_timer.start = Some(duration.as_millis()); 181 | self.task_timer.finish = None; 182 | } 183 | }; 184 | }; 185 | } 186 | 187 | fn maybe_stop_timer(&mut self, logger: &mut Logger) { 188 | if let Ok(duration) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { 189 | if self.path_buf_stack.lock().unwrap().is_empty() 190 | && *self.running_tasks.lock().unwrap() == 0 191 | && self.task_timer.start.is_some() 192 | && self.task_timer.finish.is_none() 193 | { 194 | self.task_timer.finish = Some(duration.as_millis()); 195 | } 196 | }; 197 | 198 | if self.path_buf_stack.lock().unwrap().is_empty() 199 | && *self.running_tasks.lock().unwrap() == 0 200 | { 201 | logger.stop_timer("TM-proc"); 202 | } 203 | } 204 | 205 | pub fn time_taken(&self) -> Option { 206 | self.task_timer 207 | .start 208 | .and_then(|start| self.task_timer.finish.map(|finish| finish - start)) 209 | } 210 | } 211 | 212 | impl> Default for TaskManagerLegacy { 213 | fn default() -> Self { 214 | Self::new() 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/task_manager/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::fs::{path_to_folder, DataStore, DataStoreKey, Folder, FolderEntry, FolderEntryType}; 2 | use crate::logger::Logger; 3 | use crossbeam::channel::{Receiver, Sender}; 4 | use std::ffi::OsStr; 5 | use std::marker::PhantomData; 6 | use std::path::PathBuf; 7 | 8 | #[derive(Debug)] 9 | pub struct EntryState { 10 | size: u64, 11 | } 12 | 13 | type WalkDir = jwalk::WalkDirGeneric<((), Option>)>; 14 | 15 | pub type TraversalEntry = 16 | Result>)>, jwalk::Error>; 17 | 18 | #[derive(Debug)] 19 | pub enum TraversalEvent { 20 | Entry(TraversalEntry), 21 | Finished(u64), 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct TaskManager> { 26 | pub event_tx: Sender, 27 | pub event_rx: Receiver, 28 | pub is_working: bool, 29 | _store: PhantomData, 30 | } 31 | 32 | impl> TaskManager { 33 | pub fn new() -> Self { 34 | let (entry_tx, entry_rx) = crossbeam::channel::bounded(100); 35 | Self { 36 | event_rx: entry_rx, 37 | event_tx: entry_tx, 38 | is_working: false, 39 | _store: PhantomData, 40 | } 41 | } 42 | 43 | pub fn is_done(&self) -> bool { 44 | !self.is_working 45 | } 46 | 47 | pub fn start(&mut self, input: Vec, logger: &mut Logger) { 48 | logger.start_timer("Traversal"); 49 | self.is_working = true; 50 | let entry_tx = self.event_tx.clone(); 51 | let _ = std::thread::Builder::new() 52 | .name("wiper-walk-dispatcher".to_string()) 53 | .spawn({ 54 | move || { 55 | for root_path in input.into_iter() { 56 | for entry in Self::iter_from_path(&root_path).into_iter() { 57 | if entry_tx.send(TraversalEvent::Entry(entry)).is_err() { 58 | println!("Send err: channel closed"); 59 | return; 60 | } 61 | } 62 | } 63 | let _ = entry_tx.send(TraversalEvent::Finished(0)); 64 | } 65 | }); 66 | } 67 | 68 | pub fn process_results(&mut self, store: &mut S, logger: &mut Logger) { 69 | while let Ok(event) = self.event_rx.try_recv() { 70 | match event { 71 | TraversalEvent::Entry(entry) => match entry { 72 | Ok(e) => { 73 | // Construct entry 74 | let belongs_to = e.parent_path.to_path_buf(); 75 | let title = e.file_name.to_string_lossy().to_string(); 76 | let extension = e 77 | .path() 78 | .extension() 79 | .and_then(OsStr::to_str) 80 | .unwrap_or("") 81 | .to_string(); 82 | 83 | let kind = match e.file_type().is_dir() { 84 | true => { 85 | // Create store record for folder (edge-case for last-leaf-empty 86 | // folders) 87 | let default_folder = Folder::new(title.clone()); 88 | store.set_folder(&e.path().clone(), default_folder); 89 | 90 | FolderEntryType::Folder 91 | } 92 | false => FolderEntryType::File, 93 | }; 94 | let size = match e.client_state.as_ref() { 95 | Some(Ok(my_entry)) => { 96 | if kind == FolderEntryType::Folder { 97 | // Ignore folder metadata size 98 | 0 99 | } else { 100 | my_entry.size 101 | } 102 | } 103 | _ => 0, 104 | }; 105 | 106 | let folder_entry = FolderEntry { 107 | title: title.clone(), 108 | size: Some(size), 109 | is_loaded: true, 110 | kind, 111 | }; 112 | 113 | // Add entry to parent folder 114 | let parent_folder = store.get_folder_mut(&belongs_to.to_path_buf()); 115 | match parent_folder { 116 | Some(folder) => { 117 | if !extension.is_empty() { 118 | folder.append_file_type_size(&extension, size); 119 | } 120 | folder.entries.push(folder_entry); 121 | } 122 | None => { 123 | if let Some(belongs_to_name) = belongs_to.file_name() { 124 | let mut folder = 125 | Folder::new(belongs_to_name.to_string_lossy().to_string()); 126 | folder.entries.push(folder_entry); 127 | store.set_folder(&belongs_to.to_path_buf(), folder); 128 | } 129 | } 130 | }; 131 | 132 | // Traverse tree up - update parent folder sizes 133 | if let Some(title_traverse_os) = belongs_to.file_name() { 134 | let mut title_traverse = 135 | title_traverse_os.to_string_lossy().to_string(); 136 | let mut path_traverse = belongs_to.to_path_buf(); 137 | 138 | while let Some(parent_buf) = path_traverse.parent() { 139 | if parent_buf == path_traverse { 140 | break; 141 | } 142 | if let Some(parent_folder) = 143 | store.get_folder_mut(&PathBuf::from(parent_buf)) 144 | { 145 | // Increment parent's entry size 146 | for child in parent_folder.entries.iter_mut() { 147 | if child.title == title_traverse 148 | && child.kind == FolderEntryType::Folder 149 | { 150 | child.increment_size(size); 151 | parent_folder.sorted_by = None; 152 | break; 153 | } 154 | } 155 | // Update parent's folder file_type_map 156 | if !extension.is_empty() { 157 | parent_folder.append_file_type_size(&extension, size); 158 | } 159 | title_traverse.clone_from(&parent_folder.title); 160 | path_traverse = parent_buf.to_path_buf(); 161 | } else { 162 | break; 163 | } 164 | } 165 | } 166 | } 167 | Err(_) => { 168 | logger.log("Done".into()); 169 | } 170 | }, 171 | TraversalEvent::Finished(_) => { 172 | self.is_working = false; 173 | logger.stop_timer("Traversal"); 174 | } 175 | } 176 | } 177 | } 178 | 179 | pub fn process_path_sync(&self, store: &mut S, path: &DataStoreKey) -> Vec { 180 | let mut folder_new = path_to_folder(path.clone()); 181 | let mut paths_to_process: Vec = vec![]; 182 | let mut entries_to_keep: Vec = vec![]; 183 | 184 | match store.get_folder_mut(path) { 185 | Some(folder_stored) => { 186 | // Folder already exists 187 | for child in folder_new.entries.iter_mut() { 188 | if !folder_stored.entries.iter().any(|e| e.title == child.title) { 189 | // No entry 190 | if child.kind == FolderEntryType::Folder { 191 | // Folder -> process 192 | let mut child_path = path.clone(); 193 | child_path.push(child.title.clone()); 194 | paths_to_process.push(child_path); 195 | } else { 196 | // File -> simply push 197 | folder_stored.entries.push(child.clone()); 198 | } 199 | } 200 | } 201 | } 202 | None => { 203 | // Folder does not exist 204 | for child in folder_new.entries.iter_mut() { 205 | if child.kind == FolderEntryType::Folder { 206 | // Folder -> process 207 | let mut child_path = path.clone(); 208 | child_path.push(child.title.clone()); 209 | paths_to_process.push(child_path); 210 | } else { 211 | // File -> simply push 212 | entries_to_keep.push(child.clone()); 213 | } 214 | } 215 | folder_new.entries = entries_to_keep; 216 | store.set_folder(path, folder_new.clone()); 217 | } 218 | } 219 | 220 | paths_to_process 221 | } 222 | 223 | pub fn iter_from_path(root_path: &PathBuf) -> WalkDir { 224 | let threads = num_cpus::get(); 225 | 226 | let ignore_dirs = []; 227 | 228 | WalkDir::new(root_path) 229 | .follow_links(false) 230 | .skip_hidden(false) 231 | .process_read_dir({ 232 | move |_, _, _, dir_entry_results| { 233 | dir_entry_results.iter_mut().for_each(|dir_entry_result| { 234 | if let Ok(dir_entry) = dir_entry_result { 235 | let metadata = dir_entry.metadata(); 236 | 237 | if let Ok(metadata) = metadata { 238 | dir_entry.client_state = Some(Ok(EntryState { 239 | size: metadata.len(), 240 | })); 241 | } else { 242 | dir_entry.client_state = Some(Err(metadata.unwrap_err())); 243 | } 244 | 245 | if ignore_dirs.contains(&dir_entry.path()) { 246 | dir_entry.read_children_path = None; 247 | } 248 | } 249 | }) 250 | } 251 | }) 252 | .parallelism(match threads { 253 | 0 => jwalk::Parallelism::RayonDefaultPool { 254 | busy_timeout: std::time::Duration::from_secs(1), 255 | }, 256 | 1 => jwalk::Parallelism::Serial, 257 | _ => jwalk::Parallelism::RayonExistingPool { 258 | pool: jwalk::rayon::ThreadPoolBuilder::new() 259 | .stack_size(128 * 1024) 260 | .num_threads(threads) 261 | .thread_name(|idx| format!("wiper-walk-{idx}")) 262 | .build() 263 | .expect("fields we set cannot fail") 264 | .into(), 265 | busy_timeout: None, 266 | }, 267 | }) 268 | } 269 | } 270 | 271 | impl> Default for TaskManager { 272 | fn default() -> Self { 273 | Self::new() 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/tui/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{App, AppResult}; 2 | use crate::events::EventHandler; 3 | use crate::fs::{DataStore, DataStoreKey}; 4 | use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; 5 | use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; 6 | use ratatui::backend::Backend; 7 | use ratatui::Terminal; 8 | use std::io; 9 | use std::marker::PhantomData; 10 | use std::panic; 11 | 12 | /// Representation of a terminal user interface. 13 | /// 14 | /// It is responsible for setting up the terminal, 15 | /// initializing the interface and handling the draw events. 16 | #[derive(Debug)] 17 | pub struct Tui> { 18 | /// Interface to the Terminal. 19 | terminal: Terminal, 20 | /// Terminal event handler. 21 | pub events: EventHandler, 22 | _store: PhantomData, 23 | } 24 | 25 | impl> Tui { 26 | /// Constructs a new instance of [`Tui`]. 27 | pub fn new(terminal: Terminal, events: EventHandler) -> Self { 28 | Self { 29 | terminal, 30 | events, 31 | _store: PhantomData, 32 | } 33 | } 34 | 35 | /// Initializes the terminal interface. 36 | /// 37 | /// It enables the raw mode and sets terminal properties. 38 | pub fn init(&mut self) -> AppResult<()> { 39 | terminal::enable_raw_mode()?; 40 | crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?; 41 | 42 | // Define a custom panic hook to reset the terminal properties. 43 | // This way, you won't have your terminal messed up if an unexpected error happens. 44 | let panic_hook = panic::take_hook(); 45 | panic::set_hook(Box::new(move |panic| { 46 | Self::reset().expect("failed to reset the terminal"); 47 | panic_hook(panic); 48 | })); 49 | 50 | self.terminal.hide_cursor()?; 51 | self.terminal.clear()?; 52 | Ok(()) 53 | } 54 | 55 | /// [`Draw`] the terminal interface by [`rendering`] the widgets. 56 | /// 57 | /// [`Draw`]: ratatui::Terminal::draw 58 | /// [`rendering`]: crate::ui::render 59 | pub fn draw(&mut self, app: &mut App) -> AppResult<()> { 60 | self.terminal 61 | .draw(|frame| frame.render_widget(app, frame.size()))?; 62 | Ok(()) 63 | } 64 | 65 | /// Resets the terminal interface. 66 | /// 67 | /// This function is also used for the panic hook to revert 68 | /// the terminal properties if unexpected errors occur. 69 | fn reset() -> AppResult<()> { 70 | terminal::disable_raw_mode()?; 71 | crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?; 72 | Ok(()) 73 | } 74 | 75 | /// Exits the terminal interface. 76 | /// 77 | /// It disables the raw mode and reverts back the terminal properties. 78 | pub fn exit(&mut self) -> AppResult<()> { 79 | Self::reset()?; 80 | self.terminal.show_cursor()?; 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ui/chart.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{prelude::*, widgets::*}; 2 | 3 | use crate::ui::utils::format_file_size; 4 | 5 | pub fn render_chart(area: Rect, buf: &mut Buffer, chart_data: Vec<(String, u64)>) { 6 | let block = Block::default().padding(Padding::top(1)); 7 | let inner_area = block.inner(area); 8 | Widget::render(block, area, buf); 9 | 10 | let total_size: u64 = chart_data.iter().map(|(_, size)| *size).sum(); 11 | let percentages: Vec = chart_data 12 | .iter() 13 | .map(|(_, size)| (size * 100 / total_size) as u16) 14 | .collect(); 15 | 16 | let mut constraints = Constraint::from_percentages(percentages.clone()); 17 | constraints.pop(); 18 | constraints.push(Constraint::Fill(1)); 19 | let layout = Layout::default() 20 | .direction(Direction::Horizontal) 21 | .constraints(constraints) 22 | .split(inner_area); 23 | 24 | for (i, (file_type, size)) in chart_data.iter().enumerate() { 25 | let mut text = format!("{}: {}", file_type, format_file_size(*size)); 26 | // Hide size from "short" filytypes 27 | if percentages[i] < 10 { 28 | text = file_type.to_string(); 29 | } 30 | let paragraph = Paragraph::new(text) 31 | .centered() 32 | .block(Block::default().borders(Borders::ALL)); 33 | paragraph.render(layout[i], buf); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/constants.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{prelude::*, style::palette::tailwind}; 2 | 3 | pub const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950; 4 | pub const TEXT_COLOR: Color = tailwind::SLATE.c200; 5 | pub const TABLE_HEADER_FG: Color = tailwind::SLATE.c200; 6 | pub const TABLE_HEADER_BG: Color = tailwind::SLATE.c900; 7 | pub const TEXT_SELECTED_BG: Color = tailwind::SLATE.c700; 8 | pub const TEXT_PRE_DELETED_BG: Color = tailwind::RED.c600; 9 | pub const TEXT_HIGHLIGHTED: Color = tailwind::YELLOW.c400; 10 | pub const TABLE_ICON_WIDTH: u16 = 2; 11 | pub const TABLE_NAME_WIDTH: u16 = 40; 12 | pub const TABLE_SIZE_WIDTH: u16 = 20; 13 | pub const TABLE_SPACE_WIDTH: usize = 40; 14 | 15 | // Texts 16 | pub const TEXT_UNKNOWN: &str = "N/A"; 17 | pub const TEXT_PARENT_DIR: &str = ".."; 18 | pub const TEXT_TITLE: &str = "Wiper"; 19 | pub const TEXT_HINT_NAVIGATE: &str = "←↓↑→/Enter/Backspace - navigate"; 20 | pub const TEXT_ICON_FOLDER: &str = ""; 21 | pub const TEXT_ICON_FOLDER_ASCII: &str = "[]"; 22 | -------------------------------------------------------------------------------- /src/ui/content.rs: -------------------------------------------------------------------------------- 1 | use crate::config::UIConfig; 2 | use crate::fs::Folder; 3 | use crate::fs::SortBy; 4 | use crate::logger::Logger; 5 | use crate::logger::MessageLevel; 6 | use ratatui::{prelude::*, widgets::*}; 7 | use std::time::{SystemTime, UNIX_EPOCH}; 8 | 9 | use crate::ui::constants::{ 10 | NORMAL_ROW_COLOR, TABLE_HEADER_BG, TABLE_HEADER_FG, TABLE_ICON_WIDTH, TABLE_NAME_WIDTH, 11 | TABLE_SIZE_WIDTH, TABLE_SPACE_WIDTH, TEXT_COLOR, TEXT_PRE_DELETED_BG, TEXT_SELECTED_BG, 12 | }; 13 | use crate::ui::utils::folder_to_rows; 14 | 15 | const MAX_LOG_LEN: usize = 180; 16 | #[derive(Debug)] 17 | pub struct DebugData { 18 | pub fps: String, 19 | pub skipped_frames: String, 20 | pub folders: usize, 21 | pub spin_symbol: (char, char), 22 | } 23 | 24 | pub fn render_content( 25 | area: Rect, 26 | buf: &mut Buffer, 27 | maybe_folder: Option<&Folder>, 28 | config: &UIConfig, 29 | logger: &Logger, 30 | debug_data: &DebugData, 31 | ) { 32 | let horizontal_layout = Layout::horizontal(match config.debug_enabled { 33 | true => [Constraint::Min(1), Constraint::Min(1)], 34 | false => [Constraint::Min(1), Constraint::Max(0)], 35 | }); 36 | 37 | let [content_col, debug_col] = horizontal_layout.areas(area); 38 | 39 | if let Some(folder) = maybe_folder { 40 | render_table(content_col, buf, folder, config); 41 | } 42 | 43 | if config.debug_enabled { 44 | render_debug_panel(debug_col, buf, logger, debug_data); 45 | } 46 | } 47 | 48 | pub fn render_table(area: Rect, buf: &mut Buffer, folder: &Folder, config: &UIConfig) { 49 | let block = Block::default() 50 | .padding(Padding::horizontal(1)) 51 | .borders(Borders::ALL) 52 | .border_set(symbols::border::PROPORTIONAL_TALL) 53 | .fg(TEXT_COLOR) 54 | .bg(NORMAL_ROW_COLOR); 55 | 56 | let layout = Layout::horizontal([ 57 | Constraint::Fill(1), 58 | Constraint::Length( 59 | TABLE_ICON_WIDTH + TABLE_NAME_WIDTH + TABLE_SIZE_WIDTH + TABLE_SPACE_WIDTH as u16 + 4, 60 | ), 61 | Constraint::Fill(1), 62 | ]); 63 | let [_, col_table, _] = layout.areas(area); 64 | 65 | let header_style = Style::default().fg(TABLE_HEADER_FG).bg(TABLE_HEADER_BG); 66 | let selected_style = if config.confirming_deletion { 67 | Style::default().bg(TEXT_PRE_DELETED_BG) 68 | } else { 69 | Style::default().bg(TEXT_SELECTED_BG) 70 | }; 71 | 72 | let header_titles = match config.sort_by { 73 | SortBy::Title => ["", "Name ↓", "Size", "Space"], 74 | SortBy::Size => ["", "Name", "Size ↓", "Space"], 75 | }; 76 | 77 | let header = header_titles 78 | .into_iter() 79 | .map(Cell::from) 80 | .collect::() 81 | .style(header_style) 82 | .height(1); 83 | 84 | let rows = folder_to_rows(folder, config); 85 | 86 | let table = Table::new( 87 | rows, 88 | [ 89 | Constraint::Length(TABLE_ICON_WIDTH), 90 | Constraint::Length(TABLE_NAME_WIDTH), 91 | Constraint::Length(TABLE_SIZE_WIDTH), 92 | Constraint::Length(TABLE_SPACE_WIDTH as u16), 93 | ], 94 | ) 95 | .block(block) 96 | .header(header) 97 | .highlight_symbol("> ") 98 | .highlight_style(selected_style) 99 | .highlight_spacing(HighlightSpacing::Always); 100 | 101 | StatefulWidget::render( 102 | table, 103 | col_table, 104 | buf, 105 | &mut TableState::default().with_selected(Some(folder.cursor_index)), 106 | ); 107 | } 108 | 109 | pub fn render_debug_panel(area: Rect, buf: &mut Buffer, logger: &Logger, debug_data: &DebugData) { 110 | let [top, bottom] = Layout::vertical([Constraint::Max(5), Constraint::Fill(1)]).areas(area); 111 | 112 | let stats_text = Text::from(format!( 113 | "Folders: {}\nFPS: {} | Skipped: {}", 114 | debug_data.folders, debug_data.fps, debug_data.skipped_frames 115 | )); 116 | 117 | let stats_block = Block::default() 118 | .padding(Padding::horizontal(1)) 119 | .borders(Borders::ALL) 120 | .border_set(symbols::border::PROPORTIONAL_TALL) 121 | .title(" Stats ") 122 | .title_alignment(Alignment::Center); 123 | 124 | let stats = Paragraph::new(stats_text).left_aligned().block(stats_block); 125 | 126 | Widget::render(stats, top, buf); 127 | 128 | // Logs 129 | let logs_block = Block::default() 130 | .padding(Padding::horizontal(1)) 131 | .borders(Borders::ALL) 132 | .border_set(symbols::border::PROPORTIONAL_TALL) 133 | .title(" Logs ") 134 | .title_alignment(Alignment::Center); 135 | 136 | let logs: Vec = logger 137 | .messages 138 | .iter() 139 | .enumerate() 140 | .map(|(_i, (timestamp, level, message))| { 141 | let mut message = message.clone(); 142 | let current_timestamp_ms = SystemTime::now() 143 | .duration_since(UNIX_EPOCH) 144 | .expect("Time went backwards") 145 | .as_millis(); 146 | let elapsed_ms = current_timestamp_ms - timestamp; 147 | if message.len() > MAX_LOG_LEN { 148 | message = format!( 149 | "{}..{}", 150 | &message[..MAX_LOG_LEN / 4], 151 | &message[message.len() - MAX_LOG_LEN / 4 * 3..] 152 | ); 153 | } 154 | message = format!("[{:.1}] - {}", elapsed_ms as f64 / 1000.0, message); 155 | 156 | let style = Style::default(); 157 | let style = match level { 158 | MessageLevel::Info => style.fg(TEXT_COLOR), 159 | MessageLevel::Error => style.fg(TEXT_PRE_DELETED_BG), 160 | }; 161 | ListItem::from(message).style(style) 162 | }) 163 | .collect(); 164 | 165 | let items = List::new(logs).block(logs_block); 166 | Widget::render(items, bottom, buf); 167 | } 168 | -------------------------------------------------------------------------------- /src/ui/footer.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::constants::TEXT_HINT_NAVIGATE; 2 | use ratatui::{prelude::*, widgets::*}; 3 | 4 | use super::utils::color_capital_letter; 5 | 6 | pub fn render_footer(area: Rect, buf: &mut Buffer) { 7 | let block = Block::default().padding(Padding::top(1)); 8 | let inner_area = block.inner(area); 9 | Widget::render(block, area, buf); 10 | 11 | let layout = Layout::horizontal([ 12 | Constraint::Fill(1), 13 | Constraint::Max(13), 14 | Constraint::Max(9), 15 | Constraint::Max(9), 16 | Constraint::Max(6), 17 | Constraint::Max(13), 18 | Constraint::Max(4), 19 | ]); 20 | let [col_navigate, col_version, col_explore, col_refresh, col_sort, col_delete, col_quit] = 21 | layout.areas(inner_area); 22 | 23 | let version = env!("CARGO_PKG_VERSION"); 24 | let text_version = format!("v:{}", version); 25 | let text_explore = color_capital_letter("Explore,".into(), None, None); 26 | let text_refresh = color_capital_letter("Refresh,".into(), None, None); 27 | let text_sort = color_capital_letter("Sort,".into(), None, None); 28 | let text_delete = color_capital_letter("Delete - 2x,".into(), None, None); 29 | let text_quit = color_capital_letter("Quit".into(), None, None); 30 | 31 | Paragraph::new(TEXT_HINT_NAVIGATE) 32 | .left_aligned() 33 | .render(col_navigate, buf); 34 | Paragraph::new(text_version) 35 | .left_aligned() 36 | .render(col_version, buf); 37 | Paragraph::new(text_explore) 38 | .left_aligned() 39 | .render(col_explore, buf); 40 | Paragraph::new(text_refresh) 41 | .left_aligned() 42 | .render(col_refresh, buf); 43 | Paragraph::new(text_sort) 44 | .left_aligned() 45 | .render(col_sort, buf); 46 | Paragraph::new(text_delete) 47 | .left_aligned() 48 | .render(col_delete, buf); 49 | Paragraph::new(text_quit) 50 | .left_aligned() 51 | .render(col_quit, buf); 52 | } 53 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::fs::DataStore; 2 | use crate::{app::App, fs::DataStoreKey}; 3 | use ratatui::prelude::*; 4 | use ratatui::widgets::*; 5 | 6 | mod chart; 7 | pub mod constants; 8 | mod content; 9 | mod footer; 10 | mod title; 11 | mod utils; 12 | use constants::TEXT_TITLE; 13 | pub use content::{render_content, DebugData}; 14 | pub use footer::render_footer; 15 | pub use title::render_title; 16 | 17 | use self::chart::render_chart; 18 | use self::constants::{TEXT_COLOR, TEXT_PRE_DELETED_BG}; 19 | 20 | impl> Widget for &mut App { 21 | fn render(self, area: Rect, buf: &mut Buffer) { 22 | self.pre_render(); 23 | let maybe_folder = self.store.get_current_folder(); 24 | 25 | let mut chart_data = vec![]; 26 | 27 | // Helper data 28 | let fps = self.fps_counter.update(); 29 | let (spin_left, spin_right) = self.spinner.get_icons(!self.task_manager.is_working); 30 | let debug = DebugData { 31 | folders: self.store.get_nodes_len(), 32 | fps: format!("{:.1}", fps), 33 | skipped_frames: format!("{:.1}", self.fps_counter.skipped_frames), 34 | spin_symbol: (spin_left, spin_right), 35 | }; 36 | 37 | // Main wrapper 38 | let mut title = TEXT_TITLE; 39 | let mut border_color = TEXT_COLOR; 40 | 41 | if let Some(folder) = maybe_folder { 42 | if folder.has_error { 43 | title = "Error"; 44 | border_color = TEXT_PRE_DELETED_BG; 45 | } 46 | chart_data = folder.get_chart_data(0.8, 5); 47 | } 48 | 49 | let block = Block::default() 50 | .title(format!(" {} {} {} ", spin_left, title, spin_right)) 51 | .title_alignment(Alignment::Center) 52 | .borders(Borders::ALL) 53 | .border_style(border_color) 54 | .padding(Padding::horizontal(1)) 55 | .border_set(symbols::border::DOUBLE); 56 | let inner_area = block.inner(area); 57 | Widget::render(block, area, buf); 58 | 59 | // Layout 60 | let vertical = Layout::vertical([ 61 | Constraint::Length(2), // Header - 2 lines 62 | Constraint::Fill(1), // Content - Fill the rest of the space 63 | Constraint::Length(4), // Chart - 4 lines 64 | Constraint::Length(2), // Footer - 2 lines 65 | ]); 66 | let [header_area, rest_area, chart_area, footer_area] = vertical.areas(inner_area); 67 | 68 | render_title(header_area, buf, maybe_folder, &self.ui_config); 69 | render_content( 70 | rest_area, 71 | buf, 72 | maybe_folder, 73 | &self.ui_config, 74 | &self.logger, 75 | &debug, 76 | ); 77 | render_chart(chart_area, buf, chart_data); 78 | render_footer(footer_area, buf); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/ui/title.rs: -------------------------------------------------------------------------------- 1 | use crate::config::UIConfig; 2 | use crate::fs::Folder; 3 | use ratatui::{prelude::*, widgets::*}; 4 | 5 | use crate::ui::utils::{format_file_size, value_to_box}; 6 | 7 | use super::utils::color_capital_letter; 8 | 9 | pub fn render_title( 10 | area: Rect, 11 | buf: &mut Buffer, 12 | maybe_folder: Option<&Folder>, 13 | ui_config: &UIConfig, 14 | ) { 15 | let horizontal_layout = Layout::horizontal([Constraint::Fill(1), Constraint::Max(23)]); 16 | let [left_col, right_col] = horizontal_layout.areas(area); 17 | 18 | // Folder data 19 | if let Some(folder) = maybe_folder { 20 | Paragraph::new(format!( 21 | "{} | {}", 22 | folder.title, 23 | format_file_size(folder.get_size()), 24 | )) 25 | .bold() 26 | .left_aligned() 27 | .render(left_col, buf); 28 | } 29 | 30 | // Settings 31 | let config_layout = Layout::horizontal([Constraint::Max(12), Constraint::Max(11)]); 32 | let [col_color, col_trash] = config_layout.areas(right_col); 33 | 34 | let text_color = color_capital_letter( 35 | "Colored: ".into(), 36 | None, 37 | Some(value_to_box(&ui_config.colored)), 38 | ); 39 | let text_trash = color_capital_letter( 40 | "Trash: ".into(), 41 | None, 42 | Some(value_to_box(&ui_config.move_to_trash)), 43 | ); 44 | 45 | Paragraph::new(text_color) 46 | .right_aligned() 47 | .render(col_color, buf); 48 | Paragraph::new(text_trash) 49 | .right_aligned() 50 | .render(col_trash, buf); 51 | } 52 | -------------------------------------------------------------------------------- /src/ui/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::config::UIConfig; 2 | use crate::fs::Folder; 3 | use crate::fs::FolderEntryType; 4 | use crate::ui::constants::{NORMAL_ROW_COLOR, TABLE_SPACE_WIDTH, TEXT_UNKNOWN}; 5 | use ratatui::{prelude::*, widgets::*}; 6 | 7 | use super::constants::{TEXT_HIGHLIGHTED, TEXT_ICON_FOLDER_ASCII}; 8 | 9 | pub fn format_file_size(size: u64) -> String { 10 | const KB: u64 = 1024; 11 | const MB: u64 = KB * 1024; 12 | const GB: u64 = MB * 1024; 13 | const TB: u64 = GB * 1024; 14 | 15 | if size >= TB { 16 | format!("{:.2} TB", size as f64 / TB as f64) 17 | } else if size >= GB { 18 | format!("{:.2} GB", size as f64 / GB as f64) 19 | } else if size >= MB { 20 | format!("{:.2} MB", size as f64 / MB as f64) 21 | } else if size >= KB { 22 | format!("{:.2} KB", size as f64 / KB as f64) 23 | } else { 24 | format!("{} bytes", size) 25 | } 26 | } 27 | 28 | pub fn calculate_color(percent: u64, _max_entry_size: u64) -> Color { 29 | let colors = [ 30 | Color::Rgb(0, 128, 0), // Green 31 | Color::Rgb(50, 205, 50), // LimeGreen 32 | Color::Rgb(173, 255, 47), // GreenYellow 33 | Color::Rgb(255, 255, 0), // Yellow 34 | Color::Rgb(255, 165, 0), // Orange 35 | Color::Rgb(255, 0, 0), // Red 36 | ]; 37 | 38 | let index = 39 | ((percent as f64 / TABLE_SPACE_WIDTH as f64) * (colors.len() - 1) as f64).round() as usize; 40 | 41 | colors[index] 42 | } 43 | 44 | pub fn value_to_box(value: &bool) -> String { 45 | match value { 46 | true => "[x]".to_string(), 47 | false => "[ ]".to_string(), 48 | } 49 | } 50 | 51 | pub fn folder_to_rows<'a>(folder: &'a Folder, config: &'a UIConfig) -> Vec> { 52 | let max_entry_size = folder.get_max_entry_size(); 53 | 54 | folder 55 | .to_list() 56 | .iter() 57 | .map(|item| { 58 | let (item_size, bar, color) = match item.size { 59 | Some(size) => { 60 | let percent = if max_entry_size == 0 { 61 | 0 62 | } else { 63 | (size * TABLE_SPACE_WIDTH as u64 / max_entry_size).div_euclid(1) 64 | }; 65 | let mut b = String::new(); 66 | let color = calculate_color(percent, max_entry_size); 67 | for _ in 0..percent { 68 | b.push('█'); 69 | } 70 | (Text::from(format_file_size(size)), Text::from(b), color) 71 | } 72 | None => (Text::from(TEXT_UNKNOWN), Text::from(" "), NORMAL_ROW_COLOR), 73 | }; 74 | let prefix = match item.kind == FolderEntryType::Folder { 75 | true => Text::from(TEXT_ICON_FOLDER_ASCII), 76 | false => Text::from(" "), 77 | }; 78 | 79 | let mut bar_style = Style::default(); 80 | if config.colored { 81 | bar_style = bar_style.fg(color); 82 | } 83 | 84 | Row::new(vec![ 85 | prefix, 86 | Text::from(item.title.clone()), 87 | item_size, 88 | bar.style(bar_style), 89 | ]) 90 | }) 91 | .collect() 92 | } 93 | 94 | pub fn color_capital_letter<'a>( 95 | text: String, 96 | prefix: Option, 97 | postfix: Option, 98 | ) -> Line<'a> { 99 | let mut spans = Vec::new(); 100 | 101 | if let Some(pre) = prefix { 102 | spans.push(Span::raw(pre)); 103 | } 104 | 105 | if let Some(first_char) = text.chars().next() { 106 | let first_char_upper = first_char.to_uppercase().to_string(); 107 | spans.push(Span::styled( 108 | first_char_upper, 109 | Style::default() 110 | .fg(TEXT_HIGHLIGHTED) 111 | .add_modifier(Modifier::BOLD) 112 | .add_modifier(Modifier::UNDERLINED), 113 | )); 114 | spans.push(Span::raw(text[first_char.len_utf8()..].to_string())); 115 | } 116 | 117 | if let Some(post) = postfix { 118 | spans.push(Span::raw(post)); 119 | } 120 | 121 | Line::from(spans) 122 | } 123 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | use std::time::Duration; 3 | use wiper::app::App; 4 | use wiper::config::InitConfig; 5 | use wiper::fs::{DataStore, DataStoreKey, Folder, FolderEntry, FolderEntryType, SortBy}; 6 | 7 | pub const TEST_FILE_PATH_VIEW: &str = "./tests/test_files/view"; 8 | pub const TEST_FILE_PATH_EDIT: &str = "./tests/test_files/edit"; 9 | pub fn setup_app_view>() -> App { 10 | let c = InitConfig { 11 | file_path: Some(TEST_FILE_PATH_VIEW.to_string()), 12 | }; 13 | let mut app: App = App::new(c); 14 | app.ui_config.open_file = false; 15 | app.ui_config.sort_by = SortBy::Title; 16 | app.init(); 17 | app 18 | } 19 | 20 | pub fn setup_app_edit>(postfix: &str) -> App { 21 | let c = InitConfig { 22 | file_path: Some(format!("{}_{}", TEST_FILE_PATH_EDIT, postfix)), 23 | }; 24 | let mut app: App = App::new(c); 25 | app.ui_config.open_file = false; 26 | app.ui_config.move_to_trash = false; 27 | app.ui_config.sort_by = SortBy::Title; 28 | app.init(); 29 | app 30 | } 31 | 32 | pub fn handle_tasks_synchronously>(app: &mut App) { 33 | while !app.task_manager.is_done() { 34 | app.tick(); 35 | thread::sleep(Duration::from_millis(10)); 36 | } 37 | app.pre_render(); 38 | } 39 | 40 | pub fn assert_item_at_index_is>( 41 | app: &App, 42 | index: usize, 43 | kind: FolderEntryType, 44 | ) { 45 | assert_eq!( 46 | app.store 47 | .get_current_folder() 48 | .unwrap() 49 | .entries 50 | .get(index) 51 | .unwrap() 52 | .kind, 53 | kind 54 | ); 55 | } 56 | 57 | pub fn assert_item_at_index_title>( 58 | app: &App, 59 | index: usize, 60 | title: String, 61 | ) { 62 | assert_eq!( 63 | app.store 64 | .get_current_folder() 65 | .unwrap() 66 | .entries 67 | .get(index) 68 | .unwrap() 69 | .title, 70 | title 71 | ); 72 | } 73 | 74 | pub fn assert_item_at_index_loading_state>( 75 | app: &App, 76 | index: usize, 77 | is_loaded: bool, 78 | ) { 79 | assert_eq!( 80 | app.store 81 | .get_current_folder() 82 | .unwrap() 83 | .entries 84 | .get(index) 85 | .unwrap() 86 | .is_loaded, 87 | is_loaded 88 | ); 89 | } 90 | 91 | pub fn get_entry_by_kind>( 92 | app: &App, 93 | kind: FolderEntryType, 94 | ) -> Vec { 95 | app.store 96 | .get_current_folder() 97 | .unwrap() 98 | .entries 99 | .iter() 100 | .filter(|e| e.kind == kind) 101 | .cloned() 102 | .collect() 103 | } 104 | 105 | pub fn assert_parent_folder_state>(app: &App) { 106 | assert_eq!(get_entry_by_kind(app, FolderEntryType::File).len(), 3); 107 | assert_eq!(get_entry_by_kind(app, FolderEntryType::Folder).len(), 3); 108 | } 109 | 110 | pub fn assert_parent_folder_a_state>(app: &App) { 111 | assert_eq!(get_entry_by_kind(app, FolderEntryType::File).len(), 2); 112 | assert_eq!(get_entry_by_kind(app, FolderEntryType::Folder).len(), 0); 113 | } 114 | 115 | pub fn assert_delete_folder_state>(app: &App) { 116 | assert_eq!(get_entry_by_kind(app, FolderEntryType::File).len(), 3); 117 | assert_eq!(get_entry_by_kind(app, FolderEntryType::Folder).len(), 1); 118 | } 119 | 120 | pub fn assert_cursor_index>(app: &App, index: usize) { 121 | assert_eq!(app.store.get_current_folder().unwrap().cursor_index, index); 122 | } 123 | 124 | pub fn assert_root_view_folder_sorted_by_title>(app: &App) { 125 | assert_item_at_index_title(app, 0, "..".to_string()); 126 | assert_item_at_index_title(app, 1, "a_folder".to_string()); 127 | assert_item_at_index_title(app, 2, "b_folder".to_string()); 128 | assert_item_at_index_title(app, 3, "c_folder".to_string()); 129 | assert_item_at_index_title(app, 4, "a_root_file.txt".to_string()); 130 | assert_item_at_index_title(app, 5, "d_root_file.txt".to_string()); 131 | assert_item_at_index_title(app, 6, "z_root_file.txt".to_string()); 132 | } 133 | 134 | pub fn assert_root_view_folder_sorted_by_size>(app: &App) { 135 | assert_item_at_index_title(app, 0, "..".to_string()); 136 | assert_item_at_index_title(app, 1, "b_folder".to_string()); 137 | assert_item_at_index_title(app, 2, "c_folder".to_string()); 138 | assert_item_at_index_title(app, 3, "a_folder".to_string()); 139 | assert_item_at_index_title(app, 4, "d_root_file.txt".to_string()); 140 | assert_item_at_index_title(app, 5, "a_root_file.txt".to_string()); 141 | assert_item_at_index_title(app, 6, "z_root_file.txt".to_string()); 142 | } 143 | 144 | pub fn get_current_folder>(app: &App) -> Option<&Folder> { 145 | app.store.get_current_folder() 146 | } 147 | -------------------------------------------------------------------------------- /tests/cursor.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | 3 | use crate::common::*; 4 | use wiper::app::App; 5 | 6 | mod cursor { 7 | 8 | use wiper::fs::DataStoreType; 9 | 10 | use super::*; 11 | 12 | #[test] 13 | fn updates_cursor_position() { 14 | let mut app: App = setup_app_view(); 15 | handle_tasks_synchronously(&mut app); 16 | 17 | assert_cursor_index(&app, 0); 18 | 19 | app.on_cursor_down(); 20 | assert_cursor_index(&app, 1); 21 | 22 | app.on_cursor_up(); 23 | assert_cursor_index(&app, 0); 24 | } 25 | 26 | #[test] 27 | fn stops_cursor_at_very_top() { 28 | let mut app: App = setup_app_view(); 29 | handle_tasks_synchronously(&mut app); 30 | 31 | assert_cursor_index(&app, 0); 32 | 33 | for _ in 0..10 { 34 | app.on_cursor_up(); 35 | } 36 | 37 | assert_cursor_index(&app, 0); 38 | } 39 | 40 | #[test] 41 | fn stops_cursor_at_very_bottom() { 42 | let mut app: App = setup_app_view(); 43 | handle_tasks_synchronously(&mut app); 44 | 45 | for _ in 0..20 { 46 | app.on_cursor_down(); 47 | } 48 | assert_cursor_index(&app, 6); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/delete.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | use crate::common::*; 3 | use wiper::app::App; 4 | use wiper::fs::FolderEntryType; 5 | 6 | mod delete { 7 | 8 | use wiper::fs::DataStoreType; 9 | 10 | use super::*; 11 | use std::fs::{self, File}; 12 | use std::io::Write; 13 | 14 | const TEST_FILE_SIZE: usize = 100; 15 | 16 | fn generate_lorem_ipsum() -> String { 17 | // String is exactly 100 bytes 18 | String::from("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore") 19 | } 20 | 21 | /// - folder_1 22 | /// - folder_2 23 | /// - folder_3 24 | /// - file_1 25 | /// - file_2 26 | /// - file_3 27 | /// - file_1 28 | /// - file_2 29 | /// - file_3 30 | /// - file_1 31 | /// - file_2 32 | /// - file_3 33 | fn create_testing_files(postfix: &str) { 34 | let custom_folder = format!("{}_{}", TEST_FILE_PATH_EDIT, postfix); 35 | fs::create_dir_all(&custom_folder).expect("Failed to create test folder"); 36 | 37 | let mut folder_path = custom_folder.to_string(); 38 | 39 | for folder_index in 1..4 { 40 | for file_index in 1..4 { 41 | let file_name = format!("file_to_delete_{}.txt", file_index); 42 | let file_path = format!("{}/{}", folder_path, file_name); 43 | let mut file = File::create(&file_path).expect("Failed to create test file"); 44 | writeln!(file, "{}", generate_lorem_ipsum()).expect("Failed to write to test file"); 45 | } 46 | 47 | folder_path = format!("{}/folder_to_delete_{}", folder_path, folder_index); 48 | 49 | fs::create_dir_all(&folder_path).expect("Failed to create test folder"); 50 | } 51 | } 52 | 53 | fn cleanup_testing_files(postfix: &str) { 54 | let custom_folder = format!("{}_{}", TEST_FILE_PATH_EDIT, postfix); 55 | if let Err(err) = fs::remove_dir_all(custom_folder) { 56 | eprintln!("Failed to remove test folder: {}", err); 57 | } 58 | } 59 | 60 | #[test] 61 | fn has_correct_initial_state() { 62 | let postfix = "01"; 63 | create_testing_files(postfix); 64 | let mut app: App = setup_app_edit(postfix); 65 | handle_tasks_synchronously(&mut app); 66 | 67 | assert_delete_folder_state(&app); 68 | cleanup_testing_files(postfix); 69 | } 70 | 71 | #[test] 72 | fn does_nothing_when_cursor_is_at_the_top() { 73 | let postfix = "02"; 74 | create_testing_files(postfix); 75 | let mut app: App = setup_app_edit(postfix); 76 | handle_tasks_synchronously(&mut app); 77 | 78 | assert_cursor_index(&app, 0); 79 | assert_delete_folder_state(&app); 80 | app.on_delete(); 81 | app.on_delete(); 82 | handle_tasks_synchronously(&mut app); 83 | 84 | assert_delete_folder_state(&app); 85 | cleanup_testing_files(postfix); 86 | } 87 | 88 | #[test] 89 | fn does_nothing_when_delete_pressed_once() { 90 | let postfix = "03"; 91 | create_testing_files(postfix); 92 | let mut app: App = setup_app_edit(postfix); 93 | handle_tasks_synchronously(&mut app); 94 | 95 | assert_delete_folder_state(&app); 96 | app.on_cursor_down(); 97 | app.on_delete(); 98 | handle_tasks_synchronously(&mut app); 99 | 100 | assert_eq!(get_entry_by_kind(&app, FolderEntryType::File).len(), 3); 101 | assert_eq!(get_entry_by_kind(&app, FolderEntryType::Folder).len(), 1); 102 | cleanup_testing_files(postfix); 103 | } 104 | 105 | #[test] 106 | fn resets_delete_confirmation_on_cursor_move() { 107 | let postfix = "04"; 108 | create_testing_files(postfix); 109 | let mut app: App = setup_app_edit(postfix); 110 | handle_tasks_synchronously(&mut app); 111 | 112 | app.on_delete(); 113 | app.on_cursor_down(); 114 | assert!(!app.ui_config.confirming_deletion); 115 | app.on_delete(); 116 | app.on_cursor_up(); 117 | assert!(!app.ui_config.confirming_deletion); 118 | cleanup_testing_files(postfix); 119 | } 120 | 121 | #[test] 122 | fn resets_delete_confirmation_on_folder_enter() { 123 | let postfix = "05"; 124 | create_testing_files(postfix); 125 | let mut app: App = setup_app_edit(postfix); 126 | handle_tasks_synchronously(&mut app); 127 | 128 | app.on_cursor_down(); 129 | app.on_delete(); 130 | app.on_enter(); 131 | assert!(!app.ui_config.confirming_deletion); 132 | cleanup_testing_files(postfix); 133 | } 134 | 135 | #[test] 136 | fn resets_delete_confirmation_after_deleting_folder() { 137 | let postfix = "06"; 138 | create_testing_files(postfix); 139 | let mut app: App = setup_app_edit(postfix); 140 | handle_tasks_synchronously(&mut app); 141 | 142 | app.on_cursor_down(); 143 | app.on_delete(); 144 | app.on_delete(); 145 | assert!(!app.ui_config.confirming_deletion); 146 | cleanup_testing_files(postfix); 147 | } 148 | 149 | #[test] 150 | fn resets_delete_confirmation_after_deleting_file() { 151 | let postfix = "07"; 152 | create_testing_files(postfix); 153 | let mut app: App = setup_app_edit(postfix); 154 | handle_tasks_synchronously(&mut app); 155 | 156 | app.on_cursor_down(); 157 | app.on_cursor_down(); 158 | app.on_delete(); 159 | app.on_delete(); 160 | assert!(!app.ui_config.confirming_deletion); 161 | cleanup_testing_files(postfix); 162 | } 163 | 164 | #[test] 165 | fn resets_delete_after_clicking_escape() { 166 | let postfix = "14"; 167 | create_testing_files(postfix); 168 | let mut app: App = setup_app_edit(postfix); 169 | handle_tasks_synchronously(&mut app); 170 | 171 | app.on_cursor_down(); 172 | app.on_delete(); 173 | app.on_escape(); 174 | assert!(!app.ui_config.confirming_deletion); 175 | cleanup_testing_files(postfix); 176 | } 177 | 178 | #[test] 179 | fn deletes_folder() { 180 | let postfix = "08"; 181 | create_testing_files(postfix); 182 | let mut app: App = setup_app_edit(postfix); 183 | handle_tasks_synchronously(&mut app); 184 | 185 | assert_delete_folder_state(&app); 186 | app.on_cursor_down(); 187 | app.on_delete(); 188 | app.on_delete(); 189 | handle_tasks_synchronously(&mut app); 190 | 191 | assert_eq!(get_entry_by_kind(&app, FolderEntryType::File).len(), 3); 192 | assert_eq!(get_entry_by_kind(&app, FolderEntryType::Folder).len(), 0); 193 | cleanup_testing_files(postfix); 194 | } 195 | 196 | #[test] 197 | fn deletes_file() { 198 | let postfix = "09"; 199 | create_testing_files(postfix); 200 | let mut app: App = setup_app_edit(postfix); 201 | handle_tasks_synchronously(&mut app); 202 | assert_delete_folder_state(&app); 203 | app.on_cursor_down(); 204 | app.on_cursor_down(); 205 | app.on_delete(); 206 | app.on_delete(); 207 | handle_tasks_synchronously(&mut app); 208 | 209 | assert_eq!(get_entry_by_kind(&app, FolderEntryType::File).len(), 2); 210 | assert_eq!(get_entry_by_kind(&app, FolderEntryType::Folder).len(), 1); 211 | cleanup_testing_files(postfix); 212 | } 213 | 214 | #[test] 215 | fn updated_current_folder_size() { 216 | let postfix = "10"; 217 | create_testing_files(postfix); 218 | let mut app: App = setup_app_edit(postfix); 219 | handle_tasks_synchronously(&mut app); 220 | 221 | let root_entry = get_current_folder(&app).unwrap(); 222 | assert_eq!(root_entry.get_size(), (TEST_FILE_SIZE * 9) as u64); 223 | 224 | app.on_cursor_down(); 225 | app.on_cursor_down(); 226 | app.on_delete(); 227 | app.on_delete(); 228 | handle_tasks_synchronously(&mut app); 229 | 230 | let root_entry_updated = get_current_folder(&app).unwrap(); 231 | assert_eq!(root_entry_updated.get_size(), (TEST_FILE_SIZE * 8) as u64); 232 | 233 | cleanup_testing_files(postfix); 234 | } 235 | 236 | #[test] 237 | fn deleting_file_updates_parent_folders_sizes() { 238 | let postfix = "11"; 239 | create_testing_files(postfix); 240 | let mut app: App = setup_app_edit(postfix); 241 | handle_tasks_synchronously(&mut app); 242 | 243 | let root_entry = get_current_folder(&app).unwrap(); 244 | assert_eq!(root_entry.get_size(), (TEST_FILE_SIZE * 9) as u64); 245 | 246 | app.on_cursor_down(); 247 | app.on_enter(); 248 | handle_tasks_synchronously(&mut app); 249 | 250 | let folder_1 = get_current_folder(&app).unwrap(); 251 | assert_eq!(folder_1.get_size(), (TEST_FILE_SIZE * 6) as u64); 252 | 253 | app.on_cursor_down(); 254 | app.on_enter(); 255 | handle_tasks_synchronously(&mut app); 256 | 257 | let folder_2 = get_current_folder(&app).unwrap(); 258 | assert_eq!(folder_2.get_size(), (TEST_FILE_SIZE * 3) as u64); 259 | 260 | app.on_cursor_down(); 261 | app.on_cursor_down(); 262 | app.on_delete(); 263 | app.on_delete(); 264 | handle_tasks_synchronously(&mut app); 265 | 266 | let folder_2_upd = get_current_folder(&app).unwrap(); 267 | assert_eq!(folder_2_upd.get_size(), (TEST_FILE_SIZE * 2) as u64); 268 | 269 | app.on_cursor_up(); 270 | app.on_cursor_up(); 271 | app.on_enter(); 272 | handle_tasks_synchronously(&mut app); 273 | 274 | let folder_1_upd = get_current_folder(&app).unwrap(); 275 | assert_eq!(folder_1_upd.get_size(), (TEST_FILE_SIZE * 5) as u64); 276 | assert_eq!( 277 | folder_1_upd.get_selected_entry_size(), 278 | (TEST_FILE_SIZE * 2) as u64 279 | ); 280 | 281 | app.on_cursor_up(); 282 | app.on_enter(); 283 | handle_tasks_synchronously(&mut app); 284 | 285 | let root_entry_upd = get_current_folder(&app).unwrap(); 286 | assert_eq!(root_entry_upd.get_size(), (TEST_FILE_SIZE * 8) as u64); 287 | assert_eq!( 288 | root_entry_upd.get_selected_entry_size(), 289 | (TEST_FILE_SIZE * 5) as u64 290 | ); 291 | 292 | cleanup_testing_files(postfix); 293 | } 294 | 295 | #[test] 296 | fn deleting_folder_updates_parent_folders_sizes() { 297 | let postfix = "12"; 298 | create_testing_files(postfix); 299 | let mut app: App = setup_app_edit(postfix); 300 | handle_tasks_synchronously(&mut app); 301 | 302 | let root_entry = get_current_folder(&app).unwrap(); 303 | assert_eq!(root_entry.get_size(), (TEST_FILE_SIZE * 9) as u64); 304 | 305 | app.on_cursor_down(); 306 | app.on_enter(); 307 | handle_tasks_synchronously(&mut app); 308 | 309 | let folder_1 = get_current_folder(&app).unwrap(); 310 | assert_eq!(folder_1.get_size(), (TEST_FILE_SIZE * 6) as u64); 311 | 312 | app.on_cursor_down(); 313 | app.on_delete(); 314 | app.on_delete(); 315 | handle_tasks_synchronously(&mut app); 316 | 317 | let folder_1_upd = get_current_folder(&app).unwrap(); 318 | assert_eq!(folder_1_upd.get_size(), (TEST_FILE_SIZE * 3) as u64); 319 | 320 | app.on_cursor_up(); 321 | app.on_enter(); 322 | handle_tasks_synchronously(&mut app); 323 | 324 | let root_entry_upd = get_current_folder(&app).unwrap(); 325 | assert_eq!(root_entry_upd.get_size(), (TEST_FILE_SIZE * 6) as u64); 326 | assert_eq!( 327 | root_entry_upd.get_selected_entry_size(), 328 | (TEST_FILE_SIZE * 3) as u64 329 | ); 330 | 331 | cleanup_testing_files(postfix); 332 | } 333 | 334 | #[test] 335 | fn moves_cursor_one_step_up_after_deleting_bottom_entry() { 336 | let postfix = "13"; 337 | create_testing_files(postfix); 338 | let mut app: App = setup_app_edit(postfix); 339 | handle_tasks_synchronously(&mut app); 340 | 341 | for _ in 1..20 { 342 | app.on_cursor_down(); 343 | } 344 | 345 | assert_eq!(get_current_folder(&app).unwrap().cursor_index, 4); 346 | app.on_delete(); 347 | app.on_delete(); 348 | handle_tasks_synchronously(&mut app); 349 | assert_eq!(get_current_folder(&app).unwrap().cursor_index, 3); 350 | 351 | cleanup_testing_files(postfix); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /tests/file_tree.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | 3 | use crate::common::*; 4 | use wiper::app::App; 5 | use wiper::fs::FolderEntryType; 6 | 7 | mod file_tree { 8 | 9 | use wiper::fs::{DataStore, DataStoreType}; 10 | 11 | use super::*; 12 | #[test] 13 | fn test_ordering_by_kind() { 14 | let mut app: App = setup_app_view(); 15 | handle_tasks_synchronously(&mut app); 16 | 17 | assert_item_at_index_is(&app, 0, FolderEntryType::Parent); 18 | assert_item_at_index_is(&app, 1, FolderEntryType::Folder); 19 | assert_item_at_index_is(&app, 2, FolderEntryType::Folder); 20 | assert_item_at_index_is(&app, 3, FolderEntryType::Folder); 21 | assert_item_at_index_is(&app, 4, FolderEntryType::File); 22 | assert_item_at_index_is(&app, 5, FolderEntryType::File); 23 | assert_item_at_index_is(&app, 6, FolderEntryType::File); 24 | } 25 | 26 | #[test] 27 | fn test_ordering_by_title() { 28 | let mut app: App = setup_app_view(); 29 | handle_tasks_synchronously(&mut app); 30 | 31 | assert_root_view_folder_sorted_by_title(&app); 32 | } 33 | 34 | #[test] 35 | fn test_switching_ordering_to_size() { 36 | let mut app: App = setup_app_view(); 37 | handle_tasks_synchronously(&mut app); 38 | 39 | app.on_toggle_sorting(); 40 | handle_tasks_synchronously(&mut app); 41 | 42 | assert_root_view_folder_sorted_by_size(&app); 43 | } 44 | 45 | #[test] 46 | fn test_ordering_persists_after_navigating_into_folder() { 47 | let mut app: App = setup_app_view(); 48 | handle_tasks_synchronously(&mut app); 49 | 50 | app.on_toggle_sorting(); 51 | handle_tasks_synchronously(&mut app); 52 | 53 | app.on_cursor_down(); 54 | app.on_enter(); 55 | handle_tasks_synchronously(&mut app); 56 | 57 | assert_item_at_index_title(&app, 0, "..".to_string()); 58 | assert_item_at_index_title(&app, 1, "folder2_file3.txt".to_string()); 59 | assert_item_at_index_title(&app, 2, "folder2_file2.txt".to_string()); 60 | assert_item_at_index_title(&app, 3, "folder2_file1.txt".to_string()); 61 | } 62 | 63 | #[test] 64 | fn test_ordering_persists_after_navigating_to_parent() { 65 | let mut app: App = setup_app_view(); 66 | handle_tasks_synchronously(&mut app); 67 | 68 | app.on_cursor_down(); 69 | app.on_enter(); 70 | app.on_toggle_sorting(); 71 | handle_tasks_synchronously(&mut app); 72 | 73 | app.on_enter(); 74 | handle_tasks_synchronously(&mut app); 75 | 76 | assert_root_view_folder_sorted_by_size(&app); 77 | } 78 | 79 | #[test] 80 | fn test_switching_ordering_back_to_title() { 81 | let mut app: App = setup_app_view(); 82 | handle_tasks_synchronously(&mut app); 83 | 84 | app.on_toggle_sorting(); 85 | app.on_toggle_sorting(); 86 | handle_tasks_synchronously(&mut app); 87 | 88 | assert_root_view_folder_sorted_by_title(&app); 89 | } 90 | 91 | #[test] 92 | fn has_correct_amount_file_tree_keys() { 93 | let mut app: App = setup_app_view(); 94 | handle_tasks_synchronously(&mut app); 95 | 96 | assert_eq!(app.store.get_nodes_len(), 5); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/handle_enter.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | use crate::common::*; 3 | 4 | use wiper::app::App; 5 | use wiper::fs::DataStoreType; 6 | mod handle_enter { 7 | 8 | use super::*; 9 | 10 | #[test] 11 | fn updates_current_tree_when_enters_subfolder() { 12 | let mut app: App = setup_app_view(); 13 | handle_tasks_synchronously(&mut app); 14 | 15 | app.on_cursor_down(); 16 | app.on_enter(); 17 | handle_tasks_synchronously(&mut app); 18 | 19 | assert_cursor_index(&app, 0); 20 | assert_parent_folder_a_state(&app); 21 | } 22 | 23 | #[test] 24 | fn navigates_back_to_parent_folder() { 25 | let mut app: App = setup_app_view(); 26 | handle_tasks_synchronously(&mut app); 27 | 28 | app.on_cursor_down(); 29 | app.on_enter(); 30 | handle_tasks_synchronously(&mut app); 31 | 32 | assert_parent_folder_a_state(&app); 33 | 34 | app.on_enter(); 35 | handle_tasks_synchronously(&mut app); 36 | 37 | assert_parent_folder_state(&app); 38 | assert_cursor_index(&app, 1); 39 | } 40 | 41 | #[test] 42 | fn does_nothing_when_tries_to_enter_file() { 43 | let mut app: App = setup_app_view(); 44 | handle_tasks_synchronously(&mut app); 45 | 46 | app.on_cursor_down(); 47 | app.on_cursor_down(); 48 | app.on_cursor_down(); 49 | app.on_cursor_down(); 50 | app.on_cursor_down(); 51 | assert_cursor_index(&app, 5); 52 | 53 | app.on_enter(); 54 | handle_tasks_synchronously(&mut app); 55 | 56 | assert_cursor_index(&app, 5); 57 | assert_parent_folder_state(&app); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/loading.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | 3 | use crate::common::*; 4 | use wiper::app::App; 5 | use wiper::fs::DataStoreType; 6 | 7 | mod loading { 8 | use super::*; 9 | 10 | #[test] 11 | fn root_folders_are_loaded() { 12 | let mut app: App = setup_app_view(); 13 | handle_tasks_synchronously(&mut app); 14 | assert_item_at_index_loading_state(&app, 1, true); 15 | assert_item_at_index_loading_state(&app, 2, true); 16 | assert_item_at_index_loading_state(&app, 3, true); 17 | assert_item_at_index_loading_state(&app, 4, true); 18 | assert_item_at_index_loading_state(&app, 5, true); 19 | assert_item_at_index_loading_state(&app, 6, true); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/test_files/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikebastuz/wiper/4fadf3f222d5201fb671367188bf3dc6819ffd93/tests/test_files/.DS_Store -------------------------------------------------------------------------------- /tests/test_files/.gitignore: -------------------------------------------------------------------------------- 1 | /edit* 2 | -------------------------------------------------------------------------------- /tests/test_files/README.md: -------------------------------------------------------------------------------- 1 | DO NOT CHANGE CONTENT OF THIS DIRECTORY 2 | -------------------------------------------------------------------------------- /tests/test_files/view/a_folder/folder1_file1.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | 9 | -------------------------------------------------------------------------------- /tests/test_files/view/a_folder/folder1_file2.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 9 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 10 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 11 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 12 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 13 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 14 | mollit anim id est laborum. 15 | 16 | -------------------------------------------------------------------------------- /tests/test_files/view/a_root_file.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | 9 | -------------------------------------------------------------------------------- /tests/test_files/view/b_folder/extra_weight.txt: -------------------------------------------------------------------------------- 1 | To mabe folder B bigger than C 2 | -------------------------------------------------------------------------------- /tests/test_files/view/b_folder/folder2_file1.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 9 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 10 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 11 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 12 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 13 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 14 | mollit anim id est laborum. 15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 16 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 17 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 18 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 19 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 20 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 21 | mollit anim id est laborum. 22 | 23 | -------------------------------------------------------------------------------- /tests/test_files/view/b_folder/folder2_file2.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 9 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 10 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 11 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 12 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 13 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 14 | mollit anim id est laborum. 15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 16 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 17 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 18 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 19 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 20 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 21 | mollit anim id est laborum. 22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 23 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 24 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 25 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 26 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 27 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 28 | mollit anim id est laborum. 29 | 30 | -------------------------------------------------------------------------------- /tests/test_files/view/b_folder/folder2_file3.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 9 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 10 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 11 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 12 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 13 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 14 | mollit anim id est laborum. 15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 16 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 17 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 18 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 19 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 20 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 21 | mollit anim id est laborum. 22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 23 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 24 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 25 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 26 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 27 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 28 | mollit anim id est laborum. 29 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 30 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 31 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 32 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 33 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 34 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 35 | mollit anim id est laborum. 36 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 37 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 38 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 39 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 40 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 41 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 42 | mollit anim id est laborum. 43 | 44 | -------------------------------------------------------------------------------- /tests/test_files/view/c_folder/folder2_file1.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 9 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 10 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 11 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 12 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 13 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 14 | mollit anim id est laborum. 15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 16 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 17 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 18 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 19 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 20 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 21 | mollit anim id est laborum. 22 | 23 | -------------------------------------------------------------------------------- /tests/test_files/view/c_folder/folder2_file2.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 9 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 10 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 11 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 12 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 13 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 14 | mollit anim id est laborum. 15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 16 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 17 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 18 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 19 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 20 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 21 | mollit anim id est laborum. 22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 23 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 24 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 25 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 26 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 27 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 28 | mollit anim id est laborum. 29 | 30 | -------------------------------------------------------------------------------- /tests/test_files/view/c_folder/folder2_file3.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 9 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 10 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 11 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 12 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 13 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 14 | mollit anim id est laborum. 15 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 16 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 17 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 18 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 19 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 20 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 21 | mollit anim id est laborum. 22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 23 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 24 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 25 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 26 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 27 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 28 | mollit anim id est laborum. 29 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 30 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 31 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 32 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 33 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 34 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 35 | mollit anim id est laborum. 36 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 37 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 38 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 39 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 40 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 41 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 42 | mollit anim id est laborum. 43 | 44 | -------------------------------------------------------------------------------- /tests/test_files/view/d_root_file.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 9 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 10 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 11 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 12 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 13 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 14 | mollit anim id est laborum. 15 | -------------------------------------------------------------------------------- /tests/test_files/view/z_root_file.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ 2 | Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ 3 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi \ 4 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit \ 5 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur \ 6 | sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ 7 | mollit anim id est laborum. 8 | 9 | --------------------------------------------------------------------------------