//! Threads and projects kept between runs, under //! `$XDG_DATA_HOME/vectrum-code`: one JSON file per thread in `threads/`, //! and `state.json` for the project list and the open thread. use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::session::SavedThread; use crate::settings; #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(default)] pub struct SavedProject { pub path: PathBuf, pub name: String, /// Milliseconds since the epoch. pub added: u64, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(default)] pub struct AppState { pub projects: Vec, /// The thread that was open. pub active: Option, } fn threads_dir() -> Option { Some(settings::data_dir()?.join("threads")) } fn state_path() -> Option { Some(settings::data_dir()?.join("state.json")) } /// Writes through a temporary file, so a crash mid-write can't leave a /// half-written file behind. fn write_atomically(path: &Path, contents: &[u8]) -> std::io::Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } let temp = path.with_extension("json.tmp"); std::fs::write(&temp, contents)?; std::fs::rename(temp, path) } pub fn load_state() -> AppState { state_path() .and_then(|path| std::fs::read_to_string(path).ok()) .and_then(|text| serde_json::from_str(&text).ok()) .unwrap_or_default() } pub fn save_state(state: &AppState) { let Some(path) = state_path() else { return }; if let Err(err) = write_atomically(&path, serde_json::to_string_pretty(state).unwrap_or_default().as_bytes()) { eprintln!("vectrum-code: couldn't save {}: {err}", path.display()); } } /// Every saved thread that still reads. pub fn load_threads() -> Vec { let Some(dir) = threads_dir() else { return Vec::new() }; let Ok(entries) = std::fs::read_dir(dir) else { return Vec::new() }; entries .flatten() .map(|entry| entry.path()) .filter(|path| path.extension().is_some_and(|extension| extension == "json")) .filter_map(|path| { let text = std::fs::read_to_string(&path).ok()?; match serde_json::from_str(&text) { Ok(thread) => Some(thread), Err(err) => { eprintln!("vectrum-code: skipping {}: {err}", path.display()); None } } }) .collect() } pub fn save_thread(thread: &SavedThread) { let Some(dir) = threads_dir() else { return }; let path = dir.join(format!("{}.json", thread.id)); match serde_json::to_vec(thread) { Ok(bytes) => { if let Err(err) = write_atomically(&path, &bytes) { eprintln!("vectrum-code: couldn't save {}: {err}", path.display()); } } Err(err) => eprintln!("vectrum-code: couldn't save thread {}: {err}", thread.id), } } pub fn delete_thread(id: &str) { if let Some(dir) = threads_dir() { let _ = std::fs::remove_file(dir.join(format!("{id}.json"))); } }