1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
use std::marker::PhantomData;

use super::{BaseCommand, Command};
use crate::command_set::CommandSet;
use crate::error::ShiError;
use crate::shell::Shell;
use crate::Result;

#[derive(Debug)]
/// HelpTreeCommand prints out a prettified and more complete version of the HelpCommand. It prints
/// out a tree visualization of all the commands in its shell. In particular, it shows the
/// hierarchies of commands. The output of this command is inspired by the `tree` program.
///
/// An example of what this command may produce is:
/// ```plaintext
/// Normal commands
/// ├── dog
/// └── felid
///     ├── panther
///     ├── felinae
///     │    ├── dangerous-tiger
///     │    └── domestic-cat
///     └── felinae2
///          ├── dangerous-tiger
///          └── domestic-cat
///
///
/// Builtins
/// ├── helptree
/// ├── exit
/// ├── help
/// └── history
/// ```
pub struct HelpTreeCommand<'a, S> {
    // TODO: Not sure if we need this crap.
    phantom: &'a PhantomData<S>,
}

#[derive(Clone)]
/// A helper struct that records the context needed to determine how to correctly indent a line for
/// the helptree visualization. It includes two pieces of relevant information:
///
/// * Am I the last command of my level?
/// * Of all my ancestors, were _they_ the last command of _their_ level?
///
/// These two pieces of information allow us to correctly determine spacing and connectors needed
/// to produce the tree.
///
/// IndentContexts are produced by either _indenting_ them to a new level of recursion in the tree,
/// OR, by traversing to the next element in the same level. It's methods, `indent` and `with_last`
/// correspond to these two cases respectively. In other words, a tree can either get deeper or
/// wider, respectively.
struct IndentContext {
    last: bool,
    // This is a mouthful, but the idea is that if(parent_lastness_chain[i]) implies that parent_i was
    // the last item in the level it belonged too. This is necessary to know when we need to figure
    // out if we should continue a verticle pipe.
    parent_lastness_chain: Vec<bool>,
}

impl IndentContext {
    /// Produces a new IndentContext for the next indentation level (or, perhaps more accurately,
    /// next level of the tree, or, next recursion).
    fn indent(&self, last: bool) -> Self {
        // We don't want future IndentContexts to hold references to prior IndentContexts' parent
        // chains, since they should be different.
        // There may be a way to avoid the copy and hold onto slices of a larger chain, but I do
        // not think the addition in complexity is worth the negligible performance gain (if any).
        let mut parent_chain_copy = self.parent_lastness_chain.to_vec();
        parent_chain_copy.push(last);
        IndentContext {
            last,
            parent_lastness_chain: parent_chain_copy,
        }
    }

    /// Produces a new IndentContext, but does not indent it and therefore maintains the current
    /// level of the tree. Thus, it keeps the `parent_lastness_chain` the same. However, since a
    /// new IndentContext for a given level could be the _last_ element of that level, it takes an
    /// argument for denoting that.
    fn with_last(&self, new_last: bool) -> Self {
        IndentContext {
            last: new_last,
            parent_lastness_chain: self.parent_lastness_chain.to_vec(),
        }
    }
}

impl<'a, S> Default for HelpTreeCommand<'a, S> {
    fn default() -> Self {
        Self::new()
    }
}

impl<'a, S> HelpTreeCommand<'a, S> {
    /// Creates a new HelpTreeCommand.
    pub fn new() -> HelpTreeCommand<'a, S> {
        HelpTreeCommand {
            phantom: &PhantomData,
        }
    }

    // TODO: This works, but it isn't designed in the best way possible. What we should be doing is
    // taking the commands and iterating them and their children into a tree. Then, we should pass
    // the tree of strings (or, whatever type holding the information we want to print) to a
    // function like this, responsible for rendering the tree.
    // Right now, for example, there isn't any way to test this code without creating a shell,
    // which is a code smell.
    /// Adds the given name under the given indentation context to the given vector of strings,
    /// maintaining the appearance of a tree.
    ///
    /// # Arguments
    ///
    /// * `ctx` - The context of where in the tree we are adding lines to.
    /// * `lines` - The lines of the helptree visualization. It is added to, and includes the
    /// entire tree by the end of this function.
    /// * `name` - The name of a command to add.
    fn add_name_to_lines(&self, ctx: &IndentContext, lines: &mut Vec<String>, name: &str) {
        // This is not to be confused with `lines`. Think of this as the columns; merging the
        // elements in this vector gives you a line, to be added to `lines`.
        let mut line_elems: Vec<&str> = Vec::new();

        // For each of the parents in our chain, if they were last, then we only want a space
        // because then their pipe is an elbow connector.
        //   └─ Foo
        //   │  └─ SubFoo <--- WRONG!
        // Instead we want:
        //   └─ Foo
        //      └─ SubFoo <--- RIGHT!
        // However, if they were _NOT_ last, then we want a vertical pipe, since their connector is
        // a 3-way connector. So we'd want that continuation.
        //   ├─ Foo
        //   │  └─ SubFoo <--- RIGHT!
        for parent_was_last in &ctx.parent_lastness_chain {
            if *parent_was_last {
                // If the parent was the last in the chain, we don't need to continue its vertical
                // pipe, because it will have a clean elbow cut-off.
                line_elems.push("    ");
            } else {
                line_elems.push("│   ");
            }
        }

        // If we're the last guy, we want a clean elbow cut-off, otherwise, we want a fork.
        // NOTE: This is for the _current_ command. So this is not to be confused with what we are
        // doing above with the `parent_lastness_chain`.
        if ctx.last {
            line_elems.push("└");
        } else {
            line_elems.push("├");
        }

        // Write two horizontal pipes to lead to our name, with a space for separation...
        let dash_name = format!("── {}", name);
        line_elems.push(&dash_name);

        lines.push(line_elems.join(""))
    }

    /// Adds the lines of the helptree visualization.
    ///
    /// # Arguments
    ///
    /// * `ctx` - The context of where in the tree we are adding lines to.
    /// * `lines` - The lines of the helptree visualization. It is added to, and includes the
    /// entire tree by the end of this function.
    /// * `cmds` - The set of Commands for which to create and add lines of the helptree
    /// visualization.
    fn add_tree_lines_for_children<T>(
        &self,
        ctx: &IndentContext,
        lines: &mut Vec<String>,
        cmds: &CommandSet<T>,
    ) {
        for (i, cmd) in cmds.iter().enumerate() {
            let last = i == cmds.len() - 1;

            // Because we may recurse, we'll be going into a deeper level whose lines should come
            // _after_, so add the current command's line to the vector now.
            self.add_name_to_lines(&ctx.with_last(last), lines, cmd.name());

            match &**cmd {
                Command::Leaf(_) => continue, // We can't recurse in this case.
                Command::Parent(parent_cmd) => {
                    // We need to recurse another level for our children.
                    self.add_tree_lines_for_children(
                        &ctx.indent(last),
                        lines,
                        parent_cmd.sub_commands(),
                    );
                }
            }
        }
    }

    /// Produces the helptree representation of the given Shell's commands via a `Vec<String>`.
    ///
    /// # Arguments
    ///
    /// * `shell` - The shell for which to produce the helptree.
    fn to_lines(&self, shell: &Shell<'a, S>) -> Vec<String> {
        // We tackle the initial two subtrees separately, since they have slightly differing types.
        //  1: The normal commands (state = S).
        //  2: The builtins (state = Shell<S>).
        //
        //  Since they are different types, we need to invoke `add_tree_lines_for_children()`
        //  separately for each, and combine the resulting help lines.

        // Start with an initial context with the lastness chain being empty.
        // Of course, `last` should also be false, which we ensure with `.with_last(false)` in the
        // invocations to `add_tree_lines_for_children()` below.
        let ctx = IndentContext {
            last: false,
            parent_lastness_chain: Vec::new(),
        };

        let mut lines: Vec<String> = vec![String::from("Normal commands")];
        self.add_tree_lines_for_children(&ctx.with_last(false), &mut lines, &shell.cmds.borrow());

        lines.push(String::from("\n"));

        lines.push(String::from("Builtins"));
        self.add_tree_lines_for_children(&ctx.with_last(false), &mut lines, &shell.builtins);

        lines
    }
}

impl<'a, S> BaseCommand for HelpTreeCommand<'a, S> {
    type State = Shell<'a, S>;

    fn name(&self) -> &str {
        "helptree"
    }

    fn validate_args(&self, args: &[String]) -> Result<()> {
        if !args.is_empty() {
            // TODO: We may want to make this actually take arguments, like a command name or
            // command name path.
            return Err(ShiError::ExtraArgs { got: args.to_vec() });
        }

        Ok(())
    }

    fn execute(&self, shell: &mut Shell<'a, S>, _: &[String]) -> Result<String> {
        let help_lines = self.to_lines(shell);

        Ok(help_lines.join("\n"))
    }

    fn help(&self) -> String {
        String::from("Prints a tree depiction of all commands in this shell")
    }
}