use std::collections::BTreeMap;
use std::fmt::Write;
use cuenv_core::tasks::TaskNode;
#[derive(Default)]
pub struct TaskTreeNode {
pub description: Option<String>,
pub children: BTreeMap<String, Self>,
pub is_task: bool,
}
pub fn get_task_cli_help() -> String {
use clap::CommandFactory;
let mut cmd = crate::cli::Cli::command();
for subcmd in cmd.get_subcommands_mut() {
if subcmd.get_name() == "task" {
return subcmd.render_help().to_string();
}
}
"Execute a task defined in CUE configuration\n\nUsage: cuenv task [OPTIONS] [NAME]".to_string()
}
pub fn format_task_detail(task: &cuenv_core::tasks::IndexedTask) -> String {
let mut output = String::new();
writeln!(output, "Task: {}", task.name).expect("write to string");
match &task.node {
TaskNode::Task(t) => {
if let Some(desc) = &t.description {
writeln!(output, "Description: {desc}").expect("write to string");
}
writeln!(output, "Command: {}", t.command).expect("write to string");
if !t.args.is_empty() {
writeln!(output, "Args: {:?}", t.args).expect("write to string");
}
if !t.depends_on.is_empty() {
writeln!(output, "Depends on: {:?}", t.depends_on).expect("write to string");
}
if !t.inputs.is_empty() {
writeln!(output, "Inputs: {:?}", t.inputs).expect("write to string");
}
if !t.outputs.is_empty() {
writeln!(output, "Outputs: {:?}", t.outputs).expect("write to string");
}
if let Some(params) = &t.params {
if !params.positional.is_empty() {
writeln!(output, "\nPositional Arguments:").expect("write to string");
for (i, param) in params.positional.iter().enumerate() {
let required = if param.required { " (required)" } else { "" };
let default = param
.default
.as_ref()
.map(|d| format!(" [default: {d}]"))
.unwrap_or_default();
let desc = param
.description
.as_ref()
.map(|d| format!(" - {d}"))
.unwrap_or_default();
writeln!(output, " {{{{{i}}}}}{required}{default}{desc}")
.expect("write to string");
}
}
if !params.named.is_empty() {
writeln!(output, "\nNamed Arguments:").expect("write to string");
let mut names: Vec<_> = params.named.keys().collect();
names.sort();
for name in names {
let param = ¶ms.named[name];
let short = param
.short
.as_ref()
.map(|s| format!("-{s}, "))
.unwrap_or_default();
let required = if param.required { " (required)" } else { "" };
let default = param
.default
.as_ref()
.map(|d| format!(" [default: {d}]"))
.unwrap_or_default();
let desc = param
.description
.as_ref()
.map(|d| format!(" - {d}"))
.unwrap_or_default();
writeln!(output, " {short}--{name}{required}{default}{desc}")
.expect("write to string");
}
}
}
}
TaskNode::Group(_) => {
writeln!(output, "Type: Task Group (Parallel)").expect("write to string");
}
TaskNode::Sequence(_) => {
writeln!(output, "Type: Task Sequence (Sequential)").expect("write to string");
}
}
output
}
pub fn render_task_tree(
tasks: Vec<&cuenv_core::tasks::IndexedTask>,
cwd_relative: Option<&str>,
) -> String {
let mut by_source: BTreeMap<String, Vec<&cuenv_core::tasks::IndexedTask>> = BTreeMap::new();
for task in tasks {
let source = task.source_file.clone().unwrap_or_default();
let normalized = if source == "env.cue" {
String::new()
} else {
source
};
by_source.entry(normalized).or_default().push(task);
}
let mut sources: Vec<_> = by_source.keys().cloned().collect();
sources.sort_by(|a, b| {
let proximity_a = source_proximity(a, cwd_relative);
let proximity_b = source_proximity(b, cwd_relative);
proximity_a.cmp(&proximity_b).then(a.cmp(b))
});
let mut output = String::new();
for (i, source) in sources.iter().enumerate() {
if i > 0 {
output.push('\n');
}
let header = if source.is_empty() || source == "env.cue" {
"Tasks:".to_string()
} else {
format!("Tasks from {source}:")
};
writeln!(output, "{header}").expect("write to string");
let source_tasks = &by_source[source];
render_source_tasks(source_tasks, &mut output);
}
if output.is_empty() {
output = "No tasks defined in the configuration".to_string();
}
output
}
fn source_proximity(source: &str, cwd_relative: Option<&str>) -> usize {
let source_dir = if source.is_empty() {
""
} else {
std::path::Path::new(source)
.parent()
.and_then(|p| p.to_str())
.unwrap_or("")
};
let cwd = cwd_relative.unwrap_or("");
if source_dir.is_empty() && cwd.is_empty() {
return 0;
}
if source_dir == cwd {
return 0;
}
if cwd.starts_with(source_dir)
&& (source_dir.is_empty() || cwd[source_dir.len()..].starts_with('/'))
{
let source_depth = if source_dir.is_empty() {
0
} else {
source_dir.matches('/').count() + 1
};
let cwd_depth = if cwd.is_empty() {
0
} else {
cwd.matches('/').count() + 1
};
return cwd_depth - source_depth;
}
usize::MAX / 2
}
fn render_source_tasks(tasks: &[&cuenv_core::tasks::IndexedTask], output: &mut String) {
let mut roots: BTreeMap<String, TaskTreeNode> = BTreeMap::new();
for task in tasks {
let parts: Vec<&str> = task.name.split('.').collect();
let mut current_level = &mut roots;
for (i, part) in parts.iter().enumerate() {
let is_last = i == parts.len() - 1;
let node = current_level.entry((*part).to_string()).or_default();
if is_last {
node.is_task = true;
let desc = match &task.node {
TaskNode::Task(t) => t.description.clone(),
TaskNode::Group(g) => g.description.clone(),
TaskNode::Sequence(_) => None,
};
node.description = desc;
}
current_level = &mut node.children;
}
}
let max_width = calculate_tree_width(&roots, 0);
print_tree_nodes(&roots, output, max_width, "");
}
fn calculate_tree_width(nodes: &BTreeMap<String, TaskTreeNode>, depth: usize) -> usize {
let mut max = 0;
for (name, node) in nodes {
let len = (depth * 3) + 3 + name.len();
if len > max {
max = len;
}
let child_max = calculate_tree_width(&node.children, depth + 1);
if child_max > max {
max = child_max;
}
}
max
}
fn print_tree_nodes(
nodes: &BTreeMap<String, TaskTreeNode>,
output: &mut String,
max_width: usize,
prefix: &str,
) {
let count = nodes.len();
for (i, (name, node)) in nodes.iter().enumerate() {
let is_last_item = i == count - 1;
let marker = if is_last_item { "└─ " } else { "├─ " };
let current_line_len =
prefix.chars().count() + marker.chars().count() + name.chars().count();
write!(output, "{prefix}{marker}{name}").expect("write to string");
if let Some(desc) = &node.description {
let padding = max_width.saturating_sub(current_line_len);
let dots = ".".repeat(padding + 4);
write!(output, " {dots} {desc}").expect("write to string");
}
writeln!(output).expect("write to string");
let child_prefix = if is_last_item { " " } else { "│ " };
let new_prefix = format!("{prefix}{child_prefix}");
print_tree_nodes(&node.children, output, max_width, &new_prefix);
}
}