1use crate::cli::args::{GlobalFlags, LsArgs};
30use anyhow::Result;
31use grex_core::{build_ls_tree, LsNode, LsTree};
32use tokio_util::sync::CancellationToken;
33
34pub fn run(args: LsArgs, global: &GlobalFlags, _cancel: &CancellationToken) -> Result<()> {
35 let pack_root = match args.pack_root.clone() {
36 Some(p) => p,
37 None => std::env::current_dir()?,
38 };
39
40 match build_ls_tree(&pack_root) {
41 Ok(tree) => {
42 if global.json {
43 emit_json(&tree);
44 } else {
45 render_tree(&tree);
46 }
47 Ok(())
48 }
49 Err(detail) => {
50 if global.json {
51 emit_json_error("tree", &detail);
52 } else {
53 eprintln!("grex ls: {detail}");
54 }
55 std::process::exit(2);
56 }
57 }
58}
59
60fn render_tree(tree: &LsTree) {
63 for root in &tree.tree {
64 println!("{}", node_label(root));
65 let count = root.children.len();
66 for (idx, child) in root.children.iter().enumerate() {
67 render_subtree(child, "", idx + 1 == count);
68 }
69 }
70}
71
72fn render_subtree(node: &LsNode, prefix: &str, is_last: bool) {
73 let connector = if is_last { "└── " } else { "├── " };
74 println!("{prefix}{connector}{}", node_label(node));
75 let next_prefix = format!("{prefix}{}", if is_last { " " } else { "│ " });
76 let count = node.children.len();
77 for (idx, child) in node.children.iter().enumerate() {
78 render_subtree(child, &next_prefix, idx + 1 == count);
79 }
80}
81
82fn node_label(node: &LsNode) -> String {
87 if let Some(err) = &node.error {
88 return format!("{} ({}) [error: {}]", node.name, node.pack_type, err.kind);
89 }
90 if node.unsynced {
91 return format!("{} (declared, unsynced)", node.name);
92 }
93 let marker = if node.synthetic { "~ " } else { "" };
94 let suffix = if node.synthetic {
95 format!("({}, synthetic)", node.pack_type)
96 } else {
97 format!("({})", node.pack_type)
98 };
99 format!("{marker}{} {suffix}", node.name)
100}
101
102fn emit_json(tree: &LsTree) {
105 if let Ok(s) = serde_json::to_string_pretty(tree) {
106 println!("{s}");
107 }
108}
109
110fn emit_json_error(kind: &str, message: &str) {
111 let doc = serde_json::json!({
112 "verb": "ls",
113 "error": {
114 "kind": kind,
115 "message": message,
116 },
117 });
118 if let Ok(s) = serde_json::to_string(&doc) {
119 println!("{s}");
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use grex_core::LsNodeError;
127
128 fn leaf_loaded(name: &str, pack_type: &str) -> LsNode {
129 LsNode {
130 id: 0,
131 name: name.to_string(),
132 path: name.to_string(),
133 pack_type: pack_type.to_string(),
134 synthetic: false,
135 unsynced: false,
136 error: None,
137 children: Vec::new(),
138 }
139 }
140
141 fn leaf_synthetic(name: &str) -> LsNode {
142 LsNode {
143 id: 0,
144 name: name.to_string(),
145 path: name.to_string(),
146 pack_type: "scripted".to_string(),
147 synthetic: true,
148 unsynced: false,
149 error: None,
150 children: Vec::new(),
151 }
152 }
153
154 fn leaf_unsynced(name: &str) -> LsNode {
155 LsNode {
156 id: 0,
157 name: name.to_string(),
158 path: name.to_string(),
159 pack_type: "scripted".to_string(),
160 synthetic: false,
161 unsynced: true,
162 error: None,
163 children: Vec::new(),
164 }
165 }
166
167 fn leaf_errored(name: &str, kind: &str) -> LsNode {
168 LsNode {
169 id: 0,
170 name: name.to_string(),
171 path: name.to_string(),
172 pack_type: "scripted".to_string(),
173 synthetic: false,
174 unsynced: false,
175 error: Some(LsNodeError { kind: kind.to_string(), message: "boom".to_string() }),
176 children: Vec::new(),
177 }
178 }
179
180 #[test]
181 fn label_for_declarative_has_plain_suffix() {
182 assert_eq!(node_label(&leaf_loaded("warp-cfg", "declarative")), "warp-cfg (declarative)");
183 }
184
185 #[test]
186 fn label_for_synthetic_has_tilde_prefix_and_synthetic_suffix() {
187 assert_eq!(node_label(&leaf_synthetic("algo-leet")), "~ algo-leet (scripted, synthetic)");
188 }
189
190 #[test]
191 fn label_for_meta_has_meta_suffix() {
192 assert_eq!(node_label(&leaf_loaded("dev-env", "meta")), "dev-env (meta)");
193 }
194
195 #[test]
196 fn label_for_unsynced_marks_declared_unsynced() {
197 assert_eq!(node_label(&leaf_unsynced("alpha")), "alpha (declared, unsynced)");
198 }
199
200 #[test]
201 fn label_for_errored_carries_error_kind() {
202 assert_eq!(
203 node_label(&leaf_errored("corrupt", "parse")),
204 "corrupt (scripted) [error: parse]"
205 );
206 }
207}