Skip to main content

nika_engine/display/
dag.rs

1//! Static DAG renderer -- printed once, no ANSI cursor movement.
2//!
3//! Shows task names with verb icons connected by arrows.
4//! Groups tasks by topological layer (parallel tasks on same line).
5
6use colored::Colorize;
7
8use crate::display::icons;
9
10/// A task in the static DAG, positioned by topological layer.
11pub struct StaticDagTask {
12    pub id: String,
13    pub verb: String,
14    pub layer: usize,
15}
16
17/// A directed edge between two tasks.
18pub struct StaticDagEdge {
19    pub from: String,
20    pub to: String,
21}
22
23/// Print a compact static DAG to stdout.
24///
25/// Tasks are grouped by topological layer (parallel tasks on the same line).
26/// Arrows between layers are rendered as dimmed vertical connectors.
27///
28/// ```text
29///   DAG  6 tasks . 3 layers . 5 edges
30///
31///    ✧ research       ⎈ scrape       ☄ fetch_api
32///         │               │               │
33///         ▾               ▾               ▾
34///    ❋ analyze        ✧ summarize
35///         │               │
36///         ▾               ▾
37///    ⊛ publish
38/// ```
39pub fn print_static_dag(tasks: &[StaticDagTask], edges: &[StaticDagEdge]) {
40    if tasks.is_empty() {
41        return;
42    }
43
44    // Compute summary
45    let max_layer = tasks.iter().map(|t| t.layer).max().unwrap_or(0);
46    let num_layers = max_layer + 1;
47
48    let tc = tasks.len();
49    let ec = edges.len();
50    let summary = format!(
51        "DAG  {} {} \u{00B7} {} {} \u{00B7} {} {}",
52        tc,
53        if tc == 1 { "task" } else { "tasks" },
54        num_layers,
55        if num_layers == 1 { "layer" } else { "layers" },
56        ec,
57        if ec == 1 { "edge" } else { "edges" },
58    );
59    println!("  {}", summary.cyan().bold());
60    println!();
61
62    // Render one line per layer with task names, then vertical connectors
63    for layer_idx in 0..=max_layer {
64        let layer_tasks: Vec<&StaticDagTask> =
65            tasks.iter().filter(|t| t.layer == layer_idx).collect();
66
67        // Build the task line: "icon name" separated by generous spacing
68        let line: Vec<String> = layer_tasks
69            .iter()
70            .map(|t| format!("{} {}", icons::verb_plain(&t.verb), t.id))
71            .collect();
72
73        println!("   {}", line.join("       "));
74
75        // Print vertical connectors to next layer (if not last layer)
76        if layer_idx < max_layer {
77            // Find which tasks in this layer have outgoing edges
78            let has_outgoing: Vec<bool> = layer_tasks
79                .iter()
80                .map(|t| edges.iter().any(|e| e.from == t.id))
81                .collect();
82
83            if has_outgoing.iter().any(|&b| b) {
84                // Build connector lines matching task positions
85                let mut pipe_line = String::from("   ");
86                let mut arrow_line = String::from("   ");
87
88                for (i, task) in layer_tasks.iter().enumerate() {
89                    // The icon is 1 char, then space, then task id
90                    // Center the connector under the task name
91                    let task_label_len = task.id.len() + 2; // icon + space + id
92                    let center = task_label_len / 2;
93
94                    if has_outgoing[i] {
95                        pipe_line.push_str(&" ".repeat(center));
96                        pipe_line.push_str(&format!("{}", "\u{2502}".dimmed())); // │
97                        pipe_line.push_str(&" ".repeat(task_label_len.saturating_sub(center + 1)));
98                    } else {
99                        pipe_line.push_str(&" ".repeat(task_label_len));
100                    }
101
102                    if has_outgoing[i] {
103                        arrow_line.push_str(&" ".repeat(center));
104                        arrow_line.push_str(&format!("{}", "\u{25BE}".dimmed())); // ▾
105                        arrow_line.push_str(&" ".repeat(task_label_len.saturating_sub(center + 1)));
106                    } else {
107                        arrow_line.push_str(&" ".repeat(task_label_len));
108                    }
109
110                    // Add spacing between tasks (matching the join separator)
111                    if i < layer_tasks.len() - 1 {
112                        pipe_line.push_str("       ");
113                        arrow_line.push_str("       ");
114                    }
115                }
116
117                println!("{}", pipe_line);
118                println!("{}", arrow_line);
119            }
120        }
121    }
122    println!();
123}