use crate::anchor::{self, Anchor};
use crate::event::{Body, Flag, Kind, State, VivacKind};
use crate::failure::{Failure, R};
use crate::model::{fold, Node, Tree};
use crate::outcome::{self, Outcome};
use crate::params;
use crate::store::Store;
use crate::{id, redact};
pub struct Ctx {
pub store: Store,
pub tree: Tree,
pub anchor: Box<dyn Anchor>,
}
impl Ctx {
pub fn load(store: Store) -> Result<Ctx, Failure> {
let (events, broken) = store.read_all()?;
let tree = fold(&events, broken);
let anchor = anchor::detect(&store.root);
Ok(Ctx {
store,
tree,
anchor,
})
}
fn emit(&mut self, bodies: Vec<Body>) -> R {
self.store.append(bodies.clone(), self.tree.seq)?;
let ts = crate::clock::now_rfc3339();
for c in &bodies {
let seq = self.tree.seq + 1;
self.tree.apply(seq, &ts, c);
}
Ok(())
}
fn resolve(&self, s: &str) -> Result<&crate::model::Node, Failure> {
self.tree
.resolve(s)
.ok_or_else(|| Failure::usage(format!("No such node: {s}.")))
}
}
fn vivac(
ctx: &Ctx,
kind: VivacKind,
next_intent: &str,
node_ref: Option<String>,
label: &str,
) -> Body {
let stack: Vec<(String, String)> = ctx
.tree
.stack
.iter()
.filter_map(|id| ctx.tree.node(id))
.map(|n| (n.alias(), n.title.clone()))
.collect();
let mut working_set: Vec<String> = ctx
.tree
.stack
.iter()
.filter_map(|id| ctx.tree.node(id))
.flat_map(|n| n.governs.iter().cloned())
.collect();
working_set.sort();
working_set.dedup();
Body::VivacCreated {
vivac: id::ulid(),
num: ctx.tree.next_vivac_num.max(1),
kind,
stack,
working_set,
next_intent: next_intent.to_string(),
anchor: ctx.anchor.snapshot(),
node_ref,
label: label.to_string(),
}
}
fn guard_text(fields: &[(&str, &str)]) -> R {
match redact::check_fields(fields) {
Some(h) => Err(Failure::Redaction(Box::new(h))),
None => Ok(()),
}
}
fn kind_of(raw: Option<&str>, fallback: Kind) -> Result<Kind, Failure> {
match raw {
None => Ok(fallback),
Some(s) => Kind::parse(s)
.ok_or_else(|| Failure::usage(format!("Unknown type: {s}. They are: {}", Kind::ALL))),
}
}
struct Born<'a> {
title: &'a str,
why: &'a str,
kind: Kind,
parent: Option<String>,
refs: Vec<String>,
governs: Vec<String>,
blocks: bool,
}
fn born(ctx: &Ctx, b: Born) -> Result<(Body, u64, String), Failure> {
let mut fields: Vec<(&str, &str)> = vec![("title", b.title), ("why", b.why)];
fields.extend(b.refs.iter().map(|r| ("ref", r.as_str())));
fields.extend(b.governs.iter().map(|g| ("governs", g.as_str())));
guard_text(&fields)?;
let node = id::ulid();
let num = ctx.tree.next_num.max(1);
Ok((
Body::NodeCreated {
node: node.clone(),
num,
kind: b.kind,
title: b.title.to_string(),
why: b.why.to_string(),
parent: b.parent,
blocks: b.blocks,
refs: b.refs,
governs: b.governs,
},
num,
node,
))
}
pub fn push(ctx: &mut Ctx, p: params::Push) -> Result<Outcome, Failure> {
let parent = ctx.tree.focus().map(|n| n.id.clone());
let kind = kind_of(
p.kind.as_deref(),
if parent.is_none() {
Kind::Goal
} else {
Kind::Task
},
)?;
let (ev, num, node) = born(
ctx,
Born {
title: &p.title,
why: &p.why,
kind,
parent: parent.clone(),
refs: p.refs,
governs: p.governs,
blocks: p.blocks,
},
)?;
let v = vivac(ctx, VivacKind::Push, &p.title, parent, "");
ctx.emit(vec![v, ev, Body::Pushed { node }])?;
let depth_of = ctx.tree.stack_depth();
let advice = if depth_of >= 4 {
ctx.tree.roots().first().map(|root| outcome::DepthAdvice {
depth: depth_of,
root_alias: root.alias(),
root_title: root.title.clone(),
})
} else {
None
};
Ok(Outcome::Pushed {
alias: format!("{}{}", kind.prefix(), num),
title: p.title,
blocks: p.blocks,
advice,
})
}
pub fn pop(ctx: &mut Ctx, p: params::Pop) -> Result<Outcome, Failure> {
let focus = ctx
.tree
.focus()
.ok_or_else(|| {
Failure::usage(
"The stack is empty. Open something: vivac push \"<title>\" --why \"<reason>\"",
)
})?
.clone();
let outcome_text = p.outcome.as_str();
let next = p.next.as_deref().unwrap_or(outcome_text);
guard_text(&[("outcome", outcome_text), ("next", next)])?;
let v = vivac(ctx, VivacKind::Pop, next, Some(focus.id.clone()), "");
let closed = close_node(ctx, &focus, outcome_text, p.force, true)?;
ctx.emit(vec![v])?;
let parent = match ctx.tree.node(focus.parent.as_deref().unwrap_or("")) {
Some(parent) => Some(outcome::PoppedTo {
alias: parent.alias(),
title: parent.title.clone(),
counts: ctx.tree.counts(&parent.id),
}),
None => None,
};
Ok(Outcome::Popped { closed, parent })
}
fn looks_like_an_id(s: &str) -> bool {
let s = s.trim().trim_start_matches('#');
let mut c = s.chars();
let Some(first) = c.next() else {
return false;
};
let rest = c.as_str();
if first.is_ascii_digit() {
return rest.chars().all(|c| c.is_ascii_digit());
}
!rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())
}
fn named_or_focus(
ctx: &Ctx,
node: Option<&str>,
reason: Option<&str>,
usage: &'static str,
) -> Result<(Node, String), Failure> {
let focus = || {
ctx.tree
.focus()
.cloned()
.ok_or_else(|| Failure::usage(usage))
};
match (node, reason) {
(Some(s), Some(r)) => Ok((ctx.resolve(s)?.clone(), r.to_string())),
(Some(w), None) => match ctx.tree.resolve(w) {
Some(n) => Ok((n.clone(), String::new())),
None if looks_like_an_id(w) => Err(Failure::usage(format!("No such node: {w}."))),
None => Ok((focus()?, w.to_string())),
},
_ => Ok((focus()?, String::new())),
}
}
pub fn park(ctx: &mut Ctx, p: params::Park) -> Result<Outcome, Failure> {
let (node, reason) = named_or_focus(
ctx,
p.node.as_deref(),
p.reason.as_deref(),
"usage: vivac park [<id>] [\"<reason>\"]",
)?;
let reason = reason.as_str();
guard_text(&[("reason", reason)])?;
let mut evs = vec![vivac(
ctx,
VivacKind::Park,
reason,
Some(node.id.clone()),
"",
)];
evs.push(Body::StateChanged {
node: node.id.clone(),
state: State::Suspended,
outcome: reason.to_string(),
forced: false,
});
if ctx.tree.stack.contains(&node.id) {
evs.push(Body::Popped {
node: node.id.clone(),
});
}
ctx.emit(evs)?;
Ok(Outcome::Parked {
alias: node.alias(),
title: node.title,
})
}
fn close_node(
ctx: &mut Ctx,
n: &crate::model::Node,
outcome: &str,
force: bool,
unstack: bool,
) -> Result<crate::outcome::Closed, Failure> {
if !force {
let pending_count = ctx.tree.open_blockers(&n.id);
if !pending_count.is_empty() {
let mut m = format!(
" {} CANNOT close: {} open closure condition(s)\n",
n.alias(),
pending_count.len()
);
for c in &pending_count {
m.push_str(&format!("\n {:<6} {}", c.alias(), c.title));
}
m.push_str(&format!(
"\n\n A run closes with its findings, not with its report.\n \
Closing it anyway leaves a trace: vivac done {} --force",
n.num
));
return Err(Failure::Model(m));
}
}
let mut evs = vec![Body::StateChanged {
node: n.id.clone(),
state: State::Done,
outcome: outcome.to_string(),
forced: force,
}];
if unstack && ctx.tree.stack.contains(&n.id) {
evs.push(Body::Popped { node: n.id.clone() });
}
ctx.emit(evs)?;
Ok(crate::outcome::Closed {
alias: n.alias(),
title: n.title.clone(),
force,
})
}
pub fn done(ctx: &mut Ctx, p: params::Done) -> Result<Outcome, Failure> {
let n = ctx.resolve(&p.id)?.clone();
guard_text(&[("outcome", &p.outcome)])?;
let closed = close_node(ctx, &n, &p.outcome, p.force, true)?;
Ok(Outcome::Done { closed })
}
pub fn add(ctx: &mut Ctx, p: params::Add) -> Result<Outcome, Failure> {
let parent = match &p.parent {
Some(s) => Some(ctx.resolve(s)?.id.clone()),
None => ctx.tree.focus().map(|n| n.id.clone()),
};
let kind = kind_of(
p.kind.as_deref(),
if parent.is_none() {
Kind::Goal
} else {
Kind::Task
},
)?;
let (ev, num, _) = born(
ctx,
Born {
title: &p.title,
why: &p.why,
kind,
parent: parent.clone(),
refs: p.refs,
governs: p.governs,
blocks: p.blocks,
},
)?;
ctx.emit(vec![ev])?;
let parent_info = parent
.and_then(|id| ctx.tree.node(&id))
.map(|n| outcome::AddedUnder {
alias: n.alias(),
title: n.title.clone(),
});
Ok(Outcome::Added {
alias: format!("{}{}", kind.prefix(), num),
title: p.title,
parent: parent_info,
blocks: p.blocks,
})
}
pub fn note(ctx: &mut Ctx, p: params::Note) -> Result<Outcome, Failure> {
let (n, note) = match (p.node.as_deref(), p.note.as_deref()) {
(Some(s), Some(t)) => (ctx.resolve(s)?.clone(), t.to_string()),
(Some(t), None) => {
let f = ctx
.tree
.focus()
.ok_or_else(|| Failure::usage("usage: vivac note [<id>] \"<note>\""))?;
(f.clone(), t.to_string())
}
_ => return Err(Failure::usage("usage: vivac note [<id>] \"<note>\"")),
};
guard_text(&[("note", ¬e)])?;
ctx.emit(vec![Body::NodeNoted {
node: n.id.clone(),
note,
}])?;
Ok(Outcome::Noted { alias: n.alias() })
}
pub fn block(ctx: &mut Ctx, p: params::Block) -> Result<Outcome, Failure> {
let n = ctx.resolve(&p.id)?.clone();
let Some(parent) = n.parent.as_ref().and_then(|p| ctx.tree.node(p)) else {
return Err(Failure::usage(format!(
"{} is the root: there is no parent to block.",
n.alias()
)));
};
let blocks = !p.off;
let (pa, pt) = (parent.alias(), parent.title.clone());
ctx.emit(vec![Body::BlockChanged {
node: n.id.clone(),
blocks,
}])?;
Ok(Outcome::Blocked {
alias: n.alias(),
blocks,
parent_alias: pa,
parent_title: pt,
})
}
pub fn promote(ctx: &mut Ctx, p: params::Promote) -> Result<Outcome, Failure> {
let n = match p.id {
Some(s) => ctx.resolve(&s)?.clone(),
None => ctx
.tree
.focus()
.ok_or_else(|| Failure::usage("usage: vivac promote [<id>]"))?
.clone(),
};
ctx.emit(vec![Body::Promoted { node: n.id.clone() }])?;
let parent = n
.parent
.as_ref()
.and_then(|id| ctx.tree.node(id))
.map(|parent| outcome::StillBornFrom {
alias: parent.alias(),
title: parent.title.clone(),
});
Ok(Outcome::Promoted {
alias: n.alias(),
title: n.title,
parent,
})
}
pub fn abandon(ctx: &mut Ctx, p: params::Abandon) -> Result<Outcome, Failure> {
let (n, reason) = named_or_focus(
ctx,
p.node.as_deref(),
p.reason.as_deref(),
"usage: vivac abandon [<id>] \"<reason>\"",
)?;
let reason = reason.as_str();
guard_text(&[("reason", reason)])?;
let mut rescued: std::collections::HashSet<String> = Default::default();
for s in p.rescue {
let r = ctx
.tree
.resolve(&s)
.ok_or_else(|| Failure::usage(format!("no such node: {s}")))?;
let (rid, ralias) = (r.id.clone(), r.alias());
if rid == n.id {
return Err(Failure::usage(format!(
"{ralias} is the one being abandoned; it cannot be rescued from itself"
)));
}
if !ctx.tree.descendants(&n.id).iter().any(|d| d.id == rid) {
return Err(Failure::usage(format!(
"{ralias} does not hang off {}: there is nothing to rescue it from",
n.alias()
)));
}
rescued.insert(rid.clone());
for d in ctx.tree.descendants(&rid) {
rescued.insert(d.id.clone());
}
}
let (falling, saved): (Vec<&Node>, Vec<&Node>) = ctx
.tree
.descendants(&n.id)
.into_iter()
.filter(|d| d.state.is_open())
.partition(|d| !rescued.contains(&d.id));
if !falling.is_empty() && !p.cascade {
let mut m = format!(
" {} {}\n has {} open descendant(s) with no rescue:\n",
n.alias(),
n.title,
falling.len()
);
for d in &falling {
m.push_str(&format!("\n {:<6} {}", d.alias(), d.title));
}
m.push_str("\n\n Abandon all of it: vivac abandon ");
m.push_str(&n.num.to_string());
m.push_str(" --cascade");
m.push_str("\n Save some of it: vivac abandon ");
m.push_str(&n.num.to_string());
m.push_str(" --rescue <id>");
m.push_str("\n Save it as a goal: vivac promote <id>");
return Err(Failure::Model(m));
}
let mut evs = vec![Body::StateChanged {
node: n.id.clone(),
state: State::Abandoned,
outcome: reason.to_string(),
forced: false,
}];
let falling_count = falling.len();
let saved_lines: Vec<(String, String)> =
saved.iter().map(|d| (d.alias(), d.title.clone())).collect();
for d in falling {
evs.push(Body::StateChanged {
node: d.id.clone(),
state: State::Abandoned,
outcome: format!("cascaded from {}", n.alias()),
forced: false,
});
}
let mut out_of_scope: Vec<String> = vec![n.id.clone()];
out_of_scope.extend(ctx.tree.descendants(&n.id).iter().map(|d| d.id.clone()));
for id in out_of_scope {
if ctx.tree.stack.contains(&id) {
evs.push(Body::Popped { node: id });
}
}
ctx.emit(evs)?;
Ok(Outcome::Abandoned {
alias: n.alias(),
title: n.title,
cascaded: (falling_count > 0).then_some(falling_count),
rescued: saved_lines
.into_iter()
.map(|(alias, title)| outcome::RescuedNode { alias, title })
.collect(),
})
}
pub fn focus(ctx: &mut Ctx, p: params::Focus) -> Result<Outcome, Failure> {
let n = ctx.resolve(&p.id)?.clone();
if !n.state.is_open() && !p.reopen {
if n.state != State::Suspended {
return Err(Failure::Model(format!(
" {} is {}. Going back into it undoes that claim.\n\n \
If it really was not finished: vivac focus {} --reopen",
n.alias(),
n.state.word(n.kind),
n.num
)));
}
}
let lineage: Vec<String> = ctx
.tree
.ancestors(&n.id)
.iter()
.map(|p| p.id.clone())
.collect();
let mut evs: Vec<Body> = ctx
.tree
.stack
.iter()
.filter(|id| !lineage.contains(id))
.map(|id| Body::Popped { node: id.clone() })
.collect();
if !n.state.is_open() {
evs.push(Body::StateChanged {
node: n.id.clone(),
state: State::Active,
outcome: String::new(),
forced: false,
});
}
for id in &lineage {
if !ctx.tree.stack.contains(id) {
evs.push(Body::Pushed { node: id.clone() });
}
}
let revived = !n.state.is_open();
ctx.emit(evs)?;
Ok(Outcome::Focused {
alias: n.alias(),
revived,
})
}
pub fn flag(ctx: &mut Ctx, p: params::Flag) -> Result<Outcome, Failure> {
let n = ctx.resolve(&p.id)?.clone();
let flag = Flag::parse(&p.flag).ok_or_else(|| {
Failure::usage(format!("Unknown flag: {}. They are: {}", p.flag, Flag::ALL))
})?;
if p.off {
ctx.emit(vec![Body::FlagCleared {
node: n.id.clone(),
flag,
}])?;
return Ok(Outcome::Flagged {
alias: n.alias(),
flag: flag.word().to_string(),
change: outcome::FlagChange::Off,
});
}
let reason = p.why.ok_or_else(|| {
Failure::usage(
"Missing --why. A flag with no reason informs nobody: in two weeks\n \
nobody will know what needed looking at, and they all get ignored.",
)
})?;
guard_text(&[("reason", &reason)])?;
ctx.emit(vec![Body::FlagRaised {
node: n.id.clone(),
flag,
reason: reason.clone(),
}])?;
Ok(Outcome::Flagged {
alias: n.alias(),
flag: flag.word().to_string(),
change: outcome::FlagChange::Raised {
title: n.title,
reason,
},
})
}
pub fn decide(ctx: &mut Ctx, p: params::Decide) -> Result<Outcome, Failure> {
let superseded = match &p.supersedes {
Some(s) => Some(ctx.resolve(s)?.clone()),
None => None,
};
let mut body = p.reason.clone();
if !p.alternatives.is_empty() {
body.push_str(&format!(" | discarded: {}", p.alternatives.join("; ")));
}
let parent = ctx.tree.focus().map(|n| n.id.clone());
let (ev, num, _) = born(
ctx,
Born {
title: &p.title,
why: &body,
kind: Kind::Decision,
parent,
refs: p.refs,
governs: p.governs,
blocks: p.blocks,
},
)?;
let mut evs = vec![ev];
if let Some(v) = &superseded {
evs.push(Body::StateChanged {
node: v.id.clone(),
state: State::Superseded,
outcome: format!("superseded by d{num}"),
forced: false,
});
}
ctx.emit(evs)?;
Ok(Outcome::Decided {
alias: format!("d{num}"),
title: p.title,
superseded: superseded.map(|v| outcome::SupersededNode { alias: v.alias() }),
no_alternatives: p.alternatives.is_empty(),
})
}
pub fn save(ctx: &mut Ctx, p: params::Save) -> Result<Outcome, Failure> {
guard_text(&[("label", &p.label), ("next", &p.next)])?;
let v = vivac(ctx, VivacKind::Manual, &p.next, None, &p.label);
let num = ctx.tree.next_vivac_num.max(1);
ctx.emit(vec![v])?;
let anchor = ctx.anchor.snapshot();
Ok(Outcome::Saved {
num,
label: p.label,
anchor,
next: p.next,
})
}
pub fn restore(ctx: &mut Ctx, p: params::Restore) -> Result<Outcome, Failure> {
let v = ctx
.tree
.vivac(&p.vivac)
.ok_or_else(|| Failure::usage(format!("No such vivac: {}.", p.vivac)))?
.clone();
let mut lineage = Vec::new();
let mut lost: Vec<outcome::LostNode> = Vec::new();
for (alias, title) in &v.stack {
let state = match ctx.tree.resolve(alias) {
Some(n) if n.state.is_open() => {
lineage.push(n.id.clone());
continue;
}
Some(n) => n.state.word(n.kind).to_string(),
None => "gone".to_string(),
};
lost.push(outcome::LostNode {
alias: alias.clone(),
title: title.clone(),
state,
});
}
let mut evs: Vec<Body> = ctx
.tree
.stack
.iter()
.filter(|id| !lineage.contains(id))
.map(|id| Body::Popped { node: id.clone() })
.collect();
for id in &lineage {
if !ctx.tree.stack.contains(id) {
evs.push(Body::Pushed { node: id.clone() });
}
}
let changes = ctx.anchor.changed_since(&v.anchor);
ctx.emit(evs)?;
let anchor = if v.anchor.is_empty_tree() {
outcome::RestoreAnchor::Empty
} else if changes.is_empty() {
outcome::RestoreAnchor::NoChanges {
anchor_short: v.anchor.short().to_string(),
}
} else {
outcome::RestoreAnchor::Changed {
anchor_short: v.anchor.short().to_string(),
changes: changes
.iter()
.map(|c| outcome::ChangeLine {
file_path: c.file_path.clone(),
times: c.times,
})
.collect(),
working_set: v.working_set.clone(),
}
};
Ok(Outcome::Restored {
alias: v.alias(),
kind: v.kind.word().to_string(),
ts: v.ts,
label: v.label,
next_intent: v.next_intent,
lost,
anchor,
})
}
pub fn auto_vivac(
ctx: &mut Ctx,
kind: VivacKind,
next: &str,
label: &str,
) -> Result<Outcome, Failure> {
guard_text(&[("next", next), ("label", label)])?;
let v = vivac(ctx, kind, next, None, label);
ctx.emit(vec![v])?;
Ok(Outcome::AutoStopped)
}
pub fn session_started(
ctx: &mut Ctx,
source: &str,
session: Option<String>,
) -> Result<Outcome, Failure> {
let focus = ctx.tree.focus().map(|n| n.id.clone());
let vivac = ctx.tree.vivacs.last().map(|v| v.id.clone());
ctx.emit(vec![Body::SessionStarted {
source: source.to_string(),
focus,
vivac,
session,
}])?;
Ok(Outcome::SessionOpened)
}