use crate::anchor::AnchorRef;
use crate::args::Args;
use crate::brief::clip;
use crate::event::{Body, Event, Kind, State};
use crate::failure::{Failure, R};
use crate::model::{Aggregates, Node, Tree};
use crate::output::outln;
use serde_json::json;
use std::collections::HashMap;
pub(crate) const WIDTH: usize = 62;
const ANCESTOR_CLIP: usize = WIDTH;
pub(crate) fn wrap(text: &str, width: usize, indent: &str) -> Vec<String> {
if text.trim().is_empty() {
return vec![];
}
let mut lines = Vec::new();
let mut cur = String::new();
for p in text.split_whitespace() {
if !cur.is_empty() && cur.chars().count() + 1 + p.chars().count() > width {
lines.push(format!("{indent}{cur}"));
cur = p.to_string();
} else {
if !cur.is_empty() {
cur.push(' ');
}
cur.push_str(p);
}
}
if !cur.is_empty() {
lines.push(format!("{indent}{cur}"));
}
lines
}
fn label(a: &Tree, n: &Node) -> String {
match n.state {
State::Active => n.title(a).to_string(),
e => format!("{} [{}]", n.title(a), e.word(n.kind)),
}
}
fn json_node(a: &Tree, ag: &Aggregates, n: &Node) -> serde_json::Value {
let r = ag.counts(n.num);
json!({
"id": n.id,
"alias": n.alias(),
"num": n.num,
"kind": n.kind,
"title": n.title(a),
"why": n.why(a),
"state": n.state,
"blocks": n.blocks,
"parent": n.parent.and_then(|p| a.node_by_num(p).map(|x| x.alias())),
"note": n.note(a),
"outcome": n.outcome(a),
"refs": n.refs(a),
"governs": n.governs(a),
"opened": n.opened(a),
"closed": n.closed(a),
"false_close": n.state == State::Done && ag.blockers(n.num) > 0,
"open_below": r.open_count,
"total_below": r.total,
})
}
pub(crate) fn print_json(v: serde_json::Value) -> R {
outln!(
"{}",
serde_json::to_string_pretty(&v).map_err(std::io::Error::other)?
);
Ok(())
}
pub(crate) struct Full {
created: HashMap<String, u64>,
state: HashMap<String, Vec<(u64, State)>>,
}
impl Full {
pub(crate) fn from_log(log: &[Event]) -> Full {
let mut created = HashMap::new();
let mut state: HashMap<String, Vec<(u64, State)>> = HashMap::new();
for e in log {
match &e.payload {
Body::NodeCreated { node, .. } => {
created.entry(node.clone()).or_insert(e.seq);
}
Body::StateChanged { node, state: s, .. } => {
state.entry(node.clone()).or_default().push((e.seq, *s));
}
_ => {}
}
}
Full { created, state }
}
fn state_at(&self, id: &str, seq: u64) -> State {
self.state
.get(id)
.into_iter()
.flatten()
.rfind(|(s, _)| *s <= seq)
.map(|(_, state)| *state)
.unwrap_or(State::Active)
}
}
pub(crate) fn anchor_of(a: &Tree, full: &Full, n: &Node) -> AnchorRef {
let Some(&seq) = full.created.get(&n.id) else {
return AnchorRef::default();
};
a.vivacs
.iter()
.rfind(|v| v.seq <= seq)
.map(|v| v.anchor.clone())
.unwrap_or_default()
}
pub(crate) fn standing_of<'a>(a: &'a Tree, n: &Node) -> Vec<&'a Node> {
a.children(n.num)
.into_iter()
.filter(|c| c.kind == Kind::Decision && c.state.is_open())
.collect()
}
pub(crate) fn open_then_of<'a>(a: &'a Tree, full: &Full, n: &Node) -> Vec<&'a Node> {
let (Some(&seq), Some(parent)) = (full.created.get(&n.id), n.parent) else {
return vec![];
};
a.children(parent)
.into_iter()
.filter(|c| c.id != n.id && c.num < n.num)
.filter(|c| full.state_at(&c.id, seq).is_open())
.collect()
}
fn json_node_full(a: &Tree, ag: &Aggregates, full: &Full, n: &Node) -> serde_json::Value {
let mut v = json_node(a, ag, n);
v["anchor"] = json!(anchor_of(a, full, n));
v["standing"] = json!(standing_of(a, n)
.iter()
.map(|c| json_node(a, ag, c))
.collect::<Vec<_>>());
v["open_then"] = json!(open_then_of(a, full, n)
.iter()
.map(|c| json_node(a, ag, c))
.collect::<Vec<_>>());
v
}
fn why_data_impl(a: &Tree, full: Option<&Full>, id: &str) -> Result<serde_json::Value, Failure> {
let ag = &a.aggregates();
let n = a
.resolve(id)
.ok_or_else(|| Failure::usage(format!("No such node: {id}.")))?;
let lineage = a.ancestors(n.num);
let node_json = |x: &Node| match full {
Some(f) => json_node_full(a, ag, f, x),
None => json_node(a, ag, x),
};
let siblings: Vec<_> = n
.parent
.map(|p| a.children(p))
.unwrap_or_default()
.into_iter()
.filter(|c| c.id != n.id && c.state.is_open())
.map(|c| json_node(a, ag, c))
.collect();
Ok(json!({
"node": node_json(n),
"path": lineage.iter().map(|x| node_json(x)).collect::<Vec<_>>(),
"in_parallel": siblings,
"born_here": a.children(n.num).iter().filter(|c| c.state.is_open())
.map(|c| json_node(a, ag, c)).collect::<Vec<_>>(),
"blockers": a.open_blockers(n.num).iter()
.map(|c| json_node(a, ag, c)).collect::<Vec<_>>(),
}))
}
pub fn why_data(a: &Tree, id: &str) -> Result<serde_json::Value, Failure> {
why_data_impl(a, None, id)
}
pub fn open_data(a: &Tree) -> serde_json::Value {
let mut leaves: Vec<&Node> = a
.nodes_iter()
.filter(|n| n.is_front() && !a.children(n.num).iter().any(|c| c.is_front()))
.collect();
leaves.sort_by_key(|n| n.num);
json!(leaves
.iter()
.map(|n| json!({
"alias": n.alias(),
"kind": n.kind,
"state": n.state,
"title": n.title(a),
"lineage": lineage_of(a, n),
}))
.collect::<Vec<_>>())
}
fn print_full_of(a: &Tree, full: &Full, n: &Node) {
let anchor = anchor_of(a, full, n);
if anchor.is_empty_tree() {
outln!(" anchor: none");
} else {
outln!(" anchor: {} ({})", anchor.short(), anchor.kind);
}
let standing = standing_of(a, n);
if !standing.is_empty() {
outln!(
" standing ({}): {}",
standing.len(),
standing
.iter()
.map(|d| d.alias())
.collect::<Vec<_>>()
.join(", ")
);
}
let open_then = open_then_of(a, full, n);
if !open_then.is_empty() {
outln!(
" open then ({}): {}",
open_then.len(),
open_then
.iter()
.map(|d| d.alias())
.collect::<Vec<_>>()
.join(", ")
);
}
}
pub fn why(a: &Tree, log: &[Event], args: &Args) -> R {
let ag = &a.aggregates();
let s = args
.positional(0)
.ok_or_else(|| Failure::usage("usage: vivac why <id>"))?;
let n = a
.resolve(s)
.ok_or_else(|| Failure::usage(format!("No such node: {s}.")))?;
let lineage = a.ancestors(n.num);
let full = args.has("full").then(|| Full::from_log(log));
if args.has("json") {
return print_json(match &full {
Some(f) => why_data_impl(a, Some(f), s)?,
None => why_data(a, s)?,
});
}
outln!();
outln!(" Why we are here -> {}", n.alias());
outln!(" {}", "-".repeat(66));
outln!();
for (i, p) in lineage.iter().enumerate() {
let is_last = i == lineage.len() - 1;
let clip_body = !is_last && full.is_none();
let body = |text: &str| {
if clip_body {
clip(text, ANCESTOR_CLIP)
} else {
text.to_string()
}
};
outln!(" {:<6}{}", p.alias(), label(a, p));
for l in wrap(&body(p.why(a)), WIDTH, " ") {
outln!("{l}");
}
let note = p.note(a);
for l in wrap(&format!("! {}", body(note)), WIDTH, " ") {
if !note.is_empty() {
outln!("{l}");
}
}
let outcome = p.outcome(a);
for l in wrap(&format!("= {}", body(outcome)), WIDTH, " ") {
if !outcome.is_empty() {
outln!("{l}");
}
}
if let Some(f) = &full {
print_full_of(a, f, p);
}
if !is_last {
let f = ag.counts(p.num).phrase();
if !f.is_empty() {
outln!(" ({f} below)");
}
outln!(" |");
outln!(" v");
} else {
outln!();
outln!(" ^^^ you are here");
}
}
outln!();
if let Some(parent) = n.parent {
let siblings: Vec<_> = a
.children(parent)
.into_iter()
.filter(|c| c.id != n.id && c.state.is_open())
.collect();
if !siblings.is_empty() {
outln!(" In parallel, still open ({}):", siblings.len());
for c in siblings {
outln!(" {:<6} {}", c.alias(), c.title(a));
}
outln!();
}
}
let kids: Vec<_> = a
.children(n.num)
.into_iter()
.filter(|c| c.state.is_open())
.collect();
if !kids.is_empty() {
outln!(" Born here and still open ({}):", kids.len());
for c in kids {
outln!(
" {} {:<6} {}",
if c.blocks { '*' } else { ' ' },
c.alias(),
c.title(a)
);
}
outln!();
}
for p in &lineage {
let pending_count = a.open_blockers(p.num);
if !pending_count.is_empty() && p.state.is_open() {
outln!(
" {} does not close until these close ({}):",
p.alias(),
pending_count.len()
);
for c in pending_count {
outln!(" {:<6} {}", c.alias(), c.title(a));
}
outln!();
}
}
Ok(())
}
fn branch(a: &Tree, ag: &Aggregates, n: &Node, prefix: &str, is_last: bool, show_all: bool) {
let f = ag.counts(n.num).phrase();
let mut tail = if f.is_empty() {
String::new()
} else {
format!(" ({f})")
};
let pending_count = ag.blockers(n.num);
if n.state == State::Done && pending_count > 0 {
tail.push_str(&format!(
" <== FALSE CLOSE: {pending_count} open condition(s)"
));
}
let mark = if n.blocks { "* " } else { "" };
outln!(
"{prefix}{}[{}] {:<6} {mark}{}{tail}",
if is_last { "`-- " } else { "|-- " },
n.state.mark(),
n.alias(),
n.title(a)
);
let sig = format!("{prefix}{}", if is_last { " " } else { "| " });
let children: Vec<_> = a
.children(n.num)
.into_iter()
.filter(|h| show_all || h.state.is_open() || ag.counts(h.num).open_count > 0)
.collect();
for (i, h) in children.iter().enumerate() {
branch(a, ag, h, &sig, i == children.len() - 1, show_all);
}
}
fn subtree_json(a: &Tree, ag: &Aggregates, n: &Node) -> serde_json::Value {
let mut v = json_node(a, ag, n);
v["children"] = json!(a
.children(n.num)
.iter()
.map(|h| subtree_json(a, ag, h))
.collect::<Vec<_>>());
v
}
pub fn tree(a: &Tree, args: &Args) -> R {
let ag = &a.aggregates();
let roots: Vec<&Node> = match args.positional(0) {
Some(s) => vec![a
.resolve(s)
.ok_or_else(|| Failure::usage(format!("No such node: {s}.")))?],
None => a.roots(),
};
if args.has("json") {
return print_json(json!(roots
.iter()
.map(|n| subtree_json(a, ag, n))
.collect::<Vec<_>>()));
}
if a.is_empty_tree() {
outln!(" Empty tree. vivac push \"<title>\" --why \"<reason>\"");
return Ok(());
}
let show_all = args.has("all");
outln!();
for (i, n) in roots.iter().enumerate() {
branch(a, ag, n, " ", i == roots.len() - 1, show_all);
}
outln!();
if !show_all {
outln!(" (closed nodes with no open descendants hidden; --all shows them)");
outln!();
}
Ok(())
}
pub fn open(a: &Tree, args: &Args) -> R {
let mut leaves: Vec<&Node> = a
.nodes_iter()
.filter(|n| n.is_front() && !a.children(n.num).iter().any(|c| c.is_front()))
.collect();
leaves.sort_by_key(|n| n.num);
let standing = a
.nodes_iter()
.filter(|n| n.kind == Kind::Decision && n.state.is_open())
.count();
if args.has("json") {
return print_json(open_data(a));
}
if leaves.is_empty() && standing == 0 {
outln!(" Nothing open.");
return Ok(());
}
outln!();
outln!(
" {} open front{}",
leaves.len(),
if leaves.len() == 1 { "" } else { "s" },
);
outln!();
for n in leaves {
outln!(" {:<6} {}", n.alias(), n.title(a));
let lineage = a.ancestors(n.num);
if lineage.len() > 1 {
let v: Vec<String> = lineage[..lineage.len() - 1]
.iter()
.map(|p| p.alias())
.collect();
outln!(" via {}", v.join(" > "));
}
}
if standing > 0 {
let phrase = if standing == 1 {
"1 standing decision, which is not work".to_string()
} else {
format!("{standing} standing decisions, which are not work")
};
outln!();
outln!(" + {phrase} vivac brief");
}
outln!();
Ok(())
}
pub fn triage(a: &Tree, args: &Args) -> R {
let ag = &a.aggregates();
let mut parked_nodes: Vec<&Node> = a
.nodes_iter()
.filter(|n| n.state == State::Suspended)
.collect();
let mut deep: Vec<(&Node, usize)> = a
.nodes_iter()
.filter(|n| n.is_front())
.map(|n| (n, a.under_goal(n.num).len()))
.filter(|(_, d)| *d >= 6)
.collect();
let mut orphaned: Vec<(&Node, &Node)> = a
.nodes_iter()
.filter(|n| n.is_front())
.filter_map(|n| {
let p = a.node_by_num(n.parent?)?;
(p.state == State::Abandoned).then_some((n, p))
})
.collect();
let mut false_closes: Vec<&Node> = a
.nodes_iter()
.filter(|n| n.state == State::Done && !n.forced_close && ag.blockers(n.num) > 0)
.collect();
parked_nodes.sort_by_key(|n| n.num);
deep.sort_by_key(|(n, _)| n.num);
orphaned.sort_by_key(|(n, _)| n.num);
false_closes.sort_by_key(|n| n.num);
if args.has("json") {
return print_json(json!({
"parked": parked_nodes.iter().map(|n| json_node(a, ag, n)).collect::<Vec<_>>(),
"deep": deep.iter().map(|(n, d)| {
let mut v = json_node(a, ag, n);
v["depth_from_goal"] = json!(d);
v
}).collect::<Vec<_>>(),
"orphaned_by_discard": orphaned.iter().map(|(n, p)| {
let mut v = json_node(a, ag, n);
v["discarded"] = json!(p.alias());
v["discarded_because"] = json!(p.outcome(a));
v
}).collect::<Vec<_>>(),
"false_closes": false_closes.iter().map(|n| json_node(a, ag, n)).collect::<Vec<_>>(),
}));
}
let total = parked_nodes.len() + deep.len() + orphaned.len() + false_closes.len();
if total == 0 {
outln!(" Nothing to prune.");
return Ok(());
}
outln!();
outln!(" TRIAGE - {total} thing(s) to look at");
if !parked_nodes.is_empty() {
outln!();
outln!(
" PARKED ({}) focus <id> | abandon <id>",
parked_nodes.len()
);
for n in &parked_nodes {
outln!(" {:<6} {}", n.alias(), n.title(a));
for l in wrap(n.outcome(a), WIDTH, " ") {
outln!("{l}");
}
}
}
if !deep.is_empty() {
outln!();
outln!(
" 6 OR MORE FROM ITS GOAL ({}) promote <id>",
deep.len()
);
for (n, d) in &deep {
outln!(
" {:<6} {:<40} depth {d}",
n.alias(),
clip(n.title(a), 40)
);
let v: Vec<String> = a
.under_goal(n.num)
.iter()
.rev()
.skip(1)
.rev()
.map(|p| p.alias())
.collect();
outln!(" via {}", v.join(" > "));
}
}
if !orphaned.is_empty() {
outln!();
outln!(
" SURVIVED A DISCARD ({}) abandon <id> | promote <id>",
orphaned.len()
);
for (n, p) in &orphaned {
outln!(" {:<6} {}", n.alias(), n.title(a));
outln!(
" born from {}, discarded: {}",
p.alias(),
clip(p.outcome(a), 36)
);
}
}
if !false_closes.is_empty() {
outln!();
outln!(
" FALSE CLOSES ({}) close what is left, or --force",
false_closes.len()
);
for n in &false_closes {
outln!(
" {:<6} {:<40} {} blocker(s)",
n.alias(),
clip(n.title(a), 40),
ag.blockers(n.num)
);
}
}
outln!();
Ok(())
}
pub fn parked(a: &Tree, args: &Args) -> R {
let ag = &a.aggregates();
let mut ps: Vec<&Node> = a
.nodes_iter()
.filter(|n| n.state == State::Suspended)
.collect();
ps.sort_by_key(|n| n.num);
if args.has("json") {
return print_json(json!(ps
.iter()
.map(|n| json_node(a, ag, n))
.collect::<Vec<_>>()));
}
if ps.is_empty() {
outln!(" Nothing parked.");
return Ok(());
}
outln!();
outln!(" DO NOT TOUCH NOW ({})", ps.len());
outln!();
for n in ps {
outln!(" {:<6} {}", n.alias(), n.title(a));
for l in wrap(n.outcome(a), WIDTH, " ") {
outln!("{l}");
}
}
outln!();
Ok(())
}
pub fn stack(a: &Tree, args: &Args) -> R {
let ag = &a.aggregates();
let stack: Vec<&Node> = a
.stack
.iter()
.filter_map(|&num| a.node_by_num(num))
.collect();
if args.has("json") {
return print_json(json!({
"depth": stack.len(),
"stack": stack.iter().map(|n| json_node(a, ag, n)).collect::<Vec<_>>(),
}));
}
if stack.is_empty() {
outln!(" Empty stack. vivac push \"<title>\" --why \"<reason>\"");
return Ok(());
}
outln!();
for (i, n) in stack.iter().enumerate() {
let focus = if i == stack.len() - 1 {
" <- focus"
} else {
""
};
outln!(" {}{:<6} {}{focus}", " ".repeat(i), n.alias(), n.title(a));
}
outln!();
if stack.len() >= 6 {
outln!(
" Stack {} levels deep. Almost never lack of discipline: usually",
stack.len()
);
outln!(" the root goal moved and nobody re-rooted. vivac promote");
outln!();
}
Ok(())
}
pub fn stats(a: &Tree, args: &Args) -> R {
let ag = &a.aggregates();
let mut by_state = std::collections::BTreeMap::new();
let mut orphans = 0usize;
let mut false_closes = Vec::new();
for n in a.nodes_iter() {
*by_state.entry(n.state.word(n.kind)).or_insert(0usize) += 1;
if n.parent.is_some_and(|p| a.node_by_num(p).is_none()) {
orphans += 1;
}
if n.state == State::Done && ag.blockers(n.num) > 0 {
false_closes.push(n);
}
}
let depth_of = ag.max_depth;
false_closes.sort_by_key(|n| n.num);
if args.has("json") {
return print_json(json!({
"nodes": a.total(),
"by_state": by_state,
"depth": depth_of,
"roots": a.roots().len(),
"stack": a.stack_depth(),
"orphans": orphans,
"broken_lines": a.broken_lines,
"false_closes": false_closes.iter().map(|n| json_node(a, ag, n)).collect::<Vec<_>>(),
}));
}
outln!();
outln!(" nodes {}", a.total());
for (k, v) in &by_state {
outln!(" {k:<14} {v}");
}
outln!(" depth {depth_of}");
outln!(" roots {}", a.roots().len());
outln!(" stack {}", a.stack_depth());
if orphans > 0 {
outln!(" ORPHANS {orphans} <- broken provenance");
}
if a.broken_lines > 0 {
outln!(" broken lines {} <- in .vivac/events", a.broken_lines);
}
if !false_closes.is_empty() {
outln!();
outln!(" FALSE CLOSES ({})", false_closes.len());
for n in false_closes {
outln!(" {:<6} {}", n.alias(), n.title(a));
}
}
outln!();
Ok(())
}
pub fn vivacs(a: &Tree, args: &Args) -> R {
if args.has("json") {
return print_json(json!(a
.vivacs
.iter()
.rev()
.map(|v| json!({
"id": v.id,
"alias": v.alias(),
"node_ref": v.node_ref.as_ref().and_then(|r| a.node(r).map(|n| n.alias())),
"kind": v.kind.word(),
"ts": v.ts,
"label": v.label,
"next_intent": v.next_intent,
"anchor": v.anchor,
"stack": v.stack.iter().map(|(al, t)| json!({"alias": al, "title": t}))
.collect::<Vec<_>>(),
"working_set": v.working_set,
}))
.collect::<Vec<_>>()));
}
if a.vivacs.is_empty() {
outln!(" No stops yet. vivac save \"<label>\"");
return Ok(());
}
outln!();
for v in a.vivacs.iter().rev().take(20) {
let top = v
.stack
.last()
.map(|(al, t)| format!("{al} {t}"))
.unwrap_or_else(|| "empty stack".into());
outln!(
" {:<5} {:<7} {} {}",
v.alias(),
v.kind.word(),
crate::clock::date_of(&v.ts),
top
);
if !v.label.is_empty() {
outln!(" {}", v.label);
}
if !v.next_intent.is_empty() {
outln!(" you were about to: {}", v.next_intent);
}
}
if a.vivacs.len() > 20 {
outln!();
outln!(" ... and {} more", a.vivacs.len() - 20);
}
outln!();
Ok(())
}
fn searchable<'t>(a: &'t Tree, n: &Node) -> [(&'static str, &'t str); 4] {
[
("title", n.title(a)),
("why", n.why(a)),
("note", n.note(a)),
("outcome", n.outcome(a)),
]
}
fn snippet(text: &str, terms: &[String], width: usize) -> String {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= width {
return text.split_whitespace().collect::<Vec<_>>().join(" ");
}
let mut lower = String::with_capacity(text.len());
let mut origin: Vec<usize> = Vec::with_capacity(text.len());
for (i, c) in chars.iter().enumerate() {
for lowered_char in c.to_lowercase() {
for _ in 0..lowered_char.len_utf8() {
origin.push(i);
}
lower.push(lowered_char);
}
}
let at = terms
.iter()
.filter_map(|t| lower.find(t.as_str()))
.min()
.map(|b| origin[b])
.unwrap_or(0);
let end = (at + width * 2 / 3).clamp(width, chars.len());
let start = end - width;
let mut out = String::new();
if start > 0 {
out.push_str("...");
}
out.extend(chars[start..end].iter());
if end < chars.len() {
out.push_str("...");
}
out.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn terms_of(query: &str) -> Result<Vec<String>, Failure> {
let terms: Vec<String> = query.split_whitespace().map(|t| t.to_lowercase()).collect();
if terms.is_empty() {
return Err(Failure::usage("usage: vivac find \"<text>\"".to_string()));
}
Ok(terms)
}
fn field_order(field: &str) -> u8 {
match field {
"title" => 0,
"why" => 1,
_ => 2,
}
}
fn hits_for<'t>(
a: &'t Tree,
ag: &Aggregates,
terms: &[String],
) -> Vec<(&'t Node, Vec<&'static str>)> {
let mut hits: Vec<(&Node, Vec<&'static str>)> = Vec::new();
for n in a.nodes_iter() {
let lowered: Vec<(&'static str, String)> = searchable(a, n)
.iter()
.filter(|(_, v)| !v.is_empty())
.map(|(k, v)| (*k, v.to_lowercase()))
.collect();
if !terms
.iter()
.all(|t| lowered.iter().any(|(_, v)| v.contains(t.as_str())))
{
continue;
}
let matched: Vec<&'static str> = lowered
.iter()
.filter(|(_, v)| terms.iter().any(|t| v.contains(t.as_str())))
.map(|(k, _)| *k)
.collect();
hits.push((n, matched));
}
hits.sort_by_key(|(n, matched)| {
(
field_order(matched[0]),
std::cmp::Reverse(ag.counts(n.num).total),
std::cmp::Reverse(n.num),
)
});
hits
}
fn lineage_of(a: &Tree, n: &Node) -> Vec<String> {
let line = a.ancestors(n.num);
line[..line.len().saturating_sub(1)]
.iter()
.map(|p| p.alias())
.collect()
}
fn hit_json(a: &Tree, n: &Node, matched: &[&'static str], terms: &[String]) -> serde_json::Value {
let fragments: serde_json::Map<String, serde_json::Value> = matched
.iter()
.map(|field| {
let text = searchable(a, n)
.iter()
.find(|(k, _)| k == field)
.map(|(_, v)| *v)
.unwrap_or_default();
(field.to_string(), json!(snippet(text, terms, WIDTH)))
})
.collect();
json!({
"alias": n.alias(),
"kind": n.kind,
"state": n.state,
"title": n.title(a),
"lineage": lineage_of(a, n),
"matched": fragments,
})
}
pub fn find_data(a: &Tree, query: &str) -> Result<serde_json::Value, Failure> {
let terms = terms_of(query)?;
let ag = &a.aggregates();
Ok(json!(hits_for(a, ag, &terms)
.iter()
.map(|(n, matched)| hit_json(a, n, matched, &terms))
.collect::<Vec<_>>()))
}
pub fn find(a: &Tree, args: &Args) -> R {
let query = args
.positional(0)
.ok_or_else(|| Failure::usage("usage: vivac find \"<text>\"".to_string()))?;
let terms = terms_of(query)?;
if args.has("json") {
return print_json(find_data(a, query)?);
}
let ag = &a.aggregates();
let hits = hits_for(a, ag, &terms);
if hits.is_empty() {
outln!(" Nothing matches \"{query}\".");
return Ok(());
}
outln!();
outln!(
" {} match{} for \"{}\"",
hits.len(),
if hits.len() == 1 { "" } else { "es" },
query,
);
outln!();
for (n, matched) in hits.iter().take(20) {
outln!(" {:<6} {}", n.alias(), n.title(a));
let lineage = lineage_of(a, n);
if !lineage.is_empty() {
outln!(" via {}", lineage.join(" > "));
}
for field in matched.iter().filter(|f| **f != "title") {
let text = searchable(a, n)
.iter()
.find(|(k, _)| k == field)
.map(|(_, v)| *v)
.unwrap_or_default();
outln!(" {}: {}", field, snippet(text, &terms, WIDTH));
}
}
if hits.len() > 20 {
outln!();
outln!(
" ... and {} more vivac find \"...\" --json",
hits.len() - 20
);
}
outln!();
Ok(())
}
pub(crate) fn project_name(root: &std::path::Path) -> String {
root.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "-".into())
}
fn find_data_everywhere(projects: &[(String, Tree)], terms: &[String]) -> serde_json::Value {
let mut hits = Vec::new();
for (name, tree) in projects {
let ag = &tree.aggregates();
for (n, matched) in hits_for(tree, ag, terms) {
let mut hit = hit_json(tree, n, &matched, terms);
if let serde_json::Value::Object(fields) = &mut hit {
fields.insert("project".to_string(), json!(name));
}
hits.push(hit);
}
}
json!(hits)
}
pub fn find_everywhere_data(query: &str) -> Result<serde_json::Value, Failure> {
let terms = terms_of(query)?;
let known_roots = crate::store::store_dir()
.map(|d| crate::registry::roots(&d))
.unwrap_or_default();
let mut projects: Vec<(String, Tree)> = Vec::new();
for root in known_roots {
let name = project_name(&root);
if let Ok(tree) =
crate::store::Store::open(root).and_then(|s| crate::index::load(&s, false))
{
projects.push((name, tree));
}
}
projects.sort_by(|x, y| x.0.cmp(&y.0));
Ok(find_data_everywhere(&projects, &terms))
}
pub fn find_everywhere(a: &Args) -> R {
let query = a
.positional(0)
.ok_or_else(|| Failure::usage("usage: vivac find \"<text>\"".to_string()))?;
let terms = terms_of(query)?;
let known_roots = crate::store::store_dir()
.map(|d| crate::registry::roots(&d))
.unwrap_or_default();
let mut projects: Vec<(String, Tree)> = Vec::new();
let mut unreachable: Vec<String> = Vec::new();
for root in known_roots {
let name = project_name(&root);
match crate::store::Store::open(root).and_then(|s| crate::index::load(&s, false)) {
Ok(tree) => projects.push((name, tree)),
Err(_) => unreachable.push(name),
}
}
projects.sort_by(|x, y| x.0.cmp(&y.0));
unreachable.sort();
if a.has("json") {
return print_json(find_data_everywhere(&projects, &terms));
}
type ProjectHits<'t> = (&'t str, &'t Tree, Vec<(&'t Node, Vec<&'static str>)>);
let sections: Vec<ProjectHits> = projects
.iter()
.filter_map(|(name, tree)| {
let ag = &tree.aggregates();
let hits = hits_for(tree, ag, &terms);
(!hits.is_empty()).then_some((name.as_str(), tree, hits))
})
.collect();
let total: usize = sections.iter().map(|(_, _, hits)| hits.len()).sum();
if total == 0 {
outln!(" Nothing matches \"{query}\".");
} else {
outln!();
outln!(
" {} match{} for \"{}\" across {} project{}",
total,
if total == 1 { "" } else { "es" },
query,
sections.len(),
if sections.len() == 1 { "" } else { "s" },
);
for (name, tree, hits) in §ions {
outln!();
outln!(" {name}");
for (n, matched) in hits.iter().take(20) {
outln!(" {:<6} {}", n.alias(), n.title(tree));
let lineage = lineage_of(tree, n);
if !lineage.is_empty() {
outln!(" via {}", lineage.join(" > "));
}
for field in matched.iter().filter(|f| **f != "title") {
let text = searchable(tree, n)
.iter()
.find(|(k, _)| k == field)
.map(|(_, v)| *v)
.unwrap_or_default();
outln!(" {}: {}", field, snippet(text, &terms, WIDTH));
}
}
if hits.len() > 20 {
outln!(
" ... and {} more vivac find \"...\" --everywhere --json",
hits.len() - 20
);
}
}
outln!();
}
if !unreachable.is_empty() {
outln!(
" {} project{} unreachable: {}",
unreachable.len(),
if unreachable.len() == 1 { "" } else { "s" },
unreachable.join(", ")
);
outln!();
}
Ok(())
}