mod annotations;
mod app;
mod backup;
mod help;
mod hjson_index;
mod save;
mod schema;
mod widgets;
use std::path::Path;
use anyhow::{Context, Result};
use std::path::PathBuf;
pub fn run(project: &Path) -> Result<()> {
app::run(project)
}
pub struct InPlacePatchOutcome {
pub config_path: PathBuf,
pub backup: PathBuf,
}
pub fn apply_in_place_edits(
project_root: &Path,
updates: &[(String, serde_json::Value)],
) -> Result<InPlacePatchOutcome> {
if updates.is_empty() {
anyhow::bail!("apply_in_place_edits: no updates supplied");
}
let config_path = project_root.join("inkhaven.hjson");
let source = std::fs::read_to_string(&config_path)
.with_context(|| format!("read {}", config_path.display()))?;
let backup = save::write_backup(project_root, &source)
.context("write pre-patch backup")?;
let index = hjson_index::parse(&source)
.map_err(|e| anyhow::anyhow!("parse HJSON: {e}"))?;
let edits: Vec<save::Edit> = updates
.iter()
.map(|(path, value)| {
let kind = if index.leaves.contains_key(path) {
save::EditKind::Splice
} else {
save::EditKind::Append
};
save::Edit {
path: path.clone(),
new_value: value.clone(),
kind,
}
})
.collect();
let new_source = save::apply_edits(&index, &edits)
.context("apply HJSON edits")?;
let written = save::write_atomic(&config_path, &new_source)
.context("write inkhaven.hjson")?;
Ok(InPlacePatchOutcome {
config_path: written,
backup,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn goals_edit_patches_in_place_and_preserves_comments() {
let dir = tempfile::tempdir().unwrap();
let src = "// my project\n{\n language: english\n // writing goals\n goals: {\n daily_words: 0\n streak_grace_per_week: 1\n }\n}\n";
std::fs::write(dir.path().join("inkhaven.hjson"), src).unwrap();
let outcome = apply_in_place_edits(
dir.path(),
&[
("goals.daily_words".into(), serde_json::json!(750)),
("goals.active_minutes_daily".into(), serde_json::json!(30)),
],
)
.unwrap();
let out = std::fs::read_to_string(dir.path().join("inkhaven.hjson")).unwrap();
assert!(out.contains("daily_words: 750"));
assert!(out.contains("active_minutes_daily: 30"));
assert!(out.contains("// my project"));
assert!(out.contains("// writing goals"));
assert!(out.contains("language: english"));
assert!(out.contains("streak_grace_per_week: 1"));
let backup = std::fs::read_to_string(&outcome.backup).unwrap();
assert!(backup.contains("daily_words: 0"));
serde_hjson::from_str::<serde_json::Value>(&out).expect("valid HJSON");
}
}