baobao_codegen/schema/
display.rs

1//! Command tree display formatting.
2//!
3//! This module provides declarative formatting for command trees,
4//! supporting multiple display styles.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use baobao_codegen::schema::{CommandTree, CommandTreeDisplay, DisplayStyle};
10//!
11//! let tree = CommandTree::new(&schema);
12//! let display = CommandTreeDisplay::new(&tree)
13//!     .style(DisplayStyle::WithDescriptions)
14//!     .indent("  ");
15//!
16//! println!("{}", display);
17//! ```
18
19use std::fmt;
20
21use super::CommandTree;
22
23/// Display style for command trees.
24#[derive(Debug, Clone, Copy, Default)]
25pub enum DisplayStyle {
26    /// Simple indented names only.
27    ///
28    /// ```text
29    /// hello
30    /// users
31    ///   create
32    ///   delete
33    /// ```
34    #[default]
35    Simple,
36
37    /// Names with descriptions.
38    ///
39    /// ```text
40    /// hello - Say hello
41    /// users - User management
42    ///   create - Create a user
43    ///   delete - Delete a user
44    /// ```
45    WithDescriptions,
46
47    /// Names with full command signature (args and flags).
48    ///
49    /// ```text
50    /// hello [name] [--loud]
51    /// users
52    ///   create <username> [--admin]
53    ///   delete <id> [--force]
54    /// ```
55    WithSignature,
56
57    /// Tree structure with box-drawing characters and metadata.
58    ///
59    /// ```text
60    /// ├─ hello (1 arg)
61    /// └─ users
62    ///    ├─ create (1 arg, 1 flag)
63    ///    └─ delete (1 arg)
64    /// ```
65    TreeBox,
66}
67
68/// Declarative command tree display formatter.
69#[derive(Debug, Clone)]
70pub struct CommandTreeDisplay<'a> {
71    tree: &'a CommandTree<'a>,
72    style: DisplayStyle,
73    indent_str: &'a str,
74}
75
76impl<'a> CommandTreeDisplay<'a> {
77    /// Create a new display formatter for a command tree.
78    pub fn new(tree: &'a CommandTree<'a>) -> Self {
79        Self {
80            tree,
81            style: DisplayStyle::default(),
82            indent_str: "  ",
83        }
84    }
85
86    /// Set the display style.
87    pub fn style(mut self, style: DisplayStyle) -> Self {
88        self.style = style;
89        self
90    }
91
92    /// Set the indentation string (default: two spaces).
93    pub fn indent(mut self, indent: &'a str) -> Self {
94        self.indent_str = indent;
95        self
96    }
97
98    /// Render the command tree to a string.
99    pub fn render(&self) -> String {
100        let mut output = String::new();
101        self.render_to(&mut output);
102        output
103    }
104
105    /// Render the command tree to a formatter.
106    fn render_to(&self, output: &mut String) {
107        // Get top-level commands sorted
108        let top_level: Vec<_> = self.tree.iter().filter(|cmd| cmd.depth == 0).collect();
109
110        let mut sorted: Vec<_> = top_level.iter().collect();
111        sorted.sort_by_key(|cmd| cmd.name);
112
113        match self.style {
114            DisplayStyle::Simple => self.render_simple(output, &sorted),
115            DisplayStyle::WithDescriptions => self.render_with_descriptions(output, &sorted),
116            DisplayStyle::WithSignature => self.render_with_signature(output, &sorted),
117            DisplayStyle::TreeBox => self.render_tree_box(output, &sorted),
118        }
119    }
120
121    fn render_simple(&self, output: &mut String, commands: &[&&super::FlatCommand<'a>]) {
122        for cmd in commands {
123            self.render_simple_recursive(output, cmd.name, cmd.command, 0);
124        }
125    }
126
127    fn render_simple_recursive(
128        &self,
129        output: &mut String,
130        name: &str,
131        cmd: &baobao_manifest::Command,
132        depth: usize,
133    ) {
134        // Base indent + depth-based indent
135        let indent = format!("{}{}", self.indent_str, self.indent_str.repeat(depth));
136        output.push_str(&indent);
137        output.push_str(name);
138        output.push('\n');
139
140        if cmd.has_subcommands() {
141            let mut sorted: Vec<_> = cmd.commands.iter().collect();
142            sorted.sort_by_key(|(n, _)| *n);
143            for (sub_name, sub_cmd) in sorted {
144                self.render_simple_recursive(output, sub_name, sub_cmd, depth + 1);
145            }
146        }
147    }
148
149    fn render_with_descriptions(&self, output: &mut String, commands: &[&&super::FlatCommand<'a>]) {
150        for cmd in commands {
151            self.render_descriptions_recursive(output, cmd.name, cmd.command, 0);
152        }
153    }
154
155    fn render_descriptions_recursive(
156        &self,
157        output: &mut String,
158        name: &str,
159        cmd: &baobao_manifest::Command,
160        depth: usize,
161    ) {
162        // Base indent + depth-based indent
163        let indent = format!("{}{}", self.indent_str, self.indent_str.repeat(depth));
164        output.push_str(&indent);
165        output.push_str(name);
166        output.push_str(" - ");
167        output.push_str(&cmd.description);
168        output.push('\n');
169
170        if cmd.has_subcommands() {
171            let mut sorted: Vec<_> = cmd.commands.iter().collect();
172            sorted.sort_by_key(|(n, _)| *n);
173            for (sub_name, sub_cmd) in sorted {
174                self.render_descriptions_recursive(output, sub_name, sub_cmd, depth + 1);
175            }
176        }
177    }
178
179    fn render_with_signature(&self, output: &mut String, commands: &[&&super::FlatCommand<'a>]) {
180        for cmd in commands {
181            self.render_signature_recursive(output, cmd.name, cmd.command, 0);
182        }
183    }
184
185    fn render_signature_recursive(
186        &self,
187        output: &mut String,
188        name: &str,
189        cmd: &baobao_manifest::Command,
190        depth: usize,
191    ) {
192        // Base indent + depth-based indent
193        let indent = format!("{}{}", self.indent_str, self.indent_str.repeat(depth));
194        output.push_str(&indent);
195
196        if cmd.has_subcommands() {
197            // Parent command: just name
198            output.push_str(name);
199            output.push('\n');
200
201            let mut sorted: Vec<_> = cmd.commands.iter().collect();
202            sorted.sort_by_key(|(n, _)| *n);
203            for (sub_name, sub_cmd) in sorted {
204                self.render_signature_recursive(output, sub_name, sub_cmd, depth + 1);
205            }
206        } else {
207            // Leaf command: name with signature
208            output.push_str(&Self::format_signature(name, cmd));
209            output.push('\n');
210        }
211    }
212
213    fn format_signature(name: &str, cmd: &baobao_manifest::Command) -> String {
214        let mut parts = vec![name.to_string()];
215
216        // Add args (sorted)
217        let mut sorted_args: Vec<_> = cmd.args.iter().collect();
218        sorted_args.sort_by_key(|(n, _)| *n);
219        for (arg_name, arg) in sorted_args {
220            if arg.required {
221                parts.push(format!("<{}>", arg_name));
222            } else {
223                parts.push(format!("[{}]", arg_name));
224            }
225        }
226
227        // Add flags (sorted)
228        let mut sorted_flags: Vec<_> = cmd.flags.iter().collect();
229        sorted_flags.sort_by_key(|(n, _)| *n);
230        let flags: Vec<String> = sorted_flags
231            .iter()
232            .map(|(flag_name, flag)| {
233                if let Some(short) = flag.short_char() {
234                    format!("-{}/--{}", short, flag_name)
235                } else {
236                    format!("--{}", flag_name)
237                }
238            })
239            .collect();
240
241        if !flags.is_empty() {
242            parts.push(format!("[{}]", flags.join(" ")));
243        }
244
245        parts.join(" ")
246    }
247
248    fn render_tree_box(&self, output: &mut String, commands: &[&&super::FlatCommand<'a>]) {
249        let total = commands.len();
250        for (i, cmd) in commands.iter().enumerate() {
251            let is_last = i == total - 1;
252            // Start with base indent
253            self.render_tree_box_recursive(output, cmd.name, cmd.command, self.indent_str, is_last);
254        }
255    }
256
257    fn render_tree_box_recursive(
258        &self,
259        output: &mut String,
260        name: &str,
261        cmd: &baobao_manifest::Command,
262        prefix: &str,
263        is_last: bool,
264    ) {
265        let connector = if is_last { "└─" } else { "├─" };
266        let child_prefix = if is_last { "   " } else { "│  " };
267
268        output.push_str(prefix);
269        output.push_str(connector);
270        output.push(' ');
271        output.push_str(name);
272
273        // Add metadata
274        let meta = Self::format_metadata(cmd);
275        if !meta.is_empty() {
276            output.push_str(" (");
277            output.push_str(&meta);
278            output.push(')');
279        }
280        output.push('\n');
281
282        // Recurse into subcommands
283        if cmd.has_subcommands() {
284            let mut sorted: Vec<_> = cmd.commands.iter().collect();
285            sorted.sort_by_key(|(n, _)| *n);
286            let total = sorted.len();
287
288            for (i, (sub_name, sub_cmd)) in sorted.iter().enumerate() {
289                let sub_is_last = i == total - 1;
290                let new_prefix = format!("{}{}", prefix, child_prefix);
291                self.render_tree_box_recursive(output, sub_name, sub_cmd, &new_prefix, sub_is_last);
292            }
293        }
294    }
295
296    fn format_metadata(cmd: &baobao_manifest::Command) -> String {
297        let mut meta = Vec::new();
298
299        if !cmd.args.is_empty() {
300            let count = cmd.args.len();
301            meta.push(format!(
302                "{} arg{}",
303                count,
304                if count == 1 { "" } else { "s" }
305            ));
306        }
307
308        if !cmd.flags.is_empty() {
309            let count = cmd.flags.len();
310            meta.push(format!(
311                "{} flag{}",
312                count,
313                if count == 1 { "" } else { "s" }
314            ));
315        }
316
317        meta.join(", ")
318    }
319}
320
321impl fmt::Display for CommandTreeDisplay<'_> {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        let output = self.render();
324        // Remove trailing newline for Display
325        write!(f, "{}", output.trim_end())
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    // These tests would require creating a mock Manifest, which is complex.
334    // For now, we document the expected behavior.
335
336    #[test]
337    fn test_display_style_default() {
338        assert!(matches!(DisplayStyle::default(), DisplayStyle::Simple));
339    }
340}