Skip to main content

kimun_notes/update/
state.rs

1//! Machine-managed update state (`update_state.toml`). Holds the throttle
2//! timestamp, the last-known latest version, and any version the user chose to
3//! skip. Kept strictly separate from `config.toml`, which is user-owned and
4//! never written by an automated action.
5
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8use std::io;
9use std::path::Path;
10
11const STATE_FILE: &str = "update_state.toml";
12
13const STATE_HEADER: &str = "\
14# Kimün update state — auto-generated, do not edit.
15# Tracks the last update check, the latest known release, and any version you
16# chose to skip. Your settings live in config.toml, not here.
17";
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
20pub struct UpdateState {
21    /// When the GitHub releases API was last queried.
22    #[serde(default)]
23    pub last_check: Option<DateTime<Utc>>,
24    /// Latest stable version seen at the last check (without the tag prefix).
25    #[serde(default)]
26    pub latest_version: Option<String>,
27    /// A version the user dismissed; suppresses the notification until a newer
28    /// one ships.
29    #[serde(default)]
30    pub dismissed_version: Option<String>,
31}
32
33impl UpdateState {
34    /// Load from `config_dir`, returning the default (empty) state if the file
35    /// is absent or unreadable — a missing/corrupt cache must never be fatal.
36    pub fn load(config_dir: &Path) -> Self {
37        std::fs::read_to_string(config_dir.join(STATE_FILE))
38            .ok()
39            .and_then(|raw| toml::from_str(&raw).ok())
40            .unwrap_or_default()
41    }
42
43    /// Persist to `config_dir`, prefixed with the do-not-edit header.
44    pub fn save(&self, config_dir: &Path) -> io::Result<()> {
45        let body = toml::to_string(self).map_err(io::Error::other)?;
46        std::fs::write(config_dir.join(STATE_FILE), format!("{STATE_HEADER}{body}"))
47    }
48
49    /// Whether the last check is older than `max_age` (or never happened).
50    pub fn is_stale(&self, now: DateTime<Utc>, max_age: Duration) -> bool {
51        match self.last_check {
52            Some(checked) => now.signed_duration_since(checked) >= max_age,
53            None => true,
54        }
55    }
56}