summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml1
-rw-r--r--src/aggregate.rs30
-rw-r--r--src/common.rs12
-rw-r--r--src/interactive.rs37
-rw-r--r--src/lib.rs1
-rw-r--r--src/main.rs65
-rw-r--r--src/options.rs11
-rw-r--r--tests/snapshots/failure-interactive-without-tty3
-rwxr-xr-xtests/stateless-journey.sh8
9 files changed, 117 insertions, 51 deletions
diff --git a/Cargo.toml b/Cargo.toml
index d9605db..ff8726c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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::*;
diff --git a/src/lib.rs b/src/lib.rs
index 493fb5a..947629b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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
+ }
+ )
)