use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
const APP_NAME: &str = "markdown-reader";
const STATE_FILE: &str = "state.toml";
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct TabSession {
pub file: PathBuf,
#[serde(default)]
pub scroll: u32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(from = "SessionCompat")]
pub struct Session {
pub tabs: Vec<TabSession>,
pub active: usize,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum SessionCompat {
New {
tabs: Vec<TabSession>,
active: usize,
},
Legacy {
file: PathBuf,
#[serde(default)]
scroll: u32,
},
}
impl From<SessionCompat> for Session {
fn from(v: SessionCompat) -> Self {
match v {
SessionCompat::New { tabs, active } => Self { tabs, active },
SessionCompat::Legacy { file, scroll } => Self {
tabs: vec![TabSession { file, scroll }],
active: 0,
},
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AppState {
#[serde(default)]
pub sessions: HashMap<PathBuf, Session>,
}
impl AppState {
pub fn load() -> Self {
let Some(path) = state_path() else {
return Self::default();
};
let Ok(text) = fs::read_to_string(&path) else {
return Self::default();
};
toml::from_str(&text).unwrap_or_default()
}
pub fn save(&self) {
let Some(path) = state_path() else {
return;
};
if let Some(parent) = path.parent()
&& fs::create_dir_all(parent).is_err()
{
return;
}
let Ok(text) = toml::to_string_pretty(self) else {
return;
};
let _ = fs::write(&path, text);
}
pub fn update_session(&mut self, root: &Path, tabs: Vec<TabSession>, active_idx: usize) {
self.sessions.insert(
root.to_path_buf(),
Session {
tabs,
active: active_idx,
},
);
self.save();
}
}
fn state_path() -> Option<PathBuf> {
let base = dirs::state_dir().or_else(dirs::data_dir)?;
let mut path = base;
path.push(APP_NAME);
path.push(STATE_FILE);
Some(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_legacy_deserialize() {
let toml = r#"file = "/some/file.md"
scroll = 42
"#;
let session: Session = toml::from_str(toml).expect("deserialize");
assert_eq!(session.tabs.len(), 1);
assert_eq!(session.tabs[0].file, PathBuf::from("/some/file.md"));
assert_eq!(session.tabs[0].scroll, 42);
assert_eq!(session.active, 0);
}
#[test]
fn session_new_roundtrip() {
let original = Session {
tabs: vec![
TabSession {
file: PathBuf::from("/a.md"),
scroll: 0,
},
TabSession {
file: PathBuf::from("/b.md"),
scroll: 10,
},
TabSession {
file: PathBuf::from("/c.md"),
scroll: 5,
},
],
active: 1,
};
let serialized = toml::to_string_pretty(&original).expect("serialize");
let deserialized: Session = toml::from_str(&serialized).expect("deserialize");
assert_eq!(deserialized, original);
}
}