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 let doc = serde_json::json!({
111 "workspace": tree.workspace,
112 "pack": tree.workspace,
113 "tree": tree.tree,
114 });
115 if let Ok(s) = serde_json::to_string_pretty(&doc) {
116 println!("{s}");
117 }
118}
119
120fn emit_json_error(kind: &str, message: &str) {
121 let doc = serde_json::json!({
122 "verb": "ls",
123 "error": {
124 "kind": kind,
125 "message": message,
126 },
127 });
128 if let Ok(s) = serde_json::to_string(&doc) {
129 println!("{s}");
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use grex_core::LsNodeError;
137
138 fn leaf_loaded(name: &str, pack_type: &str) -> LsNode {
139 LsNode {
140 id: 0,
141 name: name.to_string(),
142 path: name.to_string(),
143 pack_type: pack_type.to_string(),
144 synthetic: false,
145 unsynced: false,
146 error: None,
147 children: Vec::new(),
148 }
149 }
150
151 fn leaf_synthetic(name: &str) -> LsNode {
152 LsNode {
153 id: 0,
154 name: name.to_string(),
155 path: name.to_string(),
156 pack_type: "scripted".to_string(),
157 synthetic: true,
158 unsynced: false,
159 error: None,
160 children: Vec::new(),
161 }
162 }
163
164 fn leaf_unsynced(name: &str) -> LsNode {
165 LsNode {
166 id: 0,
167 name: name.to_string(),
168 path: name.to_string(),
169 pack_type: "scripted".to_string(),
170 synthetic: false,
171 unsynced: true,
172 error: None,
173 children: Vec::new(),
174 }
175 }
176
177 fn leaf_errored(name: &str, kind: &str) -> LsNode {
178 LsNode {
179 id: 0,
180 name: name.to_string(),
181 path: name.to_string(),
182 pack_type: "scripted".to_string(),
183 synthetic: false,
184 unsynced: false,
185 error: Some(LsNodeError { kind: kind.to_string(), message: "boom".to_string() }),
186 children: Vec::new(),
187 }
188 }
189
190 #[test]
191 fn label_for_declarative_has_plain_suffix() {
192 assert_eq!(node_label(&leaf_loaded("warp-cfg", "declarative")), "warp-cfg (declarative)");
193 }
194
195 #[test]
196 fn label_for_synthetic_has_tilde_prefix_and_synthetic_suffix() {
197 assert_eq!(node_label(&leaf_synthetic("algo-leet")), "~ algo-leet (scripted, synthetic)");
198 }
199
200 #[test]
201 fn label_for_meta_has_meta_suffix() {
202 assert_eq!(node_label(&leaf_loaded("dev-env", "meta")), "dev-env (meta)");
203 }
204
205 #[test]
206 fn label_for_unsynced_marks_declared_unsynced() {
207 assert_eq!(node_label(&leaf_unsynced("alpha")), "alpha (declared, unsynced)");
208 }
209
210 #[test]
211 fn label_for_errored_carries_error_kind() {
212 assert_eq!(
213 node_label(&leaf_errored("corrupt", "parse")),
214 "corrupt (scripted) [error: parse]"
215 );
216 }
217}