1382 lines
51 KiB
Rust
1382 lines
51 KiB
Rust
use std::collections::{HashMap, HashSet};
|
||
|
||
use anyhow::{Context, Result, bail};
|
||
use chrono::{Duration, Local, TimeZone};
|
||
use clap::ValueEnum;
|
||
use rusqlite::params;
|
||
|
||
use super::{Database, parse_action_spec, parse_columns_spec, parse_sections_spec};
|
||
use crate::{
|
||
filter,
|
||
macro_lang::{MacroRuntime, macro_name, parse_key_binding},
|
||
model::MacroDef,
|
||
};
|
||
|
||
const PRESET_VERSION: &str = "1";
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||
pub enum Preset {
|
||
Accounts,
|
||
Study,
|
||
Planner,
|
||
Recipes,
|
||
Rides,
|
||
People,
|
||
}
|
||
|
||
impl Preset {
|
||
pub const fn name(self) -> &'static str {
|
||
match self {
|
||
Self::Accounts => "accounts",
|
||
Self::Study => "study",
|
||
Self::Planner => "planner",
|
||
Self::Recipes => "recipes",
|
||
Self::Rides => "rides",
|
||
Self::People => "people",
|
||
}
|
||
}
|
||
}
|
||
|
||
struct PresetSpec {
|
||
title: &'static str,
|
||
description: &'static str,
|
||
categories: Vec<CategorySpec>,
|
||
views: Vec<ViewSpec>,
|
||
rules: Vec<RuleSpec>,
|
||
macros: Vec<MacroSpec>,
|
||
items: Vec<ItemSpec>,
|
||
}
|
||
|
||
struct CategorySpec {
|
||
name: &'static str,
|
||
parent: Option<&'static str>,
|
||
kind: &'static str,
|
||
match_text: &'static str,
|
||
exclusive: bool,
|
||
}
|
||
|
||
const fn category(
|
||
name: &'static str,
|
||
parent: Option<&'static str>,
|
||
kind: &'static str,
|
||
match_text: &'static str,
|
||
exclusive: bool,
|
||
) -> CategorySpec {
|
||
CategorySpec {
|
||
name,
|
||
parent,
|
||
kind,
|
||
match_text,
|
||
exclusive,
|
||
}
|
||
}
|
||
|
||
struct ViewSpec {
|
||
name: &'static str,
|
||
kind: &'static str,
|
||
filter_value: &'static str,
|
||
sort_key: &'static str,
|
||
show_done: bool,
|
||
filter_expr: &'static str,
|
||
columns: &'static str,
|
||
sections: &'static str,
|
||
}
|
||
|
||
impl ViewSpec {
|
||
const fn new(name: &'static str) -> Self {
|
||
Self {
|
||
name,
|
||
kind: "list",
|
||
filter_value: "",
|
||
sort_key: "manual",
|
||
show_done: true,
|
||
filter_expr: "",
|
||
columns: "item:48:Items,categories:27:Categories,when:20:When,priority:5:P",
|
||
sections: "",
|
||
}
|
||
}
|
||
|
||
const fn category(mut self, name: &'static str) -> Self {
|
||
self.kind = "category";
|
||
self.filter_value = name;
|
||
self
|
||
}
|
||
|
||
const fn kind(mut self, kind: &'static str, value: &'static str) -> Self {
|
||
self.kind = kind;
|
||
self.filter_value = value;
|
||
self
|
||
}
|
||
|
||
const fn sort(mut self, sort_key: &'static str) -> Self {
|
||
self.sort_key = sort_key;
|
||
self
|
||
}
|
||
|
||
const fn hide_done(mut self) -> Self {
|
||
self.show_done = false;
|
||
self
|
||
}
|
||
|
||
const fn filter(mut self, expression: &'static str) -> Self {
|
||
self.filter_expr = expression;
|
||
self
|
||
}
|
||
|
||
const fn columns(mut self, columns: &'static str) -> Self {
|
||
self.columns = columns;
|
||
self
|
||
}
|
||
|
||
const fn sections(mut self, sections: &'static str) -> Self {
|
||
self.sections = sections;
|
||
self
|
||
}
|
||
}
|
||
|
||
struct RuleSpec {
|
||
category: &'static str,
|
||
condition: &'static str,
|
||
action: &'static str,
|
||
}
|
||
|
||
const fn rule(category: &'static str, condition: &'static str, action: &'static str) -> RuleSpec {
|
||
RuleSpec {
|
||
category,
|
||
condition,
|
||
action,
|
||
}
|
||
}
|
||
|
||
struct MacroSpec {
|
||
name: &'static str,
|
||
source: &'static str,
|
||
key_binding: &'static str,
|
||
}
|
||
|
||
const fn agenda_macro(name: &'static str, source: &'static str) -> MacroSpec {
|
||
MacroSpec {
|
||
name,
|
||
source,
|
||
key_binding: "",
|
||
}
|
||
}
|
||
|
||
struct ItemSpec {
|
||
text: &'static str,
|
||
note: &'static str,
|
||
priority: i64,
|
||
when: Option<(i64, u32, u32)>,
|
||
numeric_value: Option<f64>,
|
||
recurrence: &'static str,
|
||
done: bool,
|
||
categories: &'static [&'static str],
|
||
}
|
||
|
||
impl ItemSpec {
|
||
const fn new(
|
||
text: &'static str,
|
||
note: &'static str,
|
||
categories: &'static [&'static str],
|
||
) -> Self {
|
||
Self {
|
||
text,
|
||
note,
|
||
priority: 3,
|
||
when: None,
|
||
numeric_value: None,
|
||
recurrence: "",
|
||
done: false,
|
||
categories,
|
||
}
|
||
}
|
||
|
||
const fn priority(mut self, priority: i64) -> Self {
|
||
self.priority = priority;
|
||
self
|
||
}
|
||
|
||
const fn when(mut self, days: i64, hour: u32, minute: u32) -> Self {
|
||
self.when = Some((days, hour, minute));
|
||
self
|
||
}
|
||
|
||
const fn value(mut self, value: f64) -> Self {
|
||
self.numeric_value = Some(value);
|
||
self
|
||
}
|
||
|
||
const fn recurring(mut self, recurrence: &'static str) -> Self {
|
||
self.recurrence = recurrence;
|
||
self
|
||
}
|
||
|
||
const fn done(mut self) -> Self {
|
||
self.done = true;
|
||
self
|
||
}
|
||
}
|
||
|
||
impl Database {
|
||
pub fn apply_preset(&mut self, preset: Preset) -> Result<()> {
|
||
if !self.new_document {
|
||
bail!("--preset can only initialize a new document; choose a path that does not exist");
|
||
}
|
||
let spec = preset_spec(preset);
|
||
validate_spec(&spec)?;
|
||
let now = Local::now().to_rfc3339();
|
||
let tx = self.conn.transaction()?;
|
||
tx.execute_batch(
|
||
"DELETE FROM macro_variables;
|
||
DELETE FROM macros;
|
||
DELETE FROM category_rules;
|
||
DELETE FROM view_sections;
|
||
DELETE FROM view_columns;
|
||
DELETE FROM views;
|
||
DELETE FROM dependencies;
|
||
DELETE FROM item_categories;
|
||
DELETE FROM items;
|
||
DELETE FROM categories;",
|
||
)?;
|
||
|
||
let mut category_ids = HashMap::new();
|
||
for (order, category) in spec.categories.iter().enumerate() {
|
||
let parent_id = category
|
||
.parent
|
||
.map(|parent| {
|
||
category_ids
|
||
.get(parent)
|
||
.copied()
|
||
.with_context(|| format!("preset category parent {parent:?} is missing"))
|
||
})
|
||
.transpose()?;
|
||
tx.execute(
|
||
"INSERT INTO categories(name,parent_id,kind,match_text,exclusive,sort_order)
|
||
VALUES(?1,?2,?3,?4,?5,?6)",
|
||
params![
|
||
category.name,
|
||
parent_id,
|
||
category.kind,
|
||
category.match_text,
|
||
category.exclusive as i64,
|
||
order as i64
|
||
],
|
||
)?;
|
||
category_ids.insert(category.name, tx.last_insert_rowid());
|
||
}
|
||
|
||
for (order, view) in spec.views.iter().enumerate() {
|
||
tx.execute(
|
||
"INSERT INTO views(name,kind,filter_value,sort_key,show_done,sort_order,filter_expr)
|
||
VALUES(?1,?2,?3,?4,?5,?6,?7)",
|
||
params![
|
||
view.name,
|
||
view.kind,
|
||
view.filter_value,
|
||
view.sort_key,
|
||
view.show_done as i64,
|
||
order as i64,
|
||
view.filter_expr
|
||
],
|
||
)?;
|
||
let view_id = tx.last_insert_rowid();
|
||
for (column_order, column) in parse_columns_spec(view.columns)?.iter().enumerate() {
|
||
tx.execute(
|
||
"INSERT INTO view_columns(view_id,field,heading,width,aggregate,sort_order)
|
||
VALUES(?1,?2,?3,?4,?5,?6)",
|
||
params![
|
||
view_id,
|
||
column.field,
|
||
column.heading,
|
||
column.width,
|
||
column.aggregate,
|
||
column_order as i64
|
||
],
|
||
)?;
|
||
}
|
||
for (section_order, section) in parse_sections_spec(view.sections)?.iter().enumerate() {
|
||
tx.execute(
|
||
"INSERT INTO view_sections(view_id,heading,filter_expr,collapsed,sort_order)
|
||
VALUES(?1,?2,?3,?4,?5)",
|
||
params![
|
||
view_id,
|
||
section.heading,
|
||
section.filter_expr,
|
||
section.collapsed as i64,
|
||
section_order as i64
|
||
],
|
||
)?;
|
||
}
|
||
}
|
||
|
||
for (order, preset_rule) in spec.rules.iter().enumerate() {
|
||
let category_id = category_ids[preset_rule.category];
|
||
let (action_kind, action_value) = parse_action_spec(preset_rule.action)?;
|
||
tx.execute(
|
||
"INSERT INTO category_rules(category_id,condition_expr,action_kind,action_value,enabled,sort_order)
|
||
VALUES(?1,?2,?3,?4,1,?5)",
|
||
params![
|
||
category_id,
|
||
preset_rule.condition,
|
||
action_kind,
|
||
action_value,
|
||
order as i64
|
||
],
|
||
)?;
|
||
}
|
||
|
||
for (order, preset_macro) in spec.macros.iter().enumerate() {
|
||
tx.execute(
|
||
"INSERT INTO macros(name,source,key_binding,sort_order) VALUES(?1,?2,?3,?4)",
|
||
params![
|
||
preset_macro.name,
|
||
preset_macro.source,
|
||
preset_macro.key_binding,
|
||
order as i64
|
||
],
|
||
)?;
|
||
}
|
||
|
||
for (order, item) in spec.items.iter().enumerate() {
|
||
let when_at = item
|
||
.when
|
||
.map(|(days, hour, minute)| preset_date(days, hour, minute))
|
||
.transpose()?;
|
||
let done_at = item.done.then(|| now.clone());
|
||
tx.execute(
|
||
"INSERT INTO items(text,note,priority,when_at,done_at,numeric_value,recurrence,created_at,updated_at,sort_order)
|
||
VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?8,?9)",
|
||
params![
|
||
item.text,
|
||
item.note,
|
||
item.priority,
|
||
when_at,
|
||
done_at,
|
||
item.numeric_value,
|
||
item.recurrence,
|
||
now,
|
||
order as i64
|
||
],
|
||
)?;
|
||
let item_id = tx.last_insert_rowid();
|
||
for category in item.categories {
|
||
tx.execute(
|
||
"INSERT INTO item_categories(item_id,category_id,assignment) VALUES(?1,?2,'explicit')",
|
||
params![item_id, category_ids[category]],
|
||
)?;
|
||
}
|
||
}
|
||
|
||
for (key, value) in [
|
||
("title", spec.title),
|
||
("document.description", spec.description),
|
||
("preset.name", preset.name()),
|
||
("preset.version", PRESET_VERSION),
|
||
] {
|
||
tx.execute(
|
||
"INSERT INTO meta(key,value) VALUES(?1,?2)
|
||
ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||
params![key, value],
|
||
)?;
|
||
}
|
||
tx.commit()?;
|
||
self.new_document = false;
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
fn preset_date(days: i64, hour: u32, minute: u32) -> Result<String> {
|
||
let date = Local::now().date_naive() + Duration::days(days);
|
||
let naive = date
|
||
.and_hms_opt(hour, minute, 0)
|
||
.context("preset contains an invalid time")?;
|
||
Local
|
||
.from_local_datetime(&naive)
|
||
.earliest()
|
||
.context("preset date does not exist in the local time zone")
|
||
.map(|date_time| date_time.to_rfc3339())
|
||
}
|
||
|
||
fn validate_spec(spec: &PresetSpec) -> Result<()> {
|
||
let mut categories = HashSet::new();
|
||
for category in &spec.categories {
|
||
if !categories.insert(category.name) {
|
||
bail!("preset has duplicate category {:?}", category.name);
|
||
}
|
||
if category
|
||
.parent
|
||
.is_some_and(|parent| !categories.contains(parent))
|
||
{
|
||
bail!("preset category parent must appear before its children");
|
||
}
|
||
if !matches!(category.kind, "standard" | "date" | "numeric") {
|
||
bail!("preset has unsupported category kind {:?}", category.kind);
|
||
}
|
||
}
|
||
|
||
let mut view_names = HashSet::new();
|
||
for view in &spec.views {
|
||
if !view_names.insert(view.name) {
|
||
bail!("preset has duplicate view {:?}", view.name);
|
||
}
|
||
filter::parse(view.filter_expr)?;
|
||
parse_columns_spec(view.columns)?;
|
||
parse_sections_spec(view.sections)?;
|
||
}
|
||
|
||
for preset_rule in &spec.rules {
|
||
if !categories.contains(preset_rule.category) {
|
||
bail!("preset rule category {:?} is missing", preset_rule.category);
|
||
}
|
||
filter::parse(preset_rule.condition)?;
|
||
parse_action_spec(preset_rule.action)?;
|
||
}
|
||
|
||
let definitions = spec
|
||
.macros
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(index, preset_macro)| {
|
||
let parsed_name = macro_name(preset_macro.source)?;
|
||
if !parsed_name.eq_ignore_ascii_case(preset_macro.name) {
|
||
bail!("preset macro name does not match its source");
|
||
}
|
||
if !preset_macro.key_binding.is_empty() {
|
||
parse_key_binding(preset_macro.key_binding)?;
|
||
}
|
||
Ok(MacroDef {
|
||
id: index as i64,
|
||
name: preset_macro.name.into(),
|
||
source: preset_macro.source.into(),
|
||
key_binding: preset_macro.key_binding.into(),
|
||
})
|
||
})
|
||
.collect::<Result<Vec<_>>>()?;
|
||
let mut macro_names = HashSet::new();
|
||
for definition in &definitions {
|
||
if !macro_names.insert(definition.name.to_lowercase()) {
|
||
bail!("preset has duplicate macro {:?}", definition.name);
|
||
}
|
||
MacroRuntime::new(&definition.name, &definitions, HashMap::new())?;
|
||
}
|
||
|
||
for item in &spec.items {
|
||
if item.text.trim().is_empty() {
|
||
bail!("preset item text cannot be empty");
|
||
}
|
||
if !(1..=5).contains(&item.priority) {
|
||
bail!("preset item priority is outside 1 through 5");
|
||
}
|
||
for category in item.categories {
|
||
if !categories.contains(category) {
|
||
bail!("preset item category {category:?} is missing");
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn preset_spec(preset: Preset) -> PresetSpec {
|
||
match preset {
|
||
Preset::Accounts => accounts(),
|
||
Preset::Study => study(),
|
||
Preset::Planner => planner(),
|
||
Preset::Recipes => recipes(),
|
||
Preset::Rides => rides(),
|
||
Preset::People => people(),
|
||
}
|
||
}
|
||
|
||
fn common_tail() -> Vec<ViewSpec> {
|
||
vec![
|
||
ViewSpec::new("Recently Done")
|
||
.kind("done", "14")
|
||
.sort("done"),
|
||
ViewSpec::new("Trash").kind("trash", "").sort("updated"),
|
||
]
|
||
}
|
||
|
||
fn accounts() -> PresetSpec {
|
||
let mut views = vec![
|
||
ViewSpec::new("All Items").sort("updated"),
|
||
ViewSpec::new("Accounts")
|
||
.category("Accounts")
|
||
.columns("item:36:Account,note:42:Profile,categories:28:Filed under"),
|
||
ViewSpec::new("Pipeline")
|
||
.category("Pipeline")
|
||
.sort("priority")
|
||
.hide_done()
|
||
.columns("item:42:Opportunity,categories:30:Stage,when:20:Next action,priority:5:P")
|
||
.sections("Prospects|category=Prospect;Qualified|category=Qualified;Proposals|category=Proposal;Customers|category=Customer"),
|
||
ViewSpec::new("Open Issues")
|
||
.category("Issues")
|
||
.sort("priority")
|
||
.hide_done()
|
||
.filter("not category=Resolved")
|
||
.columns("item:48:Issue,categories:28:Account / state,when:20:Due,priority:5:P"),
|
||
ViewSpec::new("Calls and Meetings")
|
||
.category("Activities")
|
||
.sort("when")
|
||
.hide_done()
|
||
.columns("item:48:Activity,categories:28:Account / type,when:20:When,priority:5:P"),
|
||
ViewSpec::new("Expenses")
|
||
.category("Expenses")
|
||
.sort("updated")
|
||
.columns("item:42:Expense,categories:28:Account,value:14:Amount:sum,note:30:Details"),
|
||
ViewSpec::new("Status Report")
|
||
.category("Status Report")
|
||
.sort("priority")
|
||
.columns("item:48:Status item,categories:28:Account,when:20:When,priority:5:P")
|
||
.sections("Urgent|priority<=2;Routine|priority>2"),
|
||
];
|
||
views.extend(common_tail());
|
||
PresetSpec {
|
||
title: "Rogue Agenda — Account Desk",
|
||
description: "A relationship workspace for accounts, opportunities, issues, activities, expenses, and status reporting.",
|
||
categories: vec![
|
||
category("Accounts", None, "standard", "account:", false),
|
||
category(
|
||
"Northstar Labs",
|
||
Some("Accounts"),
|
||
"standard",
|
||
"northstar",
|
||
false,
|
||
),
|
||
category(
|
||
"Harbor Books",
|
||
Some("Accounts"),
|
||
"standard",
|
||
"harbor",
|
||
false,
|
||
),
|
||
category("Contacts", None, "standard", "contact:", false),
|
||
category("Pipeline", None, "standard", "opportunity:", false),
|
||
category("Prospect", Some("Pipeline"), "standard", "", true),
|
||
category("Qualified", Some("Pipeline"), "standard", "", true),
|
||
category("Proposal", Some("Pipeline"), "standard", "proposal", true),
|
||
category("Customer", Some("Pipeline"), "standard", "", true),
|
||
category("Activities", None, "standard", "", false),
|
||
category("Calls", Some("Activities"), "standard", "call,phone", false),
|
||
category(
|
||
"Meetings",
|
||
Some("Activities"),
|
||
"standard",
|
||
"meet,meeting",
|
||
false,
|
||
),
|
||
category(
|
||
"Follow-ups",
|
||
Some("Activities"),
|
||
"standard",
|
||
"follow up,follow-up",
|
||
false,
|
||
),
|
||
category("Issues", None, "standard", "issue:", false),
|
||
category("Open Issue", Some("Issues"), "standard", "", true),
|
||
category("Waiting", Some("Issues"), "standard", "waiting", true),
|
||
category("Resolved", Some("Issues"), "standard", "resolved", true),
|
||
category("Expenses", None, "numeric", "expense:", false),
|
||
category("Status Report", None, "standard", "status:", false),
|
||
],
|
||
views,
|
||
rules: vec![
|
||
rule("Status Report", "priority=1 and open", "assign:"),
|
||
rule("Follow-ups", "text~follow-up", "assign:"),
|
||
],
|
||
macros: vec![
|
||
agenda_macro(
|
||
"captureaccount",
|
||
"{captureaccount}\n{inputtext;Company or account name;%name}\nn{type;Account: }{type;%name}{enter}",
|
||
),
|
||
agenda_macro(
|
||
"capturecall",
|
||
"{capturecall}\n{inputtext;Who should you call?;%name}\nn{type;Call }{type;%name}{enter}",
|
||
),
|
||
],
|
||
items: vec![
|
||
ItemSpec::new(
|
||
"Account: Northstar Labs",
|
||
"Renewable-energy analytics team. Primary contact: Mina Chen. Current objective: replace the weekly spreadsheet handoff.",
|
||
&["Northstar Labs", "Customer"],
|
||
),
|
||
ItemSpec::new(
|
||
"Contact: Mina Chen — Operations Director",
|
||
"Northstar Labs. Prefers concise email follow-ups after calls.",
|
||
&["Contacts", "Northstar Labs"],
|
||
),
|
||
ItemSpec::new(
|
||
"Opportunity: Harbor Books inventory workshop",
|
||
"Discovery found duplicate stock counts across three locations.",
|
||
&["Harbor Books", "Qualified"],
|
||
)
|
||
.priority(2)
|
||
.when(3, 10, 0),
|
||
ItemSpec::new(
|
||
"Call Mina about pilot measurements",
|
||
"Confirm which dashboard measures belong in the two-week pilot.",
|
||
&["Northstar Labs", "Calls"],
|
||
)
|
||
.priority(1)
|
||
.when(1, 14, 0),
|
||
ItemSpec::new(
|
||
"Issue: Harbor export omits back-ordered titles",
|
||
"Waiting for a sanitized export that reproduces the missing rows.",
|
||
&["Harbor Books", "Waiting"],
|
||
)
|
||
.priority(1)
|
||
.when(2, 9, 30),
|
||
ItemSpec::new(
|
||
"Expense: train to Northstar workshop",
|
||
"Return fare for the discovery workshop.",
|
||
&["Northstar Labs", "Expenses"],
|
||
)
|
||
.value(46.80)
|
||
.done(),
|
||
ItemSpec::new(
|
||
"Status: Northstar pilot scope approved",
|
||
"The pilot will cover intake latency, exception volume, and operator time.",
|
||
&["Northstar Labs", "Status Report"],
|
||
)
|
||
.priority(2),
|
||
],
|
||
}
|
||
}
|
||
|
||
fn study() -> PresetSpec {
|
||
let mut views = vec![
|
||
ViewSpec::new("All Items").sort("updated"),
|
||
ViewSpec::new("Due Review")
|
||
.sort("when")
|
||
.hide_done()
|
||
.filter("category=Cards and (overdue or due=7d)")
|
||
.columns("item:55:Question,categories:28:Deck / stage,when:20:Review,value:8:Cycle"),
|
||
ViewSpec::new("New Cards")
|
||
.category("New")
|
||
.sort("manual")
|
||
.hide_done()
|
||
.columns("item:60:Question,categories:30:Deck,note:36:Answer"),
|
||
ViewSpec::new("All Cards")
|
||
.category("Cards")
|
||
.sort("when")
|
||
.columns("item:55:Question,categories:28:Deck / stage,when:20:Review,value:8:Cycle"),
|
||
ViewSpec::new("Deck Browser")
|
||
.category("Cards")
|
||
.columns("item:52:Question,note:42:Answer,categories:28:Deck")
|
||
.sections("Computing|category=Computing;Languages|category=Languages;History|category=History"),
|
||
ViewSpec::new("Datebook").kind("datebook", "30").sort("when"),
|
||
];
|
||
views.extend(common_tail());
|
||
PresetSpec {
|
||
title: "Rogue Agenda — Study Deck",
|
||
description: "A note-backed flashcard workspace with decks, learning stages, review dates, and cycle values.",
|
||
categories: vec![
|
||
category("Cards", None, "standard", "q:", false),
|
||
category("Decks", None, "standard", "", false),
|
||
category("Computing", Some("Decks"), "standard", "", false),
|
||
category("Languages", Some("Decks"), "standard", "", false),
|
||
category("History", Some("Decks"), "standard", "", false),
|
||
category("Learning Stage", None, "standard", "", false),
|
||
category("New", Some("Learning Stage"), "standard", "", true),
|
||
category("Learning", Some("Learning Stage"), "standard", "", true),
|
||
category("Review", Some("Learning Stage"), "standard", "", true),
|
||
category("Review Date", None, "date", "", false),
|
||
category("Cycle", None, "numeric", "", false),
|
||
],
|
||
views,
|
||
rules: vec![rule("Cards", "text~q:", "assign:")],
|
||
macros: vec![
|
||
agenda_macro(
|
||
"capturecard",
|
||
"{capturecard}\n{inputtext;Question;%question}\n{inputtext;Answer;%answer}\nn{type;Q: }{type;%question}{enter}{F5}{type;%answer}{F5}",
|
||
),
|
||
agenda_macro(
|
||
"revealanswer",
|
||
"{revealanswer}\n{largebox;Question;#HIGHLIGHT_VALUE}\n{F5}",
|
||
),
|
||
],
|
||
items: vec![
|
||
ItemSpec::new(
|
||
"Q: What does ownership prevent in Rust?",
|
||
"It prevents multiple parts of a program from mutating or freeing the same value without explicit coordination.",
|
||
&["Cards", "Computing", "Review", "Cycle"],
|
||
)
|
||
.when(0, 9, 0)
|
||
.value(3.0),
|
||
ItemSpec::new(
|
||
"Q: Which SQL clause filters grouped results?",
|
||
"HAVING filters after grouping; WHERE filters rows before grouping.",
|
||
&["Cards", "Computing", "Learning", "Cycle"],
|
||
)
|
||
.when(1, 9, 0)
|
||
.value(2.0),
|
||
ItemSpec::new(
|
||
"Q: What does ‘bonjour’ mean?",
|
||
"Hello or good day in French.",
|
||
&["Cards", "Languages", "New", "Cycle"],
|
||
)
|
||
.value(0.0),
|
||
ItemSpec::new(
|
||
"Q: In which year did the first transatlantic telegraph cable succeed?",
|
||
"A durable transatlantic cable entered service in 1866.",
|
||
&["Cards", "History", "Review", "Cycle"],
|
||
)
|
||
.when(5, 9, 0)
|
||
.value(5.0),
|
||
ItemSpec::new(
|
||
"Q: What is spaced repetition trying to optimize?",
|
||
"Review near the point of forgetting so that durable recall grows with less total study time.",
|
||
&["Cards", "New", "Cycle"],
|
||
)
|
||
.value(0.0),
|
||
],
|
||
}
|
||
}
|
||
|
||
fn planner() -> PresetSpec {
|
||
let mut views = vec![
|
||
ViewSpec::new("All Items").sort("manual"),
|
||
ViewSpec::new("Today")
|
||
.kind("upcoming", "1")
|
||
.sort("when")
|
||
.hide_done(),
|
||
ViewSpec::new("This Week")
|
||
.kind("upcoming", "7")
|
||
.sort("when")
|
||
.hide_done()
|
||
.columns(
|
||
"item:50:Commitment,categories:28:Project / context,when:20:When,priority:5:P",
|
||
),
|
||
ViewSpec::new("Next Actions")
|
||
.category("Next Actions")
|
||
.sort("priority")
|
||
.hide_done()
|
||
.columns("item:52:Action,categories:30:Project / context,when:20:When,priority:5:P"),
|
||
ViewSpec::new("Projects")
|
||
.category("Projects")
|
||
.sort("priority")
|
||
.hide_done(),
|
||
ViewSpec::new("Calls and Meetings")
|
||
.category("Scheduled Activity")
|
||
.sort("when")
|
||
.hide_done(),
|
||
ViewSpec::new("Ideas")
|
||
.category("Ideas")
|
||
.columns("item:58:Idea,note:48:Notes,categories:25:Area"),
|
||
ViewSpec::new("Datebook")
|
||
.kind("datebook", "30")
|
||
.sort("when"),
|
||
];
|
||
views.extend(common_tail());
|
||
PresetSpec {
|
||
title: "Rogue Agenda — Activities Planner",
|
||
description: "An extensible daily planner for projects, next actions, scheduled activities, contexts, and ideas.",
|
||
categories: vec![
|
||
category(
|
||
"Next Actions",
|
||
None,
|
||
"standard",
|
||
"todo,call,send,buy,review",
|
||
false,
|
||
),
|
||
category("Scheduled Activity", None, "standard", "", false),
|
||
category(
|
||
"Calls",
|
||
Some("Scheduled Activity"),
|
||
"standard",
|
||
"call,phone",
|
||
false,
|
||
),
|
||
category(
|
||
"Meetings",
|
||
Some("Scheduled Activity"),
|
||
"standard",
|
||
"meet,meeting,appointment",
|
||
false,
|
||
),
|
||
category(
|
||
"Errands",
|
||
Some("Next Actions"),
|
||
"standard",
|
||
"buy,pick up,errand",
|
||
false,
|
||
),
|
||
category("Projects", None, "standard", "project:", false),
|
||
category(
|
||
"Website Refresh",
|
||
Some("Projects"),
|
||
"standard",
|
||
"website",
|
||
false,
|
||
),
|
||
category("Studio Move", Some("Projects"), "standard", "studio", false),
|
||
category("Areas", None, "standard", "", false),
|
||
category("Work", Some("Areas"), "standard", "", false),
|
||
category("Home", Some("Areas"), "standard", "", false),
|
||
category("Personal", Some("Areas"), "standard", "", false),
|
||
category("Contexts", None, "standard", "", false),
|
||
category(
|
||
"At Computer",
|
||
Some("Contexts"),
|
||
"standard",
|
||
"email,website,draft",
|
||
false,
|
||
),
|
||
category(
|
||
"On Phone",
|
||
Some("Contexts"),
|
||
"standard",
|
||
"call,phone",
|
||
false,
|
||
),
|
||
category(
|
||
"Out and About",
|
||
Some("Contexts"),
|
||
"standard",
|
||
"buy,pick up",
|
||
false,
|
||
),
|
||
category("Ideas", None, "standard", "idea:,maybe", false),
|
||
category("When", None, "date", "", false),
|
||
category("Priority", None, "numeric", "", false),
|
||
],
|
||
views,
|
||
rules: vec![
|
||
rule("Next Actions", "priority<=2 and open", "assign:"),
|
||
rule(
|
||
"Scheduled Activity",
|
||
"dated and (category=Calls or category=Meetings)",
|
||
"assign:",
|
||
),
|
||
],
|
||
macros: vec![
|
||
agenda_macro(
|
||
"quickcapture",
|
||
"{quickcapture}\n{inputtext;Capture a thought or action;%item}\nn{type;%item}{enter}",
|
||
),
|
||
agenda_macro(
|
||
"captureidea",
|
||
"{captureidea}\n{inputtext;What is the idea?;%idea}\nn{type;Idea: }{type;%idea}{enter}",
|
||
),
|
||
],
|
||
items: vec![
|
||
ItemSpec::new(
|
||
"Project: Website refresh",
|
||
"Outcome: a fast, accessible portfolio with a simpler inquiry path.",
|
||
&["Website Refresh", "Work"],
|
||
)
|
||
.priority(2),
|
||
ItemSpec::new(
|
||
"Draft website content inventory",
|
||
"List every current page and decide keep, rewrite, merge, or remove.",
|
||
&["Website Refresh", "Next Actions", "At Computer"],
|
||
)
|
||
.priority(1)
|
||
.when(1, 9, 0),
|
||
ItemSpec::new(
|
||
"Call movers for a revised studio estimate",
|
||
"Ask whether packing crates and weekend delivery are included.",
|
||
&["Studio Move", "Calls", "On Phone"],
|
||
)
|
||
.priority(2)
|
||
.when(2, 11, 0),
|
||
ItemSpec::new(
|
||
"Meet Jo for the weekly project review",
|
||
"Review current risks and choose the next visible milestone.",
|
||
&["Website Refresh", "Meetings", "Work"],
|
||
)
|
||
.priority(1)
|
||
.when(3, 15, 0)
|
||
.recurring("weekly"),
|
||
ItemSpec::new(
|
||
"Buy archival boxes for the studio move",
|
||
"Measure the largest flat work before choosing box sizes.",
|
||
&["Studio Move", "Errands", "Out and About"],
|
||
)
|
||
.when(4, 17, 0),
|
||
ItemSpec::new(
|
||
"Idea: publish a short monthly field note",
|
||
"A single observation, one image, and a link to related work.",
|
||
&["Ideas", "Work"],
|
||
),
|
||
ItemSpec::new(
|
||
"Send the accessibility checklist to Jo",
|
||
"Checklist sent with the content inventory template.",
|
||
&["Website Refresh", "Next Actions", "At Computer"],
|
||
)
|
||
.done(),
|
||
],
|
||
}
|
||
}
|
||
|
||
fn recipes() -> PresetSpec {
|
||
let mut views = vec![
|
||
ViewSpec::new("Recipe Box")
|
||
.category("Recipes")
|
||
.columns("item:42:Recipe,categories:35:Course / cuisine,note:55:Ingredients and method,value:10:Serves"),
|
||
ViewSpec::new("Favorites")
|
||
.category("Favorites")
|
||
.columns("item:45:Favorite,categories:35:Tags,note:58:Recipe,value:10:Serves"),
|
||
ViewSpec::new("Main Dishes")
|
||
.category("Main Dishes")
|
||
.columns("item:45:Main dish,categories:35:Tags,note:58:Recipe,value:10:Serves"),
|
||
ViewSpec::new("Quick Meals")
|
||
.category("Quick")
|
||
.columns("item:45:Recipe,categories:35:Tags,note:58:Method,value:10:Serves"),
|
||
ViewSpec::new("Vegetarian")
|
||
.category("Vegetarian")
|
||
.columns("item:45:Recipe,categories:35:Tags,note:58:Recipe,value:10:Serves"),
|
||
ViewSpec::new("By Course")
|
||
.category("Recipes")
|
||
.columns("item:45:Recipe,categories:35:Tags,value:10:Serves")
|
||
.sections("Breakfast|category=Breakfast;Main dishes|category='Main Dishes';Desserts|category=Desserts;Drinks|category=Drinks"),
|
||
ViewSpec::new("All Items").sort("updated"),
|
||
];
|
||
views.extend(common_tail());
|
||
PresetSpec {
|
||
title: "Rogue Agenda — Recipe Box",
|
||
description: "A browseable cooking notebook organized by course, cuisine, dietary tags, ingredients, and favorites.",
|
||
categories: vec![
|
||
category("Recipes", None, "standard", "recipe:", false),
|
||
category("Course", None, "standard", "", false),
|
||
category("Breakfast", Some("Course"), "standard", "", true),
|
||
category("Main Dishes", Some("Course"), "standard", "", true),
|
||
category("Desserts", Some("Course"), "standard", "", true),
|
||
category("Drinks", Some("Course"), "standard", "", true),
|
||
category("Cuisine", None, "standard", "", false),
|
||
category("Mediterranean", Some("Cuisine"), "standard", "", false),
|
||
category("East Asian", Some("Cuisine"), "standard", "", false),
|
||
category("Northern European", Some("Cuisine"), "standard", "", false),
|
||
category("Diet", None, "standard", "", false),
|
||
category("Vegetarian", Some("Diet"), "standard", "", false),
|
||
category("Vegan", Some("Diet"), "standard", "", false),
|
||
category("Gluten Free", Some("Diet"), "standard", "", false),
|
||
category("Quick", None, "standard", "quick,15 minute,20 minute", false),
|
||
category("Favorites", None, "standard", "favorite", false),
|
||
category("Ingredients", None, "standard", "", false),
|
||
category("Beans", Some("Ingredients"), "standard", "beans,chickpeas", false),
|
||
category("Rice", Some("Ingredients"), "standard", "rice", false),
|
||
category("Fruit", Some("Ingredients"), "standard", "apple,pear,lemon", false),
|
||
category("Servings", None, "numeric", "", false),
|
||
],
|
||
views,
|
||
rules: vec![
|
||
rule("Recipes", "text~recipe:", "assign:"),
|
||
rule("Quick", "note~minutes", "assign:"),
|
||
],
|
||
macros: vec![
|
||
agenda_macro(
|
||
"newrecipe",
|
||
"{newrecipe}\n{inputtext;Recipe name;%name}\nn{type;Recipe: }{type;%name}{enter}{F5}{type;Ingredients:}{enter;2}{type;Method:}{enter;2}{type;Notes:}{F5}",
|
||
),
|
||
agenda_macro(
|
||
"cookingnote",
|
||
"{cookingnote}\n{inputtext;What did you learn while cooking?;%note}\nn{type;Kitchen note: }{type;%note}{enter}",
|
||
),
|
||
],
|
||
items: vec![
|
||
ItemSpec::new(
|
||
"Recipe: Lemon chickpea skillet",
|
||
"Ingredients: chickpeas, spinach, lemon, garlic, olive oil, cumin.\n\nMethod: Warm garlic and cumin in oil, fold in chickpeas and spinach, then finish with lemon. About 20 minutes.",
|
||
&["Recipes", "Main Dishes", "Mediterranean", "Vegetarian", "Vegan", "Gluten Free", "Beans", "Fruit", "Quick", "Favorites", "Servings"],
|
||
)
|
||
.value(3.0),
|
||
ItemSpec::new(
|
||
"Recipe: Ginger mushroom rice bowl",
|
||
"Ingredients: cooked rice, mushrooms, ginger, scallions, tamari, sesame oil.\n\nMethod: Brown mushrooms well, add ginger and tamari, and serve over hot rice. About 20 minutes.",
|
||
&["Recipes", "Main Dishes", "East Asian", "Vegetarian", "Vegan", "Rice", "Quick", "Servings"],
|
||
)
|
||
.value(2.0),
|
||
ItemSpec::new(
|
||
"Recipe: Apple oat breakfast jars",
|
||
"Ingredients: rolled oats, grated apple, yogurt or oat milk, cinnamon, toasted seeds.\n\nMethod: Mix in jars and chill overnight.",
|
||
&["Recipes", "Breakfast", "Northern European", "Vegetarian", "Fruit", "Servings"],
|
||
)
|
||
.value(4.0),
|
||
ItemSpec::new(
|
||
"Recipe: Pear and cocoa cups",
|
||
"Ingredients: ripe pear, cocoa, thick yogurt, toasted hazelnuts.\n\nMethod: Layer diced pear with cocoa yogurt and finish with nuts.",
|
||
&["Recipes", "Desserts", "Vegetarian", "Fruit", "Quick", "Servings"],
|
||
)
|
||
.value(2.0),
|
||
ItemSpec::new(
|
||
"Recipe: Rosemary citrus cooler",
|
||
"Ingredients: orange, lemon, rosemary, sparkling water, ice.\n\nMethod: Muddle one rosemary sprig with citrus juice, strain, and top with sparkling water.",
|
||
&["Recipes", "Drinks", "Vegan", "Gluten Free", "Fruit", "Quick", "Servings"],
|
||
)
|
||
.value(4.0),
|
||
],
|
||
}
|
||
}
|
||
|
||
fn rides() -> PresetSpec {
|
||
let mut views = vec![
|
||
ViewSpec::new("Ride Log")
|
||
.category("Rides")
|
||
.sort("when")
|
||
.columns("item:45:Ride,categories:35:Bike / type / weather,when:20:Date,value:14:Distance km:sum,note:35:Notes"),
|
||
ViewSpec::new("Training")
|
||
.category("Training")
|
||
.sort("when")
|
||
.columns("item:48:Session,categories:32:Bike / route,when:20:Date,value:14:Distance km:sum"),
|
||
ViewSpec::new("Long Rides")
|
||
.category("Rides")
|
||
.filter("value>=50")
|
||
.sort("updated")
|
||
.columns("item:48:Ride,categories:35:Bike / route,when:20:Date,value:14:Distance km:avg"),
|
||
ViewSpec::new("By Bicycle")
|
||
.category("Rides")
|
||
.columns("item:45:Ride,when:20:Date,value:14:Distance km:sum,categories:35:Type / weather")
|
||
.sections("Road bike|category='Road Bike';Touring bike|category='Touring Bike';City bike|category='City Bike'"),
|
||
ViewSpec::new("Maintenance")
|
||
.category("Maintenance")
|
||
.sort("when")
|
||
.hide_done()
|
||
.columns("item:52:Maintenance,categories:30:Bicycle,when:20:Due,priority:5:P,note:40:Details"),
|
||
ViewSpec::new("Datebook").kind("datebook", "30").sort("when"),
|
||
ViewSpec::new("All Items").sort("updated"),
|
||
];
|
||
views.extend(common_tail());
|
||
PresetSpec {
|
||
title: "Rogue Agenda — Bicycle Log",
|
||
description: "A cycling log for rides, bicycles, routes, conditions, distance totals, training, and maintenance.",
|
||
categories: vec![
|
||
category("Rides", None, "standard", "ride:", false),
|
||
category("Ride Type", None, "standard", "", false),
|
||
category("Commute", Some("Ride Type"), "standard", "commute", false),
|
||
category(
|
||
"Training",
|
||
Some("Ride Type"),
|
||
"standard",
|
||
"training,interval",
|
||
false,
|
||
),
|
||
category(
|
||
"Leisure",
|
||
Some("Ride Type"),
|
||
"standard",
|
||
"leisure,tour",
|
||
false,
|
||
),
|
||
category("Bicycles", None, "standard", "", false),
|
||
category(
|
||
"Road Bike",
|
||
Some("Bicycles"),
|
||
"standard",
|
||
"road bike",
|
||
false,
|
||
),
|
||
category(
|
||
"Touring Bike",
|
||
Some("Bicycles"),
|
||
"standard",
|
||
"touring bike",
|
||
false,
|
||
),
|
||
category(
|
||
"City Bike",
|
||
Some("Bicycles"),
|
||
"standard",
|
||
"city bike",
|
||
false,
|
||
),
|
||
category("Weather", None, "standard", "", false),
|
||
category("Clear", Some("Weather"), "standard", "sunny,clear", false),
|
||
category(
|
||
"Cloudy",
|
||
Some("Weather"),
|
||
"standard",
|
||
"cloudy,overcast",
|
||
false,
|
||
),
|
||
category("Rain", Some("Weather"), "standard", "rain,wet", false),
|
||
category("Routes", None, "standard", "route:", false),
|
||
category(
|
||
"River Loop",
|
||
Some("Routes"),
|
||
"standard",
|
||
"river loop",
|
||
false,
|
||
),
|
||
category(
|
||
"North Ridge",
|
||
Some("Routes"),
|
||
"standard",
|
||
"north ridge",
|
||
false,
|
||
),
|
||
category(
|
||
"Maintenance",
|
||
None,
|
||
"standard",
|
||
"service,replace,lubricate,maintenance",
|
||
false,
|
||
),
|
||
category("Distance", None, "numeric", "", false),
|
||
],
|
||
views,
|
||
rules: vec![
|
||
rule("Rides", "text~ride:", "assign:"),
|
||
rule("Maintenance", "text~service or text~replace", "assign:"),
|
||
],
|
||
macros: vec![
|
||
agenda_macro(
|
||
"logride",
|
||
"{logride}\n{inputtext;Ride summary;%ride}\nn{type;Ride: }{type;%ride}{enter}",
|
||
),
|
||
agenda_macro(
|
||
"servicereminder",
|
||
"{servicereminder}\n{inputtext;What needs service?;%service}\nn{type;Service }{type;%service}{enter}",
|
||
),
|
||
],
|
||
items: vec![
|
||
ItemSpec::new(
|
||
"Ride: sunrise River Loop training",
|
||
"Steady endurance pace. Light headwind on the return leg; average speed 24.1 km/h.",
|
||
&[
|
||
"Rides",
|
||
"Training",
|
||
"Road Bike",
|
||
"River Loop",
|
||
"Clear",
|
||
"Distance",
|
||
],
|
||
)
|
||
.when(-5, 6, 30)
|
||
.value(42.6)
|
||
.done(),
|
||
ItemSpec::new(
|
||
"Ride: wet commute to the library",
|
||
"Low traffic and fresh brake pads worked well in the rain.",
|
||
&["Rides", "Commute", "City Bike", "Rain", "Distance"],
|
||
)
|
||
.when(-3, 8, 15)
|
||
.value(11.8)
|
||
.done(),
|
||
ItemSpec::new(
|
||
"Ride: North Ridge café tour",
|
||
"Long rolling route with a steep final climb; carry an extra bottle next time.",
|
||
&[
|
||
"Rides",
|
||
"Leisure",
|
||
"Touring Bike",
|
||
"North Ridge",
|
||
"Cloudy",
|
||
"Distance",
|
||
],
|
||
)
|
||
.when(-1, 9, 0)
|
||
.value(67.4)
|
||
.done(),
|
||
ItemSpec::new(
|
||
"Ride: short recovery spin on the River Loop",
|
||
"Keep the effort conversational and skip the hill repeats.",
|
||
&[
|
||
"Rides",
|
||
"Training",
|
||
"Road Bike",
|
||
"River Loop",
|
||
"Clear",
|
||
"Distance",
|
||
],
|
||
)
|
||
.when(2, 17, 30)
|
||
.value(24.0),
|
||
ItemSpec::new(
|
||
"Replace touring bike chain",
|
||
"Chain checker is past 0.75; inspect the cassette at the same time.",
|
||
&["Maintenance", "Touring Bike"],
|
||
)
|
||
.priority(1)
|
||
.when(4, 10, 0),
|
||
ItemSpec::new(
|
||
"Service city bike lights",
|
||
"Clean charging contacts and test the rear mount.",
|
||
&["Maintenance", "City Bike"],
|
||
)
|
||
.priority(2)
|
||
.when(7, 18, 0),
|
||
],
|
||
}
|
||
}
|
||
|
||
fn people() -> PresetSpec {
|
||
let mut views = vec![
|
||
ViewSpec::new("People Directory")
|
||
.category("People Records")
|
||
.columns("item:40:Person,categories:36:Team / status,note:58:Profile,when:20:Next review"),
|
||
ViewSpec::new("Upcoming Reviews")
|
||
.category("Reviews")
|
||
.sort("when")
|
||
.hide_done()
|
||
.columns("item:48:Review,categories:32:Team / person,when:20:When,priority:5:P,note:42:Preparation"),
|
||
ViewSpec::new("Goals")
|
||
.category("Goals")
|
||
.sort("priority")
|
||
.hide_done()
|
||
.columns("item:52:Goal,categories:34:Person / team,when:20:Target,priority:5:P,note:40:Evidence"),
|
||
ViewSpec::new("Development")
|
||
.category("Development")
|
||
.sort("when")
|
||
.hide_done()
|
||
.columns("item:50:Development action,categories:34:Person / team,when:20:Target,note:45:Notes"),
|
||
ViewSpec::new("Candidates")
|
||
.category("Candidate")
|
||
.sort("updated")
|
||
.hide_done()
|
||
.columns("item:42:Candidate,categories:34:Role / stage,note:58:Interview notes,priority:5:P"),
|
||
ViewSpec::new("Achievements")
|
||
.category("Achievements")
|
||
.sort("updated")
|
||
.columns("item:50:Achievement,categories:34:Person / team,when:20:Date,note:45:Evidence"),
|
||
ViewSpec::new("All Items").sort("updated"),
|
||
];
|
||
views.extend(common_tail());
|
||
PresetSpec {
|
||
title: "Rogue Agenda — People Manager",
|
||
description: "A lightweight people workspace for profiles, teams, goals, reviews, development, achievements, and candidates.",
|
||
categories: vec![
|
||
category("People Records", None, "standard", "person:", false),
|
||
category("Teams", None, "standard", "", false),
|
||
category("Product", Some("Teams"), "standard", "", false),
|
||
category("Operations", Some("Teams"), "standard", "", false),
|
||
category("Community", Some("Teams"), "standard", "", false),
|
||
category("People Status", None, "standard", "", false),
|
||
category("Active", Some("People Status"), "standard", "", true),
|
||
category("Candidate", Some("People Status"), "standard", "candidate:", true),
|
||
category("Alumni", Some("People Status"), "standard", "", true),
|
||
category("Reviews", None, "standard", "review:", false),
|
||
category("Goals", None, "standard", "goal:", false),
|
||
category("Development", None, "standard", "development:,learn,training", false),
|
||
category("Achievements", None, "standard", "achievement:", false),
|
||
category("Hiring", None, "standard", "interview,candidate,role:", false),
|
||
category("Review Date", None, "date", "", false),
|
||
category("Progress", None, "numeric", "", false),
|
||
],
|
||
views,
|
||
rules: vec![
|
||
rule("People Records", "text~person:", "assign:"),
|
||
rule("Reviews", "text~review:", "assign:"),
|
||
rule("Goals", "text~goal:", "assign:"),
|
||
],
|
||
macros: vec![
|
||
agenda_macro(
|
||
"addperson",
|
||
"{addperson}\n{inputtext;Person name;%name}\n{inputtext;Role or focus;%role}\nn{type;Person: }{type;%name}{type; — }{type;%role}{enter}",
|
||
),
|
||
agenda_macro(
|
||
"addgoal",
|
||
"{addgoal}\n{inputtext;Describe the goal;%goal}\nn{type;Goal: }{type;%goal}{enter}",
|
||
),
|
||
],
|
||
items: vec![
|
||
ItemSpec::new(
|
||
"Person: Amina Okafor — Product designer",
|
||
"Focus: research synthesis and accessible interaction patterns. Prefers written context before design reviews.",
|
||
&["People Records", "Product", "Active"],
|
||
),
|
||
ItemSpec::new(
|
||
"Person: Theo Martin — Operations coordinator",
|
||
"Focus: dependable handoffs and lightweight process documentation.",
|
||
&["People Records", "Operations", "Active"],
|
||
),
|
||
ItemSpec::new(
|
||
"Goal: Amina leads two customer research debriefs",
|
||
"Evidence: agendas prepared, insights circulated, and follow-up decisions recorded.",
|
||
&["Goals", "Product", "Progress"],
|
||
)
|
||
.priority(2)
|
||
.when(14, 16, 0)
|
||
.value(50.0),
|
||
ItemSpec::new(
|
||
"Review: quarterly conversation with Theo",
|
||
"Prepare examples of strong handoffs and identify one process that can be retired.",
|
||
&["Reviews", "Operations"],
|
||
)
|
||
.priority(1)
|
||
.when(5, 10, 30),
|
||
ItemSpec::new(
|
||
"Development: Amina shadows an accessibility audit",
|
||
"Pair with the audit facilitator and write a one-page reflection afterward.",
|
||
&["Development", "Product"],
|
||
)
|
||
.when(9, 13, 0),
|
||
ItemSpec::new(
|
||
"Achievement: Theo simplified the intake checklist",
|
||
"The revised checklist removed four duplicate questions and reduced incomplete requests.",
|
||
&["Achievements", "Operations"],
|
||
)
|
||
.when(-4, 9, 0)
|
||
.done(),
|
||
ItemSpec::new(
|
||
"Candidate: Jules Rivera — community producer",
|
||
"Strong facilitation examples. Next interview should explore editorial planning and conflict resolution.",
|
||
&["Candidate", "Hiring", "Community"],
|
||
)
|
||
.priority(2)
|
||
.when(3, 14, 30),
|
||
],
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use tempfile::tempdir;
|
||
|
||
#[test]
|
||
fn every_preset_builds_a_rich_valid_workspace() {
|
||
for preset in [
|
||
Preset::Accounts,
|
||
Preset::Study,
|
||
Preset::Planner,
|
||
Preset::Recipes,
|
||
Preset::Rides,
|
||
Preset::People,
|
||
] {
|
||
let directory = tempdir().unwrap();
|
||
let path = directory.path().join(format!("{}.agnd", preset.name()));
|
||
let mut database = Database::open(&path).unwrap();
|
||
database.apply_preset(preset).unwrap();
|
||
|
||
let item_count: i64 = database
|
||
.conn
|
||
.query_row("SELECT COUNT(*) FROM items", [], |row| row.get(0))
|
||
.unwrap();
|
||
assert!(item_count >= 5, "{} has too few items", preset.name());
|
||
assert!(database.categories().unwrap().len() >= 10);
|
||
assert!(database.views().unwrap().len() >= 7);
|
||
assert!(database.macros().unwrap().len() >= 2);
|
||
assert_eq!(
|
||
database
|
||
.conn
|
||
.query_row(
|
||
"SELECT value FROM meta WHERE key='preset.name'",
|
||
[],
|
||
|row| { row.get::<_, String>(0) }
|
||
)
|
||
.unwrap(),
|
||
preset.name()
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn presets_refuse_to_replace_an_existing_document() {
|
||
let directory = tempdir().unwrap();
|
||
let path = directory.path().join("existing.agnd");
|
||
Database::open(&path).unwrap();
|
||
let mut reopened = Database::open(&path).unwrap();
|
||
let error = reopened.apply_preset(Preset::Planner).unwrap_err();
|
||
assert!(error.to_string().contains("only initialize a new document"));
|
||
}
|
||
|
||
#[test]
|
||
fn a_preset_cannot_be_applied_twice_in_one_session() {
|
||
let directory = tempdir().unwrap();
|
||
let path = directory.path().join("twice.agnd");
|
||
let mut database = Database::open(&path).unwrap();
|
||
database.apply_preset(Preset::Study).unwrap();
|
||
assert!(database.apply_preset(Preset::Study).is_err());
|
||
}
|
||
}
|