use crate::cli::args::{GlobalFlags, LsArgs};
use anyhow::Result;
use grex_core::{build_ls_tree, LsNode, LsTree};
use tokio_util::sync::CancellationToken;
pub fn run(args: LsArgs, global: &GlobalFlags, _cancel: &CancellationToken) -> Result<()> {
let pack_root = match args.pack_root.clone() {
Some(p) => p,
None => std::env::current_dir()?,
};
match build_ls_tree(&pack_root) {
Ok(tree) => {
if global.json {
emit_json(&tree);
} else {
render_tree(&tree);
}
Ok(())
}
Err(detail) => {
if global.json {
emit_json_error("tree", &detail);
} else {
eprintln!("grex ls: {detail}");
}
std::process::exit(2);
}
}
}
fn render_tree(tree: &LsTree) {
for root in &tree.tree {
println!("{}", node_label(root));
let count = root.children.len();
for (idx, child) in root.children.iter().enumerate() {
render_subtree(child, "", idx + 1 == count);
}
}
}
fn render_subtree(node: &LsNode, prefix: &str, is_last: bool) {
let connector = if is_last { "└── " } else { "├── " };
println!("{prefix}{connector}{}", node_label(node));
let next_prefix = format!("{prefix}{}", if is_last { " " } else { "│ " });
let count = node.children.len();
for (idx, child) in node.children.iter().enumerate() {
render_subtree(child, &next_prefix, idx + 1 == count);
}
}
fn node_label(node: &LsNode) -> String {
if let Some(err) = &node.error {
return format!("{} ({}) [error: {}]", node.name, node.pack_type, err.kind);
}
if node.unsynced {
return format!("{} (declared, unsynced)", node.name);
}
let marker = if node.synthetic { "~ " } else { "" };
let suffix = if node.synthetic {
format!("({}, synthetic)", node.pack_type)
} else {
format!("({})", node.pack_type)
};
format!("{marker}{} {suffix}", node.name)
}
fn emit_json(tree: &LsTree) {
if let Ok(s) = serde_json::to_string_pretty(tree) {
println!("{s}");
}
}
fn emit_json_error(kind: &str, message: &str) {
let doc = serde_json::json!({
"verb": "ls",
"error": {
"kind": kind,
"message": message,
},
});
if let Ok(s) = serde_json::to_string(&doc) {
println!("{s}");
}
}
#[cfg(test)]
mod tests {
use super::*;
use grex_core::LsNodeError;
fn leaf_loaded(name: &str, pack_type: &str) -> LsNode {
LsNode {
id: 0,
name: name.to_string(),
path: name.to_string(),
pack_type: pack_type.to_string(),
synthetic: false,
unsynced: false,
error: None,
children: Vec::new(),
}
}
fn leaf_synthetic(name: &str) -> LsNode {
LsNode {
id: 0,
name: name.to_string(),
path: name.to_string(),
pack_type: "scripted".to_string(),
synthetic: true,
unsynced: false,
error: None,
children: Vec::new(),
}
}
fn leaf_unsynced(name: &str) -> LsNode {
LsNode {
id: 0,
name: name.to_string(),
path: name.to_string(),
pack_type: "scripted".to_string(),
synthetic: false,
unsynced: true,
error: None,
children: Vec::new(),
}
}
fn leaf_errored(name: &str, kind: &str) -> LsNode {
LsNode {
id: 0,
name: name.to_string(),
path: name.to_string(),
pack_type: "scripted".to_string(),
synthetic: false,
unsynced: false,
error: Some(LsNodeError { kind: kind.to_string(), message: "boom".to_string() }),
children: Vec::new(),
}
}
#[test]
fn label_for_declarative_has_plain_suffix() {
assert_eq!(node_label(&leaf_loaded("warp-cfg", "declarative")), "warp-cfg (declarative)");
}
#[test]
fn label_for_synthetic_has_tilde_prefix_and_synthetic_suffix() {
assert_eq!(node_label(&leaf_synthetic("algo-leet")), "~ algo-leet (scripted, synthetic)");
}
#[test]
fn label_for_meta_has_meta_suffix() {
assert_eq!(node_label(&leaf_loaded("dev-env", "meta")), "dev-env (meta)");
}
#[test]
fn label_for_unsynced_marks_declared_unsynced() {
assert_eq!(node_label(&leaf_unsynced("alpha")), "alpha (declared, unsynced)");
}
#[test]
fn label_for_errored_carries_error_kind() {
assert_eq!(
node_label(&leaf_errored("corrupt", "parse")),
"corrupt (scripted) [error: parse]"
);
}
}