Skip to main content

cli_engine/
tree.rs

1use clap::Command;
2use serde::{Deserialize, Serialize};
3
4/// Command tree node used by the built-in `tree` command.
5#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
6pub struct TreeNode {
7    /// Command or group name.
8    pub name: String,
9    /// One-line command or group description.
10    #[serde(skip_serializing_if = "String::is_empty")]
11    pub description: String,
12    /// Space-separated display path, including the root command.
13    pub path: String,
14    /// Visible child commands and groups.
15    #[serde(default, skip_serializing_if = "Vec::is_empty")]
16    pub children: Vec<TreeNode>,
17}
18
19impl TreeNode {
20    /// Creates a tree node with no children.
21    #[must_use]
22    pub fn new(
23        name: impl Into<String>,
24        description: impl Into<String>,
25        path: impl Into<String>,
26    ) -> Self {
27        Self {
28            name: name.into(),
29            description: description.into(),
30            path: path.into(),
31            children: Vec::new(),
32        }
33    }
34
35    /// Adds one child node.
36    #[must_use]
37    pub fn with_child(mut self, child: TreeNode) -> Self {
38        self.children.push(child);
39        self
40    }
41
42    /// Adds several child nodes.
43    #[must_use]
44    pub fn with_children(mut self, children: impl IntoIterator<Item = TreeNode>) -> Self {
45        self.children.extend(children);
46        self
47    }
48}
49
50/// Builds a tree node from explicit parts.
51#[must_use]
52pub fn build_tree_from_parts(
53    name: impl Into<String>,
54    description: impl Into<String>,
55    path: impl Into<String>,
56    children: Vec<TreeNode>,
57) -> TreeNode {
58    TreeNode::new(name, description, path).with_children(children)
59}
60
61/// Builds a tree from a `clap` command hierarchy.
62#[must_use]
63pub fn build_tree_from_clap(command: &Command) -> TreeNode {
64    build_tree_from_clap_with_path(command, command.get_name().to_owned())
65}
66
67fn build_tree_from_clap_with_path(command: &Command, path: String) -> TreeNode {
68    let children = command
69        .get_subcommands()
70        .filter(|child| !child.is_hide_set() && child.get_name() != "completion")
71        .map(|child| {
72            let child_path = format!("{path} {}", child.get_name());
73            build_tree_from_clap_with_path(child, child_path)
74        })
75        .collect::<Vec<_>>();
76
77    TreeNode {
78        name: command.get_name().to_owned(),
79        description: command
80            .get_about()
81            .map(ToString::to_string)
82            .unwrap_or_default(),
83        path,
84        children,
85    }
86}
87
88/// Renders a command tree for human output.
89#[must_use]
90pub fn render_tree_human(node: &TreeNode) -> String {
91    let mut out = String::new();
92    render_node(node, "", true, true, &mut out);
93    out
94}
95
96fn render_node(node: &TreeNode, prefix: &str, is_last: bool, is_root: bool, out: &mut String) {
97    if is_root {
98        out.push_str(&node.name);
99        out.push('\n');
100    } else {
101        let connector = if is_last { "└── " } else { "├── " };
102        out.push_str(prefix);
103        out.push_str(connector);
104        out.push_str(&node.name);
105        if !node.description.is_empty() {
106            out.push_str(" ··· ");
107            out.push_str(&node.description);
108        }
109        out.push('\n');
110    }
111
112    let child_prefix = if is_root {
113        String::new()
114    } else if is_last {
115        format!("{prefix}    ")
116    } else {
117        format!("{prefix}│   ")
118    };
119    let child_len = node.children.len();
120    for (index, child) in node.children.iter().enumerate() {
121        render_node(child, &child_prefix, index + 1 == child_len, false, out);
122    }
123}