remember the whole setup, not a third of it

The config file held one key while the program grew three axes, so
--set-default recorded the jurisdiction and silently dropped the discipline
and the usage. On this machine that produced exactly the failure it sounds
like: a terminal reckoning by ROCOR beside a desktop widget reckoning by the
Ruthenian calendar, both correct, neither agreeing.

--set-default now writes whatever was given alongside it, and an ordinary run
reads all three back with flags still winning over the file.
This commit is contained in:
funkpower
2026-09-18 12:29:57 -04:00
parent be45fa6a27
commit c3a5c67f70
4 changed files with 133 additions and 25 deletions
+11 -2
View File
@@ -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
+50 -19
View File
@@ -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<String> {
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<String>,
pub tradition: Option<String>,
/// 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<PathBuf> {
pub fn write(saved: &Saved) -> std::io::Result<PathBuf> {
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<String>, Source) {
if let Ok(v) = std::env::var(ENV_VAR) {
@@ -70,7 +101,7 @@ pub fn default_jurisdiction() -> (Option<String>, Source) {
return (Some(v), Source::Environment);
}
}
match read() {
match read().jurisdiction {
Some(v) => (Some(v), Source::Config),
None => (None, Source::Default),
}
+32 -4
View File
@@ -291,8 +291,31 @@ fn run() -> Result<i32, String> {
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<i32, String> {
// 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<i32, String> {
// 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 <jurisdiction>`, or see `typikon --list`.\n");
}
+40
View File
@@ -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);
}