use std::{fs, io::BufWriter};
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use super::*;
const STATE_FILE_NAME: &str = "state.json";
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct AppState {
pub sorter: Sorter,
pub full_screen: bool,
}
impl AppState {
pub fn load(settings: &Settings) -> anyhow::Result<Self> {
if settings.app_state_dir.is_none() {
Self::move_legacy_state();
}
let state_path = Self::get_persist_path(settings)?;
let state = if state_path.exists() {
let state_file = File::open(state_path)
.map_err(|err| anyhow!("Failed to load state file. Error info: {err}"))?;
serde_json::from_reader(state_file)
.map_err(|err| anyhow!("Failed to read state file. Error info: {err}"))?
} else {
AppState::default()
};
Ok(state)
}
fn get_persist_path(settings: &Settings) -> anyhow::Result<PathBuf> {
if let Some(path) = settings.app_state_dir.as_ref() {
Ok(path.join(STATE_FILE_NAME))
} else {
Self::default_persist_dir().map(|dir| dir.join(STATE_FILE_NAME))
}
}
pub fn save(&self, settings: &Settings) -> anyhow::Result<()> {
let state_path = Self::get_persist_path(settings)?;
if let Some(parent) = state_path.parent() {
fs::create_dir_all(parent)?;
}
let state_file = File::create(state_path)?;
let state_writer = BufWriter::new(state_file);
serde_json::to_writer_pretty(state_writer, self)?;
Ok(())
}
pub fn default_persist_dir() -> anyhow::Result<PathBuf> {
BaseDirs::new()
.map(|base_dirs| {
base_dirs
.state_dir()
.unwrap_or_else(|| base_dirs.data_dir())
.join("tui-journal")
})
.context("Config file path couldn't be retrieved")
}
fn move_legacy_state() {
let state_path = match BaseDirs::new()
.map(|base_dirs| base_dirs.state_dir().map(|state| state.join("tui-journal")))
{
Some(Some(state)) => state,
_ => return,
};
let legacy_data_dir =
match BaseDirs::new().map(|base_dirs| base_dirs.data_dir().join("tui-journal")) {
Some(path) => path,
None => return,
};
if !legacy_data_dir.exists() {
return;
}
let legacy_state_file = legacy_data_dir.join(STATE_FILE_NAME);
if !legacy_state_file.exists() {
if let Err(err) = std::fs::remove_dir_all(&legacy_data_dir) {
log::error!(
"Legacy State: Removing legacy directory failed. path: {}, Error {err}",
legacy_data_dir.display()
);
}
return;
}
let new_state_file = state_path.join(STATE_FILE_NAME);
if new_state_file.exists() {
if let Err(err) = std::fs::remove_dir_all(&legacy_data_dir) {
log::error!(
"Legacy State: Removing legacy directory failed. path: {}, Error {err}",
legacy_data_dir.display()
);
}
return;
}
if !state_path.exists() {
if let Err(err) = std::fs::create_dir_all(&state_path) {
log::error!(
"Legacy State: Creating state dir filed. Path: {}, Error {err}",
state_path.display()
);
return;
}
}
if let Err(err) = std::fs::rename(legacy_state_file, new_state_file) {
log::error!("Legacy State: Moving legacy state file failed. Error {err}");
return;
}
if let Err(err) = std::fs::remove_dir_all(&legacy_data_dir) {
log::error!(
"Legacy State: Removing legacy directory failed. path: {}, Error {err}",
legacy_data_dir.display()
);
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
#[test]
fn missing_state_loads_default() {
let dir = tempfile::Builder::new()
.prefix("state-empty")
.tempdir()
.unwrap();
let settings = Settings {
app_state_dir: Some(dir.path().to_path_buf()),
..Default::default()
};
let state = AppState::load(&settings).unwrap();
assert!(!state.full_screen);
assert_eq!(
state.sorter.get_criteria(),
Sorter::default().get_criteria()
);
assert!(matches!(state.sorter.order, SortOrder::Descending));
}
#[test]
fn save_then_load_round_trips() {
let dir = tempfile::Builder::new()
.prefix("state-save")
.tempdir()
.unwrap();
let settings = Settings {
app_state_dir: Some(dir.path().to_path_buf()),
..Default::default()
};
let mut sorter = Sorter::default();
sorter.set_criteria(vec![SortCriteria::Title]);
sorter.order = SortOrder::Ascending;
let state = AppState {
sorter,
full_screen: true,
};
state.save(&settings).unwrap();
let loaded = AppState::load(&settings).unwrap();
assert!(loaded.full_screen);
assert_eq!(loaded.sorter.get_criteria(), &[SortCriteria::Title]);
assert!(matches!(loaded.sorter.order, SortOrder::Ascending));
}
#[test]
fn custom_dir_sets_path() {
let settings = Settings {
app_state_dir: Some(PathBuf::from("/tmp/custom-state")),
..Default::default()
};
let state_path = AppState::get_persist_path(&settings).unwrap();
assert_eq!(state_path, PathBuf::from("/tmp/custom-state/state.json"));
}
}