├── .DS_Store ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── ctv.toml ├── media ├── ctv_preview.png └── ctv_print.png ├── snapcraft.yaml ├── src ├── args.rs ├── config.rs ├── main.rs ├── protocols │ ├── dir_tree.rs │ ├── file.rs │ ├── mod.rs │ └── path_type.rs └── services │ ├── group_user.rs │ ├── mod.rs │ ├── perms.rs │ ├── size.rs │ └── time.rs └── wip_features.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ANG13T/ctv/fd6b4b4d1ca3f120ba10853e2ea799f32eb287b1/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build-linux: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Build 18 | run: cargo build --verbose 19 | - name: Run tests 20 | run: cargo test --verbose 21 | build-mac: 22 | runs-on: macos-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Build 26 | run: cargo build --verbose 27 | - name: Run tests 28 | run: cargo test --verbose 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.snap -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please consider discussing the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | ## Pull Request Process 7 | 8 | 1. Ensure any install or build dependencies are removed before making a PR 9 | 2. Update the README.md (if necessary) with details of changes to the CLI, this includes new environment 10 | variables, features, helpful examples, etc. 11 | 3. You may merge the Pull Request in once you have the sign-off of @angelina-tsuboi 12 | 13 | If you need any help of clarification, please reach out to [angelina.t1832@gmail.com](mailto:angelina.t1832@gmail.com) -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.53" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" 10 | 11 | [[package]] 12 | name = "atomic" 13 | version = "0.5.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c" 16 | dependencies = [ 17 | "autocfg", 18 | ] 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.0.1" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 36 | 37 | [[package]] 38 | name = "bitflags" 39 | version = "1.3.2" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 42 | 43 | [[package]] 44 | name = "cfg-if" 45 | version = "1.0.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 48 | 49 | [[package]] 50 | name = "chrono" 51 | version = "0.4.19" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 54 | dependencies = [ 55 | "libc", 56 | "num-integer", 57 | "num-traits", 58 | "time", 59 | "winapi", 60 | ] 61 | 62 | [[package]] 63 | name = "clap" 64 | version = "3.0.14" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62" 67 | dependencies = [ 68 | "atty", 69 | "bitflags", 70 | "clap_derive", 71 | "indexmap", 72 | "lazy_static", 73 | "os_str_bytes", 74 | "strsim", 75 | "termcolor", 76 | "textwrap", 77 | ] 78 | 79 | [[package]] 80 | name = "clap_derive" 81 | version = "3.0.14" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "9a1132dc3944b31c20dd8b906b3a9f0a5d0243e092d59171414969657ac6aa85" 84 | dependencies = [ 85 | "heck", 86 | "proc-macro-error", 87 | "proc-macro2", 88 | "quote", 89 | "syn", 90 | ] 91 | 92 | [[package]] 93 | name = "colored" 94 | version = "2.0.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" 97 | dependencies = [ 98 | "atty", 99 | "lazy_static", 100 | "winapi", 101 | ] 102 | 103 | [[package]] 104 | name = "ctv" 105 | version = "0.3.3" 106 | dependencies = [ 107 | "anyhow", 108 | "chrono", 109 | "clap", 110 | "colored", 111 | "dirs", 112 | "figment", 113 | "filetime", 114 | "humansize", 115 | "libc", 116 | "serde", 117 | "toml", 118 | "users", 119 | ] 120 | 121 | [[package]] 122 | name = "dirs" 123 | version = "4.0.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 126 | dependencies = [ 127 | "dirs-sys", 128 | ] 129 | 130 | [[package]] 131 | name = "dirs-sys" 132 | version = "0.3.6" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" 135 | dependencies = [ 136 | "libc", 137 | "redox_users", 138 | "winapi", 139 | ] 140 | 141 | [[package]] 142 | name = "figment" 143 | version = "0.10.6" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "790b4292c72618abbab50f787a477014fe15634f96291de45672ce46afe122df" 146 | dependencies = [ 147 | "atomic", 148 | "pear", 149 | "serde", 150 | "toml", 151 | "uncased", 152 | "version_check", 153 | ] 154 | 155 | [[package]] 156 | name = "filetime" 157 | version = "0.2.15" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" 160 | dependencies = [ 161 | "cfg-if", 162 | "libc", 163 | "redox_syscall", 164 | "winapi", 165 | ] 166 | 167 | [[package]] 168 | name = "getrandom" 169 | version = "0.2.4" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" 172 | dependencies = [ 173 | "cfg-if", 174 | "libc", 175 | "wasi", 176 | ] 177 | 178 | [[package]] 179 | name = "hashbrown" 180 | version = "0.11.2" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 183 | 184 | [[package]] 185 | name = "heck" 186 | version = "0.4.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 189 | 190 | [[package]] 191 | name = "hermit-abi" 192 | version = "0.1.19" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 195 | dependencies = [ 196 | "libc", 197 | ] 198 | 199 | [[package]] 200 | name = "humansize" 201 | version = "1.1.1" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" 204 | 205 | [[package]] 206 | name = "indexmap" 207 | version = "1.8.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" 210 | dependencies = [ 211 | "autocfg", 212 | "hashbrown", 213 | ] 214 | 215 | [[package]] 216 | name = "inlinable_string" 217 | version = "0.1.15" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" 220 | 221 | [[package]] 222 | name = "lazy_static" 223 | version = "1.4.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 226 | 227 | [[package]] 228 | name = "libc" 229 | version = "0.2.103" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" 232 | 233 | [[package]] 234 | name = "log" 235 | version = "0.4.14" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 238 | dependencies = [ 239 | "cfg-if", 240 | ] 241 | 242 | [[package]] 243 | name = "memchr" 244 | version = "2.4.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 247 | 248 | [[package]] 249 | name = "num-integer" 250 | version = "0.1.44" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 253 | dependencies = [ 254 | "autocfg", 255 | "num-traits", 256 | ] 257 | 258 | [[package]] 259 | name = "num-traits" 260 | version = "0.2.14" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 263 | dependencies = [ 264 | "autocfg", 265 | ] 266 | 267 | [[package]] 268 | name = "os_str_bytes" 269 | version = "6.0.0" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" 272 | dependencies = [ 273 | "memchr", 274 | ] 275 | 276 | [[package]] 277 | name = "pear" 278 | version = "0.2.3" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "15e44241c5e4c868e3eaa78b7c1848cadd6344ed4f54d029832d32b415a58702" 281 | dependencies = [ 282 | "inlinable_string", 283 | "pear_codegen", 284 | "yansi", 285 | ] 286 | 287 | [[package]] 288 | name = "pear_codegen" 289 | version = "0.2.3" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "82a5ca643c2303ecb740d506539deba189e16f2754040a42901cd8105d0282d0" 292 | dependencies = [ 293 | "proc-macro2", 294 | "proc-macro2-diagnostics", 295 | "quote", 296 | "syn", 297 | ] 298 | 299 | [[package]] 300 | name = "proc-macro-error" 301 | version = "1.0.4" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 304 | dependencies = [ 305 | "proc-macro-error-attr", 306 | "proc-macro2", 307 | "quote", 308 | "syn", 309 | "version_check", 310 | ] 311 | 312 | [[package]] 313 | name = "proc-macro-error-attr" 314 | version = "1.0.4" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 317 | dependencies = [ 318 | "proc-macro2", 319 | "quote", 320 | "version_check", 321 | ] 322 | 323 | [[package]] 324 | name = "proc-macro2" 325 | version = "1.0.30" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" 328 | dependencies = [ 329 | "unicode-xid", 330 | ] 331 | 332 | [[package]] 333 | name = "proc-macro2-diagnostics" 334 | version = "0.9.1" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" 337 | dependencies = [ 338 | "proc-macro2", 339 | "quote", 340 | "syn", 341 | "version_check", 342 | "yansi", 343 | ] 344 | 345 | [[package]] 346 | name = "quote" 347 | version = "1.0.10" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" 350 | dependencies = [ 351 | "proc-macro2", 352 | ] 353 | 354 | [[package]] 355 | name = "redox_syscall" 356 | version = "0.2.10" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 359 | dependencies = [ 360 | "bitflags", 361 | ] 362 | 363 | [[package]] 364 | name = "redox_users" 365 | version = "0.4.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" 368 | dependencies = [ 369 | "getrandom", 370 | "redox_syscall", 371 | ] 372 | 373 | [[package]] 374 | name = "serde" 375 | version = "1.0.133" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" 378 | dependencies = [ 379 | "serde_derive", 380 | ] 381 | 382 | [[package]] 383 | name = "serde_derive" 384 | version = "1.0.133" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" 387 | dependencies = [ 388 | "proc-macro2", 389 | "quote", 390 | "syn", 391 | ] 392 | 393 | [[package]] 394 | name = "strsim" 395 | version = "0.10.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 398 | 399 | [[package]] 400 | name = "syn" 401 | version = "1.0.80" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" 404 | dependencies = [ 405 | "proc-macro2", 406 | "quote", 407 | "unicode-xid", 408 | ] 409 | 410 | [[package]] 411 | name = "termcolor" 412 | version = "1.1.2" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 415 | dependencies = [ 416 | "winapi-util", 417 | ] 418 | 419 | [[package]] 420 | name = "textwrap" 421 | version = "0.14.2" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" 424 | 425 | [[package]] 426 | name = "time" 427 | version = "0.1.44" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 430 | dependencies = [ 431 | "libc", 432 | "wasi", 433 | "winapi", 434 | ] 435 | 436 | [[package]] 437 | name = "toml" 438 | version = "0.5.8" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" 441 | dependencies = [ 442 | "serde", 443 | ] 444 | 445 | [[package]] 446 | name = "uncased" 447 | version = "0.9.6" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" 450 | dependencies = [ 451 | "version_check", 452 | ] 453 | 454 | [[package]] 455 | name = "unicode-xid" 456 | version = "0.2.2" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 459 | 460 | [[package]] 461 | name = "users" 462 | version = "0.11.0" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" 465 | dependencies = [ 466 | "libc", 467 | "log", 468 | ] 469 | 470 | [[package]] 471 | name = "version_check" 472 | version = "0.9.3" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 475 | 476 | [[package]] 477 | name = "wasi" 478 | version = "0.10.0+wasi-snapshot-preview1" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 481 | 482 | [[package]] 483 | name = "winapi" 484 | version = "0.3.9" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 487 | dependencies = [ 488 | "winapi-i686-pc-windows-gnu", 489 | "winapi-x86_64-pc-windows-gnu", 490 | ] 491 | 492 | [[package]] 493 | name = "winapi-i686-pc-windows-gnu" 494 | version = "0.4.0" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 497 | 498 | [[package]] 499 | name = "winapi-util" 500 | version = "0.1.5" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 503 | dependencies = [ 504 | "winapi", 505 | ] 506 | 507 | [[package]] 508 | name = "winapi-x86_64-pc-windows-gnu" 509 | version = "0.4.0" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 512 | 513 | [[package]] 514 | name = "yansi" 515 | version = "0.5.0" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" 518 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ctv" 3 | license = "MIT" 4 | version = "0.3.3" 5 | authors = ["Angelina Tsuboi "] 6 | description = "A highly configurable tree file view visualizer CLI tool" 7 | readme = "README.md" 8 | homepage = "https://github.com/angelina-tsuboi/ctv" 9 | repository = "https://github.com/angelina-tsuboi/ctv" 10 | keywords = ["cli", "treeview", "tree", "visualizer", "file"] 11 | categories = ["command-line-utilities"] 12 | edition = "2018" 13 | include = ["/src", "README.md", "contributing.md", "Cargo.toml", "media"] 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | anyhow = "1.0.53" 19 | chrono = "0.4" 20 | clap = { version = "3.0.14", features = ["derive"] } 21 | colored = "2.0.0" 22 | dirs = "4.0.0" 23 | figment = { version = "0.10.6", features = ["toml", "env"] } 24 | filetime = "0.2" 25 | humansize = "1.1.1" 26 | libc = "0.2.95" 27 | serde = { version = "1.0.127", features = ["derive"] } 28 | toml = "0.5.8" 29 | users = "0.11.0" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Angelina Tsuboi 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 | [![Github Release](https://img.shields.io/github/v/release/angelina-tsuboi/ctv?display_name=tag)](https://github.com/angelina-tsuboi/ctv/releases) [![ctv-cli](https://snapcraft.io/ctv-cli/badge.svg)](https://snapcraft.io/ctv-cli) [![Crates.io](https://img.shields.io/crates/v/ctv.svg)](https://crates.io/crates/ctv) 2 | 3 | 4 | # 🎄 ctv - configurable tree view 🎄 5 | 6 | A highly configurable tree view visualizer CLI tool written in Rust! 7 | 8 | 9 | 10 | 11 | 12 | 13 | ## What does ctv do? 14 | 15 | - Visualize your file hierarchy in a tree view 16 | - Customize the appearance of your tree 17 | - Display custom file information (permissions, time, user, etc) 18 | - Personalize tree color and text styling 19 | 20 | ## Installation 21 | 22 | ```bash 23 | # Cargo Installation 24 | cargo install ctv 25 | 26 | # Homebrew Installation 27 | brew install angelina-tsuboi/ctv/ctv 28 | 29 | # NetBSD 30 | pkgin install ctv 31 | ``` 32 | 33 | ## Using ctv 34 | 35 | ```bash 36 | ctv 37 | ``` 38 | 39 | ## Flag Options 40 | 41 | -h, --short 42 | --help Print help information 43 | -l, --limit 44 | -p, --config Show config variables and exit 45 | -V, --version Print version information 46 | 47 | # Customization 48 | 49 | ## Config File 50 | 51 | The `config.toml` file located at `~/.config/ctv.toml` allows you to customize the appearance of your tree display! 52 | If the configuration does not exist, defaults will be used. A copy of the default `ctv.toml` is available on this repository. 53 | 54 | ## Via Environment 55 | 56 | Additionally, you can specify configuration via environment variables. For example, here's two ways of specifying the sort order: 57 | 58 | ```toml 59 | # config.toml 60 | sorting = ["size", "name", "time"] 61 | ``` 62 | 63 | ```bash 64 | # command line 65 | $ CTV_SORTING='["size", "name", "time"]' ctv 66 | ``` 67 | 68 | ## 🎉 Repo Support 69 | 70 | ### Stargazers 71 | [![Stargazers repo roster for @angelina-tsuboi/ctv](https://reporoster.com/stars/angelina-tsuboi/ctv)](https://github.com/angelina-tsuboi/ctv/stargazers) 72 | 73 | 74 | ### Forkers 75 | [![Forkers repo roster for @angelina-tsuboi/ctv](https://reporoster.com/forks/angelina-tsuboi/ctv)](https://github.com/angelina-tsuboi/ctv/network/members) 76 | -------------------------------------------------------------------------------- /ctv.toml: -------------------------------------------------------------------------------- 1 | # ctv.toml 2 | # Default '~/.config/ctv.toml' 3 | 4 | file_size_position = "1" 5 | file_owner_position = "2" 6 | file_perms_position = "3" 7 | file_time_position = "4" 8 | file_extension_position = "-1" 9 | dir_name_color = "BLUE" 10 | file_name_color = "LIGHTRED" 11 | file_time_color = "LIGHTCYAN" 12 | file_size_color = "BLUE" 13 | file_owner_color = "LIGHTMAGENTA" 14 | file_perms_color = "BLUE" 15 | file_extension_color = "YELLOW" 16 | dir_name_style = "NORMAL" 17 | file_name_style = "NORMAL" 18 | file_time_style = "BOLD" 19 | file_size_style = "BOLD" 20 | file_owner_style = "NORMAL" 21 | file_perms_style = "BOLD" 22 | file_extension_style = "ITALIC" 23 | file_time_format = "%m-%d-%Y::%H:%M:%S" 24 | file_time_type = "CREATED" 25 | tree_layer_limit = "3" 26 | show_file_metadata = "TRUE" 27 | show_dir_metadata = "TRUE" 28 | pipe = "│" 29 | elbow = "└──" 30 | tee = "├──" 31 | pipe_prefix = "│" 32 | space_prefix = " " 33 | dir_color = "BLUE" 34 | symlink_color = "LIGHTMAGENTA" 35 | path_color = "WHITE" 36 | pipe_color = "YELLOW" 37 | chard_color = "YELLOW" 38 | blockd_color = "LIGHTGREEN" 39 | socket_color = "LIGHTRED" 40 | read_color = "LIGHTGREEN" 41 | write_color = "LIGHTRED" 42 | execute_color = "LIGHTGREEN" 43 | dash_color = "LIGHTBLACK" 44 | spacing = "0" 45 | show_short = "false" 46 | 47 | # Available Colors 48 | # BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, 49 | # LIGHTBLACK, LIGHTRED, LIGHTGREEN, LIGHTYELLOW, LIGHTBLUE, 50 | # LIGHTMAGENTA, LIGHTCYAN, LIGHTWHITE 51 | 52 | # Available Font Styles 53 | # NORMAL, BOLD, ITALIC, DIMMED, UNDERLINE, 54 | # BLINK, REVERSE, HIDDEN, STRICKEN 55 | -------------------------------------------------------------------------------- /media/ctv_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ANG13T/ctv/fd6b4b4d1ca3f120ba10853e2ea799f32eb287b1/media/ctv_preview.png -------------------------------------------------------------------------------- /media/ctv_print.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ANG13T/ctv/fd6b4b4d1ca3f120ba10853e2ea799f32eb287b1/media/ctv_print.png -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ctv-cli 2 | version: git 3 | summary: A highly configurable tree view file visualizer CLI tool written in Rust 4 | description: | 5 | ctv is a command line tool for visualizing 6 | your file hiearchy with a highly customizable 7 | tree view: 8 | - Visualize your file hierarchy in a tree view 9 | - Customize the appearance of your tree 10 | - Display custom file information (permissions, time, user, etc) 11 | - Personalize tree color and text styling 12 | 13 | base: core20 14 | 15 | 16 | parts: 17 | ctv: 18 | plugin: rust 19 | source: . 20 | 21 | apps: 22 | ctv: 23 | command: bin/ctv 24 | plugs: 25 | - home -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Parser, Debug)] 5 | #[clap(author, version, about)] 6 | pub struct Args { 7 | #[clap(default_value = ".")] 8 | pub dir: PathBuf, 9 | 10 | /// If provided, hide metadata and only show entry names 11 | #[clap(long, short = 'h')] 12 | pub short: bool, 13 | 14 | /// The maximum depth of the tree 15 | #[clap(short, long)] 16 | pub limit: Option, 17 | 18 | /// Show config variables and exit 19 | #[clap(long = "config", short)] 20 | pub print_config: bool, 21 | 22 | // #[clap(short = 's', long = "search", default_value = "")] 23 | // pub search: String, 24 | } 25 | 26 | pub fn parse() -> anyhow::Result { 27 | Ok(Args::try_parse()?) 28 | } 29 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use serde::{Deserialize, Serialize}; 3 | use std::path::PathBuf; 4 | 5 | #[derive(Serialize, Deserialize, Debug)] 6 | pub struct Config { 7 | pub colors: Colors, 8 | /// The order of the metadata fields shown next to each entry 9 | pub field_order: Vec, 10 | /// The maximum depth of the tree 11 | pub max_depth: usize, 12 | /// Whether to show metadata, configurable based on the entry type 13 | pub show_metadata: ShowMetadataConfig, 14 | pub styles: FieldStyles, 15 | pub symbols: Symbols, 16 | pub time: TimeConfig, 17 | pub view_format: ViewFormat, 18 | /// How directory entries should be sorted, as a list of methods and/or types. 19 | /// If only a type is provided, it will be converted to a method where `ty` is the type and `descending` is false. 20 | pub sorting: Vec, 21 | } 22 | 23 | #[derive(Serialize, Clone, Copy, Debug, PartialEq, Eq)] 24 | pub struct SortMethod { 25 | /// The key to sort by 26 | #[serde(rename = "type")] 27 | pub ty: SortType, 28 | /// Whether to invert the sorting 29 | pub descending: bool, 30 | } 31 | 32 | impl<'de> Deserialize<'de> for SortMethod { 33 | fn deserialize>(deserializer: D) -> Result { 34 | #[derive(Deserialize)] 35 | pub struct SortMethodProxy { 36 | #[serde(rename = "type")] 37 | ty: SortType, 38 | descending: bool, 39 | } 40 | #[derive(Deserialize)] 41 | #[serde(untagged)] 42 | enum WithOrWithoutOrder { 43 | WithoutOrder(SortType), 44 | WithOrder(SortMethodProxy), 45 | } 46 | let raw = WithOrWithoutOrder::deserialize(deserializer)?; 47 | Ok(match raw { 48 | WithOrWithoutOrder::WithOrder(SortMethodProxy { ty, descending }) => { 49 | SortMethod { ty, descending } 50 | } 51 | WithOrWithoutOrder::WithoutOrder(ty) => Self::from(ty), 52 | }) 53 | } 54 | } 55 | 56 | impl From for SortMethod { 57 | fn from(ty: SortType) -> Self { 58 | Self { 59 | ty, 60 | descending: false, 61 | } 62 | } 63 | } 64 | 65 | #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] 66 | #[serde(rename_all = "snake_case")] 67 | pub enum SortType { 68 | Type, 69 | Size, 70 | Name, 71 | Time, 72 | } 73 | 74 | #[derive(Serialize, Deserialize, Clone, Copy, Debug)] 75 | #[serde(rename_all = "snake_case")] 76 | pub enum FieldName { 77 | Owner, 78 | Perms, 79 | Size, 80 | Time, 81 | } 82 | 83 | pub struct FieldNameDisplay<'a> { 84 | field: FieldName, 85 | file: &'a crate::protocols::File<'a>, 86 | } 87 | 88 | impl FieldName { 89 | pub fn display<'a>(self, file: &'a crate::protocols::File<'a>) -> FieldNameDisplay<'a> { 90 | FieldNameDisplay { field: self, file } 91 | } 92 | } 93 | 94 | impl std::fmt::Display for FieldNameDisplay<'_> { 95 | fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 96 | use crate::services; 97 | match self.field { 98 | FieldName::Owner => { 99 | let owner: &str = &self.file.user; 100 | let owner = self.file.config.styles.owner.apply(owner); 101 | write!(formatter, "{}", owner) 102 | } 103 | FieldName::Perms => { 104 | let color = &self.file.config.colors.types; 105 | let letter = self.file.file_type.letter(); 106 | let letter = self.file.file_type.color(color).apply(letter); 107 | write!(formatter, "{}{}", letter, self.file.perms) 108 | } 109 | FieldName::Size => { 110 | let size = services::size::format(self.file.size); 111 | let size = self.file.config.styles.size.apply(size.as_str()); 112 | write!(formatter, "{}", size) 113 | } 114 | FieldName::Time => { 115 | let time = services::time::format(&self.file.time, &self.file.config.time.format); 116 | let time = self.file.config.styles.time.apply(time.as_str()); 117 | write!(formatter, "{}", time) 118 | } 119 | } 120 | } 121 | } 122 | 123 | impl Default for Config { 124 | fn default() -> Self { 125 | Self { 126 | colors: Default::default(), 127 | field_order: vec![ 128 | FieldName::Size, 129 | FieldName::Owner, 130 | FieldName::Perms, 131 | FieldName::Time, 132 | ], 133 | max_depth: 3, 134 | show_metadata: Default::default(), 135 | styles: Default::default(), 136 | symbols: Default::default(), 137 | time: Default::default(), 138 | view_format: Default::default(), 139 | sorting: vec![ 140 | SortType::Type.into(), 141 | SortType::Name.into(), 142 | SortType::Size.into(), 143 | ], 144 | } 145 | } 146 | } 147 | 148 | #[derive(Serialize, Deserialize, Debug)] 149 | pub struct ShowMetadataConfig { 150 | pub directory: bool, 151 | pub file: bool, 152 | } 153 | 154 | impl Default for ShowMetadataConfig { 155 | fn default() -> Self { 156 | Self { 157 | directory: true, 158 | file: true, 159 | } 160 | } 161 | } 162 | 163 | #[derive(Serialize, Deserialize, Debug)] 164 | pub struct TimeConfig { 165 | /// The type of time (created, modified, accessed) to show 166 | #[serde(rename = "type")] 167 | pub ty: TimeType, 168 | pub format: String, 169 | } 170 | 171 | impl Default for TimeConfig { 172 | fn default() -> Self { 173 | Self { 174 | ty: Default::default(), 175 | format: "%Y-%m-%dT%H:%M:%S".to_string(), 176 | } 177 | } 178 | } 179 | 180 | #[derive(Serialize, Deserialize, Debug, Clone, Copy)] 181 | #[serde(rename_all = "snake_case")] 182 | pub enum TimeType { 183 | Created, 184 | Modified, 185 | Accessed, 186 | } 187 | 188 | impl Default for TimeType { 189 | fn default() -> Self { 190 | Self::Modified 191 | } 192 | } 193 | 194 | #[derive(Serialize, Deserialize, Default, Debug)] 195 | pub struct Colors { 196 | pub types: TypeColors, 197 | pub perms: PermColors, 198 | } 199 | 200 | #[derive(Serialize, Deserialize, Debug)] 201 | pub struct TypeColors { 202 | pub block_device: Color, 203 | pub char_device: Color, 204 | pub directory: Color, 205 | pub file: Color, 206 | pub pipe: Color, 207 | pub socket: Color, 208 | pub symlink: Color, 209 | pub unknown: Color, 210 | } 211 | 212 | impl Default for TypeColors { 213 | fn default() -> Self { 214 | Self { 215 | block_device: Color::BrightGreen, 216 | char_device: Color::BrightYellow, 217 | directory: Color::Blue, 218 | file: Color::None, 219 | pipe: Color::Yellow, 220 | socket: Color::BrightRed, 221 | symlink: Color::BrightPurple, 222 | unknown: Color::BrightBlack, 223 | } 224 | } 225 | } 226 | 227 | #[derive(Serialize, Deserialize, Debug)] 228 | pub struct PermColors { 229 | pub execute: Color, 230 | /// The color if the field is not set (shown as a dash) 231 | pub none: Color, 232 | pub read: Color, 233 | pub write: Color, 234 | } 235 | 236 | impl Default for PermColors { 237 | fn default() -> Self { 238 | Self { 239 | execute: Color::BrightGreen, 240 | read: Color::BrightGreen, 241 | write: Color::BrightRed, 242 | none: Color::BrightBlack, 243 | } 244 | } 245 | } 246 | 247 | #[derive(Serialize, Deserialize, Debug)] 248 | pub struct FieldStyles { 249 | pub owner: FieldStyle, 250 | pub size: FieldStyle, 251 | pub time: FieldStyle, 252 | } 253 | 254 | impl Default for FieldStyles { 255 | fn default() -> Self { 256 | Self { 257 | owner: FieldStyle { 258 | color: Color::BrightPurple, 259 | style: Style::None, 260 | }, 261 | size: FieldStyle { 262 | color: Color::Blue, 263 | style: Style::Bold, 264 | }, 265 | time: FieldStyle { 266 | color: Color::BrightCyan, 267 | style: Style::Bold, 268 | }, 269 | } 270 | } 271 | } 272 | 273 | #[derive(Serialize, Deserialize, Debug)] 274 | pub struct FieldStyle { 275 | pub color: Color, 276 | pub style: Style, 277 | } 278 | 279 | impl FieldStyle { 280 | pub fn apply(&self, base: C) -> colored::ColoredString { 281 | self.style.apply(self.color.apply(base)) 282 | } 283 | } 284 | 285 | #[derive(Serialize, Deserialize, Clone, Copy, Debug)] 286 | #[serde(rename_all = "snake_case")] 287 | pub enum Color { 288 | None, 289 | Black, 290 | Red, 291 | Green, 292 | Yellow, 293 | Blue, 294 | Purple, 295 | Cyan, 296 | White, 297 | BrightBlack, 298 | BrightRed, 299 | BrightGreen, 300 | BrightYellow, 301 | BrightBlue, 302 | BrightPurple, 303 | BrightCyan, 304 | BrightWhite, 305 | } 306 | 307 | impl Color { 308 | pub fn apply(self, base: C) -> colored::ColoredString { 309 | match self { 310 | Self::None => base.normal(), 311 | Self::Black => base.black(), 312 | Self::Red => base.red(), 313 | Self::Green => base.green(), 314 | Self::Yellow => base.yellow(), 315 | Self::Blue => base.blue(), 316 | Self::Purple => base.purple(), 317 | Self::Cyan => base.cyan(), 318 | Self::White => base.white(), 319 | Self::BrightBlack => base.bright_black(), 320 | Self::BrightRed => base.bright_red(), 321 | Self::BrightGreen => base.bright_green(), 322 | Self::BrightYellow => base.bright_yellow(), 323 | Self::BrightBlue => base.bright_blue(), 324 | Self::BrightPurple => base.bright_purple(), 325 | Self::BrightCyan => base.bright_cyan(), 326 | Self::BrightWhite => base.bright_white(), 327 | } 328 | } 329 | } 330 | 331 | #[derive(Serialize, Deserialize, Clone, Copy, Debug)] 332 | #[serde(rename_all = "snake_case")] 333 | pub enum Style { 334 | None, 335 | Bold, 336 | Italic, 337 | Underline, 338 | } 339 | 340 | impl Style { 341 | pub fn apply(self, base: colored::ColoredString) -> colored::ColoredString { 342 | match self { 343 | // make sure to reapply the color if necessary 344 | Self::None => match base.fgcolor() { 345 | Some(color) => base.normal().color(color), 346 | None => base.normal(), 347 | }, 348 | Self::Bold => base.bold(), 349 | Self::Italic => base.italic(), 350 | Self::Underline => base.underline(), 351 | } 352 | } 353 | } 354 | 355 | #[derive(Serialize, Deserialize, Debug)] 356 | #[serde(rename_all = "snake_case")] 357 | pub enum ViewFormat { 358 | Short, 359 | Full, 360 | } 361 | 362 | impl Default for ViewFormat { 363 | fn default() -> Self { 364 | Self::Full 365 | } 366 | } 367 | 368 | #[derive(Serialize, Deserialize, Debug)] 369 | pub struct Symbols { 370 | /// Used at the end of a sequence of entries 371 | pub elbow: String, 372 | /// Used at the beginning and middle of a sequence of entries 373 | pub tee: String, 374 | /// Added as a prefix to indent subdirectory trees 375 | pub pipe: String, 376 | } 377 | 378 | impl Default for Symbols { 379 | fn default() -> Self { 380 | Self { 381 | elbow: "└──".to_string(), 382 | tee: "├──".to_string(), 383 | pipe: "│".to_string(), 384 | } 385 | } 386 | } 387 | 388 | pub fn load() -> anyhow::Result { 389 | use figment::{ 390 | providers::{Env, Format, Serialized, Toml}, 391 | Figment, 392 | }; 393 | let mut config = Figment::new(); 394 | // load defaults 395 | config = config.merge(Serialized::from( 396 | Config::default(), 397 | figment::Profile::default(), 398 | )); 399 | // load from file if present 400 | if let Some(config_file_path) = config_file_path() { 401 | config = config.merge(Toml::file(config_file_path)); 402 | } 403 | // load from environment 404 | config = config.merge(Env::prefixed("CTV_")); 405 | Ok(config.extract()?) 406 | } 407 | 408 | pub fn config_file_path() -> Option { 409 | dirs::config_dir().map(|path| path.join("ctv.toml")) 410 | } 411 | 412 | #[cfg(test)] 413 | mod test { 414 | #[test] 415 | fn test_sort_parsing() { 416 | use super::{SortMethod, SortType}; 417 | use figment::providers::{Format, Toml}; 418 | use serde::{Deserialize, Serialize}; 419 | 420 | #[derive(Serialize, Deserialize)] 421 | struct Container { 422 | inner: SortMethod, 423 | } 424 | 425 | let as_string = r#"inner = "size""#; 426 | let as_object = r#"inner = { type = "time", descending = true }"#; 427 | 428 | let as_string: Container = Toml::from_str(as_string).unwrap(); 429 | let as_object: Container = Toml::from_str(as_object).unwrap(); 430 | 431 | assert_eq!( 432 | as_string.inner, 433 | SortMethod { 434 | ty: SortType::Size, 435 | descending: false 436 | } 437 | ); 438 | assert_eq!( 439 | as_object.inner, 440 | SortMethod { 441 | ty: SortType::Time, 442 | descending: true 443 | } 444 | ); 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod config; 3 | mod protocols; 4 | mod services; 5 | 6 | fn main() -> anyhow::Result<()> { 7 | use anyhow::Context; 8 | let mut config = config::load().context("Reading configuration")?; 9 | let args = args::parse().context("Parsing arguments")?; 10 | 11 | if args.short { 12 | config.view_format = config::ViewFormat::Short; 13 | } 14 | if let Some(limit) = args.limit { 15 | config.max_depth = limit; 16 | } 17 | 18 | // if args.search.len() > 0 { 19 | // println!("searching {}", args.search); 20 | // } 21 | 22 | if args.print_config { 23 | println!("{:#?}\n{:#?}", config, args); 24 | return Ok(()); 25 | } 26 | 27 | protocols::DirTree::new(&args.dir.canonicalize()?, &config)?.write(&mut std::io::stdout()) 28 | } 29 | -------------------------------------------------------------------------------- /src/protocols/dir_tree.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::protocols::file::File; 3 | use std::borrow::Cow; 4 | use std::fs; 5 | use std::io::Write; 6 | use std::path::Path; 7 | 8 | pub struct DirTree<'a> { 9 | root: File<'a>, 10 | config: &'a Config, 11 | depth: usize, 12 | is_last: bool, 13 | } 14 | 15 | impl<'a> DirTree<'a> { 16 | pub fn new(root: &'a Path, config: &'a crate::config::Config) -> anyhow::Result { 17 | Ok(Self { 18 | root: File::new(Cow::Borrowed(root), config)?, 19 | config, 20 | depth: 0, 21 | is_last: false, 22 | }) 23 | } 24 | 25 | /// std::fmt::Display does not allow for any non-formatting errors to be propagated 26 | pub fn write(&self, formatter: &mut dyn Write) -> anyhow::Result<()> { 27 | self.write_header(formatter)?; 28 | self.write_body(formatter) 29 | } 30 | fn write_header(&self, formatter: &mut dyn Write) -> anyhow::Result<()> { 31 | // print (self.indentation - 1) pipes, then 1 tee if we have indentation at all 32 | for _ in 0..self.depth.saturating_sub(1) { 33 | write!(formatter, "{}", self.config.symbols.pipe)?; 34 | } 35 | if self.depth > 0 { 36 | write!( 37 | formatter, 38 | "{} ", 39 | if self.is_last { 40 | &self.config.symbols.elbow 41 | } else { 42 | &self.config.symbols.tee 43 | } 44 | )?; 45 | } 46 | writeln!(formatter, "{}", self.root)?; 47 | Ok(()) 48 | } 49 | fn sort_entries(&self, entries: &mut Vec>) { 50 | use crate::config::SortType; 51 | let methods = self.config.sorting.as_slice(); 52 | entries.sort_unstable_by(|a, b| { 53 | for method in methods { 54 | let cmp = match method.ty { 55 | SortType::Name => a.path.file_name().cmp(&b.path.file_name()), 56 | SortType::Size => a.size.cmp(&b.size), 57 | SortType::Time => a.time.cmp(&b.time), 58 | SortType::Type => a.file_type.cmp(&b.file_type), 59 | }; 60 | if !cmp.is_eq() { 61 | return if method.descending { 62 | cmp.reverse() 63 | } else { 64 | cmp 65 | }; 66 | } 67 | } 68 | std::cmp::Ordering::Equal 69 | }); 70 | } 71 | fn write_body(&self, formatter: &mut dyn Write) -> anyhow::Result<()> { 72 | use anyhow::Context; 73 | struct TreeContext(String); 74 | impl std::fmt::Display for TreeContext { 75 | fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 76 | write!(formatter, "Failed to generate tree for entry {:?}", self.0) 77 | } 78 | } 79 | 80 | if matches!( 81 | self.root.file_type, 82 | crate::protocols::PathType::Directory { .. } 83 | ) && self.depth < self.config.max_depth 84 | { 85 | let mut entries: Vec = fs::read_dir(&self.root.path)? 86 | .map(|entry| File::new(Cow::Owned(entry?.path()), &self.config)) 87 | .collect::>()?; 88 | self.sort_entries(&mut entries); 89 | let num_entries = entries.len(); 90 | for (idx, entry) in entries.into_iter().enumerate() { 91 | let name = entry.file_name().into_owned(); 92 | // not using Self because we need a shorter lifetime than our own, and Self forwards the lifetime parameter 93 | DirTree { 94 | root: entry, 95 | config: &self.config, 96 | depth: self.depth + 1, 97 | is_last: idx == num_entries - 1, 98 | } 99 | .write(formatter) 100 | .context(TreeContext(name))?; 101 | } 102 | } 103 | Ok(()) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/protocols/file.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::protocols::PathType; 3 | use crate::services; 4 | use std::borrow::Cow; 5 | use std::fmt::{self, Display, Formatter}; 6 | use std::path::Path; 7 | 8 | pub struct File<'a> { 9 | pub path: Cow<'a, Path>, 10 | pub file_type: PathType, 11 | pub group: String, 12 | pub user: String, 13 | pub time: filetime::FileTime, 14 | pub size: u64, 15 | pub perms: String, 16 | pub config: &'a Config, 17 | } 18 | 19 | impl<'a> File<'a> { 20 | pub fn new(path: Cow<'a, Path>, config: &'a Config) -> anyhow::Result { 21 | let metadata = path.symlink_metadata()?; 22 | let time = services::time::get(&metadata, config.time.ty); 23 | Ok(Self { 24 | group: services::group(&metadata), 25 | user: services::user(&metadata), 26 | time, 27 | size: services::size::get(&metadata), 28 | perms: services::perms::perms(&metadata, &config.colors.perms), 29 | file_type: PathType::from_path(&path, Some(metadata))?, 30 | path, 31 | config, 32 | }) 33 | } 34 | pub fn file_name(&self) -> std::borrow::Cow<'_, str> { 35 | self.path 36 | .file_name() 37 | .map(|path| path.to_string_lossy()) 38 | .unwrap_or(std::borrow::Cow::Borrowed("")) 39 | } 40 | } 41 | 42 | impl Display for File<'_> { 43 | fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { 44 | use crate::config::ViewFormat; 45 | use colored::Colorize; 46 | let file_name = self.file_name(); 47 | write!( 48 | formatter, 49 | "{}{}", 50 | self.file_type 51 | .color(&self.config.colors.types) 52 | .apply(file_name.as_ref()) 53 | .bold(), 54 | self.file_type 55 | .extra() 56 | .unwrap_or(colored::Colorize::normal("")), 57 | )?; 58 | let show_metadata = if matches!(self.file_type, PathType::Directory { .. }) { 59 | self.config.show_metadata.directory 60 | } else { 61 | self.config.show_metadata.file 62 | }; 63 | match self.config.view_format { 64 | ViewFormat::Full if show_metadata => { 65 | struct FieldDisplay<'a>(&'a File<'a>); 66 | impl Display for FieldDisplay<'_> { 67 | fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { 68 | for (index, field) in self.0.config.field_order.iter().enumerate() { 69 | if index != 0 { 70 | std::fmt::Write::write_char(formatter, ' ')?; 71 | } 72 | write!(formatter, "{}", field.display(&self.0))?; 73 | } 74 | Ok(()) 75 | } 76 | } 77 | 78 | write!( 79 | formatter, 80 | " {open_bracket}{}{close_bracket}", 81 | FieldDisplay(self), 82 | open_bracket = "[".white().bold(), 83 | close_bracket = "]".white().bold() 84 | )?; 85 | match self.file_type { 86 | PathType::Directory { num_entries } => { 87 | write!( 88 | formatter, 89 | "{}", 90 | format!(" ({} items)", num_entries).white().bold() 91 | ) 92 | } 93 | _ => Ok(()), 94 | } 95 | } 96 | _ => Ok(()), 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/protocols/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dir_tree; 2 | pub mod file; 3 | pub mod path_type; 4 | 5 | pub use dir_tree::DirTree; 6 | pub use file::File; 7 | pub use path_type::PathType; 8 | -------------------------------------------------------------------------------- /src/protocols/path_type.rs: -------------------------------------------------------------------------------- 1 | use crate::config::TypeColors; 2 | use std::fs::Metadata; 3 | use std::os::unix::fs::FileTypeExt; 4 | use std::path::{Path, PathBuf}; 5 | 6 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] 7 | pub enum PathType { 8 | BlockDevice, 9 | CharDevice, 10 | Directory { num_entries: usize }, 11 | File, 12 | Pipe, 13 | Socket, 14 | Symlink { target: PathBuf }, 15 | Unknown, 16 | } 17 | 18 | impl PathType { 19 | pub fn from_path(file: &Path, metadata: Option) -> anyhow::Result { 20 | let metadata = match metadata { 21 | Some(metadata) => metadata, 22 | None => file.symlink_metadata()?, 23 | }; 24 | let file_type = metadata.file_type(); 25 | Ok(if file_type.is_symlink() { 26 | Self::Symlink { 27 | target: std::fs::read_link(file)?, 28 | } 29 | } else if file_type.is_fifo() { 30 | Self::Pipe 31 | } else if file_type.is_char_device() { 32 | Self::CharDevice 33 | } else if file_type.is_block_device() { 34 | Self::BlockDevice 35 | } else if file_type.is_socket() { 36 | Self::Socket 37 | } else if file_type.is_file() { 38 | Self::File 39 | } else if file_type.is_dir() { 40 | Self::Directory { 41 | num_entries: std::fs::read_dir(file)?.count(), 42 | } 43 | } else { 44 | Self::Unknown 45 | }) 46 | } 47 | pub fn letter(&self) -> &'static str { 48 | match self { 49 | Self::BlockDevice => "b", 50 | Self::CharDevice => "c", 51 | Self::Directory { .. } => "d", 52 | Self::File => ".", 53 | Self::Pipe => "|", 54 | Self::Socket => "=", 55 | Self::Symlink { .. } => "l", 56 | Self::Unknown => "?", 57 | } 58 | } 59 | pub fn color(&self, colors: &TypeColors) -> crate::config::Color { 60 | match self { 61 | Self::BlockDevice => colors.block_device, 62 | Self::CharDevice => colors.char_device, 63 | Self::Directory { .. } => colors.directory, 64 | Self::File => colors.file, 65 | Self::Pipe => colors.pipe, 66 | Self::Socket => colors.socket, 67 | Self::Symlink { .. } => colors.symlink, 68 | Self::Unknown => colors.unknown, 69 | } 70 | } 71 | pub fn extra(&self) -> Option { 72 | use colored::Colorize; 73 | match self { 74 | Self::Directory { .. } => Some("/".white().bold()), 75 | Self::Pipe => Some("|".white().bold()), 76 | Self::Socket => Some("-".white().bold()), 77 | Self::Symlink { target } => Some(format!(" -> {}", target.display()).italic()), 78 | _ => None, 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/services/group_user.rs: -------------------------------------------------------------------------------- 1 | use std::fs::Metadata; 2 | use std::os::unix::fs::MetadataExt; 3 | 4 | pub fn group(metadata: &Metadata) -> String { 5 | let group = users::get_group_by_gid(metadata.gid()); 6 | if let Some(g) = group { 7 | String::from(g.name().to_string_lossy()) 8 | } else { 9 | String::from(" ") 10 | } 11 | } 12 | 13 | pub fn user(metadata: &Metadata) -> String { 14 | let user = users::get_user_by_uid(metadata.uid()); 15 | if let Some(u) = user { 16 | String::from(u.name().to_string_lossy()) 17 | } else { 18 | String::from(" ") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/services/mod.rs: -------------------------------------------------------------------------------- 1 | mod group_user; 2 | pub mod perms; 3 | pub mod size; 4 | pub mod time; 5 | 6 | pub use group_user::{group, user}; 7 | -------------------------------------------------------------------------------- /src/services/perms.rs: -------------------------------------------------------------------------------- 1 | use crate::config::PermColors; 2 | use libc::mode_t; 3 | use std::os::unix::fs::PermissionsExt; 4 | 5 | #[derive(Clone, Copy)] 6 | enum PermType { 7 | User, 8 | Group, 9 | Other, 10 | } 11 | 12 | impl PermType { 13 | fn masks(self) -> (mode_t, mode_t, mode_t) { 14 | use libc::{ 15 | S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR, 16 | }; 17 | match self { 18 | Self::User => (S_IRUSR, S_IWUSR, S_IXUSR), 19 | Self::Group => (S_IRGRP, S_IWGRP, S_IXGRP), 20 | Self::Other => (S_IROTH, S_IWOTH, S_IXOTH), 21 | } 22 | } 23 | fn check(self, mode: mode_t) -> (bool, bool, bool) { 24 | let (read, write, exec) = self.masks(); 25 | (mode & read > 0, mode & write > 0, mode & exec > 0) 26 | } 27 | 28 | pub fn format(self, mode: mode_t, colors: &PermColors) -> String { 29 | fn else_dash( 30 | cond: bool, 31 | if_true: colored::ColoredString, 32 | dash_color: crate::config::Color, 33 | ) -> colored::ColoredString { 34 | if cond { 35 | if_true 36 | } else { 37 | dash_color.apply("-") 38 | } 39 | } 40 | 41 | fn format_rwx((r, w, x): (bool, bool, bool), colors: &PermColors) -> String { 42 | format!( 43 | "{}{}{}", 44 | else_dash(r, colors.read.apply("r"), colors.none), 45 | else_dash(w, colors.write.apply("w"), colors.none), 46 | else_dash(x, colors.execute.apply("x"), colors.none), 47 | ) 48 | } 49 | 50 | format_rwx(self.check(mode), colors) 51 | } 52 | } 53 | 54 | pub fn perms(metadata: &std::fs::Metadata, colors: &PermColors) -> String { 55 | let mode = metadata.permissions().mode() as mode_t; 56 | 57 | let user = PermType::User.format(mode, colors); 58 | let group = PermType::Group.format(mode, colors); 59 | let other = PermType::Other.format(mode, colors); 60 | 61 | [user, group, other].join("") 62 | } 63 | -------------------------------------------------------------------------------- /src/services/size.rs: -------------------------------------------------------------------------------- 1 | use humansize::{file_size_opts as options, FileSize}; 2 | use std::fs::Metadata; 3 | use std::os::unix::fs::MetadataExt; 4 | 5 | pub fn get(metadata: &Metadata) -> u64 { 6 | metadata.size() 7 | } 8 | 9 | pub fn format(size: u64) -> String { 10 | size.file_size(options::CONVENTIONAL).unwrap() 11 | } 12 | -------------------------------------------------------------------------------- /src/services/time.rs: -------------------------------------------------------------------------------- 1 | use crate::config::TimeType; 2 | use filetime::FileTime; 3 | use std::fs::Metadata; 4 | 5 | pub fn get_modified(metadata: &Metadata) -> FileTime { 6 | FileTime::from_last_modification_time(metadata) 7 | } 8 | 9 | pub fn get_created(metadata: &Metadata) -> FileTime { 10 | FileTime::from_creation_time(metadata).unwrap_or(FileTime::zero()) 11 | } 12 | 13 | pub fn get_accessed(metadata: &Metadata) -> FileTime { 14 | FileTime::from_last_access_time(metadata) 15 | } 16 | 17 | pub fn get(metadata: &Metadata, time_type: TimeType) -> FileTime { 18 | match time_type { 19 | TimeType::Accessed => get_accessed(metadata), 20 | TimeType::Created => get_created(metadata), 21 | TimeType::Modified => get_modified(metadata), 22 | } 23 | } 24 | 25 | pub fn format(time: &FileTime, format: &str) -> String { 26 | let time = chrono::NaiveDateTime::from_timestamp(time.seconds(), time.nanoseconds()); 27 | let time = >::from_utc(time, *chrono::Local::now().offset()); 28 | time.format(format).to_string() 29 | } 30 | -------------------------------------------------------------------------------- /wip_features.md: -------------------------------------------------------------------------------- 1 | # Work In Progress Features 2 | 3 | Here are the following features that are currently being created. 4 | Feel free to work on integrating any of these features yourself. 5 | See `CONTRIBUTING.md` for more information about contributing. 6 | 7 | ## WIP Features List 8 | 9 | 1. Add a file / directory "peek" functionality: Search for files and be able to see parent directories leading to the file. 10 | 1. Add a file / directory search functionality: Just show files / directories that match the search query. 11 | --------------------------------------------------------------------------------