use crate::anchor::{self, Anchor};
use crate::event::{Arm, Body, Event, 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};
use std::path::Path;
pub struct Ctx {
pub store: Store,
pub tree: Tree,
pub anchor: Box<dyn Anchor>,
}
impl Ctx {
pub fn load(store: Store) -> Result<Ctx, Failure> {
Ctx::load_opt(store, true)
}
pub fn load_for_write(store: Store) -> Result<Ctx, Failure> {
Ctx::load_opt(store, false)
}
fn load_opt(store: Store, allow_index_refresh: bool) -> Result<Ctx, Failure> {
let tree = crate::index::load(&store, allow_index_refresh)?;
let anchor = anchor::detect(&store.root);
Ok(Ctx {
store,
tree,
anchor,
})
}
pub fn load_with_log(store: Store) -> Result<(Ctx, Vec<Event>), Failure> {
let (events, broken) = store.read_all()?;
let tree = fold(&events, broken);
let anchor = anchor::detect(&store.root);
Ok((
Ctx {
store,
tree,
anchor,
},
events,
))
}
fn emit(&mut self, bodies: Vec<Body>) -> R {
let already_governed = self.tree.has_governance;
self.store
.append(bodies.clone(), self.tree.seq, already_governed)?;
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(|&num| ctx.tree.node_by_num(num))
.map(|n| (n.alias(), n.title(&ctx.tree).to_string()))
.collect();
let mut working_set: Vec<String> = ctx
.tree
.stack
.iter()
.filter_map(|&num| ctx.tree.node_by_num(num))
.flat_map(|n| n.governs(&ctx.tree).into_iter().map(str::to_string))
.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 guarded_or_refused(field: &str, text: &str) -> String {
match redact::check_field(field, text) {
Some(f) => format!("refused: {}", f.rule),
None => text.to_string(),
}
}
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))),
}
}
fn validate_arm_text(s: &str) -> Result<(), Failure> {
if s.trim().is_empty() {
return Err(Failure::usage("An arm cannot be empty."));
}
if s.contains('\n') {
return Err(Failure::usage(
"An arm is one line: write it the way it would be typed.",
));
}
Ok(())
}
fn is_absolute_arm_dir(slashed: &str) -> bool {
if slashed.starts_with('/') || slashed.starts_with('~') {
return true;
}
let mut chars = slashed.chars();
matches!(
(chars.next(), chars.next()),
(Some(letter), Some(':')) if letter.is_ascii_alphabetic()
)
}
fn normalize_arm_dir(slashed: &str) -> String {
let parts: Vec<&str> = slashed
.split('/')
.filter(|c| !c.is_empty() && *c != ".")
.collect();
if parts.is_empty() {
".".to_string()
} else {
parts.join("/")
}
}
fn validate_arm_dir(raw: &str, tree_root: &Path) -> Result<String, Failure> {
let slashed = raw.replace('\\', "/");
if is_absolute_arm_dir(&slashed) {
return Err(Failure::usage(
"An arm's folder is relative to the one that holds .vivac: an \
absolute path would write this machine's layout into the log.",
));
}
if slashed.split('/').any(|c| c == "..") {
return Err(Failure::usage(
"An arm's folder has to be inside the one that holds .vivac.",
));
}
let normalized = normalize_arm_dir(&slashed);
guard_text(&[("dir", &normalized)])?;
if !tree_root.join(&normalized).is_dir() {
return Err(Failure::usage(format!(
"There is no folder {normalized} inside the one that holds .vivac."
)));
}
Ok(normalized)
}
fn needs_arm_dir_message(via_mcp: bool) -> String {
let flag = if via_mcp { "arm_dir" } else { "--arm-dir" };
format!(
"An arm needs {flag}: the folder it runs in, relative to the one \
that holds .vivac. Use . for that folder itself."
)
}
fn arm_dir_without_arm_message(via_mcp: bool) -> String {
let (dir_flag, arm_flag) = if via_mcp {
("arm_dir", "arm")
} else {
("--arm-dir", "--arm")
};
format!("{dir_flag} says where an arm runs, and no {arm_flag} was given.")
}
fn needs_dir_message(via_mcp: bool) -> String {
let flag = if via_mcp { "dir" } else { "--dir" };
format!(
"An arm needs {flag}: the folder it runs in, relative to the one \
that holds .vivac. Use . for that folder itself."
)
}
fn arms_of(
ctx: &Ctx,
raw: Vec<String>,
dir: Option<String>,
kind: Kind,
via_mcp: bool,
) -> Result<Vec<Arm>, Failure> {
if !raw.is_empty() && kind != Kind::Rule {
return Err(Failure::usage(format!(
"Only a rule has an arm; this would be {}.",
kind.with_article()
)));
}
if raw.is_empty() {
if dir.is_some() {
return Err(Failure::usage(arm_dir_without_arm_message(via_mcp)));
}
return Ok(vec![]);
}
let dir = match dir {
Some(d) if !d.trim().is_empty() => d,
_ => return Err(Failure::usage(needs_arm_dir_message(via_mcp))),
};
let normalized = validate_arm_dir(&dir, &ctx.store.root)?;
for (i, a) in raw.iter().enumerate() {
validate_arm_text(a)?;
if raw[..i].contains(a) {
return Err(Failure::usage(format!("The same arm is given twice: {a}")));
}
}
Ok(raw
.into_iter()
.map(|command| Arm {
dir: normalized.clone(),
command,
})
.collect())
}
struct Born<'a> {
title: &'a str,
why: &'a str,
kind: Kind,
parent: Option<String>,
refs: Vec<String>,
governs: Vec<String>,
blocks: bool,
arms: Vec<Arm>,
}
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())));
fields.extend(b.arms.iter().map(|a| ("arm", a.command.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,
arms: b.arms,
},
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 arms = arms_of(ctx, p.arms, p.arm_dir, kind, p.via_mcp)?;
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,
arms,
},
)?;
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.stack_bottom().map(|root| outcome::DepthAdvice {
depth: depth_of,
root_alias: root.alias(),
root_title: root.title(&ctx.tree).to_string(),
})
} 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 focus.parent.and_then(|p| ctx.tree.node_by_num(p)) {
Some(parent) => Some(outcome::PoppedTo {
alias: parent.alias(),
title: parent.title(&ctx.tree).to_string(),
counts: ctx.tree.counts(parent.num),
}),
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.num) {
evs.push(Body::Popped {
node: node.id.clone(),
});
}
ctx.emit(evs)?;
Ok(Outcome::Parked {
alias: node.alias(),
title: node.title(&ctx.tree).to_string(),
})
}
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.num);
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(&ctx.tree)));
}
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.num) {
evs.push(Body::Popped { node: n.id.clone() });
}
ctx.emit(evs)?;
Ok(crate::outcome::Closed {
alias: n.alias(),
title: n.title(&ctx.tree).to_string(),
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 arms = arms_of(ctx, p.arms, p.arm_dir, kind, p.via_mcp)?;
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,
arms,
},
)?;
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(&ctx.tree).to_string(),
});
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.and_then(|p| ctx.tree.node_by_num(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(&ctx.tree).to_string());
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
.and_then(|id| ctx.tree.node_by_num(id))
.map(|parent| outcome::StillBornFrom {
alias: parent.alias(),
title: parent.title(&ctx.tree).to_string(),
});
Ok(Outcome::Promoted {
alias: n.alias(),
title: n.title(&ctx.tree).to_string(),
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, r_num, ralias) = (r.id.clone(), r.num, 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.num).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(r_num) {
rescued.insert(d.id.clone());
}
}
let (falling, saved): (Vec<&Node>, Vec<&Node>) = ctx
.tree
.descendants(n.num)
.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(&ctx.tree),
falling.len()
);
for d in &falling {
m.push_str(&format!("\n {:<6} {}", d.alias(), d.title(&ctx.tree)));
}
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(&ctx.tree).to_string()))
.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<(u64, String)> = vec![(n.num, n.id.clone())];
out_of_scope.extend(
ctx.tree
.descendants(n.num)
.iter()
.map(|d| (d.num, d.id.clone())),
);
for (num, id) in out_of_scope {
if ctx.tree.stack.contains(&num) {
evs.push(Body::Popped { node: id });
}
}
ctx.emit(evs)?;
Ok(Outcome::Abandoned {
alias: n.alias(),
title: n.title(&ctx.tree).to_string(),
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<(u64, String)> = ctx
.tree
.ancestors(n.num)
.iter()
.map(|p| (p.num, p.id.clone()))
.collect();
let mut evs: Vec<Body> = ctx
.tree
.stack
.iter()
.filter(|num| !lineage.iter().any(|(lineage_num, _)| lineage_num == *num))
.filter_map(|&num| ctx.tree.node_by_num(num))
.map(|n| Body::Popped { node: n.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 (num, id) in &lineage {
if !ctx.tree.stack.contains(num) {
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(&ctx.tree).to_string(),
reason,
},
})
}
pub fn arm(ctx: &mut Ctx, p: params::Arm) -> Result<Outcome, Failure> {
let n = ctx.resolve(&p.id)?.clone();
if n.kind != Kind::Rule {
return Err(Failure::usage(format!(
"Only a rule has an arm; {} is {}.",
n.alias(),
n.kind.with_article()
)));
}
let dir = match &p.dir {
Some(d) if !d.trim().is_empty() => d.clone(),
_ => return Err(Failure::usage(needs_dir_message(p.via_mcp))),
};
let dir = validate_arm_dir(&dir, &ctx.store.root)?;
validate_arm_text(&p.command)?;
let has = n
.arms(&ctx.tree)
.contains(&(dir.as_str(), p.command.as_str()));
if p.off && !has {
return Err(Failure::usage(format!(
"{0} has no such arm; vivac why {0} lists the ones it has.",
n.alias()
)));
}
if !p.off && has {
return Err(Failure::usage(format!(
"{} already has that arm.",
n.alias()
)));
}
guard_text(&[("arm", &p.command)])?;
let body = if p.off {
Body::ArmRemoved {
node: n.id.clone(),
dir: dir.clone(),
command: p.command.clone(),
}
} else {
Body::ArmAdded {
node: n.id.clone(),
dir: dir.clone(),
command: p.command.clone(),
}
};
ctx.emit(vec![body])?;
Ok(Outcome::Armed {
alias: n.alias(),
dir,
arm: p.command,
change: if p.off {
outcome::ArmChange::Removed
} else {
outcome::ArmChange::Added
},
})
}
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 = match &p.parent {
Some(s) => Some(ctx.resolve(s)?.id.clone()),
None => 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,
arms: vec![],
},
)?;
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.num, 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(|num| !lineage.iter().any(|(lineage_num, _)| lineage_num == *num))
.filter_map(|&num| ctx.tree.node_by_num(num))
.map(|n| Body::Popped { node: n.id.clone() })
.collect();
for (num, id) in &lineage {
if !ctx.tree.stack.contains(num) {
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 source = guarded_or_refused("source", source);
let session = session.map(|s| guarded_or_refused("session", &s));
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,
focus,
vivac,
session,
}])?;
Ok(Outcome::SessionOpened)
}