use std::path::Path;
use crate::config::Config;
use crate::error::Result;
use crate::project::ProjectLayout;
use crate::store::Store;
use crate::store::hierarchy::Hierarchy;
use crate::store::node::NodeKind;
pub fn render(h: &Hierarchy, filter: Option<&str>) -> String {
let needle = filter.map(|s| s.to_lowercase());
let mut out = String::new();
for (node, depth) in h.flatten() {
let mut full = node.path.clone();
full.push(node.slug.clone());
let full_path = full.join("/");
if let Some(n) = needle.as_deref() {
let hit = node.title.to_lowercase().contains(n)
|| full_path.to_lowercase().contains(n);
if !hit {
continue;
}
}
let indent = " ".repeat(depth);
let glyph = match node.kind {
NodeKind::Book => "π",
NodeKind::Chapter => "βΈ",
NodeKind::Subchapter => "Β·",
NodeKind::Paragraph => "ΒΆ",
NodeKind::Image => "β£",
NodeKind::Script => "Ξ»",
};
let detail = if matches!(node.kind, NodeKind::Paragraph) {
let status = node.status.as_deref().unwrap_or("β");
let target = node
.target_words
.filter(|t| *t > 0)
.map(|t| format!("/{t}"))
.unwrap_or_default();
format!(" [{status} Β· {}{} words]", node.word_count, target)
} else {
String::new()
};
out.push_str(&format!("{indent}{glyph} {}{detail}\n", node.title));
out.push_str(&format!("{indent} {full_path}\n"));
}
out
}
pub fn run(project: &Path, filter: Option<&str>) -> Result<()> {
let layout = ProjectLayout::new(project);
layout.require_initialized()?;
let cfg = Config::load_layered(&layout.config_path())?;
let store = Store::open(layout.clone(), &cfg)?;
let h = Hierarchy::load(&store)?;
print!("{}", render(&h, filter));
Ok(())
}