diff --git a/README.md b/README.md index 7ba9bcd..a90da5f 100644 --- a/README.md +++ b/README.md @@ -100,8 +100,17 @@ $ typikon --list # every accepted value ``` The first run writes your choice to `~/.config/typikon/config.toml` and stops -asking. Change it with `--set-default`, override it for one run with the flag, -or set `TYPIKON_JURISDICTION`. +asking. `--set-default` remembers the whole invocation, not just the +jurisdiction: + +```console +$ typikon --set-default ruthenian --typikon +``` + +records the discipline and the usage alongside the church, so a terminal and a +desktop widget on the same machine cannot end up disagreeing about the same +day. Override any of it for one run with the flags, or set +`TYPIKON_JURISDICTION`. Twenty-three jurisdictions ship: the old-calendar churches (Moscow, ROCOR, Serbia, Georgia, Jerusalem, the Holy Mountain, Poland, Macedonia), the diff --git a/src/config.rs b/src/config.rs index e26f82a..2a9dd2c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,43 +26,74 @@ pub fn path() -> PathBuf { base.join("typikon").join("config.toml") } -/// The one key this file holds, read without a TOML parser because a -/// dependency for `key = "value"` is a dependency too many. -fn read() -> Option { - let text = std::fs::read_to_string(path()).ok()?; +/// Everything the file can hold. All three axes, because remembering only the +/// jurisdiction meant a terminal and a desktop widget on the same machine +/// could disagree about the same day. +#[derive(Default)] +pub struct Saved { + pub jurisdiction: Option, + pub tradition: Option, + /// True when the day should be shown at the Typikon's discipline rather + /// than at the jurisdiction's own particular law. + pub typikon: bool, +} + +/// Read without a TOML parser, because a dependency for `key = "value"` is a +/// dependency too many. +fn read() -> Saved { + let mut out = Saved::default(); + let Ok(text) = std::fs::read_to_string(path()) else { return out }; for line in text.lines() { let line = line.trim(); if line.starts_with('#') { continue; } let Some((key, value)) = line.split_once('=') else { continue }; - if key.trim() != "jurisdiction" { + let value = value.trim().trim_matches(['"', '\'']); + if value.is_empty() { continue; } - let value = value.trim().trim_matches(['"', '\'']); - if !value.is_empty() { - return Some(value.to_string()); + match key.trim() { + "jurisdiction" => out.jurisdiction = Some(value.to_string()), + "tradition" => out.tradition = Some(value.to_string()), + "typikon" => out.typikon = value == "true", + _ => {} } } - None + out } -pub fn write(jurisdiction: &str) -> std::io::Result { +pub fn write(saved: &Saved) -> std::io::Result { let p = path(); if let Some(dir) = p.parent() { std::fs::create_dir_all(dir)?; } - std::fs::write( - &p, - format!( - "# Which reckoning typikon uses when no --jurisdiction is given.\n\ - # Run `typikon --list` to see every accepted value.\n\ - jurisdiction = \"{jurisdiction}\"\n" - ), - )?; + let mut text = String::from( + "# What typikon assumes when the flags are not given.\n\ + # Run `typikon --list` to see every accepted jurisdiction.\n", + ); + if let Some(j) = &saved.jurisdiction { + text.push_str(&format!("jurisdiction = \"{j}\"\n")); + } + if let Some(t) = &saved.tradition { + text.push_str(&format!("tradition = \"{t}\"\n")); + } + if saved.typikon { + text.push_str( + "\n# Show the day at the Typikon's full discipline rather than at the\n\ + # minimum this church's particular law requires. Obligations are still\n\ + # reported either way.\n\ + typikon = true\n", + ); + } + std::fs::write(&p, text)?; Ok(p) } +pub fn saved() -> Saved { + read() +} + /// The environment beats the config file; both beat the built-in default. pub fn default_jurisdiction() -> (Option, Source) { if let Ok(v) = std::env::var(ENV_VAR) { @@ -70,7 +101,7 @@ pub fn default_jurisdiction() -> (Option, Source) { return (Some(v), Source::Environment); } } - match read() { + match read().jurisdiction { Some(v) => (Some(v), Source::Config), None => (None, Source::Default), } diff --git a/src/main.rs b/src/main.rs index a695080..34d91c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -291,8 +291,31 @@ fn run() -> Result { if let Some(name) = &args.set_default { let jur = jurisdictions::resolve(Some(name)) .map_err(|n| format!("unknown jurisdiction {n:?}; try --list"))?; - let where_ = config::write(jur.key).map_err(|e| e.to_string())?; + // Whatever else was given alongside is remembered too, so that + // `--set-default ruthenian --typikon` records the whole setup rather + // than half of it and leaves a terminal disagreeing with a widget. + let tradition = match &args.tradition { + None => None, + Some(t) => Some( + jurisdictions::Tradition::parse(t) + .ok_or_else(|| format!("unknown tradition {t:?}; try slavic or greek"))? + .key() + .to_string(), + ), + }; + let saved = config::Saved { + jurisdiction: Some(jur.key.to_string()), + tradition, + typikon: args.typikon, + }; + let where_ = config::write(&saved).map_err(|e| e.to_string())?; println!(" {} \u{2014} written to {}", jur.name, where_.display()); + if let Some(t) = &saved.tradition { + println!(" {t} usage"); + } + if saved.typikon { + println!(" shown at the Typikon's discipline"); + } return Ok(0); } @@ -307,12 +330,13 @@ fn run() -> Result { // carries. Leaked as a 'static because a Jurisdiction is otherwise a // compile-time constant, and one override per run is not worth threading // a lifetime through the whole library for. - let tradition = match &args.tradition { + let saved = config::saved(); + let tradition = match args.tradition.as_deref().or(saved.tradition.as_deref()) { None => base.tradition, Some(t) => jurisdictions::Tradition::parse(t) .ok_or_else(|| format!("unknown tradition {t:?}; try slavic or greek"))?, }; - let discipline = if args.typikon { + let discipline = if args.typikon || saved.typikon { Some(jurisdictions::RuleKind::Orthodox) } else { base.discipline @@ -399,7 +423,11 @@ fn run() -> Result { // The first run should say what it assumed, and then stop saying it. if source == Source::Default { - if let Ok(p) = config::write(jur.key) { + let first = config::Saved { + jurisdiction: Some(jur.key.to_string()), + ..Default::default() + }; + if let Ok(p) = config::write(&first) { println!(" Reckoning by the {}, written to {}.", jur.name.to_lowercase(), p.display()); println!(" Change it with `typikon --set-default `, or see `typikon --list`.\n"); } diff --git a/tests/typikon.rs b/tests/typikon.rs index 2d2fad4..cb52b22 100644 --- a/tests/typikon.rs +++ b/tests/typikon.rs @@ -585,3 +585,43 @@ fn the_override_changes_nothing_for_a_jurisdiction_already_on_the_typikon() { assert_eq!(a.obligation, b.obligation, "{}", a.date); } } + +// ────────────────────────── what gets remembered ────────────────────────── + +#[test] +fn the_config_round_trips_all_three_axes() { + use typikon::config::{self, Saved}; + // A scratch XDG_CONFIG_HOME, so the test cannot touch a real one. + let dir = std::env::temp_dir().join(format!("typikon-test-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + // SAFETY: single-threaded test, and the variable is restored below. + let prev = std::env::var_os("XDG_CONFIG_HOME"); + unsafe { std::env::set_var("XDG_CONFIG_HOME", &dir) }; + + config::write(&Saved { + jurisdiction: Some("ruthenian".into()), + tradition: Some("slavic".into()), + typikon: true, + }) + .unwrap(); + let back = config::saved(); + assert_eq!(back.jurisdiction.as_deref(), Some("ruthenian")); + assert_eq!(back.tradition.as_deref(), Some("slavic")); + assert!(back.typikon); + + // Remembering only a jurisdiction leaves the other two unset, rather than + // inventing a default that would disagree with the flags. + config::write(&Saved { jurisdiction: Some("rocor".into()), ..Default::default() }).unwrap(); + let plain = config::saved(); + assert_eq!(plain.jurisdiction.as_deref(), Some("rocor")); + assert_eq!(plain.tradition, None); + assert!(!plain.typikon); + + unsafe { + match prev { + Some(v) => std::env::set_var("XDG_CONFIG_HOME", v), + None => std::env::remove_var("XDG_CONFIG_HOME"), + } + } + let _ = std::fs::remove_dir_all(&dir); +}