summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.cargo_vcs_info.json6
-rw-r--r--Android.bp33
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml72
-rw-r--r--LICENSE20
-rw-r--r--METADATA19
-rw-r--r--MODULE_LICENSE_MIT0
-rw-r--r--OWNERS2
-rw-r--r--README.md51
-rw-r--r--cargo_embargo.json4
-rw-r--r--examples/tree.rs32
-rw-r--r--src/lib.rs210
-rw-r--r--src/tests.rs48
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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d5fcce5
--- /dev/null
+++ b/LICENSE
@@ -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
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..48bea6e
--- /dev/null
+++ b/OWNERS
@@ -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
+"#
+ )
+}