├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── screen.png ├── sfm.toml └── src ├── app ├── actions.rs ├── components │ ├── create_modal.rs │ ├── error_modal.rs │ ├── mod.rs │ ├── not_empty_dir_delete_modal.rs │ ├── panel.rs │ ├── rename_modal.rs │ ├── root.rs │ └── tab.rs ├── config │ ├── icon_cfg.rs │ ├── keyboard_cfg.rs │ ├── mod.rs │ └── program_associations.rs ├── file_system │ ├── dir_item.rs │ ├── file_item.rs │ ├── file_system_item.rs │ ├── functions.rs │ ├── mod.rs │ ├── path.rs │ └── symlink_item.rs ├── middlewares.rs ├── mod.rs ├── reducers │ ├── dir_reducer.rs │ ├── file_reducer.rs │ ├── mod.rs │ ├── panel_reducer.rs │ ├── search_reducer.rs │ ├── symlink_reducer.rs │ └── tab_reducer.rs └── state.rs ├── core ├── color_scheme.rs ├── config.rs ├── events.rs ├── key_binding.rs ├── mod.rs ├── store.rs └── ui │ ├── component.rs │ ├── component_base.rs │ └── mod.rs └── main.rs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.vscode 3 | /.idea 4 | .nvimlog 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "arrayref" 5 | version = "0.3.6" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 8 | 9 | [[package]] 10 | name = "arrayvec" 11 | version = "0.5.2" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 14 | 15 | [[package]] 16 | name = "autocfg" 17 | version = "1.0.1" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 20 | 21 | [[package]] 22 | name = "base64" 23 | version = "0.13.0" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 26 | 27 | [[package]] 28 | name = "bitflags" 29 | version = "1.2.1" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 32 | 33 | [[package]] 34 | name = "blake2b_simd" 35 | version = "0.5.11" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" 38 | dependencies = [ 39 | "arrayref", 40 | "arrayvec", 41 | "constant_time_eq", 42 | ] 43 | 44 | [[package]] 45 | name = "cassowary" 46 | version = "0.3.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 49 | 50 | [[package]] 51 | name = "cfg-if" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 55 | 56 | [[package]] 57 | name = "chrono" 58 | version = "0.4.19" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 61 | dependencies = [ 62 | "libc", 63 | "num-integer", 64 | "num-traits", 65 | "time", 66 | "winapi", 67 | ] 68 | 69 | [[package]] 70 | name = "constant_time_eq" 71 | version = "0.1.5" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 74 | 75 | [[package]] 76 | name = "crossbeam-utils" 77 | version = "0.8.1" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" 80 | dependencies = [ 81 | "autocfg", 82 | "cfg-if", 83 | "lazy_static", 84 | ] 85 | 86 | [[package]] 87 | name = "crossterm" 88 | version = "0.18.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "4e86d73f2a0b407b5768d10a8c720cf5d2df49a9efc10ca09176d201ead4b7fb" 91 | dependencies = [ 92 | "bitflags", 93 | "crossterm_winapi 0.6.2", 94 | "lazy_static", 95 | "libc", 96 | "mio", 97 | "parking_lot", 98 | "signal-hook", 99 | "winapi", 100 | ] 101 | 102 | [[package]] 103 | name = "crossterm" 104 | version = "0.19.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c" 107 | dependencies = [ 108 | "bitflags", 109 | "crossterm_winapi 0.7.0", 110 | "lazy_static", 111 | "libc", 112 | "mio", 113 | "parking_lot", 114 | "signal-hook", 115 | "winapi", 116 | ] 117 | 118 | [[package]] 119 | name = "crossterm_winapi" 120 | version = "0.6.2" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "c2265c3f8e080075d9b6417aa72293fc71662f34b4af2612d8d1b074d29510db" 123 | dependencies = [ 124 | "winapi", 125 | ] 126 | 127 | [[package]] 128 | name = "crossterm_winapi" 129 | version = "0.7.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9" 132 | dependencies = [ 133 | "winapi", 134 | ] 135 | 136 | [[package]] 137 | name = "dirs" 138 | version = "3.0.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" 141 | dependencies = [ 142 | "dirs-sys", 143 | ] 144 | 145 | [[package]] 146 | name = "dirs-sys" 147 | version = "0.3.5" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" 150 | dependencies = [ 151 | "libc", 152 | "redox_users", 153 | "winapi", 154 | ] 155 | 156 | [[package]] 157 | name = "getrandom" 158 | version = "0.1.16" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 161 | dependencies = [ 162 | "cfg-if", 163 | "libc", 164 | "wasi 0.9.0+wasi-snapshot-preview1", 165 | ] 166 | 167 | [[package]] 168 | name = "instant" 169 | version = "0.1.9" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" 172 | dependencies = [ 173 | "cfg-if", 174 | ] 175 | 176 | [[package]] 177 | name = "lazy_static" 178 | version = "1.4.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 181 | 182 | [[package]] 183 | name = "libc" 184 | version = "0.2.86" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" 187 | 188 | [[package]] 189 | name = "lock_api" 190 | version = "0.4.2" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" 193 | dependencies = [ 194 | "scopeguard", 195 | ] 196 | 197 | [[package]] 198 | name = "log" 199 | version = "0.4.14" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 202 | dependencies = [ 203 | "cfg-if", 204 | ] 205 | 206 | [[package]] 207 | name = "mio" 208 | version = "0.7.7" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7" 211 | dependencies = [ 212 | "libc", 213 | "log", 214 | "miow", 215 | "ntapi", 216 | "winapi", 217 | ] 218 | 219 | [[package]] 220 | name = "miow" 221 | version = "0.3.6" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" 224 | dependencies = [ 225 | "socket2", 226 | "winapi", 227 | ] 228 | 229 | [[package]] 230 | name = "ntapi" 231 | version = "0.3.6" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" 234 | dependencies = [ 235 | "winapi", 236 | ] 237 | 238 | [[package]] 239 | name = "num-integer" 240 | version = "0.1.44" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 243 | dependencies = [ 244 | "autocfg", 245 | "num-traits", 246 | ] 247 | 248 | [[package]] 249 | name = "num-traits" 250 | version = "0.2.14" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 253 | dependencies = [ 254 | "autocfg", 255 | ] 256 | 257 | [[package]] 258 | name = "parking_lot" 259 | version = "0.11.1" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" 262 | dependencies = [ 263 | "instant", 264 | "lock_api", 265 | "parking_lot_core", 266 | ] 267 | 268 | [[package]] 269 | name = "parking_lot_core" 270 | version = "0.8.2" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" 273 | dependencies = [ 274 | "cfg-if", 275 | "instant", 276 | "libc", 277 | "redox_syscall", 278 | "smallvec", 279 | "winapi", 280 | ] 281 | 282 | [[package]] 283 | name = "redox_syscall" 284 | version = "0.1.57" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 287 | 288 | [[package]] 289 | name = "redox_users" 290 | version = "0.3.5" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" 293 | dependencies = [ 294 | "getrandom", 295 | "redox_syscall", 296 | "rust-argon2", 297 | ] 298 | 299 | [[package]] 300 | name = "rust-argon2" 301 | version = "0.8.3" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" 304 | dependencies = [ 305 | "base64", 306 | "blake2b_simd", 307 | "constant_time_eq", 308 | "crossbeam-utils", 309 | ] 310 | 311 | [[package]] 312 | name = "scopeguard" 313 | version = "1.1.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 316 | 317 | [[package]] 318 | name = "serde" 319 | version = "1.0.123" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" 322 | 323 | [[package]] 324 | name = "sfm" 325 | version = "0.5.0" 326 | dependencies = [ 327 | "chrono", 328 | "crossterm 0.19.0", 329 | "dirs", 330 | "lazy_static", 331 | "toml", 332 | "tui", 333 | ] 334 | 335 | [[package]] 336 | name = "signal-hook" 337 | version = "0.1.17" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" 340 | dependencies = [ 341 | "libc", 342 | "mio", 343 | "signal-hook-registry", 344 | ] 345 | 346 | [[package]] 347 | name = "signal-hook-registry" 348 | version = "1.3.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" 351 | dependencies = [ 352 | "libc", 353 | ] 354 | 355 | [[package]] 356 | name = "smallvec" 357 | version = "1.6.1" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 360 | 361 | [[package]] 362 | name = "socket2" 363 | version = "0.3.19" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" 366 | dependencies = [ 367 | "cfg-if", 368 | "libc", 369 | "winapi", 370 | ] 371 | 372 | [[package]] 373 | name = "time" 374 | version = "0.1.44" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 377 | dependencies = [ 378 | "libc", 379 | "wasi 0.10.0+wasi-snapshot-preview1", 380 | "winapi", 381 | ] 382 | 383 | [[package]] 384 | name = "toml" 385 | version = "0.5.8" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 388 | dependencies = [ 389 | "serde", 390 | ] 391 | 392 | [[package]] 393 | name = "tui" 394 | version = "0.14.0" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "9ced152a8e9295a5b168adc254074525c17ac4a83c90b2716274cc38118bddc9" 397 | dependencies = [ 398 | "bitflags", 399 | "cassowary", 400 | "crossterm 0.18.2", 401 | "unicode-segmentation", 402 | "unicode-width", 403 | ] 404 | 405 | [[package]] 406 | name = "unicode-segmentation" 407 | version = "1.7.1" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" 410 | 411 | [[package]] 412 | name = "unicode-width" 413 | version = "0.1.8" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 416 | 417 | [[package]] 418 | name = "wasi" 419 | version = "0.9.0+wasi-snapshot-preview1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 422 | 423 | [[package]] 424 | name = "wasi" 425 | version = "0.10.0+wasi-snapshot-preview1" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 428 | 429 | [[package]] 430 | name = "winapi" 431 | version = "0.3.9" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 434 | dependencies = [ 435 | "winapi-i686-pc-windows-gnu", 436 | "winapi-x86_64-pc-windows-gnu", 437 | ] 438 | 439 | [[package]] 440 | name = "winapi-i686-pc-windows-gnu" 441 | version = "0.4.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 444 | 445 | [[package]] 446 | name = "winapi-x86_64-pc-windows-gnu" 447 | version = "0.4.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 450 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sfm" 3 | version = "0.5.0" 4 | authors = ["Szymon "] 5 | edition = "2018" 6 | license = "MIT" 7 | keywords = ["file-manager", "cli", "windows", "macOS", "linux"] 8 | categories = ["command-line-utilities"] 9 | repository = "https://github.com/Harunx9/sfm" 10 | description = "Simple two-panel file manager written in Rust inspired by Vim and Total Commander" 11 | 12 | [dependencies] 13 | crossterm = { version = "0.19.0", default-features = false } 14 | tui = { version = "0.14.0", default-features = false, features = ["crossterm"] } 15 | chrono = "0.4.19" 16 | dirs = "3.0.1" 17 | toml = "0.5.0" 18 | lazy_static = "1.4.0" 19 | 20 | [[bin]] 21 | name = "sfm" 22 | test = false 23 | bench = false 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Szymon Wanot 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 | # sfm - simple file manager 2 | 3 | > Simple two-panel file manager written in Rust inspired by vim and Total Commander 4 | 5 | ![screenshot](./screen.png) 6 | 7 | --- 8 | 9 | ## _Warning: Current status is "work in progress" if you run in some problem please create issue and I will check problem_ 10 | 11 | ## Features: 12 | 13 | In order to get icons please install nerd font in your system (on screen Jetbrains Mono Nerd Font Mono) 14 | 15 | 1. Current features 16 | - File management 17 | - Add file or directory 18 | - Remove file or directory 19 | - Move file or dir between panels 20 | - Open file in vi or others editor 21 | - Rename file or dir 22 | - Copy file or dir 23 | - Create symlink 24 | - Tab management 25 | - Open as tab (tabs are indicated on top of panel) 26 | - Navigate between tabs 27 | - Close tabs 28 | - Easy toml config file 29 | - Panel filtering 30 | - Select multiple items 31 | - Auto-reload on dir content change 32 | 33 | ## Keyboard config 34 | 35 | - `h` - focus left panel 36 | - `l` - focus right panel 37 | - `j` - next item 38 | - `k` - prev item 39 | - `ctrl + r` - open rename modal 40 | - `ctrl + l` - move selected item from left to right panel 41 | - `ctrl + h` - move selected item from right to left panel 42 | - `ctrl + c` - open create modal on focused panel 43 | - `ctrl + q` - quit program 44 | - `ctrl + o` - open dir in tab 45 | - `ctrl + x` - copy selected item from left panel to right panel 46 | - `ctrl + z` - copy selected item from right panel to left panel 47 | - `ctrl + s` - search in focused panel 48 | - `ctrl + j` - select next item 49 | - `ctrl + k` - select prev item 50 | - `o` - open dir or file(default: vi) 51 | - `n` - next tab 52 | - `p` - prev tab 53 | - `backspace` - navigate to dir parent 54 | - `esc` - close modal 55 | - `enter` - select modal option 56 | 57 | ## Configuration File 58 | 59 | Configuration file should be named `sfm.toml` and should be placed in `~/` or `~/.config` directories. 60 | 61 | - ### [core] section 62 | 63 | - tick_rate - update loop interval (default 240) 64 | - use_icons - turn on/off icons. Icons require NerdFonts to be installed (default false) 65 | 66 | - ### [color_scheme] section 67 | 68 | - Color names: 69 | - foreground 70 | - background 71 | - normal_black 72 | - normal_red 73 | - normal_green 74 | - normal_yellow 75 | - normal_blue 76 | - normal_magneta 77 | - normal_cyan 78 | - normal_white 79 | - light_black 80 | - light_red 81 | - light_green 82 | - light_yellow 83 | - light_blue 84 | - light_magneta 85 | - light_cyan 86 | - light_white 87 | - Color format: 88 | - Names: 89 | - Reset 90 | - Black 91 | - Red 92 | - Green 93 | - Yellow 94 | - Blue 95 | - Magneta 96 | - Cyan 97 | - Gray 98 | - DarkGrey 99 | - LightRed 100 | - LightGreen 101 | - LightYellow 102 | - LightBlue 103 | - LightCyan 104 | - White 105 | - RGB: 106 | - eg. `foreground = { red = 255, blue = 100, green = 35 }` 107 | - Indexed 108 | - eg. `background = 2` 109 | 110 | - ### [keyboard_cfg] section 111 | 112 | - Default config 113 | - `quit = { key = "q", modifier = "C" }` 114 | - `focus_left_panel = { key = "h" }` 115 | - `focus_right_panel = { key = "l" }` 116 | - `move_down = { key = "j" }` 117 | - `move_up = { key = "k" }` 118 | - `next_tab = { key = "n" }` 119 | - `prev_tab = { key = "p" }` 120 | - `close = { key = "esc" }` 121 | - `open = { key = "o" }` 122 | - `open_as_tab = { key = "o", modifier = "C" }` 123 | - `navigate_up = { key = "backspace" }` 124 | - `delete = { key = "d", modifier = "C" }` 125 | - `move_left = { key = "h", modifier = "C" }` 126 | - `move_right = { key = "l", modifier = "C" }` 127 | - `rename = { key = "r", modifier = "C" }` 128 | - `create = { key = "c", modifier = "C" }` 129 | - `accept = { key = "enter" }` 130 | - `copy_to_right = { key = "x", modifier = "C" }` 131 | - `copy_to_left = { key = "z", modifier = "C" }` 132 | 133 | - ### [icons_dir] section 134 | - In order to see icons you need nerd font patch. See in sfm.toml in repo root. 135 | - ### [icons_files] section 136 | 137 | - In order to see icons you need nerd font patch. See in sfm.toml in repo root. 138 | 139 | - ### [file_associated_programs] section 140 | - Key value pair with file extension and programs in default config all files will be opened in `vi` 141 | - eg. `rs = "nvim"` 142 | 143 | ## Installation 144 | 145 | - Via Cargo 146 | 147 | ```bash 148 | cargo install sfm 149 | 150 | ``` 151 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Harunx9/sfm/42dd3356dc0cb2b8c789d422c65817525ba7d146/screen.png -------------------------------------------------------------------------------- /sfm.toml: -------------------------------------------------------------------------------- 1 | #Example configuration 2 | #Core configuration 3 | [core] 4 | tick_rate = 240 5 | use_icons = true 6 | 7 | [color_scheme] 8 | foregorund = "White" 9 | background = "Reset" 10 | normal_black = "Black" 11 | normal_red = "Red" 12 | normal_green = "Green" 13 | normal_yellow = "Yellow" 14 | normal_blue = "Blue" 15 | normal_magneta = "Magneta" 16 | normal_cyan = "Cyan" 17 | normal_white = "White" 18 | light_black = "Gray" 19 | light_red = "LightRed" 20 | light_green = "LightGreen" 21 | light_yellow = "LightYellow" 22 | light_blue = "LightBlue" 23 | light_magneta = "LightMagenta" 24 | light_cyan = "LightCyan" 25 | light_white = "White" 26 | 27 | [keyboard_cfg] 28 | quit = { key = "q", modifier = "C" } 29 | focus_left_panel = { key = "h" } 30 | focus_right_panel = { key = "l" } 31 | move_down = { key = "j" } 32 | move_up = { key = "k" } 33 | next_tab = { key = "n" } 34 | prev_tab = { key = "p" } 35 | close = { key = "esc" } 36 | open = { key = "o" } 37 | open_as_tab = { key = "o", modifier = "C" } 38 | navigate_up = { key = "backspace" } 39 | delete = { key = "d", modifier = "C" } 40 | move_left = { key = "h", modifier = "C" } 41 | move_right = { key = "l", modifier = "C" } 42 | rename = { key = "r", modifier = "C" } 43 | create = { key = "c", modifier = "C" } 44 | accept = { key = "enter" } 45 | copy_to_left = { key = "z", modifier = "C" } 46 | copy_to_right = { key = "x", modifier = "C" } 47 | search_in_panel = { key = "s", modifier = "C" } 48 | select_prev = { key = "j", modifier = "C" } 49 | select_next = { key = "k", modifier = "C" } 50 | 51 | [icons_dir] 52 | ".git" = "" 53 | node_modules = "" 54 | default = "" 55 | 56 | [icons_files] 57 | ".gitignore" = "" 58 | ".gitmodules" = "" 59 | rs = "" 60 | cs = "" 61 | cpp = "ﭱ" 62 | c = "" 63 | hpp = "" 64 | h = "" 65 | js = "" 66 | ts = "" 67 | jsx = "" 68 | tsx = "ﰆ" 69 | html = "" 70 | css = "" 71 | sass = "" 72 | toml = "" 73 | yaml = "" 74 | php = "" 75 | py = "" 76 | rb = "" 77 | java = "" 78 | lock = "" 79 | default = "" 80 | 81 | [file_associated_programs] 82 | default = "vi" 83 | -------------------------------------------------------------------------------- /src/app/actions.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::state::{ModalType, TabIdx}; 4 | 5 | #[derive(Clone, Debug)] 6 | pub enum FileManagerActions { 7 | File(FileAction), 8 | Directory(DirectoryAction), 9 | Symlink(SymlinkAction), 10 | App(AppAction), 11 | Panel(PanelAction), 12 | Tab(TabAction), 13 | Search(SearchAction), 14 | } 15 | 16 | #[derive(Clone, Debug)] 17 | pub enum SearchAction { 18 | Start { 19 | tab: TabIdx, 20 | panel_side: PanelSide, 21 | }, 22 | Stop { 23 | tab: TabIdx, 24 | panel_side: PanelSide, 25 | }, 26 | Input { 27 | tab: TabIdx, 28 | panel_side: PanelSide, 29 | phrase: String, 30 | }, 31 | ApplySearch { 32 | tab: TabIdx, 33 | panel_side: PanelSide, 34 | }, 35 | } 36 | 37 | #[derive(Clone, Debug)] 38 | pub enum AppAction { 39 | Exit, 40 | ChildProgramClosed, 41 | FocusLeft, 42 | FocusRight, 43 | ShowModal(ModalType), 44 | CloseModal, 45 | } 46 | 47 | #[derive(Clone, Debug)] 48 | pub enum PanelAction { 49 | Next { panel: PanelSide }, 50 | Previous { panel: PanelSide }, 51 | CloseTab { tab: TabIdx, panel: PanelSide }, 52 | } 53 | 54 | #[derive(Clone, Debug)] 55 | pub enum TabAction { 56 | Next, 57 | Previous, 58 | SelectNext, 59 | SelectPrev, 60 | ClearSelection, 61 | ReloadTab { 62 | panel_side: PanelSide, 63 | path: PathBuf, 64 | }, 65 | } 66 | 67 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] 68 | pub enum PanelSide { 69 | Left, 70 | Right, 71 | } 72 | 73 | #[derive(Clone, Debug)] 74 | pub struct PanelInfo { 75 | pub path: PathBuf, 76 | pub tab: TabIdx, 77 | pub side: PanelSide, 78 | } 79 | 80 | impl PartialEq for PanelInfo { 81 | fn eq(&self, other: &PanelInfo) -> bool { 82 | self.side == other.side && self.tab == other.tab 83 | } 84 | } 85 | 86 | #[derive(Clone, Debug)] 87 | pub enum FileAction { 88 | Delete { panel: PanelInfo }, 89 | Rename { from: PanelInfo, to: PanelInfo }, 90 | Copy { from: PanelInfo, to: PanelInfo }, 91 | Move { from: PanelInfo, to: PanelInfo }, 92 | Open { panel: PanelInfo }, 93 | Create { file_name: String, panel: PanelInfo }, 94 | } 95 | 96 | #[derive(Clone, Debug)] 97 | pub enum DirectoryAction { 98 | DeleteWithContent { panel: PanelInfo }, 99 | Delete { panel: PanelInfo, is_empty: bool }, 100 | Rename { from: PanelInfo, to: PanelInfo }, 101 | Copy { from: PanelInfo, to: PanelInfo }, 102 | Move { from: PanelInfo, to: PanelInfo }, 103 | Open { panel: PanelInfo, in_new_tab: bool }, 104 | Create { dir_name: String, panel: PanelInfo }, 105 | } 106 | 107 | #[derive(Clone, Debug)] 108 | pub enum SymlinkAction { 109 | Delete { 110 | panel: PanelInfo, 111 | }, 112 | Open { 113 | panel: PanelInfo, 114 | in_new_tab: bool, 115 | }, 116 | Create { 117 | symlink_path: PathBuf, 118 | panel: PanelInfo, 119 | }, 120 | } 121 | -------------------------------------------------------------------------------- /src/app/components/create_modal.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, path::PathBuf}; 2 | 3 | use crossterm::event::{KeyCode, KeyModifiers}; 4 | use tui::{ 5 | style::Style, 6 | text::{Span, Spans}, 7 | widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph}, 8 | }; 9 | 10 | use crate::{ 11 | app::{ 12 | actions::{ 13 | AppAction, DirectoryAction, FileAction, FileManagerActions, PanelInfo, PanelSide, 14 | SymlinkAction, 15 | }, 16 | file_system::FileSystem, 17 | state::{AppState, TabIdx}, 18 | }, 19 | core::{ 20 | events::Event, 21 | store::Store, 22 | ui::{component::Component, component_base::ComponentBase}, 23 | }, 24 | }; 25 | 26 | use super::{create_modal_layout, ModalStyle}; 27 | 28 | #[derive(Clone, Default)] 29 | pub struct CreateModalProps { 30 | panel_side: Option, 31 | item_to_symlink: Option, 32 | panel_tab: TabIdx, 33 | dir_path: PathBuf, 34 | show_icons: bool, 35 | file_icon: String, 36 | dir_icon: String, 37 | symlink_icon: String, 38 | list_selector: String, 39 | modal_style: ModalStyle, 40 | } 41 | 42 | impl CreateModalProps { 43 | pub fn new( 44 | panel_side: PanelSide, 45 | panel_tab: TabIdx, 46 | dir_path: PathBuf, 47 | item_to_symlink: Option, 48 | show_icons: bool, 49 | file_icon: String, 50 | dir_icon: String, 51 | symlink_icon: String, 52 | list_selector: String, 53 | modal_style: ModalStyle, 54 | ) -> Self { 55 | Self { 56 | item_to_symlink, 57 | panel_side: Some(panel_side), 58 | panel_tab, 59 | dir_path, 60 | show_icons, 61 | file_icon, 62 | dir_icon, 63 | symlink_icon, 64 | list_selector, 65 | modal_style, 66 | } 67 | } 68 | } 69 | 70 | #[derive(Clone, Copy, PartialEq, Eq)] 71 | pub enum CreateOption { 72 | File, 73 | Dir, 74 | Symlink, 75 | } 76 | 77 | impl ToString for CreateOption { 78 | fn to_string(&self) -> String { 79 | match self { 80 | CreateOption::File => "File".to_string(), 81 | CreateOption::Dir => "Directory".to_string(), 82 | CreateOption::Symlink => "Symlink".to_string(), 83 | } 84 | } 85 | } 86 | 87 | impl From for CreateOption { 88 | fn from(source: String) -> Self { 89 | match source.as_str() { 90 | "File" => CreateOption::File, 91 | "Directory" => CreateOption::Dir, 92 | "Symlink" => CreateOption::Symlink, 93 | _ => panic!(""), 94 | } 95 | } 96 | } 97 | 98 | impl From for CreateOption { 99 | fn from(source: usize) -> Self { 100 | match source { 101 | 0 => CreateOption::File, 102 | 1 => CreateOption::Dir, 103 | 2 => CreateOption::Symlink, 104 | _ => panic!(""), 105 | } 106 | } 107 | } 108 | 109 | #[derive(Clone, Default)] 110 | pub struct CreateModalState { 111 | create_selection: Option, 112 | input: String, 113 | list_state: ListState, 114 | } 115 | 116 | pub struct CreateModalComponent { 117 | base: ComponentBase, 118 | _maker: std::marker::PhantomData, 119 | } 120 | 121 | impl CreateModalComponent { 122 | pub fn with_props(props: CreateModalProps) -> Self { 123 | CreateModalComponent { 124 | base: ComponentBase::new(Some(props), Some(CreateModalState::default())), 125 | _maker: std::marker::PhantomData, 126 | } 127 | } 128 | } 129 | 130 | impl 131 | Component, FileManagerActions> 132 | for CreateModalComponent 133 | { 134 | fn handle_event( 135 | &mut self, 136 | event: Event, 137 | store: &mut Store, FileManagerActions>, 138 | ) -> bool { 139 | let state = store.get_state(); 140 | let local_state = self.base.get_state().unwrap(); 141 | let props = self.base.get_props().unwrap(); 142 | if let Event::Keyboard(key_evt) = event { 143 | if local_state.create_selection.is_none() { 144 | if state.config.keyboard_cfg.move_up.is_pressed(key_evt) { 145 | let next_item = match local_state.list_state.selected() { 146 | Some(current) => { 147 | if current == 0 { 148 | 2 149 | } else { 150 | current - 1 151 | } 152 | } 153 | None => 0, 154 | }; 155 | self.base.set_state(|mut current_state| { 156 | current_state.list_state.select(Some(next_item)); 157 | CreateModalState { 158 | list_state: current_state.list_state, 159 | ..current_state 160 | } 161 | }); 162 | return true; 163 | } 164 | 165 | if state.config.keyboard_cfg.move_down.is_pressed(key_evt) { 166 | let next_item = match local_state.list_state.selected() { 167 | Some(current) => { 168 | if current >= 2 { 169 | 0 170 | } else { 171 | current + 1 172 | } 173 | } 174 | None => 0, 175 | }; 176 | self.base.set_state(|mut current_state| { 177 | current_state.list_state.select(Some(next_item)); 178 | CreateModalState { 179 | list_state: current_state.list_state, 180 | ..current_state 181 | } 182 | }); 183 | return true; 184 | } 185 | 186 | if state.config.keyboard_cfg.accept.is_pressed(key_evt) { 187 | self.base.set_state(|current_state| { 188 | let create_selection = 189 | CreateOption::from(current_state.list_state.selected().unwrap_or(0)); 190 | CreateModalState { 191 | create_selection: Some(create_selection), 192 | ..current_state 193 | } 194 | }); 195 | } 196 | } else if let Some(create_selection) = local_state.create_selection { 197 | if state.config.keyboard_cfg.accept.is_pressed(key_evt) 198 | && local_state.input.is_empty() == false 199 | { 200 | let panel_side = props.panel_side.unwrap(); 201 | match create_selection { 202 | CreateOption::File => { 203 | store.dispatch(FileManagerActions::File(FileAction::Create { 204 | file_name: local_state.input.clone(), 205 | panel: PanelInfo { 206 | side: panel_side, 207 | tab: props.panel_tab, 208 | path: props.dir_path, 209 | }, 210 | })) 211 | } 212 | CreateOption::Dir => { 213 | store.dispatch(FileManagerActions::Directory(DirectoryAction::Create { 214 | dir_name: local_state.input.clone(), 215 | panel: PanelInfo { 216 | side: panel_side, 217 | tab: props.panel_tab, 218 | path: props.dir_path, 219 | }, 220 | })) 221 | } 222 | CreateOption::Symlink => { 223 | let item_path = match props.panel_side.unwrap() { 224 | PanelSide::Left => state.left_panel.tabs[props.panel_tab].items 225 | [props.item_to_symlink.unwrap()] 226 | .get_path(), 227 | PanelSide::Right => state.right_panel.tabs[props.panel_tab].items 228 | [props.item_to_symlink.unwrap()] 229 | .get_path(), 230 | }; 231 | 232 | store.dispatch(FileManagerActions::Symlink(SymlinkAction::Create { 233 | symlink_path: PathBuf::from(local_state.input.clone()), 234 | panel: PanelInfo { 235 | path: item_path, 236 | side: panel_side, 237 | tab: props.panel_tab, 238 | }, 239 | })) 240 | } 241 | }; 242 | 243 | store.dispatch(FileManagerActions::App(AppAction::CloseModal)); 244 | return true; 245 | } 246 | 247 | match key_evt.code { 248 | KeyCode::Char(c) => { 249 | self.base.set_state(|current_state| { 250 | let mut current_text = current_state.input.clone(); 251 | if key_evt.modifiers == KeyModifiers::SHIFT { 252 | current_text = 253 | format!("{}{}", current_text, c.to_uppercase().to_string()); 254 | } else { 255 | current_text.push(c); 256 | } 257 | 258 | CreateModalState { 259 | input: current_text, 260 | ..current_state 261 | } 262 | }); 263 | return true; 264 | } 265 | KeyCode::Backspace => { 266 | self.base.set_state(|current_state| { 267 | let mut current_text = current_state.input.clone(); 268 | current_text.pop(); 269 | 270 | CreateModalState { 271 | input: current_text, 272 | ..current_state 273 | } 274 | }); 275 | return true; 276 | } 277 | _ => {} 278 | }; 279 | } 280 | 281 | if state.config.keyboard_cfg.close.is_pressed(key_evt) { 282 | store.dispatch(FileManagerActions::App(AppAction::CloseModal)); 283 | return true; 284 | } 285 | } 286 | false 287 | } 288 | 289 | fn render( 290 | &self, 291 | frame: &mut tui::Frame, 292 | area: Option, 293 | ) { 294 | let layout = if let Some(area) = area { 295 | create_modal_layout(50, 10, area) 296 | } else { 297 | create_modal_layout(50, 10, frame.size()) 298 | }; 299 | 300 | let mut local_state = self.base.get_state().unwrap(); 301 | let props = self.base.get_props().unwrap(); 302 | 303 | if let Some(create_selection) = local_state.create_selection { 304 | let block = Block::default() 305 | .title(Spans::from(vec![ 306 | Span::from("| "), 307 | if create_selection == CreateOption::Symlink { 308 | Span::from("Symlink path:") 309 | } else { 310 | Span::from("Item name:") 311 | }, 312 | Span::from(" |"), 313 | ])) 314 | .borders(Borders::ALL) 315 | .border_style(Style::default().fg(props.modal_style.border_color)) 316 | .border_type(tui::widgets::BorderType::Thick) 317 | .style(Style::default()); 318 | 319 | let paragraph = Paragraph::new(local_state.input) 320 | .block(block) 321 | .alignment(tui::layout::Alignment::Center); 322 | 323 | frame.render_widget(Clear, layout); 324 | frame.render_widget(paragraph, layout); 325 | } else { 326 | let mut items = if props.show_icons { 327 | vec![ 328 | ListItem::new(Spans::from(vec![ 329 | Span::from(props.file_icon), 330 | Span::from(" "), 331 | Span::from(CreateOption::File.to_string()), 332 | ])), 333 | ListItem::new(Spans::from(vec![ 334 | Span::from(props.dir_icon), 335 | Span::from(" "), 336 | Span::from(CreateOption::Dir.to_string()), 337 | ])), 338 | ] 339 | } else { 340 | vec![ 341 | ListItem::new(Spans::from(vec![Span::from( 342 | CreateOption::File.to_string(), 343 | )])), 344 | ListItem::new(Spans::from(vec![Span::from(CreateOption::Dir.to_string())])), 345 | ] 346 | }; 347 | 348 | if props.item_to_symlink.is_some() { 349 | if props.show_icons { 350 | items.push(ListItem::new(Spans::from(vec![ 351 | Span::from(props.symlink_icon), 352 | Span::from(" "), 353 | Span::from(CreateOption::Symlink.to_string()), 354 | ]))); 355 | } else { 356 | items.push(ListItem::new(Spans::from(vec![Span::from( 357 | CreateOption::Symlink.to_string(), 358 | )]))); 359 | } 360 | } 361 | 362 | let block = Block::default() 363 | .title(Spans::from(vec![ 364 | Span::from("| "), 365 | Span::from("Chose item to create:"), 366 | Span::from(" |"), 367 | ])) 368 | .borders(Borders::ALL) 369 | .border_style(Style::default().fg(props.modal_style.border_color)) 370 | .border_type(tui::widgets::BorderType::Thick) 371 | .style(Style::default().bg(tui::style::Color::Reset)); 372 | 373 | let list = List::new(items) 374 | .block(block) 375 | .highlight_style( 376 | Style::default() 377 | .fg(props.modal_style.selected_element_foreground) 378 | .bg(props.modal_style.selected_element_background), 379 | ) 380 | .highlight_symbol(props.list_selector.as_str()); 381 | 382 | frame.render_widget(Clear, layout); 383 | frame.render_stateful_widget(list, layout, &mut local_state.list_state); 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/app/components/error_modal.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app::{ 3 | actions::{AppAction, FileManagerActions}, 4 | file_system::FileSystem, 5 | state::AppState, 6 | }, 7 | core::{ 8 | events::Event, 9 | store::Store, 10 | ui::{component::Component, component_base::ComponentBase}, 11 | }, 12 | }; 13 | use std::fmt::Debug; 14 | use tui::{ 15 | style::Style, 16 | text::{Span, Spans}, 17 | widgets::{Block, BorderType, Borders, Clear, Paragraph}, 18 | }; 19 | 20 | use super::create_modal_layout; 21 | 22 | #[derive(Clone, Default)] 23 | pub struct ErrorModalComponentProps { 24 | message: Option, 25 | show_icons: bool, 26 | error_icon: String, 27 | } 28 | 29 | impl ErrorModalComponentProps { 30 | pub fn new(message: String, show_icons: bool, error_icon: String) -> Self { 31 | ErrorModalComponentProps { 32 | message: Some(message), 33 | show_icons, 34 | error_icon, 35 | } 36 | } 37 | } 38 | 39 | pub struct ErrorModalComponent { 40 | base: ComponentBase, 41 | _maker: std::marker::PhantomData, 42 | } 43 | 44 | impl ErrorModalComponent { 45 | pub fn with_props(props: ErrorModalComponentProps) -> Self { 46 | ErrorModalComponent { 47 | base: ComponentBase::new(Some(props), None), 48 | _maker: std::marker::PhantomData, 49 | } 50 | } 51 | } 52 | 53 | impl 54 | Component, FileManagerActions> 55 | for ErrorModalComponent 56 | { 57 | fn handle_event( 58 | &mut self, 59 | event: Event, 60 | store: &mut Store, FileManagerActions>, 61 | ) -> bool { 62 | let state = store.get_state(); 63 | if let Event::Keyboard(key_evt) = event { 64 | if state.config.keyboard_cfg.close.is_pressed(key_evt) { 65 | store.dispatch(FileManagerActions::App(AppAction::CloseModal)); 66 | return true; 67 | } 68 | } 69 | 70 | false 71 | } 72 | 73 | fn render( 74 | &self, 75 | frame: &mut tui::Frame, 76 | area: Option, 77 | ) { 78 | let layout = if let Some(area) = area { 79 | create_modal_layout(50, 10, area) 80 | } else { 81 | create_modal_layout(50, 10, frame.size()) 82 | }; 83 | let props = self.base.get_props().unwrap(); 84 | let message = if let Some(message) = props.message { 85 | message.clone() 86 | } else { 87 | "".to_string() 88 | }; 89 | let block = Block::default() 90 | .title(Spans::from(vec![ 91 | Span::from("| "), 92 | Span::from("Error: (Esc to close)"), 93 | Span::from(" |"), 94 | ])) 95 | .borders(Borders::ALL) 96 | .border_style(Style::default()) 97 | .border_type(BorderType::Thick) 98 | .style(Style::default()); 99 | 100 | let paragraph = Paragraph::new(message) 101 | .block(block) 102 | .alignment(tui::layout::Alignment::Center); 103 | 104 | frame.render_widget(Clear, layout); 105 | frame.render_widget(paragraph, layout); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/app/components/mod.rs: -------------------------------------------------------------------------------- 1 | use tui::{ 2 | layout::{Constraint, Direction, Layout, Rect}, 3 | style::Color, 4 | }; 5 | 6 | pub mod create_modal; 7 | pub mod error_modal; 8 | pub mod not_empty_dir_delete_modal; 9 | pub mod panel; 10 | pub mod rename_modal; 11 | pub mod root; 12 | pub mod tab; 13 | 14 | fn create_modal_layout(x_percent: u16, y_percent: u16, rect: Rect) -> Rect { 15 | let vertical_slice = Layout::default() 16 | .direction(Direction::Vertical) 17 | .constraints( 18 | [ 19 | Constraint::Percentage((100 - y_percent) / 2), 20 | Constraint::Percentage(y_percent), 21 | Constraint::Percentage((100 - y_percent) / 2), 22 | ] 23 | .as_ref(), 24 | ) 25 | .split(rect); 26 | 27 | Layout::default() 28 | .direction(Direction::Horizontal) 29 | .constraints( 30 | [ 31 | Constraint::Percentage((100 - x_percent) / 2), 32 | Constraint::Percentage(x_percent), 33 | Constraint::Percentage((100 - x_percent) / 2), 34 | ] 35 | .as_ref(), 36 | ) 37 | .split(vertical_slice[1])[1] 38 | } 39 | 40 | #[derive(Clone)] 41 | pub struct ModalStyle { 42 | pub border_color: Color, 43 | pub selected_element_background: Color, 44 | pub selected_element_foreground: Color, 45 | } 46 | 47 | impl ModalStyle { 48 | pub fn new( 49 | border_color: Color, 50 | selected_element_background: Color, 51 | selected_element_foreground: Color, 52 | ) -> Self { 53 | Self { 54 | border_color, 55 | selected_element_background, 56 | selected_element_foreground, 57 | } 58 | } 59 | } 60 | 61 | impl Default for ModalStyle { 62 | fn default() -> Self { 63 | ModalStyle { 64 | border_color: Color::Red, 65 | selected_element_background: Color::Yellow, 66 | selected_element_foreground: Color::Black, 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/components/not_empty_dir_delete_modal.rs: -------------------------------------------------------------------------------- 1 | use tui::{ 2 | backend::Backend, 3 | layout::Rect, 4 | style::Style, 5 | text::{Span, Spans}, 6 | widgets::{Block, Borders, Clear, List, ListItem, ListState}, 7 | Frame, 8 | }; 9 | 10 | use crate::{ 11 | app::{ 12 | actions::{AppAction, PanelSide}, 13 | state::TabIdx, 14 | }, 15 | core::{store::Store, ui::component_base::ComponentBase}, 16 | }; 17 | use crate::{ 18 | app::{ 19 | actions::{DirectoryAction, FileManagerActions, PanelInfo}, 20 | file_system::FileSystem, 21 | state::AppState, 22 | }, 23 | core::{events::Event, ui::component::Component}, 24 | }; 25 | use std::{fmt::Debug, marker::PhantomData, path::PathBuf}; 26 | 27 | use super::{create_modal_layout, ModalStyle}; 28 | 29 | #[derive(Clone, Default)] 30 | pub struct NotEmptyDirDeleteModalComponentProps { 31 | panel_side: Option, 32 | panel_tab: TabIdx, 33 | path: PathBuf, 34 | list_selector: String, 35 | modal_style: ModalStyle, 36 | } 37 | 38 | impl NotEmptyDirDeleteModalComponentProps { 39 | pub fn new( 40 | panel_side: Option, 41 | panel_tab: TabIdx, 42 | path: PathBuf, 43 | list_selector: String, 44 | modal_style: ModalStyle, 45 | ) -> Self { 46 | NotEmptyDirDeleteModalComponentProps { 47 | panel_side, 48 | panel_tab, 49 | path, 50 | list_selector, 51 | modal_style, 52 | } 53 | } 54 | } 55 | 56 | #[derive(Clone, Copy, PartialEq, Eq)] 57 | pub enum Options { 58 | Ok, 59 | Cancel, 60 | } 61 | 62 | impl ToString for Options { 63 | fn to_string(&self) -> String { 64 | match self { 65 | Options::Ok => "Ok".to_string(), 66 | Options::Cancel => "Cancel".to_string(), 67 | } 68 | } 69 | } 70 | 71 | impl From for Options { 72 | fn from(source: String) -> Self { 73 | match source.as_str() { 74 | "Ok" => Options::Ok, 75 | "Cancel" => Options::Cancel, 76 | _ => panic!(""), 77 | } 78 | } 79 | } 80 | 81 | impl From for Options { 82 | fn from(source: usize) -> Self { 83 | match source { 84 | 0 => Options::Ok, 85 | 1 => Options::Cancel, 86 | _ => panic!(""), 87 | } 88 | } 89 | } 90 | 91 | #[derive(Clone, Default)] 92 | pub struct NotEmptyDirDeleteModalComponentState { 93 | list_state: ListState, 94 | } 95 | 96 | pub struct NotEmptyDirDeleteModalComponent { 97 | base: ComponentBase, 98 | _marker: PhantomData, 99 | } 100 | 101 | impl 102 | NotEmptyDirDeleteModalComponent 103 | { 104 | pub fn new(props: NotEmptyDirDeleteModalComponentProps) -> Self { 105 | NotEmptyDirDeleteModalComponent { 106 | base: ComponentBase::new( 107 | Some(props), 108 | Some(NotEmptyDirDeleteModalComponentState::default()), 109 | ), 110 | _marker: PhantomData, 111 | } 112 | } 113 | } 114 | 115 | impl 116 | Component, FileManagerActions> 117 | for NotEmptyDirDeleteModalComponent 118 | { 119 | fn handle_event( 120 | &mut self, 121 | event: Event, 122 | store: &mut Store, FileManagerActions>, 123 | ) -> bool { 124 | let state = store.get_state(); 125 | let local_state = self.base.get_state().unwrap(); 126 | if let Event::Keyboard(key_evt) = event { 127 | if state.config.keyboard_cfg.move_up.is_pressed(key_evt) { 128 | let next_item = match local_state.list_state.selected() { 129 | Some(current) => { 130 | if current == 0 { 131 | 2 132 | } else { 133 | current - 1 134 | } 135 | } 136 | None => 0, 137 | }; 138 | self.base.set_state(|mut current_state| { 139 | current_state.list_state.select(Some(next_item)); 140 | NotEmptyDirDeleteModalComponentState { 141 | list_state: current_state.list_state, 142 | ..current_state 143 | } 144 | }); 145 | return true; 146 | } 147 | 148 | if state.config.keyboard_cfg.move_down.is_pressed(key_evt) { 149 | let next_item = match local_state.list_state.selected() { 150 | Some(current) => { 151 | if current >= 2 { 152 | 0 153 | } else { 154 | current + 1 155 | } 156 | } 157 | None => 0, 158 | }; 159 | self.base.set_state(|mut current_state| { 160 | current_state.list_state.select(Some(next_item)); 161 | NotEmptyDirDeleteModalComponentState { 162 | list_state: current_state.list_state, 163 | ..current_state 164 | } 165 | }); 166 | return true; 167 | } 168 | 169 | if state.config.keyboard_cfg.accept.is_pressed(key_evt) { 170 | if let Some(selected) = local_state.list_state.selected() { 171 | let props = self.base.get_props().unwrap(); 172 | let option = Options::from(selected); 173 | match option { 174 | Options::Ok => store.dispatch(FileManagerActions::Directory( 175 | DirectoryAction::DeleteWithContent { 176 | panel: PanelInfo { 177 | side: props.panel_side.unwrap(), 178 | tab: props.panel_tab, 179 | path: props.path, 180 | }, 181 | }, 182 | )), 183 | _ => {} 184 | } 185 | store.dispatch(FileManagerActions::App(AppAction::CloseModal)); 186 | } 187 | } 188 | 189 | if state.config.keyboard_cfg.close.is_pressed(key_evt) { 190 | store.dispatch(FileManagerActions::App(AppAction::CloseModal)); 191 | return true; 192 | } 193 | } 194 | false 195 | } 196 | 197 | fn render(&self, frame: &mut Frame, area: Option) { 198 | let layout = if let Some(area) = area { 199 | create_modal_layout(50, 10, area) 200 | } else { 201 | create_modal_layout(50, 10, frame.size()) 202 | }; 203 | 204 | let props = self.base.get_props().unwrap(); 205 | let mut local_state = self.base.get_state().unwrap(); 206 | 207 | let items = vec![ 208 | ListItem::new(Spans::from(vec![Span::from(Options::Ok.to_string())])), 209 | ListItem::new(Spans::from(vec![Span::from(Options::Cancel.to_string())])), 210 | ]; 211 | 212 | let block = Block::default() 213 | .title(Spans::from(vec![ 214 | Span::from("| "), 215 | Span::from("This directory is not empty do you want to remove it ?"), 216 | Span::from(" |"), 217 | ])) 218 | .borders(Borders::ALL) 219 | .border_style(Style::default().fg(props.modal_style.border_color)) 220 | .border_type(tui::widgets::BorderType::Thick) 221 | .style(Style::default().bg(tui::style::Color::Reset)); 222 | 223 | let list = List::new(items) 224 | .block(block) 225 | .highlight_style( 226 | Style::default() 227 | .bg(props.modal_style.selected_element_background) 228 | .fg(props.modal_style.selected_element_foreground), 229 | ) 230 | .highlight_symbol(props.list_selector.as_str()); 231 | 232 | frame.render_widget(Clear, layout); 233 | frame.render_stateful_widget(list, layout, &mut local_state.list_state); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/app/components/panel.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use tui::{ 3 | backend::Backend, 4 | layout::{Constraint, Layout, Rect}, 5 | style::{Color, Style}, 6 | text::{Span, Spans}, 7 | widgets::{Block, Borders, Tabs}, 8 | Frame, 9 | }; 10 | 11 | use crate::{ 12 | app::{ 13 | actions::{FileManagerActions, PanelAction, PanelSide}, 14 | config::icon_cfg::IconsConfig, 15 | file_system::FileSystem, 16 | state::{AppState, PanelState}, 17 | }, 18 | core::{ 19 | config::CoreConfig, 20 | events::Event, 21 | store::Store, 22 | ui::{component::Component, component_base::ComponentBase}, 23 | }, 24 | }; 25 | 26 | use super::tab::{TabComponent, TabComponentProps}; 27 | 28 | #[derive(Clone, Default, Debug)] 29 | pub struct PanelComponentProps { 30 | tabs: Vec, 31 | current_tab: usize, 32 | is_focused: bool, 33 | show_icons: bool, 34 | tab_search: bool, 35 | } 36 | 37 | #[derive(Clone, Default, Debug)] 38 | struct TabInfo { 39 | pub name: String, 40 | pub icon: String, 41 | } 42 | 43 | #[derive(Clone, Debug)] 44 | pub struct PanelComponentState { 45 | side: Option, 46 | } 47 | 48 | impl Default for PanelComponentState { 49 | fn default() -> Self { 50 | PanelComponentState { side: None } 51 | } 52 | } 53 | 54 | pub struct PanelStyle { 55 | active_border_color: Color, 56 | active_tab_bg: Color, 57 | active_tab_fg: Color, 58 | } 59 | 60 | impl Default for PanelStyle { 61 | fn default() -> Self { 62 | PanelStyle { 63 | active_border_color: Color::Blue, 64 | active_tab_bg: Color::Red, 65 | active_tab_fg: Color::Black, 66 | } 67 | } 68 | } 69 | 70 | pub struct PanelComponent { 71 | base: ComponentBase, 72 | tab: TabComponent, 73 | style: PanelStyle, 74 | _marker: std::marker::PhantomData, 75 | } 76 | 77 | impl PanelComponent { 78 | pub fn new( 79 | props: PanelComponentProps, 80 | state: PanelComponentState, 81 | tab: TabComponent, 82 | ) -> Self { 83 | PanelComponent { 84 | base: ComponentBase::new(Some(props), Some(state)), 85 | tab, 86 | style: PanelStyle::default(), 87 | _marker: std::marker::PhantomData, 88 | } 89 | } 90 | 91 | pub fn empty() -> Self { 92 | PanelComponent { 93 | base: ComponentBase::new(None, None), 94 | tab: TabComponent::empty(), 95 | style: PanelStyle::default(), 96 | _marker: std::marker::PhantomData, 97 | } 98 | } 99 | 100 | pub fn with_panel_state( 101 | panel_state: PanelState, 102 | side: PanelSide, 103 | icons: &IconsConfig, 104 | core: &CoreConfig, 105 | ) -> Self { 106 | let tabs: Vec<_> = panel_state 107 | .tabs 108 | .iter() 109 | .map(|tab| TabInfo { 110 | name: tab.name.clone(), 111 | icon: icons.get_dir_icon(tab.name.clone()), 112 | }) 113 | .collect(); 114 | let tab_state = panel_state.tabs[panel_state.current_tab].clone(); 115 | let has_displayed_tabs = tabs.is_empty() == false; 116 | let panel_props = PanelComponentProps { 117 | tabs, 118 | current_tab: panel_state.current_tab, 119 | is_focused: panel_state.is_focused, 120 | show_icons: icons.use_icons, 121 | tab_search: tab_state.search_mode, 122 | }; 123 | 124 | let state = PanelComponentState { 125 | side: Some(side.clone()), 126 | }; 127 | 128 | let tab = TabComponent::new( 129 | Some(TabComponentProps::new( 130 | tab_state, 131 | has_displayed_tabs, 132 | panel_state.is_focused, 133 | side, 134 | icons.use_icons, 135 | core.list_arrow.clone(), 136 | )), 137 | None, 138 | ); 139 | 140 | PanelComponent::new(panel_props, state, tab) 141 | } 142 | 143 | pub fn tab_in_search_mode(&self) -> bool { 144 | self.base.get_props().unwrap().tab_search 145 | } 146 | } 147 | 148 | impl 149 | Component, FileManagerActions> for PanelComponent 150 | { 151 | fn on_tick(&mut self, store: &mut Store, FileManagerActions>) { 152 | self.tab.on_tick(store); 153 | } 154 | 155 | fn handle_event( 156 | &mut self, 157 | event: Event, 158 | store: &mut Store, FileManagerActions>, 159 | ) -> bool { 160 | let state = store.get_state(); 161 | let props = self.base.get_props().unwrap(); 162 | let panel_side = self.base.get_state().unwrap().side.unwrap(); 163 | if props.tab_search == false { 164 | if let Event::Keyboard(key_evt) = event { 165 | if state.config.keyboard_cfg.next_tab.is_pressed(key_evt) 166 | && props.is_focused 167 | && props.tabs.len() > 1 168 | { 169 | store.dispatch(FileManagerActions::Panel(PanelAction::Next { 170 | panel: panel_side, 171 | })); 172 | return true; 173 | } 174 | 175 | if state.config.keyboard_cfg.prev_tab.is_pressed(key_evt) 176 | && props.is_focused 177 | && props.tabs.len() > 1 178 | { 179 | store.dispatch(FileManagerActions::Panel(PanelAction::Previous { 180 | panel: panel_side, 181 | })); 182 | return true; 183 | } 184 | 185 | if state.config.keyboard_cfg.close.is_pressed(key_evt) 186 | && props.is_focused 187 | && props.tabs.len() > 1 188 | { 189 | store.dispatch(FileManagerActions::Panel(PanelAction::CloseTab { 190 | panel: panel_side, 191 | tab: props.current_tab, 192 | })); 193 | return true; 194 | } 195 | } 196 | } 197 | 198 | self.tab.handle_event(event, store) 199 | } 200 | 201 | fn render(&self, frame: &mut Frame, area: Option) { 202 | let props = self.base.get_props().unwrap(); 203 | let show_icons = props.show_icons; 204 | if props.tabs.len() > 1 { 205 | let tabs_items: Vec = props 206 | .tabs 207 | .iter() 208 | .enumerate() 209 | .map(|(idx, val)| { 210 | if idx == props.current_tab { 211 | let style = Style::default() 212 | .fg(self.style.active_tab_fg) 213 | .bg(self.style.active_tab_bg); 214 | if show_icons { 215 | Spans::from(vec![ 216 | Span::styled(val.icon.clone(), style), 217 | Span::styled(" ", style), 218 | Span::styled(val.name.clone(), style), 219 | ]) 220 | } else { 221 | Spans::from(vec![Span::styled(val.name.clone(), style)]) 222 | } 223 | } else { 224 | if show_icons { 225 | Spans::from(vec![ 226 | Span::styled(val.icon.clone(), Style::default()), 227 | Span::styled(" ", Style::default()), 228 | Span::styled(val.name.clone(), Style::default()), 229 | ]) 230 | } else { 231 | Spans::from(vec![Span::styled(val.name.clone(), Style::default())]) 232 | } 233 | } 234 | }) 235 | .collect(); 236 | 237 | let style = Style::default(); 238 | if props.is_focused { 239 | style.fg(self.style.active_border_color); 240 | } 241 | 242 | let tabs = 243 | Tabs::new(tabs_items).block(Block::default().style(style).borders(Borders::ALL)); 244 | 245 | let layout = Layout::default() 246 | .constraints([Constraint::Length(3), Constraint::Min(0)]) 247 | .split(area.unwrap()); 248 | 249 | frame.render_widget(tabs, layout[0]); 250 | self.tab.render(frame, Some(layout[1])); 251 | } else { 252 | self.tab.render(frame, area); 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/app/components/rename_modal.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, path::PathBuf}; 2 | 3 | use crossterm::event::{KeyCode, KeyModifiers}; 4 | use tui::{ 5 | style::Style, 6 | text::{Span, Spans}, 7 | widgets::{Block, Borders, Clear, Paragraph}, 8 | }; 9 | 10 | use crate::{ 11 | app::{ 12 | actions::{ 13 | AppAction, DirectoryAction, FileAction, FileManagerActions, PanelInfo, PanelSide, 14 | }, 15 | file_system::{file_system_item::FileSystemItem, FileSystem}, 16 | state::{AppState, TabIdx}, 17 | }, 18 | core::{ 19 | events::Event, 20 | store::Store, 21 | ui::{component::Component, component_base::ComponentBase}, 22 | }, 23 | }; 24 | 25 | use super::{create_modal_layout, ModalStyle}; 26 | 27 | #[derive(Clone, Default)] 28 | pub struct RenameModalComponentProps { 29 | item_to_rename: Option, 30 | panel_side: Option, 31 | panel_tab: TabIdx, 32 | modal_style: ModalStyle, 33 | } 34 | 35 | impl RenameModalComponentProps { 36 | pub fn new( 37 | item_to_rename: Option, 38 | panel_side: Option, 39 | panel_tab: TabIdx, 40 | modal_style: ModalStyle, 41 | ) -> Self { 42 | Self { 43 | item_to_rename, 44 | panel_side, 45 | panel_tab, 46 | modal_style, 47 | } 48 | } 49 | } 50 | 51 | #[derive(Clone, Default, Debug)] 52 | pub struct RenameModalComponentState { 53 | input: String, 54 | } 55 | 56 | pub struct RenameModalComponent { 57 | base: ComponentBase, 58 | _maker: std::marker::PhantomData, 59 | } 60 | 61 | impl RenameModalComponent { 62 | pub fn with_props(props: RenameModalComponentProps) -> Self { 63 | let item = props.item_to_rename.clone().unwrap(); 64 | 65 | RenameModalComponent { 66 | base: ComponentBase::new( 67 | Some(props), 68 | Some(RenameModalComponentState { 69 | input: item.get_path().to_str().unwrap().to_string(), 70 | }), 71 | ), 72 | _maker: std::marker::PhantomData, 73 | } 74 | } 75 | } 76 | 77 | impl 78 | Component, FileManagerActions> 79 | for RenameModalComponent 80 | { 81 | fn handle_event( 82 | &mut self, 83 | event: Event, 84 | store: &mut Store, FileManagerActions>, 85 | ) -> bool { 86 | let state = store.get_state(); 87 | let local_state = self.base.get_state().unwrap(); 88 | let props = self.base.get_props().unwrap(); 89 | if let Event::Keyboard(key_evt) = event { 90 | if state.config.keyboard_cfg.accept.is_pressed(key_evt) 91 | && local_state.input.is_empty() == false 92 | { 93 | let panel_side = props.panel_side.unwrap(); 94 | let item = props.item_to_rename.unwrap(); 95 | match item { 96 | FileSystemItem::Directory(dir) => { 97 | store.dispatch(FileManagerActions::Directory(DirectoryAction::Rename { 98 | from: PanelInfo { 99 | side: panel_side.clone(), 100 | tab: props.panel_tab, 101 | path: dir.get_path(), 102 | }, 103 | to: PanelInfo { 104 | side: panel_side.clone(), 105 | tab: props.panel_tab, 106 | path: PathBuf::from(local_state.input), 107 | }, 108 | })); 109 | } 110 | FileSystemItem::File(file) => { 111 | store.dispatch(FileManagerActions::File(FileAction::Rename { 112 | from: PanelInfo { 113 | side: panel_side.clone(), 114 | tab: props.panel_tab, 115 | path: file.get_path(), 116 | }, 117 | to: PanelInfo { 118 | side: panel_side.clone(), 119 | tab: props.panel_tab, 120 | path: PathBuf::from(local_state.input), 121 | }, 122 | })); 123 | } 124 | FileSystemItem::Symlink(_) => {} 125 | FileSystemItem::Unknown => {} 126 | }; 127 | 128 | store.dispatch(FileManagerActions::App(AppAction::CloseModal)); 129 | return true; 130 | } 131 | 132 | match key_evt.code { 133 | KeyCode::Char(c) => { 134 | self.base.set_state(|current_state| { 135 | let mut current_text = current_state.input.clone(); 136 | if key_evt.modifiers == KeyModifiers::SHIFT { 137 | current_text = 138 | format!("{}{}", current_text, c.to_uppercase().to_string()); 139 | } else { 140 | current_text.push(c); 141 | } 142 | 143 | RenameModalComponentState { 144 | input: current_text, 145 | } 146 | }); 147 | return true; 148 | } 149 | KeyCode::Backspace => { 150 | self.base.set_state(|current_state| { 151 | let mut current_text = current_state.input.clone(); 152 | current_text.pop(); 153 | 154 | RenameModalComponentState { 155 | input: current_text, 156 | } 157 | }); 158 | return true; 159 | } 160 | _ => {} 161 | }; 162 | 163 | if state.config.keyboard_cfg.close.is_pressed(key_evt) { 164 | store.dispatch(FileManagerActions::App(AppAction::CloseModal)); 165 | return true; 166 | } 167 | } 168 | false 169 | } 170 | 171 | fn render( 172 | &self, 173 | frame: &mut tui::Frame, 174 | area: Option, 175 | ) { 176 | let layout = if let Some(area) = area { 177 | create_modal_layout(50, 10, area) 178 | } else { 179 | create_modal_layout(50, 10, frame.size()) 180 | }; 181 | 182 | let local_state = self.base.get_state().unwrap(); 183 | let block = Block::default() 184 | .title(Spans::from(vec![ 185 | Span::from("| "), 186 | Span::from("Item name:"), 187 | Span::from(" |"), 188 | ])) 189 | .borders(Borders::ALL) 190 | .border_style(Style::default()) 191 | .border_type(tui::widgets::BorderType::Thick) 192 | .style(Style::default()); 193 | 194 | let paragraph = Paragraph::new(local_state.input) 195 | .block(block) 196 | .alignment(tui::layout::Alignment::Center); 197 | 198 | frame.render_widget(Clear, layout); 199 | frame.render_widget(paragraph, layout); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/app/components/root.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use tui::{ 3 | backend::Backend, 4 | layout::{Constraint, Direction, Layout, Rect}, 5 | }; 6 | 7 | use crate::{ 8 | app::{ 9 | actions::{AppAction, FileManagerActions, PanelSide}, 10 | file_system::FileSystem, 11 | state::{AppState, ModalType}, 12 | }, 13 | core::{ 14 | events::Event, 15 | store::Store, 16 | ui::{component::Component, component_base::ComponentBase}, 17 | }, 18 | }; 19 | 20 | use super::{ 21 | create_modal::{CreateModalComponent, CreateModalProps}, 22 | error_modal::{ErrorModalComponent, ErrorModalComponentProps}, 23 | not_empty_dir_delete_modal::{ 24 | NotEmptyDirDeleteModalComponent, NotEmptyDirDeleteModalComponentProps, 25 | }, 26 | panel::PanelComponent, 27 | rename_modal::{RenameModalComponent, RenameModalComponentProps}, 28 | ModalStyle, 29 | }; 30 | 31 | #[derive(Clone, Default)] 32 | pub struct RootComponentState { 33 | focused_panel: Option, 34 | } 35 | 36 | pub struct RootComponent { 37 | base: ComponentBase<(), RootComponentState>, 38 | left_panel: PanelComponent, 39 | right_panel: PanelComponent, 40 | create_modal: Option>, 41 | rename_modal: Option>, 42 | error_modal: Option>, 43 | non_empty_dir_delete_modal: Option>, 44 | _maker: std::marker::PhantomData, 45 | } 46 | 47 | impl RootComponent { 48 | pub fn new() -> Self { 49 | RootComponent { 50 | base: ComponentBase::new(None, Some(RootComponentState::default())), 51 | left_panel: PanelComponent::empty(), 52 | right_panel: PanelComponent::empty(), 53 | create_modal: None, 54 | rename_modal: None, 55 | error_modal: None, 56 | non_empty_dir_delete_modal: None, 57 | _maker: std::marker::PhantomData, 58 | } 59 | } 60 | 61 | fn map_state(&mut self, store: &Store, FileManagerActions>) { 62 | let state = store.get_state(); 63 | if state.left_panel.is_focused { 64 | self.base.set_state(|_current_state| RootComponentState { 65 | focused_panel: Some(PanelSide::Left), 66 | }); 67 | } else if state.right_panel.is_focused { 68 | self.base.set_state(|_current_state| RootComponentState { 69 | focused_panel: Some(PanelSide::Right), 70 | }); 71 | } else { 72 | self.base.set_state(|_current_state| RootComponentState { 73 | focused_panel: None, 74 | }); 75 | } 76 | self.left_panel = PanelComponent::with_panel_state( 77 | state.left_panel, 78 | PanelSide::Left, 79 | &state.config.icons, 80 | &state.config.core_cfg, 81 | ); 82 | self.right_panel = PanelComponent::with_panel_state( 83 | state.right_panel, 84 | PanelSide::Right, 85 | &state.config.icons, 86 | &state.config.core_cfg, 87 | ); 88 | if let Some(modal_type) = state.modal.clone() { 89 | match modal_type { 90 | ModalType::CreateModal { 91 | item_index, 92 | panel_side, 93 | panel_tab, 94 | panel_tab_path, 95 | } => { 96 | if self.create_modal.is_none() { 97 | self.create_modal = 98 | Some(CreateModalComponent::with_props(CreateModalProps::new( 99 | panel_side, 100 | panel_tab, 101 | panel_tab_path, 102 | item_index, 103 | state.config.icons.use_icons, 104 | state.config.icons.get_file_icon("default".to_string()), 105 | state.config.icons.get_dir_icon("default".to_string()), 106 | state.config.icons.get_file_icon("symlink".to_string()), 107 | state.config.core_cfg.list_arrow.clone(), 108 | ModalStyle::new( 109 | state.config.core_cfg.color_scheme.normal_yellow, 110 | state.config.core_cfg.color_scheme.light_cyan, 111 | state.config.core_cfg.color_scheme.normal_black, 112 | ), 113 | ))); 114 | } 115 | } 116 | ModalType::ErrorModal(error_modal) => { 117 | if self.error_modal.is_none() { 118 | self.error_modal = Some(ErrorModalComponent::with_props( 119 | ErrorModalComponentProps::new( 120 | error_modal, 121 | state.config.icons.use_icons, 122 | state.config.icons.get_file_icon("warn".to_string()), 123 | ), 124 | )); 125 | } 126 | } 127 | ModalType::RenameModal { 128 | panel_side, 129 | panel_tab, 130 | item, 131 | } => { 132 | if self.rename_modal.is_none() { 133 | self.rename_modal = Some(RenameModalComponent::with_props( 134 | RenameModalComponentProps::new( 135 | Some(item), 136 | Some(panel_side), 137 | panel_tab, 138 | ModalStyle::new( 139 | state.config.core_cfg.color_scheme.normal_yellow, 140 | state.config.core_cfg.color_scheme.light_cyan, 141 | state.config.core_cfg.color_scheme.normal_black, 142 | ), 143 | ), 144 | )); 145 | } 146 | } 147 | ModalType::DeleteDirWithContent { 148 | panel_side, 149 | panel_tab, 150 | path, 151 | } => { 152 | if self.non_empty_dir_delete_modal.is_none() { 153 | self.non_empty_dir_delete_modal = 154 | Some(NotEmptyDirDeleteModalComponent::new( 155 | NotEmptyDirDeleteModalComponentProps::new( 156 | Some(panel_side), 157 | panel_tab, 158 | path, 159 | state.config.core_cfg.list_arrow.clone(), 160 | ModalStyle::new( 161 | state.config.core_cfg.color_scheme.normal_yellow, 162 | state.config.core_cfg.color_scheme.light_cyan, 163 | state.config.core_cfg.color_scheme.normal_black, 164 | ), 165 | ), 166 | )); 167 | } 168 | } 169 | }; 170 | } 171 | if self.create_modal.is_some() && state.modal.is_none() { 172 | self.create_modal = None; 173 | } 174 | 175 | if self.rename_modal.is_some() && state.modal.is_none() { 176 | self.rename_modal = None; 177 | } 178 | 179 | if self.error_modal.is_some() && state.modal.is_none() { 180 | self.error_modal = None; 181 | } 182 | 183 | if self.non_empty_dir_delete_modal.is_some() && state.modal.is_none() { 184 | self.non_empty_dir_delete_modal = None; 185 | } 186 | } 187 | } 188 | 189 | impl 190 | Component, FileManagerActions> for RootComponent 191 | { 192 | fn on_tick(&mut self, store: &mut Store, FileManagerActions>) { 193 | self.left_panel.on_tick(store); 194 | self.right_panel.on_tick(store); 195 | 196 | if store.is_dirty() { 197 | self.map_state(store); 198 | store.clean(); 199 | } 200 | } 201 | 202 | fn on_init(&mut self, store: &Store, FileManagerActions>) { 203 | self.map_state(store); 204 | } 205 | 206 | fn handle_event( 207 | &mut self, 208 | event: Event, 209 | store: &mut Store, FileManagerActions>, 210 | ) -> bool { 211 | let state = store.get_state(); 212 | 213 | if self.left_panel.tab_in_search_mode() == false 214 | && self.right_panel.tab_in_search_mode() == false 215 | { 216 | if let Event::Keyboard(key_evt) = event { 217 | if state.config.keyboard_cfg.quit.is_pressed(key_evt) { 218 | store.dispatch(FileManagerActions::App(AppAction::Exit)); 219 | return true; 220 | } 221 | 222 | if let Some(ref mut error_modal) = self.error_modal { 223 | let result = error_modal.handle_event(event, store); 224 | self.map_state(store); 225 | store.clean(); 226 | 227 | return result; 228 | } 229 | 230 | if let Some(ref mut non_empty_dir_delete_modal) = self.non_empty_dir_delete_modal { 231 | let result = non_empty_dir_delete_modal.handle_event(event, store); 232 | self.map_state(store); 233 | store.clean(); 234 | 235 | return result; 236 | } 237 | 238 | if let Some(ref mut create_modal) = self.create_modal { 239 | let result = create_modal.handle_event(event, store); 240 | self.map_state(store); 241 | 242 | return result; 243 | } 244 | 245 | if let Some(ref mut rename_modal) = self.rename_modal { 246 | let result = rename_modal.handle_event(event, store); 247 | self.map_state(store); 248 | store.clean(); 249 | 250 | return result; 251 | } 252 | 253 | if state 254 | .config 255 | .keyboard_cfg 256 | .focus_left_panel 257 | .is_pressed(key_evt) 258 | { 259 | store.dispatch(FileManagerActions::App(AppAction::FocusLeft)); 260 | self.map_state(store); 261 | store.clean(); 262 | 263 | return true; 264 | } 265 | 266 | if state 267 | .config 268 | .keyboard_cfg 269 | .focus_right_panel 270 | .is_pressed(key_evt) 271 | { 272 | store.dispatch(FileManagerActions::App(AppAction::FocusRight)); 273 | self.map_state(store); 274 | store.clean(); 275 | 276 | return true; 277 | } 278 | } 279 | } 280 | 281 | let mut result = self.left_panel.handle_event(event, store); 282 | if result == true { 283 | self.map_state(store); 284 | store.clean(); 285 | 286 | return result; 287 | } 288 | result = self.right_panel.handle_event(event, store); 289 | self.map_state(store); 290 | store.clean(); 291 | 292 | result 293 | } 294 | 295 | fn render(&self, frame: &mut tui::Frame, _area: Option) { 296 | let local_state = self.base.get_state().unwrap(); 297 | let layout = Layout::default() 298 | .direction(Direction::Horizontal) 299 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) 300 | .split(frame.size()); 301 | self.left_panel.render(frame, Some(layout[0])); 302 | self.right_panel.render(frame, Some(layout[1])); 303 | if let Some(ref create_modal) = self.create_modal { 304 | if let Some(focused_panel) = local_state.focused_panel.clone() { 305 | match focused_panel { 306 | PanelSide::Left => create_modal.render(frame, Some(layout[0])), 307 | PanelSide::Right => create_modal.render(frame, Some(layout[1])), 308 | }; 309 | } else { 310 | create_modal.render(frame, None); 311 | } 312 | } 313 | 314 | if let Some(ref rename_modal) = self.rename_modal { 315 | if let Some(focused_panel) = local_state.focused_panel.clone() { 316 | match focused_panel { 317 | PanelSide::Left => rename_modal.render(frame, Some(layout[0])), 318 | PanelSide::Right => rename_modal.render(frame, Some(layout[1])), 319 | }; 320 | } else { 321 | rename_modal.render(frame, None); 322 | } 323 | } 324 | 325 | if let Some(ref non_empty_dir_delete_modal) = self.non_empty_dir_delete_modal { 326 | if let Some(focused_panel) = local_state.focused_panel.clone() { 327 | match focused_panel { 328 | PanelSide::Left => non_empty_dir_delete_modal.render(frame, Some(layout[0])), 329 | PanelSide::Right => non_empty_dir_delete_modal.render(frame, Some(layout[1])), 330 | }; 331 | } else { 332 | non_empty_dir_delete_modal.render(frame, None); 333 | } 334 | } 335 | 336 | if let Some(ref error_modal) = self.error_modal { 337 | if let Some(focused_panel) = local_state.focused_panel.clone() { 338 | match focused_panel { 339 | PanelSide::Left => error_modal.render(frame, Some(layout[0])), 340 | PanelSide::Right => error_modal.render(frame, Some(layout[1])), 341 | }; 342 | } else { 343 | error_modal.render(frame, None); 344 | } 345 | } 346 | 347 | if let Some(ref error_modal) = self.error_modal { 348 | if let Some(focused_panel) = local_state.focused_panel.clone() { 349 | match focused_panel { 350 | PanelSide::Left => error_modal.render(frame, Some(layout[0])), 351 | PanelSide::Right => error_modal.render(frame, Some(layout[1])), 352 | }; 353 | } else { 354 | error_modal.render(frame, None); 355 | } 356 | } 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/app/config/icon_cfg.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use toml::Value; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct IconsConfig { 7 | pub use_icons: bool, 8 | dir_icons: HashMap, 9 | files_icon: HashMap, 10 | } 11 | 12 | impl Default for IconsConfig { 13 | fn default() -> Self { 14 | IconsConfig { 15 | use_icons: false, 16 | dir_icons: get_default_dir_icons(), 17 | files_icon: get_default_files_icons(), 18 | } 19 | } 20 | } 21 | 22 | fn get_default_dir_icons() -> HashMap { 23 | let mut icon_map = HashMap::new(); 24 | icon_map.insert(".git".to_string(), "📁".to_string()); 25 | icon_map.insert("node_modules".to_string(), "📁".to_string()); 26 | icon_map.insert("default".to_string(), "📁".to_string()); 27 | 28 | icon_map 29 | } 30 | 31 | fn get_default_files_icons() -> HashMap { 32 | let mut icon_map = HashMap::new(); 33 | //GIT 34 | icon_map.insert(".gitignore".to_string(), "📄".to_string()); 35 | icon_map.insert(".gitmodules".to_string(), "📄".to_string()); 36 | 37 | //PROGRAMMING LANGUAGES 38 | icon_map.insert("rs".to_string(), "📄".to_string()); 39 | icon_map.insert("cs".to_string(), "📄".to_string()); 40 | icon_map.insert("cpp".to_string(), "📄".to_string()); 41 | icon_map.insert("c".to_string(), "📄".to_string()); 42 | icon_map.insert("hpp".to_string(), "📄".to_string()); 43 | icon_map.insert("h".to_string(), "📄".to_string()); 44 | icon_map.insert("js".to_string(), "📄".to_string()); 45 | icon_map.insert("ts".to_string(), "📄".to_string()); 46 | icon_map.insert("jsx".to_string(), "📄".to_string()); 47 | icon_map.insert("tsx".to_string(), "📄".to_string()); 48 | icon_map.insert("html".to_string(), "📄".to_string()); 49 | icon_map.insert("css".to_string(), "📄".to_string()); 50 | icon_map.insert("sass".to_string(), "📄".to_string()); 51 | icon_map.insert("toml".to_string(), "📄".to_string()); 52 | icon_map.insert("yaml".to_string(), "📄".to_string()); 53 | icon_map.insert("php".to_string(), "📄".to_string()); 54 | icon_map.insert("py".to_string(), "📄".to_string()); 55 | icon_map.insert("rb".to_string(), "📄".to_string()); 56 | icon_map.insert("java".to_string(), "📄".to_string()); 57 | icon_map.insert("lock".to_string(), "📄".to_string()); 58 | icon_map.insert("default".to_string(), "📄".to_string()); 59 | icon_map.insert("symlink".to_string(), "🎯".to_string()); 60 | icon_map.insert("warn".to_string(), "📄".to_string()); 61 | 62 | icon_map 63 | } 64 | 65 | impl IconsConfig { 66 | pub fn get_dir_icon(&self, dir_name: String) -> String { 67 | match self.dir_icons.get(&dir_name) { 68 | Some(icon) => icon.clone(), 69 | None => self.dir_icons["default"].clone(), 70 | } 71 | } 72 | 73 | pub fn get_file_icon(&self, file_name: String) -> String { 74 | match self.files_icon.get(&file_name) { 75 | Some(icon) => icon.clone(), 76 | None => self.files_icon["default"].clone(), 77 | } 78 | } 79 | 80 | pub fn update_from_file(&mut self, cfg: &Value) { 81 | if let Some(core) = cfg.get("core") { 82 | if let Some(use_icons) = core.get("use_icons") { 83 | if let Value::Boolean(value) = use_icons { 84 | self.use_icons = value.clone(); 85 | } 86 | } 87 | } 88 | 89 | if let Some(icons_files) = cfg.get("icons_files") { 90 | if let Value::Table(values) = icons_files { 91 | for (key, value) in values.iter() { 92 | self.files_icon 93 | .insert(key.clone(), value.as_str().unwrap().to_string()); 94 | } 95 | } 96 | } 97 | 98 | if let Some(icons_dir) = cfg.get("icons_dir") { 99 | if let Value::Table(values) = icons_dir { 100 | for (key, value) in values.iter() { 101 | self.dir_icons 102 | .insert(key.clone(), value.as_str().unwrap().to_string()); 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/app/config/keyboard_cfg.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, KeyModifiers}; 2 | use toml::Value; 3 | 4 | use crate::core::key_binding::KeyBinding; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct KeyboardConfig { 8 | pub quit: KeyBinding, 9 | pub focus_left_panel: KeyBinding, 10 | pub focus_right_panel: KeyBinding, 11 | pub move_down: KeyBinding, 12 | pub move_up: KeyBinding, 13 | pub next_tab: KeyBinding, 14 | pub prev_tab: KeyBinding, 15 | pub close: KeyBinding, 16 | pub open: KeyBinding, 17 | pub open_as_tab: KeyBinding, 18 | pub navigate_up: KeyBinding, 19 | pub delete: KeyBinding, 20 | pub move_left: KeyBinding, 21 | pub move_right: KeyBinding, 22 | pub rename: KeyBinding, 23 | pub create: KeyBinding, 24 | pub accept: KeyBinding, 25 | pub copy_to_left: KeyBinding, 26 | pub copy_to_right: KeyBinding, 27 | pub search_in_panel: KeyBinding, 28 | pub select_prev: KeyBinding, 29 | pub select_next: KeyBinding, 30 | } 31 | 32 | impl KeyboardConfig { 33 | pub fn update_from_file(&mut self, cfg: &Value) { 34 | if let Some(keyboard_cfg) = cfg.get("keyboard_cfg") { 35 | if let Value::Table(keyboard_cfg) = keyboard_cfg { 36 | if let Some(quit) = keyboard_cfg.get("quit") { 37 | if let Value::Table(key_binding) = quit { 38 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 39 | let modifier = if key_binding.contains_key("modifier") { 40 | map_modifier(key_binding["modifier"].as_str().unwrap()) 41 | } else { 42 | KeyModifiers::empty() 43 | }; 44 | 45 | self.quit = KeyBinding::with_modifiers(key_code, modifier); 46 | } 47 | } 48 | 49 | if let Some(focus_left_panel) = keyboard_cfg.get("focus_left_panel") { 50 | if let Value::Table(key_binding) = focus_left_panel { 51 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 52 | let modifier = if key_binding.contains_key("modifier") { 53 | map_modifier(key_binding["modifier"].as_str().unwrap()) 54 | } else { 55 | KeyModifiers::empty() 56 | }; 57 | 58 | self.focus_left_panel = KeyBinding::with_modifiers(key_code, modifier); 59 | } 60 | } 61 | 62 | if let Some(focus_right_panel) = keyboard_cfg.get("focus_right_panel") { 63 | if let Value::Table(key_binding) = focus_right_panel { 64 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 65 | let modifier = if key_binding.contains_key("modifier") { 66 | map_modifier(key_binding["modifier"].as_str().unwrap()) 67 | } else { 68 | KeyModifiers::empty() 69 | }; 70 | 71 | self.focus_right_panel = KeyBinding::with_modifiers(key_code, modifier); 72 | } 73 | } 74 | 75 | if let Some(move_down) = keyboard_cfg.get("move_down") { 76 | if let Value::Table(key_binding) = move_down { 77 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 78 | let modifier = if key_binding.contains_key("modifier") { 79 | map_modifier(key_binding["modifier"].as_str().unwrap()) 80 | } else { 81 | KeyModifiers::empty() 82 | }; 83 | 84 | self.move_down = KeyBinding::with_modifiers(key_code, modifier); 85 | } 86 | } 87 | 88 | if let Some(move_up) = keyboard_cfg.get("move_up") { 89 | if let Value::Table(key_binding) = move_up { 90 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 91 | let modifier = if key_binding.contains_key("modifier") { 92 | map_modifier(key_binding["modifier"].as_str().unwrap()) 93 | } else { 94 | KeyModifiers::empty() 95 | }; 96 | 97 | self.move_up = KeyBinding::with_modifiers(key_code, modifier); 98 | } 99 | } 100 | 101 | if let Some(next_tab) = keyboard_cfg.get("next_tab") { 102 | if let Value::Table(key_binding) = next_tab { 103 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 104 | let modifier = if key_binding.contains_key("modifier") { 105 | map_modifier(key_binding["modifier"].as_str().unwrap()) 106 | } else { 107 | KeyModifiers::empty() 108 | }; 109 | 110 | self.next_tab = KeyBinding::with_modifiers(key_code, modifier); 111 | } 112 | } 113 | 114 | if let Some(prev_tab) = keyboard_cfg.get("prev_tab") { 115 | if let Value::Table(key_binding) = prev_tab { 116 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 117 | let modifier = if key_binding.contains_key("modifier") { 118 | map_modifier(key_binding["modifier"].as_str().unwrap()) 119 | } else { 120 | KeyModifiers::empty() 121 | }; 122 | 123 | self.prev_tab = KeyBinding::with_modifiers(key_code, modifier); 124 | } 125 | } 126 | 127 | if let Some(close) = keyboard_cfg.get("close") { 128 | if let Value::Table(key_binding) = close { 129 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 130 | let modifier = if key_binding.contains_key("modifier") { 131 | map_modifier(key_binding["modifier"].as_str().unwrap()) 132 | } else { 133 | KeyModifiers::empty() 134 | }; 135 | 136 | self.close = KeyBinding::with_modifiers(key_code, modifier); 137 | } 138 | } 139 | if let Some(open) = keyboard_cfg.get("open") { 140 | if let Value::Table(key_binding) = open { 141 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 142 | let modifier = if key_binding.contains_key("modifier") { 143 | map_modifier(key_binding["modifier"].as_str().unwrap()) 144 | } else { 145 | KeyModifiers::empty() 146 | }; 147 | 148 | self.open = KeyBinding::with_modifiers(key_code, modifier); 149 | } 150 | } 151 | 152 | if let Some(open_as_tab) = keyboard_cfg.get("open_as_tab") { 153 | if let Value::Table(key_binding) = open_as_tab { 154 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 155 | let modifier = if key_binding.contains_key("modifier") { 156 | map_modifier(key_binding["modifier"].as_str().unwrap()) 157 | } else { 158 | KeyModifiers::empty() 159 | }; 160 | 161 | self.open_as_tab = KeyBinding::with_modifiers(key_code, modifier); 162 | } 163 | } 164 | 165 | if let Some(navigate_up) = keyboard_cfg.get("navigate_up") { 166 | if let Value::Table(key_binding) = navigate_up { 167 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 168 | let modifier = if key_binding.contains_key("modifier") { 169 | map_modifier(key_binding["modifier"].as_str().unwrap()) 170 | } else { 171 | KeyModifiers::empty() 172 | }; 173 | 174 | self.navigate_up = KeyBinding::with_modifiers(key_code, modifier); 175 | } 176 | } 177 | 178 | if let Some(delete) = keyboard_cfg.get("delete") { 179 | if let Value::Table(key_binding) = delete { 180 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 181 | let modifier = if key_binding.contains_key("modifier") { 182 | map_modifier(key_binding["modifier"].as_str().unwrap()) 183 | } else { 184 | KeyModifiers::empty() 185 | }; 186 | 187 | self.delete = KeyBinding::with_modifiers(key_code, modifier); 188 | } 189 | } 190 | 191 | if let Some(move_left) = keyboard_cfg.get("move_left") { 192 | if let Value::Table(key_binding) = move_left { 193 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 194 | let modifier = if key_binding.contains_key("modifier") { 195 | map_modifier(key_binding["modifier"].as_str().unwrap()) 196 | } else { 197 | KeyModifiers::empty() 198 | }; 199 | 200 | self.move_left = KeyBinding::with_modifiers(key_code, modifier); 201 | } 202 | } 203 | 204 | if let Some(move_right) = keyboard_cfg.get("move_right") { 205 | if let Value::Table(key_binding) = move_right { 206 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 207 | let modifier = if key_binding.contains_key("modifier") { 208 | map_modifier(key_binding["modifier"].as_str().unwrap()) 209 | } else { 210 | KeyModifiers::empty() 211 | }; 212 | 213 | self.move_right = KeyBinding::with_modifiers(key_code, modifier); 214 | } 215 | } 216 | 217 | if let Some(rename) = keyboard_cfg.get("rename") { 218 | if let Value::Table(key_binding) = rename { 219 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 220 | let modifier = if key_binding.contains_key("modifier") { 221 | map_modifier(key_binding["modifier"].as_str().unwrap()) 222 | } else { 223 | KeyModifiers::empty() 224 | }; 225 | 226 | self.rename = KeyBinding::with_modifiers(key_code, modifier); 227 | } 228 | } 229 | 230 | if let Some(create) = keyboard_cfg.get("create") { 231 | if let Value::Table(key_binding) = create { 232 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 233 | let modifier = if key_binding.contains_key("modifier") { 234 | map_modifier(key_binding["modifier"].as_str().unwrap()) 235 | } else { 236 | KeyModifiers::empty() 237 | }; 238 | 239 | self.create = KeyBinding::with_modifiers(key_code, modifier); 240 | } 241 | } 242 | 243 | if let Some(accept) = keyboard_cfg.get("accept") { 244 | if let Value::Table(key_binding) = accept { 245 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 246 | let modifier = if key_binding.contains_key("modifier") { 247 | map_modifier(key_binding["modifier"].as_str().unwrap()) 248 | } else { 249 | KeyModifiers::empty() 250 | }; 251 | 252 | self.accept = KeyBinding::with_modifiers(key_code, modifier); 253 | } 254 | } 255 | 256 | if let Some(copy_to_left) = keyboard_cfg.get("copy_to_left") { 257 | if let Value::Table(key_binding) = copy_to_left { 258 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 259 | let modifier = if key_binding.contains_key("modifier") { 260 | map_modifier(key_binding["modifier"].as_str().unwrap()) 261 | } else { 262 | KeyModifiers::empty() 263 | }; 264 | 265 | self.copy_to_left = KeyBinding::with_modifiers(key_code, modifier); 266 | } 267 | } 268 | 269 | if let Some(copy_to_right) = keyboard_cfg.get("copy_to_right") { 270 | if let Value::Table(key_binding) = copy_to_right { 271 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 272 | let modifier = if key_binding.contains_key("modifier") { 273 | map_modifier(key_binding["modifier"].as_str().unwrap()) 274 | } else { 275 | KeyModifiers::empty() 276 | }; 277 | 278 | self.copy_to_right = KeyBinding::with_modifiers(key_code, modifier); 279 | } 280 | } 281 | 282 | if let Some(search_in_panel) = keyboard_cfg.get("search_in_panel") { 283 | if let Value::Table(key_binding) = search_in_panel { 284 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 285 | let modifier = if key_binding.contains_key("modifier") { 286 | map_modifier(key_binding["modifier"].as_str().unwrap()) 287 | } else { 288 | KeyModifiers::empty() 289 | }; 290 | 291 | self.search_in_panel = KeyBinding::with_modifiers(key_code, modifier); 292 | } 293 | } 294 | 295 | if let Some(select_prev) = keyboard_cfg.get("select_prev") { 296 | if let Value::Table(key_binding) = select_prev { 297 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 298 | let modifier = if key_binding.contains_key("modifier") { 299 | map_modifier(key_binding["modifier"].as_str().unwrap()) 300 | } else { 301 | KeyModifiers::empty() 302 | }; 303 | 304 | self.select_prev = KeyBinding::with_modifiers(key_code, modifier); 305 | } 306 | } 307 | 308 | if let Some(select_next) = keyboard_cfg.get("select_next") { 309 | if let Value::Table(key_binding) = select_next { 310 | let key_code = map_key(key_binding["key"].as_str().unwrap()); 311 | let modifier = if key_binding.contains_key("modifier") { 312 | map_modifier(key_binding["modifier"].as_str().unwrap()) 313 | } else { 314 | KeyModifiers::empty() 315 | }; 316 | 317 | self.select_next = KeyBinding::with_modifiers(key_code, modifier); 318 | } 319 | } 320 | } 321 | } 322 | } 323 | } 324 | 325 | impl Default for KeyboardConfig { 326 | fn default() -> Self { 327 | KeyboardConfig { 328 | quit: KeyBinding::with_modifiers(KeyCode::Char('q'), KeyModifiers::CONTROL), 329 | focus_left_panel: KeyBinding::new(KeyCode::Char('h')), 330 | focus_right_panel: KeyBinding::new(KeyCode::Char('l')), 331 | move_down: KeyBinding::new(KeyCode::Char('j')), 332 | move_up: KeyBinding::new(KeyCode::Char('k')), 333 | next_tab: KeyBinding::new(KeyCode::Char('n')), 334 | prev_tab: KeyBinding::new(KeyCode::Char('p')), 335 | close: KeyBinding::new(KeyCode::Esc), 336 | open: KeyBinding::new(KeyCode::Char('o')), 337 | open_as_tab: KeyBinding::with_modifiers(KeyCode::Char('o'), KeyModifiers::CONTROL), 338 | navigate_up: KeyBinding::new(KeyCode::Backspace), 339 | delete: KeyBinding::with_modifiers(KeyCode::Char('d'), KeyModifiers::CONTROL), 340 | move_left: KeyBinding::with_modifiers(KeyCode::Char('h'), KeyModifiers::CONTROL), 341 | move_right: KeyBinding::with_modifiers(KeyCode::Char('l'), KeyModifiers::CONTROL), 342 | rename: KeyBinding::with_modifiers(KeyCode::Char('r'), KeyModifiers::CONTROL), 343 | create: KeyBinding::with_modifiers(KeyCode::Char('c'), KeyModifiers::CONTROL), 344 | accept: KeyBinding::new(KeyCode::Enter), 345 | copy_to_right: KeyBinding::with_modifiers(KeyCode::Char('x'), KeyModifiers::CONTROL), 346 | copy_to_left: KeyBinding::with_modifiers(KeyCode::Char('z'), KeyModifiers::CONTROL), 347 | search_in_panel: KeyBinding::with_modifiers(KeyCode::Char('s'), KeyModifiers::CONTROL), 348 | select_prev: KeyBinding::with_modifiers(KeyCode::Char('k'), KeyModifiers::CONTROL), 349 | select_next: KeyBinding::with_modifiers(KeyCode::Char('j'), KeyModifiers::CONTROL), 350 | } 351 | } 352 | } 353 | 354 | fn map_key(key: &str) -> KeyCode { 355 | match key.to_lowercase().as_str() { 356 | "backspace" => KeyCode::Backspace, 357 | "enter" => KeyCode::Enter, 358 | "left" => KeyCode::Left, 359 | "right" => KeyCode::Right, 360 | "up" => KeyCode::Up, 361 | "down" => KeyCode::Down, 362 | "home" => KeyCode::Home, 363 | "end" => KeyCode::End, 364 | "page_up" => KeyCode::PageUp, 365 | "page_down" => KeyCode::PageDown, 366 | "tab" => KeyCode::Tab, 367 | "back_tab" => KeyCode::BackTab, 368 | "delete" => KeyCode::Delete, 369 | "insert" => KeyCode::Insert, 370 | "esc" => KeyCode::Esc, 371 | "f1" => KeyCode::F(1), 372 | "f2" => KeyCode::F(2), 373 | "f3" => KeyCode::F(3), 374 | "f4" => KeyCode::F(4), 375 | "f5" => KeyCode::F(5), 376 | "f6" => KeyCode::F(6), 377 | "f7" => KeyCode::F(7), 378 | "f8" => KeyCode::F(8), 379 | "f9" => KeyCode::F(9), 380 | "f10" => KeyCode::F(10), 381 | "f11" => KeyCode::F(11), 382 | "f12" => KeyCode::F(12), 383 | n => { 384 | let mut chars = n.chars(); 385 | KeyCode::Char(chars.next().unwrap()) 386 | } 387 | } 388 | } 389 | 390 | fn map_modifier(modifier: &str) -> KeyModifiers { 391 | match modifier.to_lowercase().as_str() { 392 | "c" => KeyModifiers::CONTROL, 393 | "s" => KeyModifiers::SHIFT, 394 | "a" => KeyModifiers::ALT, 395 | _ => KeyModifiers::NONE, 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/app/config/mod.rs: -------------------------------------------------------------------------------- 1 | use toml::Value; 2 | 3 | use crate::core::config::CoreConfig; 4 | use std::path::Path; 5 | 6 | use self::{ 7 | icon_cfg::IconsConfig, keyboard_cfg::KeyboardConfig, 8 | program_associations::FileAssociatedPrograms, 9 | }; 10 | 11 | use super::file_system::{functions::expand_if_contains_tilde, FileSystem}; 12 | 13 | pub mod icon_cfg; 14 | pub mod keyboard_cfg; 15 | pub mod program_associations; 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct Config { 19 | pub core_cfg: CoreConfig, 20 | pub keyboard_cfg: KeyboardConfig, 21 | pub icons: IconsConfig, 22 | pub file_associated_programs: FileAssociatedPrograms, 23 | } 24 | 25 | impl Default for Config { 26 | fn default() -> Self { 27 | Config { 28 | core_cfg: CoreConfig::default(), 29 | keyboard_cfg: KeyboardConfig::default(), 30 | icons: IconsConfig::default(), 31 | file_associated_programs: FileAssociatedPrograms::default(), 32 | } 33 | } 34 | } 35 | 36 | impl Config { 37 | pub fn load_or_default, TFileSystem: FileSystem>( 38 | paths: Vec, 39 | file_system: &TFileSystem, 40 | ) -> Self { 41 | let mut cfg = Config::default(); 42 | if let Some(config_content) = read_config_file_to_string(paths, file_system) { 43 | if let Ok(toml_mapped_values) = config_content.parse::() { 44 | cfg.icons.update_from_file(&toml_mapped_values); 45 | cfg.keyboard_cfg.update_from_file(&toml_mapped_values); 46 | cfg.file_associated_programs 47 | .update_from_file(&toml_mapped_values); 48 | cfg.core_cfg.update_from_file(&toml_mapped_values); 49 | } 50 | } 51 | cfg 52 | } 53 | } 54 | 55 | fn read_config_file_to_string, TFileSystem: FileSystem>( 56 | paths: Vec, 57 | file_system: &TFileSystem, 58 | ) -> Option { 59 | for path in paths { 60 | if let Some(path) = expand_if_contains_tilde(path) { 61 | match file_system.read_to_string(&path) { 62 | Some(content) => return Some(content.clone()), 63 | None => continue, 64 | } 65 | } 66 | } 67 | None 68 | } 69 | -------------------------------------------------------------------------------- /src/app/config/program_associations.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use toml::Value; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct FileAssociatedPrograms { 7 | bindings: HashMap, 8 | } 9 | 10 | impl Default for FileAssociatedPrograms { 11 | fn default() -> Self { 12 | let mut bindings = HashMap::new(); 13 | bindings.insert("default".to_string(), "nvim".to_string()); 14 | 15 | FileAssociatedPrograms { bindings } 16 | } 17 | } 18 | 19 | impl FileAssociatedPrograms { 20 | pub fn update_from_file(&mut self, cfg: &Value) { 21 | if let Some(file_associated_programs) = cfg.get("file_associated_programs") { 22 | if let Value::Table(associated_programs_map) = file_associated_programs { 23 | for (key, val) in associated_programs_map.iter() { 24 | self.bindings 25 | .insert(key.clone(), val.as_str().unwrap().to_string()); 26 | } 27 | } 28 | } 29 | } 30 | pub fn get_program_name(&self, file_extension: String) -> String { 31 | match self.bindings.get(&file_extension) { 32 | Some(name) => name.clone(), 33 | None => self.bindings[&"default".to_string()].clone(), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/file_system/dir_item.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use chrono::{DateTime, Local}; 4 | use tui::{ 5 | layout::Rect, 6 | text::{Span, Spans}, 7 | }; 8 | 9 | use crate::core::ToSpans; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct DirItem { 13 | name: String, 14 | path: PathBuf, 15 | last_modification: DateTime, 16 | icon: String, 17 | is_empty: bool, 18 | } 19 | 20 | impl DirItem { 21 | pub fn new( 22 | name: String, 23 | path: PathBuf, 24 | last_modification: DateTime, 25 | icon: String, 26 | is_empty: bool, 27 | ) -> Self { 28 | DirItem { 29 | name, 30 | path, 31 | last_modification, 32 | icon, 33 | is_empty, 34 | } 35 | } 36 | 37 | pub fn get_name(&self) -> String { 38 | self.name.clone() 39 | } 40 | 41 | pub fn get_path(&self) -> PathBuf { 42 | self.path.clone() 43 | } 44 | 45 | pub fn is_visible(&self) -> bool { 46 | self.name.starts_with('.') 47 | } 48 | 49 | pub fn is_empty(&self) -> bool { 50 | self.is_empty 51 | } 52 | } 53 | 54 | impl ToSpans for DirItem { 55 | fn to_spans(&self, _area: Rect, show_icons: bool) -> Spans { 56 | if show_icons { 57 | Spans::from(vec![ 58 | Span::from(" "), 59 | Span::from(self.icon.clone()), 60 | Span::from(" "), 61 | Span::from( 62 | self.last_modification 63 | .format("%Y-%m-%d %H:%M:%S") 64 | .to_string(), 65 | ), 66 | Span::from(" "), 67 | Span::from(self.name.clone()), 68 | ]) 69 | } else { 70 | Spans::from(vec![ 71 | Span::from(" "), 72 | Span::from( 73 | self.last_modification 74 | .format("%Y-%m-%d %H:%M:%S") 75 | .to_string(), 76 | ), 77 | Span::from(" "), 78 | Span::from(self.name.clone()), 79 | ]) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/file_system/file_item.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use chrono::{DateTime, Local}; 4 | use tui::{ 5 | layout::Rect, 6 | text::{Span, Spans}, 7 | }; 8 | 9 | use crate::core::ToSpans; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct FileItem { 13 | name: String, 14 | path: PathBuf, 15 | last_modification: DateTime, 16 | icon: String, 17 | } 18 | 19 | impl FileItem { 20 | pub fn new( 21 | name: String, 22 | path: PathBuf, 23 | last_modification: DateTime, 24 | icon: String, 25 | ) -> Self { 26 | FileItem { 27 | name, 28 | path, 29 | last_modification, 30 | icon, 31 | } 32 | } 33 | 34 | pub fn get_name(&self) -> String { 35 | self.name.clone() 36 | } 37 | 38 | pub fn get_path(&self) -> PathBuf { 39 | self.path.clone() 40 | } 41 | 42 | pub fn is_visible(&self) -> bool { 43 | self.name.starts_with('.') 44 | } 45 | } 46 | 47 | impl ToSpans for FileItem { 48 | fn to_spans(&self, _area: Rect, show_icons: bool) -> Spans { 49 | if show_icons { 50 | Spans::from(vec![ 51 | Span::from(" "), 52 | Span::from(self.icon.clone()), 53 | Span::from(" "), 54 | Span::from( 55 | self.last_modification 56 | .format("%Y-%m-%d %H:%M:%S") 57 | .to_string(), 58 | ), 59 | Span::from(" "), 60 | Span::from(self.name.clone()), 61 | ]) 62 | } else { 63 | Spans::from(vec![ 64 | Span::from(" "), 65 | Span::from( 66 | self.last_modification 67 | .format("%Y-%m-%d %H:%M:%S") 68 | .to_string(), 69 | ), 70 | Span::from(" "), 71 | Span::from(self.name.clone()), 72 | ]) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/file_system/file_system_item.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use tui::{layout::Rect, text::Spans}; 4 | 5 | use crate::core::ToSpans; 6 | 7 | use super::{dir_item::DirItem, file_item::FileItem, symlink_item::SymlinkItem}; 8 | 9 | #[derive(Clone, Debug)] 10 | pub enum FileSystemItem { 11 | Directory(DirItem), 12 | File(FileItem), 13 | Symlink(SymlinkItem), 14 | Unknown, 15 | } 16 | 17 | impl FileSystemItem { 18 | pub fn get_path(&self) -> PathBuf { 19 | match self { 20 | FileSystemItem::Directory(dir) => dir.get_path(), 21 | FileSystemItem::File(file) => file.get_path(), 22 | FileSystemItem::Symlink(symlink) => symlink.get_path(), 23 | FileSystemItem::Unknown => PathBuf::new(), 24 | } 25 | } 26 | 27 | pub fn get_name(&self) -> String { 28 | match self { 29 | FileSystemItem::Directory(dir) => dir.get_name(), 30 | FileSystemItem::File(file) => file.get_name(), 31 | FileSystemItem::Symlink(symlink) => symlink.get_name(), 32 | FileSystemItem::Unknown => "".to_string(), 33 | } 34 | } 35 | 36 | pub fn is_symlink(&self) -> bool { 37 | match self { 38 | FileSystemItem::Directory(_) => false, 39 | FileSystemItem::File(_) => false, 40 | FileSystemItem::Symlink(_) => true, 41 | FileSystemItem::Unknown => false, 42 | } 43 | } 44 | 45 | pub fn is_file(&self) -> bool { 46 | match self { 47 | FileSystemItem::Directory(_) => false, 48 | FileSystemItem::File(_) => true, 49 | FileSystemItem::Symlink(_) => false, 50 | FileSystemItem::Unknown => false, 51 | } 52 | } 53 | 54 | pub fn is_dir(&self) -> bool { 55 | match self { 56 | FileSystemItem::Directory(_) => true, 57 | FileSystemItem::File(_) => false, 58 | FileSystemItem::Symlink(_) => false, 59 | FileSystemItem::Unknown => false, 60 | } 61 | } 62 | 63 | pub fn is_visible(&self) -> bool { 64 | match self { 65 | FileSystemItem::Directory(dir) => dir.is_visible(), 66 | FileSystemItem::File(file) => file.is_visible(), 67 | FileSystemItem::Symlink(_) => true, 68 | FileSystemItem::Unknown => false, 69 | } 70 | } 71 | } 72 | 73 | impl ToSpans for FileSystemItem { 74 | fn to_spans(&self, area: Rect, show_icons: bool) -> Spans { 75 | match self { 76 | FileSystemItem::Directory(dir) => dir.to_spans(area, show_icons), 77 | FileSystemItem::File(file) => file.to_spans(area, show_icons), 78 | FileSystemItem::Symlink(symlink) => symlink.to_spans(area, show_icons), 79 | FileSystemItem::Unknown => Spans::default(), 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/file_system/functions.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | use std::os::unix::fs; 3 | #[cfg(windows)] 4 | use std::os::windows::fs; 5 | 6 | use std::{ 7 | fs::{read_link, DirEntry, Metadata}, 8 | io, 9 | path::{Path, PathBuf}, 10 | time::SystemTime, 11 | }; 12 | 13 | use chrono::{DateTime, Local}; 14 | 15 | use crate::app::config::icon_cfg::IconsConfig; 16 | 17 | use super::{ 18 | dir_item::DirItem, file_item::FileItem, file_system_item::FileSystemItem, 19 | symlink_item::SymlinkItem, 20 | }; 21 | 22 | #[cfg(unix)] 23 | pub fn create_link>(symlink_path: TPath, item_path: TPath) -> io::Result<()> { 24 | let symlink_path = expand_if_contains_tilde(symlink_path).unwrap(); 25 | fs::symlink(item_path, symlink_path) 26 | } 27 | 28 | #[cfg(windows)] 29 | pub fn create_link>(symlink_path: TPath, item_path: TPath) -> io::Result<()> { 30 | let symlink_path = expand_if_contains_tilde(symlink_path).unwrap(); 31 | if item_path.is_dir() { 32 | fs::symlink_dir(item_path, symlink_path) 33 | } else { 34 | fs::symlink_file(item_path, symlink_path) 35 | } 36 | } 37 | 38 | //From: https://stackoverflow.com/questions/54267608/expand-tilde-in-rust-path-idiomatically 39 | pub fn expand_if_contains_tilde>(input: TPath) -> Option { 40 | let path = input.as_ref(); 41 | if path.starts_with("~") == false { 42 | return Some(path.to_path_buf()); 43 | } 44 | if path == Path::new("~") { 45 | return dirs::home_dir(); 46 | } 47 | 48 | dirs::home_dir().map(|mut home_path| { 49 | if home_path == Path::new("/") { 50 | // Corner case: `h` root directory; 51 | // don't prepend extra `/`, just drop the tilde. 52 | path.strip_prefix("~").unwrap().to_path_buf() 53 | } else { 54 | home_path.push(path.strip_prefix("~/").unwrap()); 55 | home_path 56 | } 57 | }) 58 | } 59 | 60 | pub fn map_dir_entry_to_file_system_item( 61 | dir_entry: DirEntry, 62 | icons: &IconsConfig, 63 | ) -> FileSystemItem { 64 | if let Ok(metadata) = dir_entry.metadata() { 65 | let (name, path, modified) = get_file_system_item_props(dir_entry, &metadata); 66 | let file_type = metadata.file_type(); 67 | if file_type.is_file() { 68 | let file_extensions = name.split('.').last().unwrap_or(""); 69 | return FileSystemItem::File(FileItem::new( 70 | name.to_string(), 71 | path, 72 | modified, 73 | icons.get_file_icon(file_extensions.to_string()), 74 | )); 75 | } 76 | 77 | if file_type.is_dir() { 78 | return FileSystemItem::Directory(DirItem::new( 79 | name.to_string(), 80 | path.clone(), 81 | modified, 82 | icons.get_dir_icon(name), 83 | path.read_dir() 84 | .map(|mut i| i.next().is_none()) 85 | .unwrap_or(false), 86 | )); 87 | } 88 | 89 | if file_type.is_symlink() { 90 | let file_extensions = name.split('.').last().unwrap_or(""); 91 | match read_link(path.clone()) { 92 | Ok(target) => { 93 | return FileSystemItem::Symlink(SymlinkItem::new( 94 | name.to_string(), 95 | path, 96 | target.clone(), 97 | modified, 98 | if target.is_file() { 99 | icons.get_file_icon(file_extensions.to_string()) 100 | } else { 101 | icons.get_dir_icon(name) 102 | }, 103 | )) 104 | } 105 | Err(_) => { 106 | return FileSystemItem::Symlink(SymlinkItem::new( 107 | name.to_string(), 108 | path.clone(), 109 | path, 110 | modified, 111 | icons.get_file_icon(file_extensions.to_string()), 112 | )) 113 | } 114 | } 115 | } 116 | 117 | FileSystemItem::Unknown 118 | } else { 119 | FileSystemItem::Unknown 120 | } 121 | } 122 | 123 | fn get_file_system_item_props( 124 | dir_entry: DirEntry, 125 | metadata: &Metadata, 126 | ) -> (String, PathBuf, DateTime) { 127 | let modified: DateTime = if let Ok(last_modified) = metadata.modified() { 128 | last_modified.into() 129 | } else { 130 | SystemTime::now().into() 131 | }; 132 | 133 | let entry_name = dir_entry.file_name(); 134 | let name = if let Some(name) = entry_name.to_str() { 135 | name 136 | } else { 137 | "" 138 | }; 139 | let path_buffer = dir_entry.path(); 140 | 141 | (name.to_string(), path_buffer, modified) 142 | } 143 | -------------------------------------------------------------------------------- /src/app/file_system/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | path::Path, 4 | }; 5 | use std::{io, path::PathBuf}; 6 | 7 | use self::{ 8 | file_system_item::FileSystemItem, 9 | functions::{create_link, map_dir_entry_to_file_system_item}, 10 | }; 11 | 12 | use super::config::icon_cfg::IconsConfig; 13 | 14 | pub mod dir_item; 15 | pub mod file_item; 16 | pub mod file_system_item; 17 | pub mod functions; 18 | pub mod symlink_item; 19 | 20 | pub trait FileSystem { 21 | fn exist>(&self, path: TPath) -> bool; 22 | fn get_dir_info>(&self, path: TPath) -> Option; 23 | fn list_dir>(&self, path: TPath, icons: &IconsConfig) 24 | -> Vec; 25 | fn read_to_string>(&self, path: TPath) -> Option; 26 | fn delete_file>(&mut self, path: TPath) -> io::Result<()>; 27 | fn delete_dir>(&mut self, path: TPath) -> io::Result<()>; 28 | fn delete_empty_dir>(&mut self, path: TPath) -> io::Result<()>; 29 | fn rename_item>(&mut self, source: TPath, target: TPath) -> io::Result<()>; 30 | fn create_symlink>( 31 | &mut self, 32 | source: TPath, 33 | target: TPath, 34 | ) -> io::Result<()>; 35 | fn create_file>(&mut self, path: TPath) -> io::Result; 36 | fn create_dir>(&mut self, path: TPath) -> io::Result<()>; 37 | fn copy_file>(&mut self, source: TPath, target: TPath) -> io::Result; 38 | fn copy_dir>(&mut self, source: TPath, target: TPath) -> io::Result; 39 | } 40 | 41 | #[derive(Clone, Debug, Default)] 42 | pub struct PhysicalFileSystem; 43 | 44 | impl FileSystem for PhysicalFileSystem { 45 | fn get_dir_info>(&self, path: TPath) -> Option { 46 | DirInfo::new(&path) 47 | } 48 | 49 | fn list_dir>( 50 | &self, 51 | path: TPath, 52 | icons: &IconsConfig, 53 | ) -> Vec { 54 | match fs::read_dir(path) { 55 | Ok(mut iter) => { 56 | let mut result = Vec::new(); 57 | while let Some(load_result) = iter.next() { 58 | if let Ok(dir_entry) = load_result { 59 | result.push(map_dir_entry_to_file_system_item(dir_entry, icons)); 60 | } 61 | } 62 | 63 | result.sort_by(|one, two| one.get_name().cmp(&two.get_name())); 64 | 65 | result 66 | } 67 | Err(_) => Vec::new(), 68 | } 69 | } 70 | 71 | fn read_to_string>(&self, path: TPath) -> Option { 72 | match fs::read_to_string(path) { 73 | Ok(content) => return Some(content.clone()), 74 | Err(_) => None, 75 | } 76 | } 77 | 78 | fn delete_file>(&mut self, path: TPath) -> io::Result<()> { 79 | fs::remove_file(path) 80 | } 81 | 82 | fn delete_dir>(&mut self, path: TPath) -> io::Result<()> { 83 | fs::remove_dir_all(path) 84 | } 85 | 86 | fn rename_item>(&mut self, source: TPath, target: TPath) -> io::Result<()> { 87 | fs::rename(source, target) 88 | } 89 | 90 | fn create_symlink>( 91 | &mut self, 92 | source: TPath, 93 | target: TPath, 94 | ) -> io::Result<()> { 95 | create_link(target, source) 96 | } 97 | 98 | fn create_file>(&mut self, path: TPath) -> io::Result { 99 | File::create(path) 100 | } 101 | 102 | fn create_dir>(&mut self, path: TPath) -> io::Result<()> { 103 | fs::create_dir(path) 104 | } 105 | 106 | fn delete_empty_dir>(&mut self, path: TPath) -> io::Result<()> { 107 | fs::remove_dir(path) 108 | } 109 | 110 | fn copy_file>(&mut self, source: TPath, target: TPath) -> io::Result { 111 | fs::copy(source, target) 112 | } 113 | 114 | fn copy_dir>(&mut self, source: TPath, target: TPath) -> io::Result { 115 | fs::create_dir_all(target.as_ref())?; 116 | for entry in fs::read_dir(source)? { 117 | let entry = entry?; 118 | let file_type = entry.file_type()?; 119 | if file_type.is_dir() { 120 | self.copy_dir(entry.path(), target.as_ref().join(entry.file_name()))?; 121 | } else { 122 | self.copy_file(entry.path(), target.as_ref().join(entry.file_name()))?; 123 | } 124 | } 125 | Ok(0) 126 | } 127 | 128 | fn exist>(&self, path: TPath) -> bool { 129 | let path = path.as_ref(); 130 | if path.is_dir() || path.is_file() { 131 | return path.exists(); 132 | } 133 | return true; 134 | } 135 | } 136 | 137 | #[derive(Clone, Debug)] 138 | pub struct DirInfo { 139 | pub name: String, 140 | pub path: PathBuf, 141 | } 142 | 143 | impl DirInfo { 144 | pub fn new>(path: &TPath) -> Option { 145 | if let Ok(path_buffer) = fs::canonicalize(path) { 146 | let name = if let Some(file_name) = path_buffer.file_name() { 147 | file_name.to_str().unwrap_or("") 148 | } else { 149 | "" 150 | }; 151 | let path = path_buffer.as_path().to_str().unwrap_or(""); 152 | return Some(DirInfo { 153 | name: name.to_string(), 154 | path: PathBuf::from(path), 155 | }); 156 | } 157 | None 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/app/file_system/path.rs: -------------------------------------------------------------------------------- 1 | use dirs; 2 | use std::path::{Path, PathBuf}; 3 | -------------------------------------------------------------------------------- /src/app/file_system/symlink_item.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use chrono::{DateTime, Local}; 4 | use tui::{ 5 | layout::Rect, 6 | text::{Span, Spans}, 7 | }; 8 | 9 | use crate::core::ToSpans; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct SymlinkItem { 13 | name: String, 14 | path: PathBuf, 15 | target: PathBuf, 16 | last_modification: DateTime, 17 | icon: String, 18 | } 19 | 20 | impl SymlinkItem { 21 | pub fn new( 22 | name: String, 23 | path: PathBuf, 24 | target: PathBuf, 25 | last_modification: DateTime, 26 | icon: String, 27 | ) -> Self { 28 | Self { 29 | name, 30 | path, 31 | target, 32 | last_modification, 33 | icon, 34 | } 35 | } 36 | 37 | pub fn get_name(&self) -> String { 38 | self.name.clone() 39 | } 40 | 41 | pub fn get_path(&self) -> PathBuf { 42 | self.path.clone() 43 | } 44 | 45 | pub fn is_visible(&self) -> bool { 46 | self.name.starts_with('.') 47 | } 48 | } 49 | 50 | impl ToSpans for SymlinkItem { 51 | fn to_spans(&self, _area: Rect, show_icons: bool) -> Spans { 52 | if show_icons { 53 | Spans::from(vec![ 54 | Span::from(" "), 55 | Span::from(self.icon.clone()), 56 | Span::from(" "), 57 | Span::from( 58 | self.last_modification 59 | .format("%Y-%m-%d %H:%M:%S") 60 | .to_string(), 61 | ), 62 | Span::from(" "), 63 | Span::from(self.name.clone()), 64 | Span::from(" -> "), 65 | Span::from(self.target.to_str().unwrap_or("")), 66 | ]) 67 | } else { 68 | Spans::from(vec![ 69 | Span::from(" "), 70 | Span::from( 71 | self.last_modification 72 | .format("%Y-%m-%d %H:%M:%S") 73 | .to_string(), 74 | ), 75 | Span::from(" "), 76 | Span::from(self.name.clone()), 77 | Span::from(" -> "), 78 | Span::from(self.target.to_str().unwrap_or("")), 79 | ]) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/middlewares.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | actions::{ 3 | AppAction, DirectoryAction, FileAction, FileManagerActions, PanelInfo, SymlinkAction, 4 | }, 5 | file_system::FileSystem, 6 | state::{AppState, ModalType}, 7 | }; 8 | use crate::core::store::Store; 9 | use std::{fmt::Debug, fs}; 10 | 11 | pub fn symlink_middleware( 12 | store: &mut Store, FileManagerActions>, 13 | action: FileManagerActions, 14 | ) -> Option { 15 | match action { 16 | FileManagerActions::Symlink(symlink_action) => symlink_resolver(store, symlink_action), 17 | _ => Some(action), 18 | } 19 | } 20 | 21 | fn symlink_resolver( 22 | _: &mut Store, FileManagerActions>, 23 | symlink_action: SymlinkAction, 24 | ) -> Option { 25 | match symlink_action { 26 | SymlinkAction::Open { panel, in_new_tab } => match fs::read_link(panel.path.as_path()) { 27 | Ok(link_path) => { 28 | if link_path.is_dir() { 29 | Some(FileManagerActions::Directory(DirectoryAction::Open { 30 | panel: PanelInfo { 31 | path: link_path, 32 | tab: panel.tab, 33 | side: panel.side, 34 | }, 35 | in_new_tab, 36 | })) 37 | } else { 38 | Some(FileManagerActions::File(FileAction::Open { 39 | panel: PanelInfo { 40 | path: link_path, 41 | tab: panel.tab, 42 | side: panel.side, 43 | }, 44 | })) 45 | } 46 | } 47 | Err(err) => Some(FileManagerActions::App(AppAction::ShowModal( 48 | ModalType::ErrorModal(format!("{}", err)), 49 | ))), 50 | }, 51 | _ => Some(FileManagerActions::Symlink(symlink_action)), 52 | } 53 | } 54 | 55 | pub fn dir_middleware( 56 | store: &mut Store, FileManagerActions>, 57 | action: FileManagerActions, 58 | ) -> Option { 59 | match action { 60 | FileManagerActions::Directory(dir_action) => directory_resolver(store, dir_action), 61 | _ => Some(action), 62 | } 63 | } 64 | 65 | fn directory_resolver( 66 | _: &mut Store, FileManagerActions>, 67 | dir_action: DirectoryAction, 68 | ) -> Option { 69 | match dir_action { 70 | DirectoryAction::Delete { panel, is_empty } => { 71 | if is_empty { 72 | Some(FileManagerActions::Directory(DirectoryAction::Delete { 73 | panel, 74 | is_empty, 75 | })) 76 | } else { 77 | Some(FileManagerActions::App(AppAction::ShowModal( 78 | ModalType::DeleteDirWithContent { 79 | panel_side: panel.side, 80 | panel_tab: panel.tab, 81 | path: panel.path, 82 | }, 83 | ))) 84 | } 85 | } 86 | _ => Some(FileManagerActions::Directory(dir_action)), 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod actions; 2 | pub mod components; 3 | pub mod config; 4 | pub mod file_system; 5 | pub mod middlewares; 6 | pub mod reducers; 7 | pub mod state; 8 | -------------------------------------------------------------------------------- /src/app/reducers/file_reducer.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::{ffi::OsStr, path::PathBuf}; 3 | 4 | use crate::app::{ 5 | actions::{FileAction, PanelInfo, PanelSide}, 6 | config::{icon_cfg::IconsConfig, program_associations::FileAssociatedPrograms}, 7 | file_system::{file_system_item::FileSystemItem, FileSystem}, 8 | state::{AppState, ChildProgramDesc, PanelState, TabIdx, TabState}, 9 | }; 10 | 11 | use super::{reload_tab, reload_tab_contain_item, reload_tab_with_path}; 12 | 13 | pub fn file_reducer( 14 | state: AppState, 15 | file_action: FileAction, 16 | ) -> AppState { 17 | match file_action { 18 | FileAction::Delete { panel } => delete_file(state, panel), 19 | FileAction::Rename { from, to } => rename_file(state, from, to), 20 | FileAction::Move { from, to } => rename_file(state, from, to), 21 | FileAction::Open { panel } => open_file(state, panel), 22 | FileAction::Create { file_name, panel } => create_file(state, file_name, panel), 23 | FileAction::Copy { from, to } => copy_file(state, from, to), 24 | } 25 | } 26 | 27 | fn copy_file( 28 | mut state: AppState, 29 | from: PanelInfo, 30 | to: PanelInfo, 31 | ) -> AppState { 32 | match to.side { 33 | PanelSide::Left => AppState { 34 | left_panel: PanelState { 35 | tabs: copy_file_to_tab( 36 | from.path, 37 | to.path, 38 | to.tab, 39 | state.left_panel.tabs, 40 | &mut state.file_system, 41 | &state.config.icons, 42 | ), 43 | ..state.left_panel 44 | }, 45 | right_panel: PanelState { 46 | tabs: reload_tab( 47 | from.tab, 48 | state.right_panel.tabs, 49 | &mut state.file_system, 50 | &state.config.icons, 51 | ), 52 | ..state.right_panel 53 | }, 54 | ..state 55 | }, 56 | PanelSide::Right => AppState { 57 | right_panel: PanelState { 58 | tabs: copy_file_to_tab( 59 | from.path, 60 | to.path, 61 | to.tab, 62 | state.right_panel.tabs, 63 | &mut state.file_system, 64 | &state.config.icons, 65 | ), 66 | ..state.right_panel 67 | }, 68 | left_panel: PanelState { 69 | tabs: reload_tab( 70 | from.tab, 71 | state.left_panel.tabs, 72 | &mut state.file_system, 73 | &state.config.icons, 74 | ), 75 | ..state.left_panel 76 | }, 77 | ..state 78 | }, 79 | } 80 | } 81 | 82 | fn create_file( 83 | mut state: AppState, 84 | file_name: String, 85 | panel: PanelInfo, 86 | ) -> AppState { 87 | match panel.side { 88 | PanelSide::Left => AppState { 89 | left_panel: PanelState { 90 | tabs: create_file_in_tab( 91 | file_name, 92 | panel.path.clone(), 93 | panel.tab, 94 | state.left_panel.tabs, 95 | &mut state.file_system, 96 | &state.config.icons, 97 | ), 98 | ..state.left_panel 99 | }, 100 | right_panel: PanelState { 101 | tabs: reload_tab_with_path( 102 | panel.path.as_path(), 103 | state.right_panel.tabs, 104 | &mut state.file_system, 105 | &state.config.icons, 106 | ), 107 | ..state.right_panel 108 | }, 109 | ..state 110 | }, 111 | PanelSide::Right => AppState { 112 | right_panel: PanelState { 113 | tabs: create_file_in_tab( 114 | file_name, 115 | panel.path.clone(), 116 | panel.tab, 117 | state.right_panel.tabs, 118 | &mut state.file_system, 119 | &state.config.icons, 120 | ), 121 | ..state.right_panel 122 | }, 123 | left_panel: PanelState { 124 | tabs: reload_tab_with_path( 125 | panel.path.as_path(), 126 | state.left_panel.tabs, 127 | &mut state.file_system, 128 | &state.config.icons, 129 | ), 130 | ..state.left_panel 131 | }, 132 | ..state 133 | }, 134 | } 135 | } 136 | 137 | fn open_file( 138 | state: AppState, 139 | panel: PanelInfo, 140 | ) -> AppState { 141 | AppState { 142 | child_program: open_file_from_tab(panel.path, &state.config.file_associated_programs), 143 | ..state 144 | } 145 | } 146 | 147 | fn delete_file( 148 | mut state: AppState, 149 | panel: PanelInfo, 150 | ) -> AppState { 151 | match panel.side { 152 | PanelSide::Left => AppState { 153 | left_panel: PanelState { 154 | tabs: delete_file_from_tab( 155 | panel.path.clone(), 156 | panel.tab, 157 | state.left_panel.tabs, 158 | &mut state.file_system, 159 | &state.config.icons, 160 | ), 161 | ..state.left_panel 162 | }, 163 | right_panel: PanelState { 164 | tabs: reload_tab_contain_item( 165 | panel.path.clone(), 166 | state.right_panel.tabs, 167 | &mut state.file_system, 168 | &state.config.icons, 169 | ), 170 | ..state.right_panel 171 | }, 172 | ..state 173 | }, 174 | PanelSide::Right => AppState { 175 | right_panel: PanelState { 176 | tabs: delete_file_from_tab( 177 | panel.path.clone(), 178 | panel.tab, 179 | state.right_panel.tabs, 180 | &mut state.file_system, 181 | &state.config.icons, 182 | ), 183 | ..state.right_panel 184 | }, 185 | left_panel: PanelState { 186 | tabs: reload_tab_contain_item( 187 | panel.path.clone(), 188 | state.left_panel.tabs, 189 | &mut state.file_system, 190 | &state.config.icons, 191 | ), 192 | ..state.left_panel 193 | }, 194 | ..state 195 | }, 196 | } 197 | } 198 | 199 | fn rename_file( 200 | mut state: AppState, 201 | from: PanelInfo, 202 | to: PanelInfo, 203 | ) -> AppState { 204 | match to.side { 205 | PanelSide::Left => AppState { 206 | left_panel: PanelState { 207 | tabs: rename_file_in_tab( 208 | from.path, 209 | to.path, 210 | to.tab, 211 | state.left_panel.tabs, 212 | &mut state.file_system, 213 | &state.config.icons, 214 | ), 215 | ..state.left_panel 216 | }, 217 | right_panel: PanelState { 218 | tabs: reload_tab( 219 | from.tab, 220 | state.right_panel.tabs, 221 | &mut state.file_system, 222 | &state.config.icons, 223 | ), 224 | ..state.right_panel 225 | }, 226 | ..state 227 | }, 228 | PanelSide::Right => AppState { 229 | right_panel: PanelState { 230 | tabs: rename_file_in_tab( 231 | from.path, 232 | to.path, 233 | to.tab, 234 | state.right_panel.tabs, 235 | &mut state.file_system, 236 | &state.config.icons, 237 | ), 238 | ..state.right_panel 239 | }, 240 | left_panel: PanelState { 241 | tabs: reload_tab( 242 | from.tab, 243 | state.left_panel.tabs, 244 | &mut state.file_system, 245 | &state.config.icons, 246 | ), 247 | ..state.right_panel 248 | }, 249 | ..state 250 | }, 251 | } 252 | } 253 | 254 | fn create_file_in_tab( 255 | file_name: String, 256 | dir_path: PathBuf, 257 | tab: TabIdx, 258 | mut tabs: Vec>, 259 | file_system: &mut TFileSystem, 260 | icons: &IconsConfig, 261 | ) -> Vec> { 262 | let mut result = Vec::>::new(); 263 | for (idx, tab_state) in tabs.iter_mut().enumerate() { 264 | if idx == tab { 265 | if dir_path.exists() { 266 | let mut file_path = dir_path.clone(); 267 | file_path.push(file_name.clone()); 268 | match file_system.create_file(&file_path) { 269 | Ok(_) => { 270 | result.push(TabState::with_dir(dir_path.as_path(), file_system, icons)) 271 | } 272 | Err(_) => {} 273 | } 274 | } else { 275 | result.push(tab_state.clone()); 276 | } 277 | } else { 278 | result.push(tab_state.clone()); 279 | } 280 | } 281 | 282 | result 283 | } 284 | 285 | fn open_file_from_tab( 286 | path: PathBuf, 287 | file_associated_programs: &FileAssociatedPrograms, 288 | ) -> Option { 289 | if path.is_file() && path.exists() { 290 | let file_extension = path.extension().unwrap_or(OsStr::new("")); 291 | Some(ChildProgramDesc { 292 | program_name: file_associated_programs 293 | .get_program_name(String::from(file_extension.to_str().unwrap())), 294 | args: vec![String::from(path.to_str().unwrap())], 295 | }) 296 | } else { 297 | None 298 | } 299 | } 300 | 301 | fn delete_file_from_tab( 302 | path: PathBuf, 303 | current_tab: TabIdx, 304 | mut tabs: Vec>, 305 | file_system: &mut TFileSystem, 306 | icons: &IconsConfig, 307 | ) -> Vec> { 308 | let mut result = Vec::>::new(); 309 | 310 | for (idx, tab_state) in tabs.iter_mut().enumerate() { 311 | if idx == current_tab { 312 | let item_to_delete = tab_state 313 | .items 314 | .iter() 315 | .find(|item| item.is_file() && item.get_path().eq(&path)); 316 | if let Some(item) = item_to_delete { 317 | if let FileSystemItem::File(file) = item { 318 | match file_system.delete_file(&file.get_path()) { 319 | Ok(_) => result.push(TabState::with_dir( 320 | tab_state.path.as_path(), 321 | file_system, 322 | icons, 323 | )), 324 | Err(_) => {} //TODO: add error handling to state 325 | } 326 | } else { 327 | result.push(tab_state.clone()); 328 | } 329 | } else { 330 | result.push(tab_state.clone()); 331 | } 332 | } else { 333 | result.push(tab_state.clone()); 334 | } 335 | } 336 | 337 | result 338 | } 339 | 340 | fn copy_file_to_tab( 341 | from: PathBuf, 342 | to: PathBuf, 343 | current_tab: TabIdx, 344 | mut tabs: Vec>, 345 | file_system: &mut TFileSystem, 346 | icons: &IconsConfig, 347 | ) -> Vec> { 348 | let mut result = Vec::>::new(); 349 | for (idx, tab_state) in tabs.iter_mut().enumerate() { 350 | if idx == current_tab { 351 | match file_system.copy_file(from.as_path(), to.as_path()) { 352 | Ok(_) => result.push(TabState::with_dir( 353 | tab_state.path.as_path(), 354 | file_system, 355 | icons, 356 | )), 357 | Err(_) => {} 358 | } 359 | } else { 360 | result.push(tab_state.clone()); 361 | } 362 | } 363 | 364 | result 365 | } 366 | 367 | fn rename_file_in_tab( 368 | from: PathBuf, 369 | to: PathBuf, 370 | current_tab: TabIdx, 371 | mut tabs: Vec>, 372 | file_system: &mut TFileSystem, 373 | icons: &IconsConfig, 374 | ) -> Vec> { 375 | let mut result = Vec::>::new(); 376 | for (idx, tab_state) in tabs.iter_mut().enumerate() { 377 | if idx == current_tab { 378 | match file_system.rename_item(&from.as_path(), &to.as_path()) { 379 | Ok(_) => result.push(TabState::with_dir( 380 | tab_state.path.as_path(), 381 | file_system, 382 | icons, 383 | )), 384 | Err(_) => {} //TODO: add error handling to state 385 | } 386 | } else { 387 | result.push(tab_state.clone()); 388 | } 389 | } 390 | 391 | result 392 | } 393 | -------------------------------------------------------------------------------- /src/app/reducers/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | actions::{AppAction, FileManagerActions}, 3 | config::icon_cfg::IconsConfig, 4 | file_system::FileSystem, 5 | state::{AppState, PanelState, TabIdx, TabState}, 6 | }; 7 | use std::{ 8 | fmt::Debug, 9 | path::{Path, PathBuf}, 10 | }; 11 | 12 | mod dir_reducer; 13 | mod file_reducer; 14 | mod panel_reducer; 15 | mod search_reducer; 16 | mod symlink_reducer; 17 | mod tab_reducer; 18 | 19 | use dir_reducer::dir_reducer; 20 | use file_reducer::file_reducer; 21 | use panel_reducer::panel_reducer; 22 | use search_reducer::search_reducer; 23 | use symlink_reducer::symlink_reducer; 24 | use tab_reducer::tab_reducer; 25 | 26 | pub fn root_reducer( 27 | state: AppState, 28 | action: FileManagerActions, 29 | ) -> AppState { 30 | match action { 31 | FileManagerActions::App(app_action) => app_reducer(state.clone(), app_action), 32 | FileManagerActions::File(file_action) => file_reducer(state.clone(), file_action), 33 | FileManagerActions::Directory(dir_action) => dir_reducer(state.clone(), dir_action), 34 | FileManagerActions::Symlink(symlink_action) => { 35 | symlink_reducer(state.clone(), symlink_action) 36 | } 37 | FileManagerActions::Panel(panel_action) => panel_reducer(state.clone(), panel_action), 38 | FileManagerActions::Tab(tab_action) => tab_reducer(state.clone(), tab_action), 39 | FileManagerActions::Search(search_action) => search_reducer(state.clone(), search_action), 40 | } 41 | } 42 | 43 | fn app_reducer( 44 | state: AppState, 45 | app_action: AppAction, 46 | ) -> AppState { 47 | match app_action { 48 | AppAction::Exit => AppState { 49 | app_exit: true, 50 | ..state 51 | }, 52 | AppAction::FocusLeft => AppState { 53 | left_panel: PanelState { 54 | is_focused: true, 55 | ..state.left_panel 56 | }, 57 | right_panel: PanelState { 58 | is_focused: false, 59 | ..state.right_panel 60 | }, 61 | ..state 62 | }, 63 | AppAction::FocusRight => AppState { 64 | left_panel: PanelState { 65 | is_focused: false, 66 | ..state.left_panel 67 | }, 68 | right_panel: PanelState { 69 | is_focused: true, 70 | ..state.right_panel 71 | }, 72 | ..state 73 | }, 74 | AppAction::ChildProgramClosed => AppState { 75 | child_program: None, 76 | ..state 77 | }, 78 | AppAction::ShowModal(modal_type) => AppState { 79 | modal: Some(modal_type), 80 | ..state 81 | }, 82 | AppAction::CloseModal => AppState { 83 | modal: None, 84 | ..state 85 | }, 86 | } 87 | } 88 | 89 | fn reload_tab( 90 | tab: TabIdx, 91 | tabs: Vec>, 92 | file_system: &mut TFileSystem, 93 | icons_cfg: &IconsConfig, 94 | ) -> Vec> { 95 | let mut result = Vec::>::new(); 96 | for (idx, tab_state) in tabs.iter().enumerate() { 97 | if idx == tab { 98 | result.push(TabState::with_dir( 99 | tab_state.path.as_path(), 100 | file_system, 101 | icons_cfg, 102 | )); 103 | } else { 104 | result.push(tab_state.clone()); 105 | } 106 | } 107 | 108 | result 109 | } 110 | 111 | fn reload_tab_with_path( 112 | tab_path: &Path, 113 | tabs: Vec>, 114 | file_system: &TFileSystem, 115 | icons_cfg: &IconsConfig, 116 | ) -> Vec> { 117 | let mut result = Vec::>::new(); 118 | for tab_state in tabs.iter() { 119 | if tab_state.path == tab_path { 120 | result.push(TabState::with_dir( 121 | tab_state.path.as_path(), 122 | file_system, 123 | icons_cfg, 124 | )); 125 | } else { 126 | result.push(tab_state.clone()); 127 | } 128 | } 129 | 130 | result 131 | } 132 | 133 | fn reload_tab_contain_item( 134 | path: PathBuf, 135 | tabs: Vec>, 136 | file_system: &TFileSystem, 137 | icons_cfg: &IconsConfig, 138 | ) -> Vec> { 139 | let mut result = Vec::>::new(); 140 | for tab_state in tabs.iter() { 141 | result.push(reload_if_contain( 142 | tab_state, 143 | path.clone(), 144 | file_system, 145 | icons_cfg, 146 | )); 147 | } 148 | 149 | result 150 | } 151 | 152 | fn reload_if_contain( 153 | tab_state: &TabState, 154 | path: PathBuf, 155 | file_system: &TFileSystem, 156 | icons_cfg: &IconsConfig, 157 | ) -> TabState { 158 | if tab_state.items.iter().any(|i| i.get_path() == path) { 159 | TabState::with_dir(tab_state.path.as_path(), file_system, icons_cfg) 160 | } else { 161 | tab_state.clone() 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/app/reducers/panel_reducer.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{ 2 | actions::{PanelAction, PanelSide}, 3 | file_system::FileSystem, 4 | state::{AppState, PanelState, TabIdx}, 5 | }; 6 | use std::fmt::Debug; 7 | 8 | pub fn panel_reducer( 9 | state: AppState, 10 | panel_action: PanelAction, 11 | ) -> AppState { 12 | match panel_action { 13 | PanelAction::Next { panel } => next_tab(state, panel), 14 | PanelAction::Previous { panel } => prev_tab(state, panel), 15 | PanelAction::CloseTab { tab, panel } => close_tab(state, tab, panel), 16 | } 17 | } 18 | 19 | fn close_tab( 20 | state: AppState, 21 | tab: TabIdx, 22 | panel: PanelSide, 23 | ) -> AppState { 24 | match panel { 25 | PanelSide::Left => AppState { 26 | left_panel: close_tab_in_panel(state.left_panel, tab), 27 | ..state 28 | }, 29 | PanelSide::Right => AppState { 30 | right_panel: close_tab_in_panel(state.right_panel, tab), 31 | ..state 32 | }, 33 | } 34 | } 35 | 36 | fn close_tab_in_panel( 37 | panel_state: PanelState, 38 | tab: TabIdx, 39 | ) -> PanelState { 40 | let tabs: Vec<_> = panel_state 41 | .tabs 42 | .iter() 43 | .enumerate() 44 | .filter(|&(i, _)| i != tab) 45 | .map(|(_, v)| v.clone()) 46 | .collect(); 47 | 48 | let tabs_len = tabs.len(); 49 | PanelState { 50 | tabs, 51 | current_tab: if panel_state.current_tab > tab { 52 | panel_state.current_tab - 1 53 | } else if tab >= tabs_len { 54 | tabs_len - 1 55 | } else if panel_state.current_tab == 0 { 56 | 0 57 | } else { 58 | panel_state.current_tab 59 | }, 60 | is_focused: panel_state.is_focused, 61 | marker: std::marker::PhantomData, 62 | } 63 | } 64 | 65 | fn prev_tab( 66 | state: AppState, 67 | panel: PanelSide, 68 | ) -> AppState { 69 | match panel { 70 | PanelSide::Left => AppState { 71 | left_panel: PanelState { 72 | current_tab: if state.left_panel.current_tab == 0 { 73 | state.left_panel.tabs.len() - 1 74 | } else { 75 | state.left_panel.current_tab - 1 76 | }, 77 | ..state.left_panel 78 | }, 79 | ..state 80 | }, 81 | PanelSide::Right => AppState { 82 | right_panel: PanelState { 83 | current_tab: if state.right_panel.current_tab == 0 { 84 | state.right_panel.tabs.len() - 1 85 | } else { 86 | state.right_panel.current_tab - 1 87 | }, 88 | ..state.right_panel 89 | }, 90 | ..state 91 | }, 92 | } 93 | } 94 | 95 | fn next_tab( 96 | state: AppState, 97 | panel: PanelSide, 98 | ) -> AppState { 99 | match panel { 100 | PanelSide::Left => AppState { 101 | left_panel: PanelState { 102 | current_tab: if state.left_panel.current_tab >= state.left_panel.tabs.len() - 1 { 103 | 0 104 | } else { 105 | state.left_panel.current_tab + 1 106 | }, 107 | ..state.left_panel 108 | }, 109 | ..state 110 | }, 111 | PanelSide::Right => AppState { 112 | right_panel: PanelState { 113 | current_tab: if state.right_panel.current_tab >= state.right_panel.tabs.len() - 1 { 114 | 0 115 | } else { 116 | state.right_panel.current_tab + 1 117 | }, 118 | ..state.right_panel 119 | }, 120 | ..state 121 | }, 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/app/reducers/search_reducer.rs: -------------------------------------------------------------------------------- 1 | use tui::widgets::ListState; 2 | 3 | use crate::app::{ 4 | actions::{PanelSide, SearchAction}, 5 | file_system::FileSystem, 6 | state::{AppState, PanelState, TabIdx, TabState}, 7 | }; 8 | use std::fmt::Debug; 9 | 10 | pub fn search_reducer( 11 | state: AppState, 12 | search_action: SearchAction, 13 | ) -> AppState { 14 | match search_action { 15 | SearchAction::Start { tab, panel_side } => start_search(state.clone(), tab, panel_side), 16 | SearchAction::Stop { tab, panel_side } => stop_search(state.clone(), tab, panel_side), 17 | SearchAction::Input { 18 | tab, 19 | panel_side, 20 | phrase, 21 | } => change_input(state.clone(), tab, panel_side, phrase), 22 | SearchAction::ApplySearch { tab, panel_side } => { 23 | apply_search(state.clone(), tab, panel_side) 24 | } 25 | } 26 | } 27 | 28 | fn apply_search( 29 | state: AppState, 30 | tab: usize, 31 | panel_side: PanelSide, 32 | ) -> AppState { 33 | match panel_side { 34 | PanelSide::Left => AppState { 35 | left_panel: PanelState { 36 | tabs: apply_search_in_tab(state.left_panel.tabs, tab), 37 | ..state.left_panel 38 | }, 39 | ..state 40 | }, 41 | PanelSide::Right => AppState { 42 | right_panel: PanelState { 43 | tabs: apply_search_in_tab(state.right_panel.tabs, tab), 44 | ..state.right_panel 45 | }, 46 | ..state 47 | }, 48 | } 49 | } 50 | 51 | fn change_input( 52 | state: AppState, 53 | tab: usize, 54 | panel_side: PanelSide, 55 | phrase: String, 56 | ) -> AppState { 57 | match panel_side { 58 | PanelSide::Left => AppState { 59 | left_panel: PanelState { 60 | tabs: input_search_in_tab(state.left_panel.tabs, tab, phrase), 61 | ..state.left_panel 62 | }, 63 | ..state 64 | }, 65 | PanelSide::Right => AppState { 66 | right_panel: PanelState { 67 | tabs: input_search_in_tab(state.right_panel.tabs, tab, phrase), 68 | ..state.right_panel 69 | }, 70 | ..state 71 | }, 72 | } 73 | } 74 | 75 | fn stop_search( 76 | state: AppState, 77 | tab: usize, 78 | panel_side: PanelSide, 79 | ) -> AppState { 80 | match panel_side { 81 | PanelSide::Left => AppState { 82 | left_panel: PanelState { 83 | tabs: stop_search_in_tab(state.left_panel.tabs, tab), 84 | ..state.left_panel 85 | }, 86 | ..state 87 | }, 88 | PanelSide::Right => AppState { 89 | right_panel: PanelState { 90 | tabs: stop_search_in_tab(state.right_panel.tabs, tab), 91 | ..state.right_panel 92 | }, 93 | ..state 94 | }, 95 | } 96 | } 97 | 98 | fn start_search( 99 | state: AppState, 100 | tab: usize, 101 | panel_side: PanelSide, 102 | ) -> AppState { 103 | match panel_side { 104 | PanelSide::Left => AppState { 105 | left_panel: PanelState { 106 | tabs: start_search_in_tab(state.left_panel.tabs, tab), 107 | ..state.left_panel 108 | }, 109 | ..state 110 | }, 111 | PanelSide::Right => AppState { 112 | right_panel: PanelState { 113 | tabs: start_search_in_tab(state.right_panel.tabs, tab), 114 | ..state.right_panel 115 | }, 116 | ..state 117 | }, 118 | } 119 | } 120 | 121 | fn start_search_in_tab( 122 | tabs: Vec>, 123 | tab: TabIdx, 124 | ) -> Vec> { 125 | let mut result = Vec::>::new(); 126 | 127 | for (idx, tab_state) in tabs.iter().enumerate() { 128 | if idx == tab { 129 | result.push(TabState { 130 | search_mode: true, 131 | ..tab_state.clone() 132 | }); 133 | } else { 134 | result.push(tab_state.clone()); 135 | } 136 | } 137 | 138 | result 139 | } 140 | 141 | fn stop_search_in_tab( 142 | tabs: Vec>, 143 | tab: TabIdx, 144 | ) -> Vec> { 145 | let mut result = Vec::>::new(); 146 | 147 | for (idx, tab_state) in tabs.iter().enumerate() { 148 | if idx == tab { 149 | result.push(TabState { 150 | search_mode: false, 151 | phrase: String::from(""), 152 | ..tab_state.clone() 153 | }); 154 | } else { 155 | result.push(tab_state.clone()); 156 | } 157 | } 158 | 159 | result 160 | } 161 | 162 | fn input_search_in_tab( 163 | tabs: Vec>, 164 | tab: TabIdx, 165 | phrase: String, 166 | ) -> Vec> { 167 | let mut result = Vec::>::new(); 168 | 169 | for (idx, tab_state) in tabs.iter().enumerate() { 170 | if idx == tab && tab_state.search_mode { 171 | result.push(TabState { 172 | phrase: phrase.clone(), 173 | ..tab_state.clone() 174 | }); 175 | } else { 176 | result.push(tab_state.clone()); 177 | } 178 | } 179 | 180 | result 181 | } 182 | 183 | fn apply_search_in_tab( 184 | tabs: Vec>, 185 | tab: usize, 186 | ) -> Vec> { 187 | let mut result = Vec::>::new(); 188 | 189 | for (idx, tab_state) in tabs.iter().enumerate() { 190 | if idx == tab { 191 | result.push(TabState { 192 | search_mode: false, 193 | tab_state: ListState::default(), 194 | ..tab_state.clone() 195 | }); 196 | } else { 197 | result.push(tab_state.clone()); 198 | } 199 | } 200 | 201 | result 202 | } 203 | -------------------------------------------------------------------------------- /src/app/reducers/symlink_reducer.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, path::PathBuf}; 2 | 3 | use crate::app::{ 4 | actions::{PanelSide, SymlinkAction}, 5 | file_system::FileSystem, 6 | state::{AppState, PanelState, TabIdx, TabState}, 7 | }; 8 | 9 | pub fn symlink_reducer( 10 | state: AppState, 11 | action: SymlinkAction, 12 | ) -> AppState { 13 | match action { 14 | SymlinkAction::Create { 15 | symlink_path, 16 | panel, 17 | } => create_symlink(state, symlink_path, panel), 18 | _ => state, 19 | } 20 | } 21 | 22 | fn create_symlink( 23 | mut state: AppState, 24 | symlink_path: PathBuf, 25 | panel: crate::app::actions::PanelInfo, 26 | ) -> AppState { 27 | match panel.side { 28 | PanelSide::Left => AppState { 29 | left_panel: PanelState { 30 | tabs: create_symlink_in_tab( 31 | symlink_path, 32 | panel.tab, 33 | panel.path, 34 | &mut state.file_system, 35 | state.left_panel.tabs, 36 | ), 37 | ..state.left_panel 38 | }, 39 | ..state 40 | }, 41 | PanelSide::Right => AppState { 42 | right_panel: PanelState { 43 | tabs: create_symlink_in_tab( 44 | symlink_path, 45 | panel.tab, 46 | panel.path, 47 | &mut state.file_system, 48 | state.right_panel.tabs, 49 | ), 50 | ..state.right_panel 51 | }, 52 | ..state 53 | }, 54 | } 55 | } 56 | 57 | fn create_symlink_in_tab( 58 | symlink_path: PathBuf, 59 | tab: TabIdx, 60 | path: PathBuf, 61 | file_system: &mut TFileSystem, 62 | tabs: Vec>, 63 | ) -> Vec> { 64 | let mut result = Vec::>::new(); 65 | 66 | for (idx, tab_state) in tabs.iter().enumerate() { 67 | if idx == tab { 68 | match file_system.create_symlink(&symlink_path, &path) { 69 | Ok(_) => result.push(tab_state.clone()), 70 | Err(err) => { 71 | eprintln!("{}", err); 72 | result.push(tab_state.clone()) 73 | } 74 | } 75 | } else { 76 | result.push(tab_state.clone()); 77 | } 78 | } 79 | 80 | result 81 | } 82 | -------------------------------------------------------------------------------- /src/app/reducers/tab_reducer.rs: -------------------------------------------------------------------------------- 1 | use crate::app::{ 2 | actions::{PanelSide, TabAction}, 3 | file_system::FileSystem, 4 | state::{AppState, PanelState, TabState}, 5 | }; 6 | use std::{fmt::Debug, path::PathBuf}; 7 | 8 | use super::reload_tab_with_path; 9 | 10 | pub fn tab_reducer( 11 | state: AppState, 12 | tab_action: TabAction, 13 | ) -> AppState { 14 | match tab_action { 15 | TabAction::Next => select_next(state), 16 | TabAction::Previous => select_previous(state), 17 | TabAction::SelectNext => select_multiple_next(state), 18 | TabAction::SelectPrev => select_multiple_prev(state), 19 | TabAction::ClearSelection => clear_selections(state), 20 | TabAction::ReloadTab { panel_side, path } => reload_state_tab(state, panel_side, path), 21 | } 22 | } 23 | 24 | fn reload_state_tab( 25 | state: AppState, 26 | panel_side: PanelSide, 27 | path: PathBuf, 28 | ) -> AppState { 29 | match panel_side { 30 | PanelSide::Left => AppState { 31 | left_panel: PanelState { 32 | tabs: reload_tab_with_path( 33 | path.as_ref(), 34 | state.left_panel.tabs, 35 | &state.file_system, 36 | &state.config.icons, 37 | ), 38 | ..state.left_panel 39 | }, 40 | ..state 41 | }, 42 | PanelSide::Right => AppState { 43 | right_panel: PanelState { 44 | tabs: reload_tab_with_path( 45 | path.as_ref(), 46 | state.right_panel.tabs, 47 | &state.file_system, 48 | &state.config.icons, 49 | ), 50 | ..state.right_panel 51 | }, 52 | ..state 53 | }, 54 | } 55 | } 56 | 57 | fn clear_selections( 58 | state: AppState, 59 | ) -> AppState { 60 | if state.left_panel.is_focused { 61 | AppState { 62 | left_panel: PanelState { 63 | tabs: clear_selections_in_tab(state.left_panel.current_tab, state.left_panel.tabs), 64 | ..state.left_panel 65 | }, 66 | ..state 67 | } 68 | } else if state.right_panel.is_focused { 69 | AppState { 70 | right_panel: PanelState { 71 | tabs: clear_selections_in_tab( 72 | state.right_panel.current_tab, 73 | state.right_panel.tabs, 74 | ), 75 | ..state.right_panel 76 | }, 77 | ..state 78 | } 79 | } else { 80 | AppState { ..state } 81 | } 82 | } 83 | 84 | fn select_multiple_prev( 85 | state: AppState, 86 | ) -> AppState { 87 | if state.left_panel.is_focused { 88 | AppState { 89 | left_panel: PanelState { 90 | tabs: select_multiple_prev_in_tab( 91 | state.left_panel.current_tab, 92 | state.left_panel.tabs, 93 | ), 94 | ..state.left_panel 95 | }, 96 | ..state 97 | } 98 | } else if state.right_panel.is_focused { 99 | AppState { 100 | right_panel: PanelState { 101 | tabs: select_multiple_prev_in_tab( 102 | state.right_panel.current_tab, 103 | state.right_panel.tabs, 104 | ), 105 | ..state.right_panel 106 | }, 107 | ..state 108 | } 109 | } else { 110 | AppState { ..state } 111 | } 112 | } 113 | 114 | fn select_multiple_next( 115 | state: AppState, 116 | ) -> AppState { 117 | if state.left_panel.is_focused { 118 | AppState { 119 | left_panel: PanelState { 120 | tabs: select_multiple_next_in_tab( 121 | state.left_panel.current_tab, 122 | state.left_panel.tabs, 123 | ), 124 | ..state.left_panel 125 | }, 126 | ..state 127 | } 128 | } else if state.right_panel.is_focused { 129 | AppState { 130 | right_panel: PanelState { 131 | tabs: select_multiple_next_in_tab( 132 | state.right_panel.current_tab, 133 | state.right_panel.tabs, 134 | ), 135 | ..state.right_panel 136 | }, 137 | ..state 138 | } 139 | } else { 140 | AppState { ..state } 141 | } 142 | } 143 | 144 | fn select_next( 145 | state: AppState, 146 | ) -> AppState { 147 | if state.left_panel.is_focused { 148 | AppState { 149 | left_panel: PanelState { 150 | tabs: select_next_element(state.left_panel.current_tab, state.left_panel.tabs), 151 | ..state.left_panel 152 | }, 153 | ..state 154 | } 155 | } else if state.right_panel.is_focused { 156 | AppState { 157 | right_panel: PanelState { 158 | tabs: select_next_element(state.right_panel.current_tab, state.right_panel.tabs), 159 | ..state.right_panel 160 | }, 161 | ..state 162 | } 163 | } else { 164 | AppState { ..state } 165 | } 166 | } 167 | 168 | fn select_previous( 169 | state: AppState, 170 | ) -> AppState { 171 | if state.left_panel.is_focused { 172 | AppState { 173 | left_panel: PanelState { 174 | tabs: select_prev_element(state.left_panel.current_tab, state.left_panel.tabs), 175 | ..state.left_panel 176 | }, 177 | ..state 178 | } 179 | } else if state.right_panel.is_focused { 180 | AppState { 181 | right_panel: PanelState { 182 | tabs: select_prev_element(state.right_panel.current_tab, state.right_panel.tabs), 183 | ..state.right_panel 184 | }, 185 | ..state 186 | } 187 | } else { 188 | AppState { ..state } 189 | } 190 | } 191 | 192 | fn select_next_element( 193 | current_tab: usize, 194 | items: Vec>, 195 | ) -> Vec> { 196 | let mut result = Vec::>::new(); 197 | for (idx, val) in items.iter().enumerate() { 198 | let filtered_items = val.filtered_items(); 199 | if idx == current_tab && filtered_items.is_empty() == false { 200 | let next_tab = match val.tab_state.selected() { 201 | Some(current) => { 202 | if current >= filtered_items.len() - 1 { 203 | 0 204 | } else { 205 | current + 1 206 | } 207 | } 208 | None => 0, 209 | }; 210 | let mut tab_state = val.tab_state.clone(); 211 | tab_state.select(Some(next_tab)); 212 | result.push(TabState { 213 | tab_state, 214 | selected: vec![filtered_items[next_tab].clone()], 215 | ..val.clone() 216 | }) 217 | } else { 218 | result.push(val.clone()); 219 | } 220 | } 221 | 222 | result 223 | } 224 | 225 | fn select_prev_element( 226 | current_tab: usize, 227 | items: Vec>, 228 | ) -> Vec> { 229 | let mut result = Vec::>::new(); 230 | for (idx, val) in items.iter().enumerate() { 231 | let filtered_items = val.filtered_items(); 232 | if idx == current_tab && filtered_items.is_empty() == false { 233 | let prev_tab = match val.tab_state.selected() { 234 | Some(current) => { 235 | if current == 0 { 236 | val.filtered_items().len() - 1 237 | } else { 238 | current - 1 239 | } 240 | } 241 | None => 0, 242 | }; 243 | let mut tab_state = val.tab_state.clone(); 244 | tab_state.select(Some(prev_tab)); 245 | result.push(TabState { 246 | tab_state, 247 | selected: vec![filtered_items[prev_tab].clone()], 248 | ..val.clone() 249 | }); 250 | } else { 251 | result.push(val.clone()); 252 | } 253 | } 254 | 255 | result 256 | } 257 | 258 | fn select_multiple_next_in_tab( 259 | current_tab: usize, 260 | tabs: Vec>, 261 | ) -> Vec> { 262 | let mut result = Vec::>::new(); 263 | for (idx, val) in tabs.iter().enumerate() { 264 | let filtered_items = val.filtered_items(); 265 | if idx == current_tab && filtered_items.is_empty() == false { 266 | let next_tab = match val.tab_state.selected() { 267 | Some(current) => { 268 | if current >= filtered_items.len() - 1 { 269 | 0 270 | } else { 271 | current + 1 272 | } 273 | } 274 | None => 0, 275 | }; 276 | 277 | let mut tab_state = val.tab_state.clone(); 278 | tab_state.select(Some(next_tab)); 279 | 280 | let mut selected = val.selected.clone(); 281 | selected.push(filtered_items[next_tab].clone()); 282 | 283 | result.push(TabState { 284 | tab_state, 285 | selected, 286 | ..val.clone() 287 | }); 288 | } else { 289 | result.push(val.clone()); 290 | } 291 | } 292 | 293 | result 294 | } 295 | 296 | fn select_multiple_prev_in_tab( 297 | current_tab: usize, 298 | tabs: Vec>, 299 | ) -> Vec> { 300 | let mut result = Vec::>::new(); 301 | for (idx, val) in tabs.iter().enumerate() { 302 | let filtered_items = val.filtered_items(); 303 | if idx == current_tab && filtered_items.is_empty() == false { 304 | let prev_tab = match val.tab_state.selected() { 305 | Some(current) => { 306 | if current == 0 { 307 | val.filtered_items().len() - 1 308 | } else { 309 | current - 1 310 | } 311 | } 312 | None => 0, 313 | }; 314 | 315 | let mut tab_state = val.tab_state.clone(); 316 | tab_state.select(Some(prev_tab)); 317 | 318 | let mut selected = val.selected.clone(); 319 | selected.push(filtered_items[prev_tab].clone()); 320 | 321 | result.push(TabState { 322 | tab_state, 323 | selected, 324 | ..val.clone() 325 | }); 326 | } else { 327 | result.push(val.clone()); 328 | } 329 | } 330 | 331 | result 332 | } 333 | 334 | fn clear_selections_in_tab( 335 | current_tab: usize, 336 | tabs: Vec>, 337 | ) -> Vec> { 338 | let mut result = Vec::>::new(); 339 | for (idx, val) in tabs.iter().enumerate() { 340 | let filtered_items = val.filtered_items(); 341 | if idx == current_tab && filtered_items.is_empty() == false { 342 | let mut tab_state = val.tab_state.clone(); 343 | tab_state.select(None); 344 | result.push(TabState { 345 | tab_state, 346 | selected: Vec::new(), 347 | ..val.clone() 348 | }); 349 | } else { 350 | result.push(val.clone()); 351 | } 352 | } 353 | 354 | result 355 | } 356 | -------------------------------------------------------------------------------- /src/app/state.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use tui::widgets::ListState; 5 | 6 | use super::{ 7 | actions::PanelSide, 8 | config::{icon_cfg::IconsConfig, Config}, 9 | file_system::{file_system_item::FileSystemItem, DirInfo, FileSystem}, 10 | }; 11 | 12 | pub type TabIdx = usize; 13 | 14 | #[derive(Clone, Debug)] 15 | pub struct ChildProgramDesc { 16 | pub program_name: String, 17 | pub args: Vec, 18 | } 19 | 20 | #[derive(Clone, Debug)] 21 | pub struct AppState { 22 | pub left_panel: PanelState, 23 | pub right_panel: PanelState, 24 | pub app_exit: bool, 25 | pub config: Config, 26 | pub child_program: Option, 27 | pub modal: Option, 28 | pub file_system: TFileSystem, 29 | } 30 | 31 | impl AppState { 32 | pub fn new(config: Config, file_system: TFileSystem) -> Self { 33 | let mut state = AppState::default(); 34 | state.file_system = file_system; 35 | state.config = config; 36 | 37 | state 38 | } 39 | } 40 | 41 | impl Default for AppState { 42 | fn default() -> Self { 43 | AppState { 44 | left_panel: PanelState::default(), 45 | right_panel: PanelState::default(), 46 | app_exit: false, 47 | config: Config::default(), 48 | child_program: None, 49 | modal: None, 50 | file_system: TFileSystem::default(), 51 | } 52 | } 53 | } 54 | 55 | #[derive(Clone, Debug)] 56 | pub struct PanelState { 57 | pub tabs: Vec>, 58 | pub is_focused: bool, 59 | pub current_tab: TabIdx, 60 | pub marker: std::marker::PhantomData, 61 | } 62 | 63 | impl Default for PanelState { 64 | fn default() -> Self { 65 | PanelState { 66 | tabs: vec![TabState::default()], 67 | is_focused: false, 68 | current_tab: 0, 69 | marker: std::marker::PhantomData, 70 | } 71 | } 72 | } 73 | 74 | #[derive(Clone, Debug)] 75 | pub struct TabState { 76 | pub name: String, 77 | pub icon: String, 78 | pub path: PathBuf, 79 | pub items: Vec, 80 | pub selected: Vec, 81 | pub tab_state: ListState, 82 | pub search_mode: bool, 83 | pub phrase: String, 84 | pub marker: std::marker::PhantomData, 85 | } 86 | 87 | impl Default for TabState { 88 | fn default() -> Self { 89 | TabState::with_dir( 90 | &Path::new("."), 91 | &TFileSystem::default(), 92 | &IconsConfig::default(), 93 | ) 94 | } 95 | } 96 | 97 | impl TabState { 98 | pub fn with_dir(dir_path: &Path, file_system: &TFileSystem, icons: &IconsConfig) -> Self { 99 | let dir_info = DirInfo::new(&dir_path).unwrap(); 100 | let items = file_system.list_dir(&dir_info.path, icons); 101 | TabState { 102 | name: dir_info.name.clone(), 103 | icon: icons.get_dir_icon(dir_info.name.clone()), 104 | path: dir_info.path.clone(), 105 | items, 106 | selected: Vec::new(), 107 | tab_state: ListState::default(), 108 | search_mode: false, 109 | phrase: String::from(""), 110 | marker: std::marker::PhantomData, 111 | } 112 | } 113 | 114 | pub fn filtered_items(&self) -> Vec<&FileSystemItem> { 115 | if self.phrase.is_empty() { 116 | self.items.iter().collect() 117 | } else { 118 | self.items 119 | .iter() 120 | .filter(|item| { 121 | item.get_name() 122 | .to_lowercase() 123 | .contains(&self.phrase.to_lowercase()) 124 | }) 125 | .collect() 126 | } 127 | } 128 | } 129 | 130 | #[derive(Clone, Debug)] 131 | pub enum ModalType { 132 | RenameModal { 133 | panel_side: PanelSide, 134 | panel_tab: TabIdx, 135 | item: FileSystemItem, 136 | }, 137 | CreateModal { 138 | item_index: Option, 139 | panel_side: PanelSide, 140 | panel_tab: TabIdx, 141 | panel_tab_path: PathBuf, 142 | }, 143 | ErrorModal(String), 144 | DeleteDirWithContent { 145 | panel_side: PanelSide, 146 | panel_tab: TabIdx, 147 | path: PathBuf, 148 | }, 149 | } 150 | -------------------------------------------------------------------------------- /src/core/color_scheme.rs: -------------------------------------------------------------------------------- 1 | use toml::Value; 2 | use tui::style::Color; 3 | 4 | #[derive(Clone, Copy, Debug)] 5 | pub struct ColorScheme { 6 | pub foregorund: Color, 7 | pub background: Color, 8 | 9 | pub normal_black: Color, 10 | pub normal_red: Color, 11 | pub normal_green: Color, 12 | pub normal_yellow: Color, 13 | pub normal_blue: Color, 14 | pub normal_magneta: Color, 15 | pub normal_cyan: Color, 16 | pub normal_white: Color, 17 | 18 | pub light_black: Color, 19 | pub light_red: Color, 20 | pub light_green: Color, 21 | pub light_yellow: Color, 22 | pub light_blue: Color, 23 | pub light_magneta: Color, 24 | pub light_cyan: Color, 25 | pub light_white: Color, 26 | } 27 | 28 | impl ColorScheme { 29 | pub fn new( 30 | foregorund: Color, 31 | background: Color, 32 | normal_black: Color, 33 | normal_red: Color, 34 | normal_green: Color, 35 | normal_yellow: Color, 36 | normal_blue: Color, 37 | normal_magneta: Color, 38 | normal_cyan: Color, 39 | normal_white: Color, 40 | light_black: Color, 41 | light_red: Color, 42 | light_green: Color, 43 | light_yellow: Color, 44 | light_blue: Color, 45 | light_magneta: Color, 46 | light_cyan: Color, 47 | light_white: Color, 48 | ) -> Self { 49 | Self { 50 | foregorund, 51 | background, 52 | normal_black, 53 | normal_red, 54 | normal_green, 55 | normal_yellow, 56 | normal_blue, 57 | normal_magneta, 58 | normal_cyan, 59 | normal_white, 60 | light_black, 61 | light_red, 62 | light_green, 63 | light_yellow, 64 | light_blue, 65 | light_magneta, 66 | light_cyan, 67 | light_white, 68 | } 69 | } 70 | 71 | pub fn update_from_file(&mut self, cfg: &Value) { 72 | if let Some(foregorund) = cfg.get("foregorund") { 73 | self.foregorund = map_color(&foregorund); 74 | } 75 | 76 | if let Some(background) = cfg.get("background") { 77 | self.background = map_color(&background); 78 | } 79 | 80 | if let Some(normal_black) = cfg.get("normal_black") { 81 | self.normal_black = map_color(&normal_black); 82 | } 83 | 84 | if let Some(normal_red) = cfg.get("normal_red") { 85 | self.normal_red = map_color(&normal_red); 86 | } 87 | 88 | if let Some(normal_green) = cfg.get("normal_green") { 89 | self.normal_green = map_color(&normal_green); 90 | } 91 | 92 | if let Some(normal_yellow) = cfg.get("normal_yellow") { 93 | self.normal_yellow = map_color(&normal_yellow); 94 | } 95 | 96 | if let Some(normal_blue) = cfg.get("normal_blue") { 97 | self.normal_blue = map_color(&normal_blue); 98 | } 99 | 100 | if let Some(normal_magneta) = cfg.get("normal_magneta") { 101 | self.normal_magneta = map_color(&normal_magneta); 102 | } 103 | 104 | if let Some(normal_cyan) = cfg.get("normal_cyan") { 105 | self.normal_cyan = map_color(&normal_cyan); 106 | } 107 | 108 | if let Some(normal_white) = cfg.get("normal_white") { 109 | self.normal_white = map_color(&normal_white); 110 | } 111 | 112 | if let Some(light_black) = cfg.get("light_black") { 113 | self.light_black = map_color(&light_black); 114 | } 115 | 116 | if let Some(light_red) = cfg.get("light_red") { 117 | self.light_red = map_color(&light_red); 118 | } 119 | 120 | if let Some(light_green) = cfg.get("light_green") { 121 | self.light_green = map_color(&light_green); 122 | } 123 | 124 | if let Some(light_yellow) = cfg.get("light_yellow") { 125 | self.light_yellow = map_color(&light_yellow); 126 | } 127 | 128 | if let Some(light_blue) = cfg.get("light_blue") { 129 | self.light_blue = map_color(&light_blue); 130 | } 131 | 132 | if let Some(light_magneta) = cfg.get("light_magneta") { 133 | self.light_magneta = map_color(&light_magneta); 134 | } 135 | 136 | if let Some(light_cyan) = cfg.get("light_cyan") { 137 | self.light_cyan = map_color(&light_cyan); 138 | } 139 | 140 | if let Some(light_white) = cfg.get("light_white") { 141 | self.light_white = map_color(&light_white); 142 | } 143 | } 144 | } 145 | 146 | fn map_color(value: &Value) -> Color { 147 | match value { 148 | Value::String(s) => match s.as_str() { 149 | "Reset" => Color::Reset, 150 | "Black" => Color::Black, 151 | "Red" => Color::Red, 152 | "Green" => Color::Green, 153 | "Yellow" => Color::Yellow, 154 | "Blue" => Color::Blue, 155 | "Magenta" => Color::Magenta, 156 | "Cyan" => Color::Cyan, 157 | "Gray" => Color::Gray, 158 | "DarkGray" => Color::DarkGray, 159 | "LightRed" => Color::LightRed, 160 | "LightGreen" => Color::LightGreen, 161 | "LightYellow" => Color::LightYellow, 162 | "LightBlue" => Color::LightBlue, 163 | "LightMagenta" => Color::LightMagenta, 164 | "LightCyan" => Color::LightCyan, 165 | "White" => Color::White, 166 | _ => Color::Reset, 167 | }, 168 | Value::Integer(i) => Color::Indexed(i.clone() as u8), 169 | Value::Table(t) => { 170 | let red = t["red"].as_integer().unwrap().clone() as u8; 171 | let green = t["green"].as_integer().unwrap().clone() as u8; 172 | let blue = t["blue"].as_integer().unwrap().clone() as u8; 173 | Color::Rgb(red, green, blue) 174 | } 175 | _ => Color::Reset, 176 | } 177 | } 178 | 179 | impl Default for ColorScheme { 180 | fn default() -> Self { 181 | ColorScheme::new( 182 | Color::White, 183 | Color::Reset, 184 | Color::Black, 185 | Color::Red, 186 | Color::Green, 187 | Color::Yellow, 188 | Color::Blue, 189 | Color::Magenta, 190 | Color::Cyan, 191 | Color::White, 192 | Color::Gray, 193 | Color::LightRed, 194 | Color::LightGreen, 195 | Color::LightYellow, 196 | Color::LightBlue, 197 | Color::LightMagenta, 198 | Color::LightCyan, 199 | Color::White, 200 | ) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/core/config.rs: -------------------------------------------------------------------------------- 1 | use toml::Value; 2 | 3 | use super::color_scheme::ColorScheme; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct CoreConfig { 7 | pub tick_rate: u64, 8 | pub color_scheme: ColorScheme, 9 | pub list_arrow: String, 10 | } 11 | 12 | impl Default for CoreConfig { 13 | fn default() -> Self { 14 | CoreConfig { 15 | tick_rate: 240, 16 | color_scheme: ColorScheme::default(), 17 | list_arrow: ">>".to_string(), 18 | } 19 | } 20 | } 21 | 22 | impl CoreConfig { 23 | pub fn update_from_file(&mut self, cfg: &Value) { 24 | if let Some(core) = cfg.get("core") { 25 | if let Value::Table(core) = core { 26 | if let Some(tick_rate) = core.get("tick_rate") { 27 | if let Value::Integer(tick_rate) = tick_rate { 28 | self.tick_rate = tick_rate.clone() as u64; 29 | } 30 | } 31 | 32 | if let Some(list_arrow) = core.get("list_arrow") { 33 | if let Value::String(list_arrow) = list_arrow { 34 | self.list_arrow = list_arrow.clone(); 35 | } 36 | } 37 | } 38 | } 39 | 40 | if let Some(color_scheme) = cfg.get("color_scheme") { 41 | self.color_scheme.update_from_file(color_scheme); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/events.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{self, KeyEvent, MouseEvent}; 2 | use std::{ 3 | sync::mpsc::RecvError, 4 | sync::{ 5 | atomic::{AtomicBool, Ordering}, 6 | mpsc::{channel, Receiver}, 7 | Arc, 8 | }, 9 | thread, 10 | thread::JoinHandle, 11 | time::Duration, 12 | time::Instant, 13 | }; 14 | 15 | use super::config::CoreConfig; 16 | #[derive(Clone, Copy, Debug)] 17 | pub struct Size { 18 | pub width: u16, 19 | pub height: u16, 20 | } 21 | 22 | #[derive(Copy, Clone, Debug)] 23 | pub enum Event { 24 | Mouse(MouseEvent), 25 | Keyboard(KeyEvent), 26 | Resize(Size), 27 | Error(Error), 28 | Tick, 29 | } 30 | 31 | #[derive(Debug, Copy, Clone)] 32 | pub enum Error { 33 | MessagePoolError, 34 | EventReadError, 35 | } 36 | 37 | impl ToString for Error { 38 | fn to_string(&self) -> String { 39 | match self { 40 | Error::MessagePoolError => "Error happend on message pool".to_string(), 41 | Error::EventReadError => "Error on event read".to_string(), 42 | } 43 | } 44 | } 45 | 46 | pub struct EventQueue { 47 | receiver: Receiver, 48 | skip_input_event: Arc, 49 | _runner_handle: JoinHandle<()>, 50 | } 51 | 52 | impl EventQueue { 53 | pub fn start() -> Self { 54 | EventQueue::start_with_config(CoreConfig::default()) 55 | } 56 | 57 | pub fn start_with_config(config: CoreConfig) -> Self { 58 | let (sender, receiver) = channel(); 59 | let tick_rate = Duration::from_millis(config.tick_rate); 60 | let skip_input_event = Arc::new(AtomicBool::new(false)); 61 | 62 | let skip_event_read = skip_input_event.clone(); 63 | let runner_handle = thread::spawn(move || { 64 | let mut last_tick = Instant::now(); 65 | loop { 66 | let timeout = tick_rate 67 | .checked_sub(last_tick.elapsed()) 68 | .unwrap_or_else(|| Duration::from_millis(0)); 69 | 70 | if skip_event_read.load(Ordering::Relaxed) { 71 | continue; 72 | } 73 | 74 | if event::poll(timeout).unwrap() { 75 | match event::read() { 76 | Ok(event) => { 77 | match event { 78 | event::Event::Key(key) => { 79 | sender.send(Event::Keyboard(key)).unwrap(); 80 | } 81 | event::Event::Mouse(mouse) => { 82 | sender.send(Event::Mouse(mouse)).unwrap(); 83 | } 84 | event::Event::Resize(width, height) => { 85 | sender.send(Event::Resize(Size { width, height })).unwrap(); 86 | } 87 | }; 88 | } 89 | Err(_err) => { 90 | sender.send(Event::Error(Error::EventReadError)).unwrap(); 91 | } 92 | }; 93 | } 94 | 95 | if last_tick.elapsed() >= tick_rate { 96 | sender.send(Event::Tick).unwrap(); 97 | last_tick = Instant::now(); 98 | } 99 | } 100 | }); 101 | 102 | EventQueue { 103 | receiver, 104 | skip_input_event: skip_input_event.clone(), 105 | _runner_handle: runner_handle, 106 | } 107 | } 108 | 109 | pub fn lock_event_read(&mut self) { 110 | self.skip_input_event.store(true, Ordering::Relaxed); 111 | } 112 | 113 | pub fn unlock_event_read(&mut self) { 114 | self.skip_input_event.store(false, Ordering::Relaxed); 115 | } 116 | 117 | pub fn pool(&self) -> Result { 118 | self.receiver.recv() 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/core/key_binding.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | pub struct KeyBinding { 5 | key: KeyCode, 6 | modifiers: KeyModifiers, 7 | } 8 | 9 | impl KeyBinding { 10 | pub fn with_modifiers(key: KeyCode, modifiers: KeyModifiers) -> Self { 11 | KeyBinding { key, modifiers } 12 | } 13 | 14 | pub fn new(key: KeyCode) -> Self { 15 | KeyBinding { 16 | key, 17 | modifiers: KeyModifiers::empty(), 18 | } 19 | } 20 | 21 | pub fn is_pressed(&self, key_evt: KeyEvent) -> bool { 22 | self.modifiers == key_evt.modifiers && self.key == key_evt.code 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | use tui::{layout::Rect, text::Spans}; 2 | 3 | pub mod color_scheme; 4 | pub mod config; 5 | pub mod events; 6 | pub mod key_binding; 7 | pub mod store; 8 | pub mod ui; 9 | 10 | pub trait ToSpans { 11 | fn to_spans(&self, area: Rect, show_icons: bool) -> Spans; 12 | } 13 | -------------------------------------------------------------------------------- /src/core/store.rs: -------------------------------------------------------------------------------- 1 | pub struct Store 2 | where 3 | TState: Default + Clone, 4 | TAction: Clone, 5 | { 6 | is_dirty: bool, 7 | state: TState, 8 | root_reducer: RootReducer, 9 | listeners: Vec>, 10 | middlewares: Vec>, 11 | } 12 | 13 | impl Store 14 | where 15 | TState: Default + Clone, 16 | TAction: Clone, 17 | { 18 | pub fn new(root_reducer: RootReducer) -> Self { 19 | Store::with_state(root_reducer, TState::default()) 20 | } 21 | 22 | pub fn with_state(root_reducer: RootReducer, state: TState) -> Self { 23 | Store { 24 | is_dirty: false, 25 | state, 26 | root_reducer, 27 | listeners: Vec::new(), 28 | middlewares: Vec::new(), 29 | } 30 | } 31 | 32 | pub fn mark_as_dirty(&mut self) { 33 | self.is_dirty = true 34 | } 35 | 36 | pub fn clean(&mut self) { 37 | self.is_dirty = false 38 | } 39 | 40 | pub fn is_dirty(&self) -> bool { 41 | self.is_dirty 42 | } 43 | 44 | pub fn get_state(&self) -> TState { 45 | self.state.clone() 46 | } 47 | 48 | pub fn dispatch(&mut self, action: TAction) { 49 | if self.middlewares.is_empty() == false { 50 | self.dispatch_middlewares(0, action); 51 | } else { 52 | self.state = self.dispatch_reducer(action); 53 | } 54 | self.mark_as_dirty(); 55 | } 56 | 57 | pub fn register_listener(&mut self, listener: Listener) { 58 | self.listeners.push(listener); 59 | } 60 | 61 | pub fn register_middleware(&mut self, middleware: Middleware) { 62 | self.middlewares.push(middleware); 63 | } 64 | 65 | fn dispatch_reducer(&self, action: TAction) -> TState { 66 | (self.root_reducer)(self.state.clone(), action) 67 | } 68 | 69 | fn dispatch_middlewares(&mut self, order: usize, action: TAction) { 70 | if order == self.middlewares.len() { 71 | self.state = self.dispatch_reducer(action.clone()); 72 | return; 73 | } 74 | 75 | if let Some(middleware_action) = self.middlewares[order](self, action.clone()) { 76 | self.dispatch_middlewares(order + 1, middleware_action.clone()); 77 | } 78 | } 79 | } 80 | 81 | type RootReducer = fn(TState, TAction) -> TState; 82 | type Listener = fn(&TState); 83 | type Middleware = fn(&mut Store, TAction) -> Option; 84 | -------------------------------------------------------------------------------- /src/core/ui/component.rs: -------------------------------------------------------------------------------- 1 | use tui::{backend::Backend, layout::Rect, Frame}; 2 | 3 | use crate::core::store::Store; 4 | 5 | pub trait Component 6 | where 7 | TEvent: Clone, 8 | TGlobalState: Default + Clone, 9 | TAction: Clone, 10 | { 11 | fn on_init(&mut self, _store: &Store) {} 12 | fn handle_event(&mut self, _event: TEvent, _store: &mut Store) -> bool { 13 | true 14 | } 15 | fn on_tick(&mut self, _store: &mut Store) {} 16 | fn render(&self, frame: &mut Frame, area: Option); 17 | } 18 | -------------------------------------------------------------------------------- /src/core/ui/component_base.rs: -------------------------------------------------------------------------------- 1 | pub struct ComponentBase 2 | where 3 | TProps: Default + Clone, 4 | TState: Default + Clone, 5 | { 6 | props: Option, 7 | state: Option, 8 | } 9 | 10 | impl Default for ComponentBase 11 | where 12 | TProps: Default + Clone, 13 | TState: Default + Clone, 14 | { 15 | fn default() -> Self { 16 | ComponentBase { 17 | props: None, 18 | state: None, 19 | } 20 | } 21 | } 22 | 23 | impl ComponentBase 24 | where 25 | TProps: Default + Clone, 26 | TState: Default + Clone, 27 | { 28 | pub fn new(props: Option, state: Option) -> Self { 29 | ComponentBase { props, state } 30 | } 31 | 32 | pub fn get_props(&self) -> Option { 33 | self.props.clone() 34 | } 35 | 36 | pub fn get_state(&self) -> Option { 37 | self.state.clone() 38 | } 39 | 40 | pub fn set_state(&mut self, callback: StateSetter) 41 | where 42 | StateSetter: Fn(TState) -> TState, 43 | { 44 | if let Some(state) = self.state.clone() { 45 | self.state = Some(callback(state.clone())) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/core/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod component; 2 | pub mod component_base; 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | use crate::core::events::Event; 5 | use crate::core::ui::component::Component; 6 | use crate::core::{events::EventQueue, store::Store}; 7 | use std::{error::Error, io::stdout, process::Command}; 8 | 9 | use app::{ 10 | actions::FileManagerActions, 11 | components::root::RootComponent, 12 | config::Config, 13 | file_system::PhysicalFileSystem, 14 | middlewares::{dir_middleware, symlink_middleware}, 15 | reducers::root_reducer, 16 | state::AppState, 17 | }; 18 | 19 | use crossterm::{ 20 | event::{DisableMouseCapture, EnableMouseCapture}, 21 | execute, 22 | terminal::disable_raw_mode, 23 | terminal::{enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 24 | }; 25 | use tui::{backend::CrosstermBackend, Terminal}; 26 | 27 | lazy_static! { 28 | static ref CONFIG_PATHS: Vec = 29 | vec!["~/sfm.toml".to_string(), "~/.config/sfm.toml".to_string()]; 30 | } 31 | 32 | pub mod app; 33 | pub mod core; 34 | 35 | fn main() -> Result<(), Box> { 36 | let file_system = PhysicalFileSystem::default(); 37 | let cfg = Config::load_or_default(CONFIG_PATHS.to_vec(), &file_system); 38 | enable_raw_mode()?; 39 | let mut stdout = stdout(); 40 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 41 | 42 | let backend = CrosstermBackend::new(stdout); 43 | 44 | let mut terminal = Terminal::new(backend)?; 45 | let mut event_queue = EventQueue::start_with_config(cfg.core_cfg.clone()); 46 | 47 | let mut store = Store::, FileManagerActions>::with_state( 48 | root_reducer, 49 | AppState::::new(cfg, file_system), 50 | ); 51 | 52 | terminal.clear()?; 53 | 54 | let mut root_component = RootComponent::new(); 55 | store.dispatch(FileManagerActions::App(app::actions::AppAction::FocusLeft)); 56 | store.register_middleware(symlink_middleware); 57 | store.register_middleware(dir_middleware); 58 | root_component.on_init(&store); 59 | 60 | loop { 61 | terminal.draw(|f| root_component.render(f, None))?; 62 | 63 | let state = store.get_state(); 64 | 65 | if let Ok(event) = event_queue.pool() { 66 | if let Event::Tick = event { 67 | root_component.on_tick(&mut store); 68 | } else { 69 | root_component.handle_event(event, &mut store); 70 | } 71 | } 72 | 73 | if let Some(program_desc) = state.child_program { 74 | event_queue.lock_event_read(); 75 | match Command::new(program_desc.program_name) 76 | .args(program_desc.args.as_slice()) 77 | .spawn() 78 | { 79 | Ok(mut child) => { 80 | child.wait().expect(""); 81 | store.dispatch(FileManagerActions::App( 82 | app::actions::AppAction::ChildProgramClosed, 83 | )); 84 | terminal.clear()?; 85 | terminal.draw(|f| root_component.render(f, None))?; 86 | event_queue.unlock_event_read(); 87 | } 88 | Err(_) => {} 89 | }; 90 | } 91 | 92 | if state.app_exit { 93 | terminal.clear()?; 94 | disable_raw_mode()?; 95 | execute!( 96 | terminal.backend_mut(), 97 | LeaveAlternateScreen, 98 | DisableMouseCapture 99 | )?; 100 | terminal.show_cursor()?; 101 | break; 102 | } 103 | } 104 | 105 | Ok(()) 106 | } 107 | --------------------------------------------------------------------------------