From f1ec96bcf72f8cff71bf1caca55f960c84ca4996 Mon Sep 17 00:00:00 2001 From: Canop Date: Fri, 2 Aug 2019 15:12:11 +0200 Subject: whalespotting mode This mode is activated by the --sizes option and makes a few changes to the broot logic in order to ease size survey: - sizes computed and displayed - only one level of tree - size based ordering - hidden files and gitignored ones are shown (by default) --- CHANGELOG.md | 4 ++ Cargo.lock | 8 ++-- Cargo.toml | 4 +- README.md | 16 +++++--- img/20190802-sizes.png | Bin 0 -> 38176 bytes src/browser_states.rs | 4 +- src/cli.rs | 9 ++++- src/displayable_tree.rs | 40 ++++++++++++------ src/file_sizes/file_sizes_default.rs | 76 +++++++++++++++++++++++++++++++++++ src/file_sizes/file_sizes_unix.rs | 11 ++++- src/file_sizes/file_sizes_windows.rs | 66 ------------------------------ src/file_sizes/mod.rs | 33 ++++++++------- src/flat_tree.rs | 25 ++++++++---- src/skin.rs | 9 ++--- src/tree_build.rs | 6 +++ website/docs/img/20190802-sizes.png | Bin 0 -> 38176 bytes website/docs/index.md | 13 ++++-- 17 files changed, 198 insertions(+), 126 deletions(-) create mode 100644 img/20190802-sizes.png create mode 100644 src/file_sizes/file_sizes_default.rs delete mode 100644 src/file_sizes/file_sizes_windows.rs create mode 100644 website/docs/img/20190802-sizes.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 850f06a..a033973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ + +### v0.9.3 - 2019-08-02 +Launching broot with `--sizes` now sets a set of features enabling fast "whale spotting" navigation + ### v0.9.2 - 2019-07-31 Fix non consistent builds due to lack of precise versionning in crossterm subcrate versionning diff --git a/Cargo.lock b/Cargo.lock index 8ae383e..45aef5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,7 +86,7 @@ dependencies = [ [[package]] name = "broot" -version = "0.9.1" +version = "0.9.3" dependencies = [ "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -106,7 +106,7 @@ dependencies = [ "opener 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "simplelog 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", - "termimad 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "termimad 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "umask 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "users 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -595,7 +595,7 @@ dependencies = [ [[package]] name = "termimad" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "crossbeam 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -781,7 +781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum syn 0.15.30 (registry+https://github.com/rust-lang/crates.io-index)" = "66c8865bf5a7cbb662d8b011950060b3c8743dca141b054bf7195b20d314d8e2" "checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015" "checksum term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" -"checksum termimad 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a1f13066780ec95e8d43f336b92d770fbef329c1b19d8ed78c7c015d2b0ba091" +"checksum termimad 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8e719aff75d732980c604d2c5a28f2d5c532f1e72cbd6eee5bae3a66547226a8" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" diff --git a/Cargo.toml b/Cargo.toml index c2cde65..1255eec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "broot" -version = "0.9.2" +version = "0.9.3" authors = ["dystroy "] repository = "https://github.com/Canop/broot" description = "Fuzzy Search + tree + cd" @@ -30,7 +30,7 @@ crossterm_style = "=0.3.3" crossterm_terminal = "=0.2.4" crossterm_utils = "=0.2.3" umask = "0.1.4" -termimad = "0.6.2" +termimad = "0.6.4" #termimad = { path = "../termimad" } [target.'cfg(unix)'.dependencies] diff --git a/README.md b/README.md index c9ad8cc..fd6cbba 100644 --- a/README.md +++ b/README.md @@ -54,18 +54,22 @@ For example, assuming you look for your one file whose name contains `abc` in a And if you look for a filename *ending* in `abc` then you may anchor the regex: `abc$/`. -### See what takes space: - -![size](img/20190128-only-folders-with-size.png) - -To toggle size display, type `:s`. Sizes are computed in the background, you don't have to wait for them when you navigate. - ### Apply a personal shortcut to a file: ![size](img/20190128-edit.png) Just find the file you want to edit with a few keystrokes, type `:e`, then `` (you should define your preferred editor, see [documentation](documentation.md#verbs)). +### See what takes space: + +![size](img/20190802-sizes.png) + +If you start broot with the `--sizes` option, you get a mode tailored to "whale spotting" nagigation, making it easy to determine what files or folders take space. + +And you keep all broot tools, like filtering or the ability to delete or open files and directories. + +Sizes are computed in the background, you don't have to wait for them when you navigate. + ### More... See **[Broot's web site](https://dystroy.org/broot)** for instructions regarding installation and usage. diff --git a/img/20190802-sizes.png b/img/20190802-sizes.png new file mode 100644 index 0000000..f1a1c2b Binary files /dev/null and b/img/20190802-sizes.png differ diff --git a/src/browser_states.rs b/src/browser_states.rs index 0061b13..527e611 100644 --- a/src/browser_states.rs +++ b/src/browser_states.rs @@ -169,9 +169,9 @@ impl BrowserState { screen.write_status_text( if tree.selection == 0 { if has_pattern { - "Hit to go back, to go up, '?' for help, or a few letters to search" - } else { "Hit to remove the filter, to go up, '?' for help" + } else { + "Hit to go back, to go up, '?' for help, or a few letters to search" } } else { let line = &tree.lines[tree.selection]; diff --git a/src/cli.rs b/src/cli.rs index f020a56..e25a046 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,7 @@ use crossterm_style::Color; use termimad::{Alignment, MadSkin}; use crate::errors::{ProgramError, TreeBuildError}; -use crate::tree_options::TreeOptions; +use crate::tree_options::{OptionBool, TreeOptions}; pub struct AppLaunchArgs { pub root: PathBuf, // what should be the initial root @@ -132,9 +132,14 @@ pub fn read_launch_args() -> Result { } let root = root.canonicalize()?; let mut tree_options = TreeOptions::default(); + tree_options.show_sizes = cli_args.is_present("sizes"); + if tree_options.show_sizes { + // by default, if we're asked to show the size, we show all files + tree_options.show_hidden = true; + tree_options.respect_git_ignore = OptionBool::No; + } tree_options.only_folders = cli_args.is_present("only-folders"); tree_options.show_hidden = cli_args.is_present("hidden"); - tree_options.show_sizes = cli_args.is_present("sizes"); tree_options.show_dates = cli_args.is_present("dates"); tree_options.show_permissions = cli_args.is_present("permissions"); if let Some(respect_ignore) = cli_args.value_of("gitignore") { diff --git a/src/displayable_tree.rs b/src/displayable_tree.rs index bb4982e..cc1d8fa 100644 --- a/src/displayable_tree.rs +++ b/src/displayable_tree.rs @@ -1,5 +1,6 @@ use std::fmt; use std::time::SystemTime; +use crossterm_style::{ObjectStyle}; use crossterm_terminal::{ClearType, Terminal}; use chrono::offset::Local; use chrono::DateTime; @@ -17,6 +18,7 @@ use crate::skin::{Skin, SkinEntry}; use crossterm_style::{Color, Colored}; use crossterm_cursor::TerminalCursor; +use termimad::ProgressBar; /// A tree wrapper implementing Display /// which can be used either @@ -50,6 +52,24 @@ impl<'s, 't> DisplayableTree<'s, 't> { } } + fn name_style( + &self, + line: &TreeLine, + ) -> &ObjectStyle { + match &line.line_type { + LineType::Dir => &self.skin.directory, + LineType::File => { + if line.is_exe() { + &self.skin.exe + } else { + &self.skin.file + } + } + LineType::SymLinkToFile(_) | LineType::SymLinkToDir(_) => &self.skin.link, + LineType::Pruning => &self.skin.pruning, + } + } + fn write_line_size( &self, f: &mut fmt::Formatter<'_>, @@ -57,21 +77,15 @@ impl<'s, 't> DisplayableTree<'s, 't> { total_size: Size, ) -> fmt::Result { if let Some(s) = line.size { - let dr: usize = s.discrete_ratio(total_size, 8) as usize; - let s: Vec = s.to_string().chars().collect(); - let mut bar = String::new(); - for i in 0..dr { - bar.push(if i < s.len() { s[i] } else { ' ' }); - } - self.skin.size_bar.write(f, &bar)?; - let mut no_bar = String::new(); - for i in dr..8 { - no_bar.push(if i < s.len() { s[i] } else { ' ' }); + let pb = ProgressBar::new(s.part_of(total_size), 10); + let style = self.name_style(line); + if let Some(fg) = style.fg_color { + write!(f, "{}{:>5} {:<10} ", Colored::Fg(fg), s.to_string(), pb) + } else { + write!(f, "{:>5} {:<10} ", s.to_string(), pb) } - self.skin.size_no_bar.write(f, &no_bar)?; - write!(f, " ") } else { - self.skin.tree.write(f, "──────── ") + self.skin.tree.write(f, "──────────────── ") } } diff --git a/src/file_sizes/file_sizes_default.rs b/src/file_sizes/file_sizes_default.rs new file mode 100644 index 0000000..bed8584 --- /dev/null +++ b/src/file_sizes/file_sizes_default.rs @@ -0,0 +1,76 @@ +//! size computation for non linux + +use crate::task_sync::TaskLifetime; +use crossbeam::channel::unbounded; +use crossbeam::sync::WaitGroup; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicIsize, AtomicUsize, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +pub fn compute_dir_size(path: &Path, tl: &TaskLifetime) -> Option { + let size = Arc::new(AtomicUsize::new(0)); + + // this MPMC channel contains the directory paths which must be handled + let (dirs_sender, dirs_receiver) = unbounded(); + + // busy is the number of directories which are either being processed or queued + // We use this count to determine when threads can stop waiting for tasks + let busy = Arc::new(AtomicIsize::new(0)); + busy.fetch_add(1, Ordering::Relaxed); + dirs_sender.send(Some(PathBuf::from(path))).unwrap(); + + let wg = WaitGroup::new(); + let period = Duration::from_micros(50); + for _ in 0..8 { + let size = Arc::clone(&size); + let busy = Arc::clone(&busy); + let wg = wg.clone(); + let (dirs_sender, dirs_receiver) = (dirs_sender.clone(), dirs_receiver.clone()); + let tl = tl.clone(); + thread::spawn(move || { + loop { + let o = dirs_receiver.recv_timeout(period); + if let Ok(Some(open_dir)) = o { + if let Ok(entries) = fs::read_dir(&open_dir) { + for e in entries.flatten() { + if let Ok(md) = e.metadata() { + if md.is_dir() { + busy.fetch_add(1, Ordering::Relaxed); + dirs_sender.send(Some(e.path())).unwrap(); + } + size.fetch_add(md.len() as usize, Ordering::Relaxed); + } + } + } + busy.fetch_sub(1, Ordering::Relaxed); + dirs_sender.send(None).unwrap(); + } else if busy.load(Ordering::Relaxed) < 1 { + break; + } + if tl.is_expired() { + break; + } + } + drop(wg); + }); + } + wg.wait(); + + if tl.is_expired() { + return None; + } + let size: usize = size.load(Ordering::Relaxed); + let size: u64 = size as u64; + Some(size) +} + + +pub fn compute_file_size(path: &Path) -> u64 { + match fs::metadata(path) { + Ok(m) => m.len(), + Err(_) => 0, + } +} diff --git a/src/file_sizes/file_sizes_unix.rs b/src/file_sizes/file_sizes_unix.rs index a99d81f..19b99f6 100644 --- a/src/file_sizes/file_sizes_unix.rs +++ b/src/file_sizes/file_sizes_unix.rs @@ -3,6 +3,7 @@ use crossbeam::channel::unbounded; use crossbeam::sync::WaitGroup; use std::collections::HashSet; use std::fs; +//use std::os::linux::fs::MetadataExt; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::{atomic::{AtomicIsize, AtomicU64, Ordering}, Arc, Mutex}; @@ -48,7 +49,8 @@ pub fn compute_dir_size(path: &Path, tl: &TaskLifetime) -> Option { continue; // let's not add the size } } - size.fetch_add(md.len(), Ordering::Relaxed); + let file_size = if md.blocks()==0 { 0 } else { md.size() }; + size.fetch_add(file_size, Ordering::Relaxed); } } } @@ -72,3 +74,10 @@ pub fn compute_dir_size(path: &Path, tl: &TaskLifetime) -> Option { let size = size.load(Ordering::Relaxed); Some(size) } + +pub fn compute_file_size(path: &Path) -> u64 { + match fs::metadata(path) { + Ok(m) => if m.blocks()==0 { 0 } else { m.size() }, + Err(_) => 0, + } +} diff --git a/src/file_sizes/file_sizes_windows.rs b/src/file_sizes/file_sizes_windows.rs deleted file mode 100644 index 8b4d6c1..0000000 --- a/src/file_sizes/file_sizes_windows.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::task_sync::TaskLifetime; -use crossbeam::channel::unbounded; -use crossbeam::sync::WaitGroup; -use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicIsize, AtomicUsize, Ordering}; -use std::sync::Arc; -use std::thread; -use std::time::Duration; - -pub fn compute_dir_size(path: &Path, tl: &TaskLifetime) -> Option { - let size = Arc::new(AtomicUsize::new(0)); - - // this MPMC channel contains the directory paths which must be handled - let (dirs_sender, dirs_receiver) = unbounded(); - - // busy is the number of directories which are either being processed or queued - // We use this count to determine when threads can stop waiting for tasks - let busy = Arc::new(AtomicIsize::new(0)); - busy.fetch_add(1, Ordering::Relaxed); - dirs_sender.send(Some(PathBuf::from(path))).unwrap(); - - let wg = WaitGroup::new(); - let period = Duration::from_micros(50); - for _ in 0..8 { - let size = Arc::clone(&size); - let busy = Arc::clone(&busy); - let wg = wg.clone(); - let (dirs_sender, dirs_receiver) = (dirs_sender.clone(), dirs_receiver.clone()); - let tl = tl.clone(); - thread::spawn(move || { - loop { - let o = dirs_receiver.recv_timeout(period); - if let Ok(Some(open_dir)) = o { - if let Ok(entries) = fs::read_dir(&open_dir) { - for e in entries.flatten() { - if let Ok(md) = e.metadata() { - if md.is_dir() { - busy.fetch_add(1, Ordering::Relaxed); - dirs_sender.send(Some(e.path())).unwrap(); - } - size.fetch_add(md.len() as usize, Ordering::Relaxed); - } - } - } - busy.fetch_sub(1, Ordering::Relaxed); - dirs_sender.send(None).unwrap(); - } else if busy.load(Ordering::Relaxed) < 1 { - break; - } - if tl.is_expired() { - break; - } - } - drop(wg); - }); - } - wg.wait(); - - if tl.is_expired() { - return None; - } - let size: usize = size.load(Ordering::Relaxed); - let size: u64 = size as u64; - Some(size) -} diff --git a/src/file_sizes/mod.rs b/src/file_sizes/mod.rs index 43bd59c..be1042e 100644 --- a/src/file_sizes/mod.rs +++ b/src/file_sizes/mod.rs @@ -5,7 +5,6 @@ /// twice an inode. use crate::task_sync::TaskLifetime; use std::collections::HashMap; -use std::fs; use std::ops::AddAssign; use std::path::{Path, PathBuf}; use std::sync::Mutex; @@ -18,10 +17,7 @@ pub struct Size(u64); impl Size { pub fn from_file(path: &Path) -> Size { - Size(match fs::metadata(path) { - Ok(m) => m.len(), - Err(_) => 0, - }) + Size(compute_file_size(path)) } /// Return the size of the directory, either by computing it of by @@ -51,17 +47,17 @@ impl Size { pub fn to_string(self) -> String { let mut v = self.0; let mut i = 0; - while v >= 9000 && i < SIZE_NAMES.len() - 1 { + while v >= 5000 && i < SIZE_NAMES.len() - 1 { v >>= 10; i += 1; } format!("{}{}", v, &SIZE_NAMES[i]) } - pub fn discrete_ratio(self, max: Size, r: u64) -> u64 { - if max.0 == 0 || self.0 == 0 { - 0 + pub fn part_of(&self, total: Size) -> f32 { + if total.0 <= 0 { + 0.0 } else { - ((r as f64) * (self.0 as f64).cbrt() / (max.0 as f64).cbrt()).round() as u64 + self.0 as f32 / total.0 as f32 } } } @@ -94,10 +90,19 @@ fn compute_dir_size(path: &Path, tl: &TaskLifetime) -> Option { file_sizes_unix::compute_dir_size(path, tl) } -#[cfg(windows)] -mod file_sizes_windows; +#[cfg(unix)] +fn compute_file_size(path: &Path) -> u64 { + file_sizes_unix::compute_file_size(path) +} -#[cfg(windows)] +#[cfg(not(unix))] +mod file_sizes_default; + +#[cfg(not(unix))] fn compute_dir_size(path: &Path, tl: &TaskLifetime) -> Option { - file_sizes_windows::compute_dir_size(path, tl) + file_sizes_default::compute_dir_size(path, tl) +} +#[cfg(not(unix))] +fn compute_file_size(path: &Path) -> u64 { + file_sizes_default::compute_file_size(path) } diff --git a/src/flat_tree.rs b/src/flat_tree.rs index 61c6b76..61eb68b 100644 --- a/src/flat_tree.rs +++ b/src/flat_tree.rs @@ -1,6 +1,6 @@ /// In the flat_tree structure, every "node" is just a line, there's /// no link from a child to its parent or from a parent to its children. -use std::cmp::{self, Ordering}; +use std::cmp::{self, Ord, PartialOrd, Ordering}; use std::fs; use std::mem; use std::path::{Path, PathBuf}; @@ -157,11 +157,8 @@ impl PartialOrd for TreeLine { impl Tree { pub fn refresh(&mut self, page_height: usize) -> Result<(), errors::TreeBuildError> { - let builder = - TreeBuilder::from(self.root().to_path_buf(), self.options.clone(), page_height)?; - debug!("remove 3"); + let builder = TreeBuilder::from(self.root().to_path_buf(), self.options.clone(), page_height)?; let mut tree = builder.build(&TaskLifetime::unlimited()).unwrap(); // should not fail - debug!("remove 4"); // we save the old selection to try restore it let selected_path = self.selected_line().path.to_path_buf(); mem::swap(&mut self.lines, &mut tree.lines); @@ -174,8 +171,8 @@ impl Tree { // - sort the lines // - compute left branchs pub fn after_lines_changed(&mut self) { - // we sort the lines - self.lines.sort(); + // we sort the lines (this is mandatory to avoid crashes) + self.lines[1..].sort(); for i in 1..self.lines.len() { for d in 0..self.lines[i].left_branchs.len() { @@ -372,15 +369,29 @@ impl Tree { self.lines[i].size = Some(Size::from_file(&self.lines[i].path)); } } + self.sort_siblings_by_size(); } pub fn fetch_some_missing_dir_size(&mut self, tl: &TaskLifetime) { for i in 1..self.lines.len() { if self.lines[i].size.is_none() && self.lines[i].line_type == LineType::Dir { self.lines[i].size = Size::from_dir(&self.lines[i].path, tl); + self.sort_siblings_by_size(); return; } } } + /// Sort files according to their size + /// + /// Warning: must not be called if there's more than one level displayed! + /// (a better sort should be devised but it's unsure whether it would be + /// readable enough) + fn sort_siblings_by_size(&mut self) { + self.lines[1..].sort_by(|a, b| { + let asize = a.size.map_or(0, |s| s.into()); + let bsize = b.size.map_or(0, |s| s.into()); + bsize.cmp(&asize) + }); + } pub fn total_size(&self) -> Size { if let Some(size) = self.lines[0].size { // if the real total size is computed, it's in the root line diff --git a/src/skin.rs b/src/skin.rs index 9d92c24..fd285eb 100644 --- a/src/skin.rs +++ b/src/skin.rs @@ -94,19 +94,18 @@ Skin! { pruning: gray(17), None; {Italic} permissions: gray(15), None; dates: ansi(109), None; - selected_line: None, gray(3); - size_bar: Some(White), Some(DarkBlue); - size_no_bar: gray(15), gray(2); + selected_line: None, gray(4); + size_bar: Some(White), gray(2); char_match: Some(Green), None; file_error: Some(Red), None; flag_label: gray(15), gray(1); - flag_value: Some(Blue), gray(1); + flag_value: Some(Blue), gray(1); {Bold} input: Some(White), None; spinner: gray(10), gray(2); status_error: Some(White), Some(Red); status_normal: Some(White), gray(2); scrollbar_track: gray(7), None; - scrollbar_thumb: ansi(178), None; + scrollbar_thumb: gray(22), None; help_paragraph: gray(20), None; help_bold: ansi(178), None; {Bold} help_italic: ansi(229), None; {Italic} diff --git a/src/tree_build.rs b/src/tree_build.rs index c9c47f5..9761bf6 100644 --- a/src/tree_build.rs +++ b/src/tree_build.rs @@ -369,6 +369,12 @@ impl TreeBuilder { } } else { // this depth is finished, we must go deeper + if self.options.show_sizes { + // both for technical reasons (bad sort) and ergonomics + // ones (it proved to be hard to read), we don't want + // a deep tree when looking at sizes. + break; + } if next_level_dirs.is_empty() { // except there's nothing deeper break; diff --git a/website/docs/img/20190802-sizes.png b/website/docs/img/20190802-sizes.png new file mode 100644 index 0000000..f1a1c2b Binary files /dev/null and b/website/docs/img/20190802-sizes.png differ diff --git a/website/docs/index.md b/website/docs/index.md index 1ffac21..66a8d32 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -44,7 +44,7 @@ Once the file you want is selected you can # Manipulate your files -![size](img/20190306-mv.png) +![mv](img/20190306-mv.png) Most often you move your files in the blind. You do a few `ls` before, then your manipulation, and maybe you check after. @@ -58,11 +58,16 @@ Move, copy, rm, mkdir, are built in and you can add your own shortcuts. Just find the file you want to edit with a few keystrokes, type `:e`, then `` (you should define your preferred editor, see [documentation](documentation/usage.md#verbs)). -# See what takes space +### See what takes space: -![size](img/20190128-only-folders-with-size.png) +![size](img/20190802-sizes.png) + +If you start broot with the `--sizes` option, you get a mode tailored to "whale spotting" nagigation, making it easy to determine what files or folders take space. + +And you keep all broot tools, like filtering or the ability to delete or open files and directories. + +Sizes are computed in the background, you don't have to wait for them when you navigate. -To toggle size display, type `:s`. Sizes are computed in the background, you don't have to wait for them when you navigate. # More... -- cgit v1.2.3