exa/src/output/tree.rs

280 lines
9.3 KiB
Rust
Raw Normal View History

//! Tree structures, such as `├──` or `└──`, used in a tree view.
//!
//! ## Constructing Tree Views
//!
//! When using the `--tree` argument, instead of a vector of cells, each row
//! has a `depth` field that indicates how far deep in the tree it is: the top
//! level has depth 0, its children have depth 1, and *their* children have
//! depth 2, and so on.
//!
//! On top of this, it also has a `last` field that specifies whether this is
//! the last row of this particular consecutive set of rows. This doesnt
//! affect the files information; its just used to display a different set of
//! Unicode tree characters! The resulting table looks like this:
//!
//! ```text
//! ┌───────┬───────┬───────────────────────┐
//! │ Depth │ Last │ Output │
//! ├───────┼───────┼───────────────────────┤
//! │ 0 │ │ documents │
//! │ 1 │ false │ ├── this_file.txt │
//! │ 1 │ false │ ├── that_file.txt │
//! │ 1 │ false │ ├── features │
//! │ 2 │ false │ │ ├── feature_1.rs │
//! │ 2 │ false │ │ ├── feature_2.rs │
//! │ 2 │ true │ │ └── feature_3.rs │
//! │ 1 │ true │ └── pictures │
//! │ 2 │ false │ ├── garden.jpg │
//! │ 2 │ false │ ├── flowers.jpg │
//! │ 2 │ false │ ├── library.png │
//! │ 2 │ true │ └── space.tiff │
//! └───────┴───────┴───────────────────────┘
//! ```
//!
//! Creating the table like this means that each file has to be tested to see
//! if its the last one in the group. This is usually done by putting all the
//! files in a vector beforehand, getting its length, then comparing the index
//! of each file to see if its the last one. (As some files may not be
//! successfully `stat`ted, we dont know how many files are going to exist in
//! each directory)
#[derive(PartialEq, Debug, Copy, Clone)]
2015-12-22 02:14:32 +00:00
pub enum TreePart {
/// Rightmost column, *not* the last in the directory.
Edge,
/// Not the rightmost column, and the directory has not finished yet.
Line,
/// Rightmost column, and the last in the directory.
Corner,
/// Not the rightmost column, and the directory *has* finished.
Blank,
}
impl TreePart {
/// Turn this tree part into ASCII-licious box drawing characters!
/// (Warning: not actually ASCII)
pub fn ascii_art(self) -> &'static str {
match self {
Self::Edge => "├──",
Self::Line => "",
Self::Corner => "└──",
Self::Blank => " ",
2015-12-22 02:14:32 +00:00
}
}
}
/// A **tree trunk** builds up arrays of tree parts over multiple depths.
#[derive(Debug, Default)]
pub struct TreeTrunk {
/// A stack tracks which tree characters should be printed. Its
/// necessary to maintain information about the previously-printed
/// lines, as the output will change based on any previous entries.
stack: Vec<TreePart>,
/// A tuple for the last depth and last parameters that are passed in.
last_params: Option<TreeParams>,
}
#[derive(Debug, Copy, Clone)]
pub struct TreeParams {
/// How many directories deep into the tree structure this is. Directories
/// on top have depth 0.
depth: TreeDepth,
/// Whether this is the last entry in the directory.
last: bool,
}
#[derive(Debug, Copy, Clone)]
pub struct TreeDepth(pub usize);
impl TreeTrunk {
/// Calculates the tree parts for an entry at the given depth and
/// last-ness. The depth is used to determine where in the stack the tree
/// part should be inserted, and the last-ness is used to determine which
/// type of tree part to insert.
///
/// This takes a `&mut self` because the results of each file are stored
/// and used in future rows.
pub fn new_row(&mut self, params: TreeParams) -> &[TreePart] {
// If this isnt our first iteration, then update the tree parts thus
// far to account for there being another row after it.
if let Some(last) = self.last_params {
self.stack[last.depth.0] = if last.last { TreePart::Blank }
else { TreePart::Line };
}
// Make sure the stack has enough space, then add or modify another
// part into it.
self.stack.resize(params.depth.0 + 1, TreePart::Edge);
self.stack[params.depth.0] = if params.last { TreePart::Corner }
else { TreePart::Edge };
self.last_params = Some(params);
// Return the tree parts as a slice of the stack.
//
// Ignore the first element here to prevent a zeroth level from
// appearing before the very first directory. This level would
// join unrelated directories without connecting to anything:
//
// with [0..] with [1..]
// ========== ==========
// ├── folder folder
// │ └── file └── file
// └── folder folder
// └── file └──file
//
&self.stack[1..]
}
}
impl TreeParams {
pub fn new(depth: TreeDepth, last: bool) -> Self {
Self { depth, last }
}
2017-07-03 22:25:56 +00:00
pub fn is_at_root(&self) -> bool {
self.depth.0 == 0
}
}
impl TreeDepth {
pub fn root() -> Self {
Self(0)
2017-07-03 22:25:56 +00:00
}
pub fn deeper(self) -> Self {
Self(self.0 + 1)
}
/// Creates an iterator that, as well as yielding each value, yields a
/// `TreeParams` with the current depth and last flag filled in.
pub fn iterate_over<I, T>(self, inner: I) -> Iter<I>
where I: ExactSizeIterator + Iterator<Item = T>
{
Iter { current_depth: self, inner }
}
}
pub struct Iter<I> {
current_depth: TreeDepth,
inner: I,
}
impl<I, T> Iterator for Iter<I>
where I: ExactSizeIterator + Iterator<Item = T>
{
type Item = (TreeParams, T);
fn next(&mut self) -> Option<Self::Item> {
let t = self.inner.next()?;
// TODO: use exact_size_is_empty API soon
let params = TreeParams::new(self.current_depth, self.inner.len() == 0);
Some((params, t))
}
}
#[cfg(test)]
mod trunk_test {
use super::*;
fn params(depth: usize, last: bool) -> TreeParams {
TreeParams::new(TreeDepth(depth), last)
}
#[test]
fn empty_at_first() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, true)), &[ ]);
}
#[test]
fn one_child() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, true)), &[ ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
#[test]
fn two_children() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, true)), &[ ]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
#[test]
fn two_times_two_children() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, false)), &[ ]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(params(0, true)), &[ ]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
}
#[test]
fn two_times_two_nested_children() {
let mut tt = TreeTrunk::default();
assert_eq!(tt.new_row(params(0, true)), &[ ]);
assert_eq!(tt.new_row(params(1, false)), &[ TreePart::Edge ]);
assert_eq!(tt.new_row(params(2, false)), &[ TreePart::Line, TreePart::Edge ]);
assert_eq!(tt.new_row(params(2, true)), &[ TreePart::Line, TreePart::Corner ]);
assert_eq!(tt.new_row(params(1, true)), &[ TreePart::Corner ]);
assert_eq!(tt.new_row(params(2, false)), &[ TreePart::Blank, TreePart::Edge ]);
assert_eq!(tt.new_row(params(2, true)), &[ TreePart::Blank, TreePart::Corner ]);
}
}
#[cfg(test)]
mod iter_test {
use super::*;
#[test]
fn test_iteration() {
let foos = &[ "first", "middle", "last" ];
let mut iter = TreeDepth::root().iterate_over(foos.into_iter());
let next = iter.next().unwrap();
assert_eq!(&"first", next.1);
assert_eq!(false, next.0.last);
let next = iter.next().unwrap();
assert_eq!(&"middle", next.1);
assert_eq!(false, next.0.last);
let next = iter.next().unwrap();
assert_eq!(&"last", next.1);
assert_eq!(true, next.0.last);
assert!(iter.next().is_none());
}
#[test]
fn test_empty() {
let nothing: &[usize] = &[];
let mut iter = TreeDepth::root().iterate_over(nothing.into_iter());
assert!(iter.next().is_none());
}
}