KeiSeiKit-1.0/_primitives/_rust/kei-brain-view/src/render.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

138 lines
4.3 KiB
Rust

//! ASCII tree rendering with optional ANSI color.
//!
//! Constructor Pattern: one cube = render entrypoints + color helpers.
//! Colors obey `NO_COLOR` convention (https://no-color.org) — present
//! env var of any value disables ANSI escape codes at runtime.
use crate::error::{BrainViewError, Result, MAX_TREE_DEPTH};
use crate::graph::{Graph, Node};
use std::collections::HashSet;
const ANSI_RESET: &str = "\x1b[0m";
const ANSI_GREEN: &str = "\x1b[32m";
const ANSI_RED: &str = "\x1b[31m";
const ANSI_YELLOW: &str = "\x1b[33m";
const ANSI_CYAN: &str = "\x1b[36m";
const ANSI_DIM: &str = "\x1b[2m";
/// Render the full graph as an indented text tree (roots first, BFS
/// children). Colors status labels when stdout is not redirected and
/// `NO_COLOR` is unset.
pub fn render_ascii(graph: &Graph) -> String {
let colored = color_enabled();
render_ascii_with_color(graph, colored)
}
/// Explicit-color variant — used by tests to assert color-free output.
pub fn render_ascii_with_color(graph: &Graph, colored: bool) -> String {
let mut out = String::new();
for &root_idx in &graph.roots {
render_subtree(graph, root_idx, 0, &mut out, colored);
}
out
}
/// Render the lineage (ancestors + focus + descendants) for a single DNA.
pub fn render_lineage(graph: &Graph, focus: &Node, colored: bool) -> Result<String> {
let mut out = String::new();
let ancestors = walk_ancestors(graph, focus)?;
out.push_str("ANCESTORS:\n");
for (depth, n) in ancestors.iter().enumerate() {
out.push_str(&format_line(n, depth, colored));
}
out.push_str(&format!("FOCUS ({} matches):\n", 1));
out.push_str(&format_line(focus, 0, colored));
out.push_str("DESCENDANTS:\n");
render_subtree_by_branch(graph, &focus.branch, 0, &mut out, colored);
Ok(out)
}
fn render_subtree(graph: &Graph, idx: usize, depth: usize, out: &mut String, colored: bool) {
let n = graph.node(idx);
out.push_str(&format_line(n, depth, colored));
if depth + 1 > MAX_TREE_DEPTH {
return;
}
if let Some(kids) = graph.children_of.get(&n.branch) {
for &k in kids {
render_subtree(graph, k, depth + 1, out, colored);
}
}
}
fn render_subtree_by_branch(
graph: &Graph,
parent_branch: &str,
depth: usize,
out: &mut String,
colored: bool,
) {
if depth > MAX_TREE_DEPTH {
return;
}
if let Some(kids) = graph.children_of.get(parent_branch) {
for &k in kids {
let n = graph.node(k);
out.push_str(&format_line(n, depth + 1, colored));
render_subtree_by_branch(graph, &n.branch, depth + 1, out, colored);
}
}
}
fn walk_ancestors<'a>(graph: &'a Graph, focus: &'a Node) -> Result<Vec<&'a Node>> {
let mut out = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let mut cur_parent = focus.parent_branch.clone();
let mut steps = 0usize;
while let Some(pb) = cur_parent {
steps += 1;
if steps > MAX_TREE_DEPTH {
return Err(BrainViewError::MaxDepthExceeded(MAX_TREE_DEPTH));
}
if !seen.insert(pb.clone()) {
break;
}
let Some(&idx) = graph.by_branch.get(&pb) else {
break;
};
let n = graph.node(idx);
out.push(n);
cur_parent = n.parent_branch.clone();
}
out.reverse();
Ok(out)
}
fn format_line(n: &Node, depth: usize, colored: bool) -> String {
let indent = " ".repeat(depth);
let status = colorize_status(&n.status, colored);
let dna = n.dna.as_deref().unwrap_or("-");
let dna_short = dna.chars().take(20).collect::<String>();
let dna_fmt = if colored {
format!("{ANSI_DIM}{dna_short}{ANSI_RESET}")
} else {
dna_short
};
format!(
"{indent}- [{status}] {id} branch={branch} dna={dna_fmt}\n",
id = n.id,
branch = n.branch,
)
}
fn colorize_status(status: &str, colored: bool) -> String {
if !colored {
return status.to_string();
}
let code = match status {
"done" | "merged" => ANSI_GREEN,
"failed" | "rejected" => ANSI_RED,
"running" => ANSI_YELLOW,
_ => ANSI_CYAN,
};
format!("{code}{status}{ANSI_RESET}")
}
fn color_enabled() -> bool {
std::env::var_os("NO_COLOR").is_none()
}