├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── img └── screen.png └── src ├── computer.rs ├── file_info.rs ├── main.rs └── screen.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /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 = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "1.3.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "0.1.10" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 22 | 23 | [[package]] 24 | name = "cloudabi" 25 | version = "0.0.3" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 28 | dependencies = [ 29 | "bitflags", 30 | ] 31 | 32 | [[package]] 33 | name = "crossbeam" 34 | version = "0.7.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" 37 | dependencies = [ 38 | "cfg-if", 39 | "crossbeam-channel", 40 | "crossbeam-deque", 41 | "crossbeam-epoch", 42 | "crossbeam-queue", 43 | "crossbeam-utils", 44 | ] 45 | 46 | [[package]] 47 | name = "crossbeam-channel" 48 | version = "0.4.4" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" 51 | dependencies = [ 52 | "crossbeam-utils", 53 | "maybe-uninit", 54 | ] 55 | 56 | [[package]] 57 | name = "crossbeam-deque" 58 | version = "0.7.4" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" 61 | dependencies = [ 62 | "crossbeam-epoch", 63 | "crossbeam-utils", 64 | "maybe-uninit", 65 | ] 66 | 67 | [[package]] 68 | name = "crossbeam-epoch" 69 | version = "0.8.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" 72 | dependencies = [ 73 | "autocfg", 74 | "cfg-if", 75 | "crossbeam-utils", 76 | "lazy_static", 77 | "maybe-uninit", 78 | "memoffset", 79 | "scopeguard", 80 | ] 81 | 82 | [[package]] 83 | name = "crossbeam-queue" 84 | version = "0.2.3" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" 87 | dependencies = [ 88 | "cfg-if", 89 | "crossbeam-utils", 90 | "maybe-uninit", 91 | ] 92 | 93 | [[package]] 94 | name = "crossbeam-utils" 95 | version = "0.7.2" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 98 | dependencies = [ 99 | "autocfg", 100 | "cfg-if", 101 | "lazy_static", 102 | ] 103 | 104 | [[package]] 105 | name = "crossterm" 106 | version = "0.17.7" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "6f4919d60f26ae233e14233cc39746c8c8bb8cd7b05840ace83604917b51b6c7" 109 | dependencies = [ 110 | "bitflags", 111 | "crossterm_winapi", 112 | "lazy_static", 113 | "libc", 114 | "mio", 115 | "parking_lot", 116 | "signal-hook", 117 | "winapi", 118 | ] 119 | 120 | [[package]] 121 | name = "crossterm_winapi" 122 | version = "0.6.2" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "c2265c3f8e080075d9b6417aa72293fc71662f34b4af2612d8d1b074d29510db" 125 | dependencies = [ 126 | "winapi", 127 | ] 128 | 129 | [[package]] 130 | name = "lazy_static" 131 | version = "1.4.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 134 | 135 | [[package]] 136 | name = "libc" 137 | version = "0.2.147" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 140 | 141 | [[package]] 142 | name = "lock_api" 143 | version = "0.3.4" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" 146 | dependencies = [ 147 | "scopeguard", 148 | ] 149 | 150 | [[package]] 151 | name = "log" 152 | version = "0.4.19" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" 155 | 156 | [[package]] 157 | name = "maybe-uninit" 158 | version = "2.0.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 161 | 162 | [[package]] 163 | name = "memoffset" 164 | version = "0.5.6" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" 167 | dependencies = [ 168 | "autocfg", 169 | ] 170 | 171 | [[package]] 172 | name = "minimad" 173 | version = "0.6.9" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "cb5ed7ea8b54916318ac7ec6ff06b2b428fdf9cb54d1867714f699dbd1659ad4" 176 | dependencies = [ 177 | "lazy_static", 178 | ] 179 | 180 | [[package]] 181 | name = "mio" 182 | version = "0.7.14" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" 185 | dependencies = [ 186 | "libc", 187 | "log", 188 | "miow", 189 | "ntapi", 190 | "winapi", 191 | ] 192 | 193 | [[package]] 194 | name = "miow" 195 | version = "0.3.7" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 198 | dependencies = [ 199 | "winapi", 200 | ] 201 | 202 | [[package]] 203 | name = "ntapi" 204 | version = "0.3.7" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" 207 | dependencies = [ 208 | "winapi", 209 | ] 210 | 211 | [[package]] 212 | name = "open" 213 | version = "1.7.1" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "dcea7a30d6b81a2423cc59c43554880feff7b57d12916f231a79f8d6d9470201" 216 | dependencies = [ 217 | "pathdiff", 218 | "winapi", 219 | ] 220 | 221 | [[package]] 222 | name = "parking_lot" 223 | version = "0.10.2" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" 226 | dependencies = [ 227 | "lock_api", 228 | "parking_lot_core", 229 | ] 230 | 231 | [[package]] 232 | name = "parking_lot_core" 233 | version = "0.7.3" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "b93f386bb233083c799e6e642a9d73db98c24a5deeb95ffc85bf281255dffc98" 236 | dependencies = [ 237 | "cfg-if", 238 | "cloudabi", 239 | "libc", 240 | "redox_syscall", 241 | "smallvec", 242 | "winapi", 243 | ] 244 | 245 | [[package]] 246 | name = "pathdiff" 247 | version = "0.2.1" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" 250 | 251 | [[package]] 252 | name = "proc-macro2" 253 | version = "1.0.63" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" 256 | dependencies = [ 257 | "unicode-ident", 258 | ] 259 | 260 | [[package]] 261 | name = "quote" 262 | version = "1.0.29" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" 265 | dependencies = [ 266 | "proc-macro2", 267 | ] 268 | 269 | [[package]] 270 | name = "redox_syscall" 271 | version = "0.1.57" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 274 | 275 | [[package]] 276 | name = "scopeguard" 277 | version = "1.1.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 280 | 281 | [[package]] 282 | name = "signal-hook" 283 | version = "0.1.17" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" 286 | dependencies = [ 287 | "libc", 288 | "mio", 289 | "signal-hook-registry", 290 | ] 291 | 292 | [[package]] 293 | name = "signal-hook-registry" 294 | version = "1.4.1" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 297 | dependencies = [ 298 | "libc", 299 | ] 300 | 301 | [[package]] 302 | name = "smallvec" 303 | version = "1.10.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 306 | 307 | [[package]] 308 | name = "syn" 309 | version = "2.0.22" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" 312 | dependencies = [ 313 | "proc-macro2", 314 | "quote", 315 | "unicode-ident", 316 | ] 317 | 318 | [[package]] 319 | name = "termimad" 320 | version = "0.9.1" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "fe9709f7deb2582e81b8bffd71ddc33ca46857c30fee361bcf6c0fd63caf8146" 323 | dependencies = [ 324 | "crossbeam", 325 | "crossterm", 326 | "lazy_static", 327 | "minimad", 328 | "thiserror", 329 | "unicode-width", 330 | ] 331 | 332 | [[package]] 333 | name = "thiserror" 334 | version = "1.0.40" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" 337 | dependencies = [ 338 | "thiserror-impl", 339 | ] 340 | 341 | [[package]] 342 | name = "thiserror-impl" 343 | version = "1.0.40" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" 346 | dependencies = [ 347 | "proc-macro2", 348 | "quote", 349 | "syn", 350 | ] 351 | 352 | [[package]] 353 | name = "unicode-ident" 354 | version = "1.0.9" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 357 | 358 | [[package]] 359 | name = "unicode-width" 360 | version = "0.1.10" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 363 | 364 | [[package]] 365 | name = "whalespotter" 366 | version = "1.0.0" 367 | dependencies = [ 368 | "crossbeam", 369 | "crossterm", 370 | "lazy_static", 371 | "open", 372 | "termimad", 373 | ] 374 | 375 | [[package]] 376 | name = "winapi" 377 | version = "0.3.9" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 380 | dependencies = [ 381 | "winapi-i686-pc-windows-gnu", 382 | "winapi-x86_64-pc-windows-gnu", 383 | ] 384 | 385 | [[package]] 386 | name = "winapi-i686-pc-windows-gnu" 387 | version = "0.4.0" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 390 | 391 | [[package]] 392 | name = "winapi-x86_64-pc-windows-gnu" 393 | version = "0.4.0" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 396 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "whalespotter" 3 | version = "1.0.0" 4 | authors = ["dystroy "] 5 | repository = "https://github.com/Canop/broot" 6 | description = "Find Big and Fat Files and Folders" 7 | edition = "2018" 8 | keywords = ["cli"] 9 | license = "MIT" 10 | categories = ["command-line-utilities"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | lazy_static = "1.3" 15 | crossbeam = "0.7" 16 | crossterm = "0.17.7" 17 | termimad = "=0.9.1" 18 | open = "1.3.1" 19 | 20 | [profile.release] 21 | lto = true # link time optimization - roughly halves the size of the exec 22 | codegen-units = 1 # this removes a few hundred bytes from the final exec size 23 | 24 | [patch.crates-io] 25 | # termimad = { path = "../termimad" } 26 | # crossterm = { path = "../crossterm-rs/crossterm" } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Denys Séguret - dys@dystroy.org 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 | [![MIT][s2]][l2] [![Latest Version][s1]][l1] [![Chat on Miaou][s3]][l3] 2 | 3 | [s1]: https://img.shields.io/crates/v/whalespotter.svg 4 | [l1]: https://crates.io/crates/whalespotter 5 | 6 | [s2]: https://img.shields.io/badge/license-MIT-blue.svg 7 | [l2]: LICENSE 8 | 9 | [s3]: https://miaou.dystroy.org/static/shields/room.svg 10 | [l3]: https://miaou.dystroy.org/3?broot 11 | 12 | a convenient application to fast locate fat files and folders (linux & mac) 13 | 14 | ![screen](img/screen.png) 15 | 16 | ## Installation 17 | 18 | The simplest solution is to execute 19 | 20 | cargo install --locked whalespotter 21 | 22 | ## Usage 23 | 24 | Pass the desired path: 25 | 26 | whalespotter ~ 27 | 28 | * Hit *ctrl-q* to quit 29 | * *↑* and *↓* to select and *enter* to open", 30 | * *enter* to open the selected directory (in whalespotter) or file (with `xdg-open`) 31 | * *esc* to either unselect, or go to parent, or quit 32 | * *pageUp* and *pageDown* to scroll 33 | * *F5* to refresh 34 | 35 | Note: reported sizes take blocks into account, so they may be smaller than the nominal size for the rare sparse files. 36 | 37 | Whalespotter is dedicated to one use case: spotting big directories and files. 38 | If your goal is to clean your disk, you might be interested in the more recent set of tools I developped. 39 | They're described in [this blog post](https://dystroy.org/blog/reclaim-space-on-disk/). 40 | -------------------------------------------------------------------------------- /img/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/whalespotter/d146d6892056a0312eedd07e739bb70fd845aadc/img/screen.png -------------------------------------------------------------------------------- /src/computer.rs: -------------------------------------------------------------------------------- 1 | use crate::file_info::FileInfo; 2 | use crossbeam::channel::{unbounded, Receiver, Sender}; 3 | use std::{ 4 | os::unix::fs::MetadataExt, // TODO windows compatibility... 5 | path::{Path, PathBuf}, 6 | sync::{ 7 | atomic::{AtomicUsize, Ordering}, 8 | Arc, 9 | }, 10 | thread, 11 | }; 12 | 13 | pub enum ComputationEvent { 14 | Finished, 15 | FileInfo(FileInfo), // TODO add progress 16 | } 17 | 18 | pub struct Computer { 19 | tx: Sender, 20 | pub rx: Receiver, 21 | task_count: Arc, 22 | } 23 | 24 | impl Computer { 25 | pub fn new() -> Self { 26 | let (tx, rx) = unbounded(); 27 | let task_count = Arc::new(AtomicUsize::new(0)); 28 | Self { tx, rx, task_count } 29 | } 30 | pub fn do_children(&mut self, root: &Path) { 31 | lazy_static! { 32 | static ref PROC: PathBuf = Path::new("/proc").to_path_buf(); 33 | } 34 | let thread_count = Arc::new(AtomicUsize::new(0)); 35 | let start_task_count = self.task_count.fetch_add(1, Ordering::Relaxed) + 1; 36 | for entry in root.read_dir().expect("read_dir call failed").flatten() { 37 | if entry.path() == *PROC { 38 | continue; // size of this dir doesn't mean anything useful, let's just forget it 39 | } 40 | if let Ok(md) = entry.metadata() { 41 | if md.is_file() { 42 | let nominal_size = md.size(); 43 | let block_size = md.blocks() * md.blksize(); 44 | let size = nominal_size.min(block_size); 45 | self.tx 46 | .send(ComputationEvent::FileInfo(FileInfo { 47 | path: entry.path(), 48 | file_count: 1, 49 | size, 50 | is_dir: false, 51 | })) 52 | .unwrap(); 53 | } else if md.is_dir() { 54 | let tx = self.tx.clone(); 55 | thread_count.fetch_add(1, Ordering::Relaxed); 56 | let thread_count = Arc::clone(&thread_count); 57 | let task_count = Arc::clone(&self.task_count); 58 | thread::spawn(move || { 59 | let fi = FileInfo::from_dir(entry.path()); 60 | // we check we didn't finish an obsolete task 61 | let current_task_count = task_count.load(Ordering::Relaxed); 62 | if current_task_count != start_task_count { 63 | return; 64 | } 65 | tx.send(ComputationEvent::FileInfo(fi)).unwrap(); 66 | let remaining_thread_count = thread_count.fetch_sub(1, Ordering::Relaxed); 67 | if remaining_thread_count == 1 { 68 | tx.send(ComputationEvent::Finished).unwrap(); 69 | } 70 | }); 71 | } 72 | } 73 | } 74 | if thread_count.load(Ordering::Relaxed) == 0 { 75 | // there was no folder 76 | self.tx.send(ComputationEvent::Finished).unwrap(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/file_info.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | fs, 4 | os::unix::fs::MetadataExt, // TODO windows compatibility... 5 | path::PathBuf, 6 | }; 7 | 8 | pub struct FileInfo { 9 | pub path: PathBuf, 10 | pub file_count: u64, 11 | pub size: u64, 12 | pub is_dir: bool, 13 | } 14 | impl FileInfo { 15 | /// implements a very crude file walker (much could be optimized) 16 | pub fn from_dir(path: PathBuf) -> FileInfo { 17 | let mut file_count = 1; 18 | let mut size = 0; 19 | let mut inodes = HashSet::::default(); // to avoid counting twice an inode 20 | let mut dirs: Vec = Vec::new(); 21 | dirs.push(path.clone()); 22 | while let Some(dir) = dirs.pop() { 23 | if let Ok(entries) = fs::read_dir(&dir) { 24 | for e in entries.flatten() { 25 | file_count += 1; 26 | if let Ok(md) = e.metadata() { 27 | if md.is_dir() { 28 | dirs.push(e.path()); 29 | } else if md.nlink() > 1 { 30 | if !inodes.insert(md.ino()) { 31 | // it was already in the set 32 | continue; // let's not add the size 33 | } 34 | } 35 | size += md.len(); 36 | } 37 | } 38 | } 39 | } 40 | FileInfo { 41 | path, 42 | file_count, 43 | size, 44 | is_dir: true, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use(select)] 2 | extern crate crossbeam; 3 | #[macro_use] 4 | extern crate lazy_static; 5 | 6 | mod computer; 7 | mod file_info; 8 | mod screen; 9 | 10 | use { 11 | crate::{ 12 | computer::{ComputationEvent, Computer}, 13 | screen::Screen, 14 | }, 15 | crossterm::{ 16 | cursor, 17 | event::KeyCode, 18 | queue, 19 | terminal::{EnterAlternateScreen, LeaveAlternateScreen}, 20 | }, 21 | std::{ 22 | io::Write, 23 | path::{Path, PathBuf}, 24 | }, 25 | termimad::{Event, EventSource}, 26 | }; 27 | 28 | /// find the path to open first: either a passed one 29 | /// or the current dir. 30 | fn starting_path() -> PathBuf { 31 | if let Some(s) = std::env::args().last() { 32 | let path = Path::new(&s); 33 | if path.is_dir() { 34 | if let Ok(path) = path.canonicalize() { 35 | return path; 36 | } 37 | } 38 | } 39 | std::env::current_dir().unwrap_or_else(|_| Path::new("/").to_path_buf()) 40 | } 41 | 42 | const ENTER: Event = Event::simple_key(KeyCode::Enter); 43 | const F5: Event = Event::simple_key(KeyCode::F(5)); 44 | const ESC: Event = Event::simple_key(KeyCode::Esc); 45 | const HOME: Event = Event::simple_key(KeyCode::Home); 46 | const END: Event = Event::simple_key(KeyCode::End); 47 | const PAGE_UP: Event = Event::simple_key(KeyCode::PageUp); 48 | const PAGE_DOWN: Event = Event::simple_key(KeyCode::PageDown); 49 | const UP: Event = Event::simple_key(KeyCode::Up); 50 | const DOWN: Event = Event::simple_key(KeyCode::Down); 51 | const CTRL_Q: Event = Event::crtl_key(KeyCode::Char('q')); 52 | 53 | fn main() -> termimad::Result<()> { 54 | let mut w = std::io::stderr(); 55 | queue!(w, EnterAlternateScreen)?; 56 | queue!(w, cursor::Hide)?; // hiding the cursor 57 | 58 | let mut screen = Screen::new(starting_path()); 59 | let event_source = EventSource::new()?; 60 | let rx_user = event_source.receiver(); 61 | 62 | let mut computer = Computer::new(); 63 | computer.do_children(screen.get_root()); 64 | 65 | loop { 66 | screen.display(&mut w)?; 67 | select! { 68 | recv(computer.rx) -> comp_event => { 69 | match comp_event { 70 | Ok(ComputationEvent::FileInfo(fi)) => { 71 | screen.add_to_total_size(fi.size); 72 | screen.list_view.add_row(fi); 73 | } 74 | Ok(ComputationEvent::Finished) => { 75 | screen.set_finished(); 76 | } 77 | _ => { 78 | // can this really happen ? 79 | } 80 | } 81 | } 82 | recv(rx_user) -> user_event => { 83 | if let Ok(user_event) = user_event { 84 | let mut quit = false; 85 | match user_event { 86 | ENTER => { 87 | let fi = screen.list_view.get_selection(); 88 | if let Some(fi) = fi { 89 | if fi.is_dir { 90 | let path = fi.path.clone(); 91 | screen.set_new_root(path); 92 | computer.do_children(screen.get_root()); 93 | } else { 94 | open::that(&fi.path).unwrap(); // TODO display an error if it fails 95 | } 96 | } 97 | } 98 | F5 => { 99 | screen.set_new_root(screen.get_root().to_path_buf()); 100 | computer.do_children(screen.get_root()); 101 | } 102 | ESC => { 103 | if screen.list_view.has_selection() { 104 | screen.list_view.unselect(); 105 | } else { 106 | let path = screen.get_root().parent().map(|p| p.to_path_buf()); 107 | if let Some(path) = path { 108 | screen.set_new_root(path); 109 | computer.do_children(screen.get_root()); 110 | } else { 111 | quit = true; 112 | } 113 | } 114 | } 115 | HOME => { 116 | screen.list_view.select_first_line(); 117 | } 118 | END => { 119 | screen.list_view.select_last_line(); 120 | } 121 | PAGE_UP => { 122 | screen.list_view.try_scroll_pages(-1); 123 | } 124 | PAGE_DOWN => { 125 | screen.list_view.try_scroll_pages(1); 126 | } 127 | UP => { 128 | screen.list_view.try_select_next(true); 129 | } 130 | DOWN => { 131 | screen.list_view.try_select_next(false); 132 | } 133 | CTRL_Q => { 134 | quit = true; 135 | } 136 | Event::Wheel(lines_count) => { 137 | screen.list_view.try_scroll_lines(lines_count); 138 | } 139 | _ => { 140 | //input_field.apply_event(&user_event); 141 | } 142 | }; 143 | event_source.unblock(quit); // if quit is true, this will lead to channel closing 144 | } else { 145 | // The channel has been closed, which means the event source 146 | // has properly released its resources, we may quit. 147 | break; 148 | } 149 | } 150 | } 151 | } 152 | queue!(w, cursor::Show)?; 153 | queue!(w, LeaveAlternateScreen)?; 154 | w.flush()?; 155 | Ok(()) 156 | } 157 | -------------------------------------------------------------------------------- /src/screen.rs: -------------------------------------------------------------------------------- 1 | use crossterm::{ 2 | queue, 3 | style::{Attribute, Color::*}, 4 | terminal::{self, Clear, ClearType}, 5 | }; 6 | use std::{ 7 | path::{Path, PathBuf}, 8 | sync::{ 9 | atomic::{AtomicU64, Ordering}, 10 | Arc, 11 | }, 12 | }; 13 | use termimad::{ 14 | ansi, Alignment, Area, CompoundStyle, ListView, ListViewCell, ListViewColumn, MadSkin, 15 | ProgressBar, Result, 16 | }; 17 | 18 | use crate::file_info::FileInfo; 19 | 20 | pub struct Screen<'t> { 21 | root: PathBuf, 22 | finished: bool, 23 | pub list_view: ListView<'t, FileInfo>, 24 | skin: &'t MadSkin, 25 | dimensions: (u16, u16), 26 | total_size: Arc, 27 | } 28 | impl<'t> Screen<'t> { 29 | pub fn new(root: PathBuf) -> Self { 30 | lazy_static! { 31 | static ref SKIN: MadSkin = make_skin(); 32 | } 33 | let total_size = Arc::new(AtomicU64::new(0)); 34 | let column_total_size = Arc::clone(&total_size); 35 | let columns = vec![ 36 | ListViewColumn::new( 37 | "name", 38 | 10, 39 | 50, 40 | Box::new(|fi: &FileInfo| { 41 | ListViewCell::new( 42 | fi.path.file_name().unwrap().to_string_lossy().to_string(), 43 | if fi.is_dir { 44 | &SKIN.bold 45 | } else { 46 | &SKIN.paragraph.compound_style 47 | }, 48 | ) 49 | }), 50 | ) 51 | .with_align(Alignment::Left), 52 | ListViewColumn::new( 53 | "items", 54 | 7, 55 | 9, 56 | Box::new(|fi: &FileInfo| { 57 | ListViewCell::new(u64_to_str(fi.file_count), &SKIN.paragraph.compound_style) 58 | }), 59 | ) 60 | .with_align(Alignment::Right), 61 | ListViewColumn::new( 62 | "size", 63 | 5, 64 | 6, 65 | Box::new(|fi: &FileInfo| { 66 | ListViewCell::new(u64_to_str(fi.size), &SKIN.paragraph.compound_style) 67 | }), 68 | ) 69 | .with_align(Alignment::Right), 70 | ListViewColumn::new( 71 | "size", 72 | 13, 73 | 13, 74 | Box::new(move |fi: &FileInfo| { 75 | let total_size = column_total_size.load(Ordering::Relaxed); 76 | ListViewCell::new( 77 | if total_size > 0 { 78 | let part = (fi.size as f32) / (total_size as f32); 79 | format!("{:>3.0}% {}", 100.0 * part, ProgressBar::new(part, 8)) 80 | } else { 81 | "".to_owned() 82 | }, 83 | if fi.is_dir { 84 | &SKIN.bold 85 | } else { 86 | &SKIN.paragraph.compound_style 87 | }, 88 | ) 89 | }), 90 | ) 91 | .with_align(Alignment::Left), 92 | ]; 93 | let area = Area::new(0, 1, 10, 10); 94 | let mut list_view = ListView::new(area, columns, &SKIN); 95 | list_view.sort(Box::new(|a, b| b.size.cmp(&a.size))); 96 | Self { 97 | root, 98 | skin: &SKIN, 99 | list_view, 100 | dimensions: (0, 0), 101 | total_size, 102 | finished: false, 103 | } 104 | } 105 | pub fn set_new_root(&mut self, path: PathBuf) { 106 | self.root = path; 107 | self.total_size.store(0, Ordering::Relaxed); 108 | self.list_view.clear_rows(); 109 | self.finished = false; 110 | } 111 | pub fn set_finished(&mut self) { 112 | self.finished = true; 113 | } 114 | pub fn add_to_total_size(&mut self, to_add: u64) { 115 | self.total_size.fetch_add(to_add, Ordering::Relaxed); 116 | } 117 | pub fn get_root(&self) -> &Path { 118 | &self.root 119 | } 120 | pub fn display(&mut self, writer: &mut W) -> Result<()> 121 | where 122 | W: std::io::Write, 123 | { 124 | let (width, height) = terminal::size()?; 125 | if (width, height) != self.dimensions { 126 | queue!(writer, Clear(ClearType::All))?; 127 | self.dimensions = (width, height); 128 | self.list_view.area.width = width; 129 | self.list_view.area.height = height - 4; 130 | self.list_view.update_dimensions(); 131 | } 132 | let title = if self.finished { 133 | format!("# **{}**", self.root.as_os_str().to_string_lossy()) 134 | } else { 135 | format!( 136 | "# **{}** *computing...*", 137 | self.root.as_os_str().to_string_lossy() 138 | ) 139 | }; 140 | self.skin 141 | .write_in_area_on(writer, &title, &Area::new(0, 0, width, 1))?; 142 | self.skin.write_in_area_on( 143 | writer, 144 | "Hit *ctrl-q* to quit, *esc* to go to parent, *↑* and *↓* to select, and *enter* to open", 145 | &Area::new(0, height-2, width, 1), 146 | )?; 147 | self.list_view.write_on(writer) 148 | } 149 | } 150 | 151 | fn make_skin() -> MadSkin { 152 | let mut skin = MadSkin::default(); 153 | skin.headers[0].compound_style = CompoundStyle::with_attr(Attribute::Bold); 154 | skin.headers[0].align = Alignment::Left; 155 | skin.italic.set_fg(ansi(225)); 156 | skin.bold = CompoundStyle::with_fg(Blue); 157 | skin 158 | } 159 | 160 | const SIZE_NAMES: &[&str] = &["", "K", "M", "G", "T", "P", "E", "Z", "Y"]; 161 | /// format a number of as a string 162 | pub fn u64_to_str(mut v: u64) -> String { 163 | let mut i = 0; 164 | while v >= 2300 && i < SIZE_NAMES.len() - 1 { 165 | v >>= 10; 166 | i += 1; 167 | } 168 | format!("{}{}", v, &SIZE_NAMES[i]) 169 | } 170 | --------------------------------------------------------------------------------