diff options
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/aggregate.rs | 30 | ||||
-rw-r--r-- | src/common.rs | 12 | ||||
-rw-r--r-- | src/interactive.rs | 37 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/main.rs | 65 | ||||
-rw-r--r-- | src/options.rs | 11 | ||||
-rw-r--r-- | tests/snapshots/failure-interactive-without-tty | 3 | ||||
-rwxr-xr-x | tests/stateless-journey.sh | 8 |
9 files changed, 117 insertions, 51 deletions
@@ -17,6 +17,7 @@ jwalk = "0.4.0" byte-unit = "2.1.0" termion = "1.5.2" atty = "0.2.11" +tui = "0.6.0" [[bin]] name="dua" diff --git a/src/aggregate.rs b/src/aggregate.rs index cb179a5..c62ea82 100644 --- a/src/aggregate.rs +++ b/src/aggregate.rs @@ -13,9 +13,10 @@ pub fn aggregate( compute_total: bool, sort_by_size_in_bytes: bool, paths: impl IntoIterator<Item = impl AsRef<Path>>, -) -> Result<WalkResult, Error> { +) -> Result<(WalkResult, Statistics), Error> { let mut res = WalkResult::default(); - res.stats.smallest_file_in_bytes = u64::max_value(); + let mut stats = Statistics::default(); + stats.smallest_file_in_bytes = u64::max_value(); let mut total = 0; let mut num_roots = 0; let mut aggregates = Vec::new(); @@ -24,7 +25,7 @@ pub fn aggregate( let mut num_bytes = 0u64; let mut num_errors = 0u64; for entry in options.iter_from_path(path.as_ref(), Sorting::None) { - res.stats.files_traversed += 1; + stats.files_traversed += 1; match entry { Ok(entry) => { let file_size = match entry.metadata { @@ -38,10 +39,8 @@ pub fn aggregate( "we ask for metadata, so we at least have Some(Err(..))). Issue in jwalk?" ), }; - res.stats.largest_file_in_bytes = - res.stats.largest_file_in_bytes.max(file_size); - res.stats.smallest_file_in_bytes = - res.stats.smallest_file_in_bytes.min(file_size); + stats.largest_file_in_bytes = stats.largest_file_in_bytes.max(file_size); + stats.smallest_file_in_bytes = stats.smallest_file_in_bytes.min(file_size); num_bytes += file_size; } Err(_) => num_errors += 1, @@ -64,8 +63,8 @@ pub fn aggregate( res.num_errors += num_errors; } - if res.stats.files_traversed == 0 { - res.stats.smallest_file_in_bytes = 0; + if stats.files_traversed == 0 { + stats.smallest_file_in_bytes = 0; } if sort_by_size_in_bytes { @@ -92,7 +91,7 @@ pub fn aggregate( color::Fg(color::Reset), )?; } - Ok(res) + Ok((res, stats)) } fn path_color(path: impl AsRef<Path>) -> Box<dyn fmt::Display> { @@ -127,3 +126,14 @@ fn write_path<C: fmt::Display>( path_color_reset = options.color.display(color::Fg(color::Reset)), ) } + +/// Statistics obtained during a filesystem walk +#[derive(Default, Debug)] +pub struct Statistics { + /// The amount of files we have seen + pub files_traversed: u64, + /// The size of the smallest file encountered in bytes + pub smallest_file_in_bytes: u64, + /// The size of the largest file encountered in bytes + pub largest_file_in_bytes: u64, +} diff --git a/src/common.rs b/src/common.rs index 33fddf8..fb6ff03 100644 --- a/src/common.rs +++ b/src/common.rs @@ -96,21 +96,9 @@ impl WalkOptions { } } -/// Statistics obtained during a filesystem walk -#[derive(Default, Debug)] -pub struct Statistics { - /// The amount of files we have seen - pub files_traversed: u64, - /// The size of the smallest file encountered in bytes - pub smallest_file_in_bytes: u64, - /// The size of the largest file encountered in bytes - pub largest_file_in_bytes: u64, -} - /// Information we gather during a filesystem walk #[derive(Default)] pub struct WalkResult { /// The amount of io::errors we encountered. Can happen when fetching meta-data, or when reading the directory contents. pub num_errors: u64, - pub stats: Statistics, } diff --git a/src/interactive.rs b/src/interactive.rs new file mode 100644 index 0000000..e7b4d59 --- /dev/null +++ b/src/interactive.rs @@ -0,0 +1,37 @@ +mod app { + use crate::{WalkOptions, WalkResult}; + use failure::Error; + use std::io; + use std::path::PathBuf; + use termion::input::{Keys, TermReadEventsAndRaw}; + use tui::{backend::Backend, Terminal}; + + pub struct App {} + + impl App { + pub fn event_loop<B, R>( + &mut self, + terminal: &mut Terminal<B>, + keys: Keys<R>, + ) -> Result<WalkResult, Error> + where + B: Backend, + R: io::Read + TermReadEventsAndRaw, + { + unimplemented!() + } + + pub fn initialize<B>( + terminal: &mut Terminal<B>, + options: WalkOptions, + input: Vec<PathBuf>, + ) -> Result<App, Error> + where + B: Backend, + { + Ok(App {}) + } + } +} + +pub use self::app::*; @@ -3,6 +3,7 @@ extern crate jwalk; mod aggregate; mod common; +pub mod interactive; pub use aggregate::aggregate; pub use common::*; diff --git a/src/main.rs b/src/main.rs index 66ec8f2..6c0d89b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,10 +5,12 @@ extern crate structopt; use structopt::StructOpt; use dua::{ByteFormat, Color}; -use failure::Error; +use failure::{Error, ResultExt}; use failure_tools::ok_or_exit; -use std::path::PathBuf; -use std::{fs, io, io::Write, process}; +use std::{fs, io, io::Write, path::PathBuf, process}; +use termion::input::TermRead; +use termion::{raw::IntoRawMode, screen::AlternateScreen}; +use tui::{backend::TermionBackend, Terminal}; mod options; @@ -27,51 +29,62 @@ fn run() -> Result<(), Error> { Color::None }, }; - let (show_statistics, res) = match opt.command { + let res = match opt.command { + Some(Interactive { input }) => { + let mut terminal = { + let stdout = io::stdout().into_raw_mode()?; + let stdout = AlternateScreen::from(stdout); + let backend = TermionBackend::new(stdout); + Terminal::new(backend) + .with_context(|_| "Interactive mode requires a connected terminal")? + }; + let mut app = dua::interactive::App::initialize(&mut terminal, walk_options, input)?; + app.event_loop(&mut terminal, io::stdin().keys())? + } Some(Aggregate { input, no_total, no_sort, statistics, - }) => ( - statistics, - dua::aggregate( + }) => { + let (res, stats) = dua::aggregate( stdout_locked, walk_options, !no_total, !no_sort, - if input.len() == 0 { - cwd_dirlist()? - } else { - input - }, - )?, - ), - None => ( - false, + paths_from(input)?, + )?; + if statistics { + writeln!(io::stderr(), "{:?}", stats).ok(); + } + res + } + None => { dua::aggregate( stdout_locked, walk_options, true, true, - if opt.input.len() == 0 { - cwd_dirlist()? - } else { - opt.input - }, - )?, - ), + paths_from(opt.input)?, + )? + .0 + } }; - if show_statistics { - writeln!(io::stderr(), "{:?}", res.stats).ok(); - } if res.num_errors > 0 { process::exit(1); } Ok(()) } +fn paths_from(paths: Vec<PathBuf>) -> Result<Vec<PathBuf>, io::Error> { + if paths.len() == 0 { + cwd_dirlist() + } else { + Ok(paths) + } +} + fn cwd_dirlist() -> Result<Vec<PathBuf>, io::Error> { let mut v: Vec<_> = fs::read_dir(".")? .filter_map(|e| { diff --git a/src/options.rs b/src/options.rs index 76c0abc..6d686c7 100644 --- a/src/options.rs +++ b/src/options.rs @@ -40,13 +40,20 @@ pub struct Args { #[structopt(short = "f", long = "format")] pub format: Option<ByteFormat>, - /// One or more input files. If unset, we will use all entries in the current working directory. + /// One or more input files or directories. If unset, we will use all entries in the current working directory. #[structopt(parse(from_os_str))] pub input: Vec<PathBuf>, } #[derive(Debug, StructOpt)] pub enum Command { + /// Launch the terminal user interface + #[structopt(name = "interactive", alias = "i")] + Interactive { + /// One or more input files or directories. If unset, we will use all entries in the current working directory. + #[structopt(parse(from_os_str))] + input: Vec<PathBuf>, + }, /// Aggregrate the consumed space of one or more directories or files #[structopt(name = "aggregate", alias = "a")] Aggregate { @@ -60,7 +67,7 @@ pub enum Command { /// If set, no total column will be computed for multiple inputs #[structopt(long = "no-total")] no_total: bool, - /// One or more input files. If unset, we will use all entries in the current working directory. + /// One or more input files or directories. If unset, we will use all entries in the current working directory. #[structopt(parse(from_os_str))] input: Vec<PathBuf>, }, diff --git a/tests/snapshots/failure-interactive-without-tty b/tests/snapshots/failure-interactive-without-tty new file mode 100644 index 0000000..e0065af --- /dev/null +++ b/tests/snapshots/failure-interactive-without-tty @@ -0,0 +1,3 @@ +[?1049h[?1049lerror: Interactive mode requires a connected terminal +Caused by: + 1: Inappropriate ioctl for device (os error 25)
\ No newline at end of file diff --git a/tests/stateless-journey.sh b/tests/stateless-journey.sh index 5625035..6c112b7 100755 --- a/tests/stateless-journey.sh +++ b/tests/stateless-journey.sh @@ -44,7 +44,7 @@ WITH_FAILURE=1 (with "no option to adjust the total" it "produces a human-readable aggregate, with total" && { WITH_SNAPSHOT="$snapshot/success-no-arguments-multiple-input-paths" \ - expect_run ${SUCCESSFULLY} "$exe" aggregate . . dir ./dir/ ./dir/sub + expect_run ${SUCCESSFULLY} "$exe" a . . dir ./dir/ ./dir/sub } ) (with "the --no-total option set" @@ -96,4 +96,10 @@ WITH_FAILURE=1 ) ) ) + (with "interactive mode" + it "fails as there is no TTY connected" && { + WITH_SNAPSHOT="$snapshot/failure-interactive-without-tty" \ + expect_run ${WITH_FAILURE} "$exe" i + } + ) ) |