diff options
-rw-r--r-- | .cargo_vcs_info.json | 6 | ||||
-rw-r--r-- | Android.bp | 33 | ||||
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 72 | ||||
-rw-r--r-- | LICENSE | 20 | ||||
-rw-r--r-- | METADATA | 19 | ||||
-rw-r--r-- | MODULE_LICENSE_MIT | 0 | ||||
-rw-r--r-- | OWNERS | 2 | ||||
-rw-r--r-- | README.md | 51 | ||||
-rw-r--r-- | cargo_embargo.json | 4 | ||||
-rw-r--r-- | examples/tree.rs | 32 | ||||
-rw-r--r-- | src/lib.rs | 210 | ||||
-rw-r--r-- | src/tests.rs | 48 |
13 files changed, 504 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..d9469a7 --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "b333eaccb707d862feb45d5202de63ff26b77839" + }, + "path_in_vcs": "" +}
\ No newline at end of file diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..2df15a2 --- /dev/null +++ b/Android.bp @@ -0,0 +1,33 @@ +// This file is generated by cargo_embargo. +// Do not modify this file as changes will be overridden on upgrade. + +rust_library { + name: "libtermtree", + host_supported: true, + crate_name: "termtree", + cargo_env_compat: true, + cargo_pkg_version: "0.4.1", + srcs: ["src/lib.rs"], + edition: "2018", + apex_available: [ + "//apex_available:platform", + "//apex_available:anyapex", + ], + product_available: true, + vendor_available: true, +} + +rust_test { + name: "termtree_test_src_lib", + host_supported: true, + crate_name: "termtree", + cargo_env_compat: true, + cargo_pkg_version: "0.4.1", + srcs: ["src/lib.rs"], + test_suites: ["general-tests"], + auto_gen_config: true, + test_options: { + unit_test: true, + }, + edition: "2018", +} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..69736c1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "termtree" +version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f0e5f2b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,72 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "termtree" +version = "0.4.1" +include = [ + "src/**/*", + "Cargo.toml", + "LICENSE*", + "README.md", + "examples/**/*", +] +description = "Visualize tree-like data on the command-line" +homepage = "https://github.com/rust-cli/termtree" +documentation = "https://docs.rs/termtree" +readme = "README.md" +keywords = [ + "cli", + "tree", + "dag", +] +categories = [ + "command-line-interface", + "visualization", +] +license = "MIT" +repository = "https://github.com/rust-cli/termtree" + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "Unreleased" +replace = "{{version}}" +min = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = '\.\.\.HEAD' +replace = "...{{tag_name}}" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "ReleaseDate" +replace = "{{date}}" +min = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "<!-- next-header -->" +replace = """ +<!-- next-header --> +## [Unreleased] - ReleaseDate +""" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "<!-- next-url -->" +replace = """ +<!-- next-url --> +[Unreleased]: https://github.com/rust-cli/termtree/compare/{{tag_name}}...HEAD""" +exactly = 1 @@ -0,0 +1,20 @@ +Copyright (c) 2017 Doug Tangren + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..8bc6904 --- /dev/null +++ b/METADATA @@ -0,0 +1,19 @@ +name: "termtree" +description: "Visualize tree-like data on the command-line" +third_party { + identifier { + type: "crates.io" + value: "https://crates.io/crates/termtree" + } + identifier { + type: "Archive" + value: "https://static.crates.io/crates/termtree/termtree-0.4.1.crate" + } + version: "0.4.1" + license_type: NOTICE + last_upgrade_date { + year: 2023 + month: 11 + day: 6 + } +} diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_MIT @@ -0,0 +1,2 @@ +# Bug component: 688011 +include platform/prebuilts/rust:main:/OWNERS diff --git a/README.md b/README.md new file mode 100644 index 0000000..71d4594 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# termtree [![Main](https://github.com/rust-cli/termtree/actions/workflows/main.yml/badge.svg)](https://github.com/rust-cli/termtree/actions/workflows/main.yml) + +> Visualize tree-like data on the command-line + +[API documentation](https://docs.rs/termtree) + +## Example + +An example program is provided under the "examples" directory to mimic the `tree(1)` +linux program + +```bash +$ cargo run --example tree target + Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs + Running `target/debug/examples/tree target` +target +└── debug + ├── .cargo-lock + ├── .fingerprint + | └── termtree-21a5bdbd42e0b6da + | ├── dep-example-tree + | ├── dep-lib-termtree + | ├── example-tree + | ├── example-tree.json + | ├── lib-termtree + | └── lib-termtree.json + ├── build + ├── deps + | └── libtermtree.rlib + ├── examples + | ├── tree + | └── tree.dSYM + | └── Contents + | ├── Info.plist + | └── Resources + | └── DWARF + | └── tree + ├── libtermtree.rlib + └── native +``` + +## Related Crates + +- [`treeline`](https://crates.io/crates/treeline): termtree was forked from this. +- [`tree_decorator`](https://crates.io/crates/tree_decorator) +- [`xtree`](https://crates.io/crates/xtree) +- [`ptree`](https://crates.io/crates/ptree) + +## License + +Licensed under MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) diff --git a/cargo_embargo.json b/cargo_embargo.json new file mode 100644 index 0000000..d40889a --- /dev/null +++ b/cargo_embargo.json @@ -0,0 +1,4 @@ +{ + "run_cargo": false, + "tests": true +} diff --git a/examples/tree.rs b/examples/tree.rs new file mode 100644 index 0000000..0a9bca3 --- /dev/null +++ b/examples/tree.rs @@ -0,0 +1,32 @@ +use termtree::Tree; + +use std::path::Path; +use std::{env, fs, io}; + +fn label<P: AsRef<Path>>(p: P) -> String { + p.as_ref().file_name().unwrap().to_str().unwrap().to_owned() +} + +fn tree<P: AsRef<Path>>(p: P) -> io::Result<Tree<String>> { + let result = fs::read_dir(&p)?.filter_map(|e| e.ok()).fold( + Tree::new(label(p.as_ref().canonicalize()?)), + |mut root, entry| { + let dir = entry.metadata().unwrap(); + if dir.is_dir() { + root.push(tree(entry.path()).unwrap()); + } else { + root.push(Tree::new(label(entry.path()))); + } + root + }, + ); + Ok(result) +} + +fn main() { + let dir = env::args().nth(1).unwrap_or_else(|| String::from(".")); + match tree(dir) { + Ok(tree) => println!("{}", tree), + Err(err) => println!("error: {}", err), + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7893d6d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,210 @@ +#![allow(clippy::branches_sharing_code)] + +#[cfg(test)] +mod tests; + +use std::collections::VecDeque; +use std::fmt::{self, Display}; +use std::rc::Rc; + +/// a simple recursive type which is able to render its +/// components in a tree-like format +#[derive(Debug, Clone)] +pub struct Tree<D: Display> { + pub root: D, + pub leaves: Vec<Tree<D>>, + multiline: bool, + glyphs: GlyphPalette, +} + +impl<D: Display> Tree<D> { + pub fn new(root: D) -> Self { + Tree { + root, + leaves: Vec::new(), + multiline: false, + glyphs: GlyphPalette::new(), + } + } + + pub fn with_leaves(mut self, leaves: impl IntoIterator<Item = impl Into<Tree<D>>>) -> Self { + self.leaves = leaves.into_iter().map(Into::into).collect(); + self + } + + /// Ensure all lines for `root` are indented + pub fn with_multiline(mut self, yes: bool) -> Self { + self.multiline = yes; + self + } + + /// Customize the rendering of this node + pub fn with_glyphs(mut self, glyphs: GlyphPalette) -> Self { + self.glyphs = glyphs; + self + } +} + +impl<D: Display> Tree<D> { + /// Ensure all lines for `root` are indented + pub fn set_multiline(&mut self, yes: bool) -> &mut Self { + self.multiline = yes; + self + } + + /// Customize the rendering of this node + pub fn set_glyphs(&mut self, glyphs: GlyphPalette) -> &mut Self { + self.glyphs = glyphs; + self + } +} + +impl<D: Display> Tree<D> { + pub fn push(&mut self, leaf: impl Into<Tree<D>>) -> &mut Self { + self.leaves.push(leaf.into()); + self + } +} + +impl<D: Display> From<D> for Tree<D> { + fn from(inner: D) -> Self { + Self::new(inner) + } +} + +impl<D: Display> Extend<D> for Tree<D> { + fn extend<T: IntoIterator<Item = D>>(&mut self, iter: T) { + self.leaves.extend(iter.into_iter().map(Into::into)) + } +} + +impl<D: Display> Extend<Tree<D>> for Tree<D> { + fn extend<T: IntoIterator<Item = Tree<D>>>(&mut self, iter: T) { + self.leaves.extend(iter) + } +} + +impl<D: Display> Display for Tree<D> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.root.fmt(f)?; // Pass along `f.alternate()` + writeln!(f)?; + let mut queue = DisplauQueue::new(); + let no_space = Rc::new(Vec::new()); + enqueue_leaves(&mut queue, self, no_space); + while let Some((last, leaf, spaces)) = queue.pop_front() { + let mut prefix = ( + if last { + leaf.glyphs.last_item + } else { + leaf.glyphs.middle_item + }, + leaf.glyphs.item_indent, + ); + + if leaf.multiline { + let rest_prefix = ( + if last { + leaf.glyphs.last_skip + } else { + leaf.glyphs.middle_skip + }, + leaf.glyphs.skip_indent, + ); + debug_assert_eq!(prefix.0.chars().count(), rest_prefix.0.chars().count()); + debug_assert_eq!(prefix.1.chars().count(), rest_prefix.1.chars().count()); + + let root = if f.alternate() { + format!("{:#}", leaf.root) + } else { + format!("{:}", leaf.root) + }; + for line in root.lines() { + // print single line + for s in spaces.as_slice() { + if *s { + self.glyphs.last_skip.fmt(f)?; + self.glyphs.skip_indent.fmt(f)?; + } else { + self.glyphs.middle_skip.fmt(f)?; + self.glyphs.skip_indent.fmt(f)?; + } + } + prefix.0.fmt(f)?; + prefix.1.fmt(f)?; + line.fmt(f)?; + writeln!(f)?; + prefix = rest_prefix; + } + } else { + // print single line + for s in spaces.as_slice() { + if *s { + self.glyphs.last_skip.fmt(f)?; + self.glyphs.skip_indent.fmt(f)?; + } else { + self.glyphs.middle_skip.fmt(f)?; + self.glyphs.skip_indent.fmt(f)?; + } + } + prefix.0.fmt(f)?; + prefix.1.fmt(f)?; + leaf.root.fmt(f)?; // Pass along `f.alternate()` + writeln!(f)?; + } + + // recurse + if !leaf.leaves.is_empty() { + let s: &Vec<bool> = &spaces; + let mut child_spaces = s.clone(); + child_spaces.push(last); + let child_spaces = Rc::new(child_spaces); + enqueue_leaves(&mut queue, leaf, child_spaces); + } + } + Ok(()) + } +} + +type DisplauQueue<'t, D> = VecDeque<(bool, &'t Tree<D>, Rc<Vec<bool>>)>; + +fn enqueue_leaves<'t, D: Display>( + queue: &mut DisplauQueue<'t, D>, + parent: &'t Tree<D>, + spaces: Rc<Vec<bool>>, +) { + for (i, leaf) in parent.leaves.iter().rev().enumerate() { + let last = i == 0; + queue.push_front((last, leaf, spaces.clone())); + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct GlyphPalette { + pub middle_item: &'static str, + pub last_item: &'static str, + pub item_indent: &'static str, + + pub middle_skip: &'static str, + pub last_skip: &'static str, + pub skip_indent: &'static str, +} + +impl GlyphPalette { + pub const fn new() -> Self { + Self { + middle_item: "├", + last_item: "└", + item_indent: "── ", + + middle_skip: "│", + last_skip: " ", + skip_indent: " ", + } + } +} + +impl Default for GlyphPalette { + fn default() -> Self { + Self::new() + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..4d60cf1 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,48 @@ +use super::*; + +#[test] +fn render_tree_root() { + let tree = Tree::new("foo"); + assert_eq!(format!("{}", tree), "foo\n") +} + +#[test] +fn render_tree_with_leaves() { + let tree = Tree::new("foo").with_leaves([Tree::new("bar").with_leaves(["baz"])]); + assert_eq!( + format!("{}", tree), + r#"foo +└── bar + └── baz +"# + ) +} + +#[test] +fn render_tree_with_multiple_leaves() { + let tree = Tree::new("foo").with_leaves(["bar", "baz"]); + assert_eq!( + format!("{}", tree), + r#"foo +├── bar +└── baz +"# + ) +} + +#[test] +fn render_tree_with_multiline_leaf() { + let tree = Tree::new("foo").with_leaves([ + Tree::new("hello\nworld").with_multiline(true), + Tree::new("goodbye\nworld").with_multiline(true), + ]); + assert_eq!( + format!("{}", tree), + r#"foo +├── hello +│ world +└── goodbye + world +"# + ) +} |