use crate::Graph;
use std::{collections::HashSet, io, io::Write, str::FromStr};
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
pub enum Charset {
#[cfg_attr(not(target_os = "windows"), default)]
Utf8,
#[cfg_attr(target_os = "windows", default)]
Ascii,
}
impl FromStr for Charset {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"utf8" => Ok(Charset::Utf8),
"ascii" => Ok(Charset::Ascii),
s => Err(format!("invalid charset: {s}")),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TreeOptions {
pub charset: Charset,
pub no_dedupe: bool,
}
struct Symbols {
down: &'static str,
tee: &'static str,
ell: &'static str,
right: &'static str,
}
static UTF8_SYMBOLS: Symbols = Symbols { down: "│", tee: "├", ell: "└", right: "─" };
static ASCII_SYMBOLS: Symbols = Symbols { down: "|", tee: "|", ell: "`", right: "-" };
pub fn print(graph: &Graph, opts: &TreeOptions, out: &mut dyn Write) -> io::Result<()> {
let symbols = match opts.charset {
Charset::Utf8 => &UTF8_SYMBOLS,
Charset::Ascii => &ASCII_SYMBOLS,
};
let mut visited_imports = HashSet::new();
let mut levels_continue = Vec::new();
let mut write_stack = Vec::new();
for (node_index, _) in graph.input_nodes().enumerate() {
print_node(
graph,
node_index,
symbols,
opts.no_dedupe,
&mut visited_imports,
&mut levels_continue,
&mut write_stack,
out,
)?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn print_node(
graph: &Graph,
node_index: usize,
symbols: &Symbols,
no_dedupe: bool,
visited_imports: &mut HashSet<usize>,
levels_continue: &mut Vec<bool>,
write_stack: &mut Vec<usize>,
out: &mut dyn Write,
) -> io::Result<()> {
let new_node = no_dedupe || visited_imports.insert(node_index);
if let Some((last_continues, rest)) = levels_continue.split_last() {
for continues in rest {
let c = if *continues { symbols.down } else { " " };
write!(out, "{c} ")?;
}
let c = if *last_continues { symbols.tee } else { symbols.ell };
write!(out, "{0}{1}{1} ", c, symbols.right)?;
}
let in_cycle = write_stack.contains(&node_index);
let has_deps = graph.has_outgoing_edges(node_index);
let star = if (new_node && !in_cycle) || !has_deps { "" } else { " (*)" };
writeln!(out, "{}{star}", graph.display_node(node_index))?;
if !new_node || in_cycle {
return Ok(())
}
write_stack.push(node_index);
print_imports(
graph,
node_index,
symbols,
no_dedupe,
visited_imports,
levels_continue,
write_stack,
out,
)?;
write_stack.pop();
Ok(())
}
#[allow(clippy::too_many_arguments, clippy::ptr_arg)]
fn print_imports(
graph: &Graph,
node_index: usize,
symbols: &Symbols,
no_dedupe: bool,
visited_imports: &mut HashSet<usize>,
levels_continue: &mut Vec<bool>,
write_stack: &mut Vec<usize>,
out: &mut dyn Write,
) -> io::Result<()> {
let imports = graph.imported_nodes(node_index);
if imports.is_empty() {
return Ok(())
}
let mut iter = imports.iter().peekable();
while let Some(import) = iter.next() {
levels_continue.push(iter.peek().is_some());
print_node(
graph,
*import,
symbols,
no_dedupe,
visited_imports,
levels_continue,
write_stack,
out,
)?;
levels_continue.pop();
}
Ok(())
}