use std::path::{Path, PathBuf}; use tracing::warn; use super::snapshot::{parse_snapshot, snapshot_file_version, SessionSnapshot, SNAPSHOT_VERSION}; fn session_path() -> PathBuf { crate::config::config_dir().join("session.json") } pub(super) fn save_to_path(path: &Path, snapshot: &SessionSnapshot) -> std::io::Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } let json = serde_json::to_string_pretty(snapshot)?; let tmp_path = path.with_extension("json.tmp"); std::fs::write(&tmp_path, &json)?; if let Err(err) = std::fs::rename(&tmp_path, path) { let _ = std::fs::remove_file(&tmp_path); return Err(err); } Ok(()) } pub(super) fn clear_path(path: &Path) -> std::io::Result<()> { match std::fs::remove_file(path) { Ok(()) => Ok(()), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(err) => Err(err), } } pub fn save(snapshot: &SessionSnapshot) { let path = session_path(); if let Err(err) = save_to_path(&path, snapshot) { crate::logging::session_save_failed(&path, &err.to_string()); return; } crate::logging::session_saved(&path, snapshot.workspaces.len()); } pub fn clear() { let path = session_path(); if let Err(err) = clear_path(&path) { crate::logging::session_clear_failed(&path, &err.to_string()); return; } crate::logging::session_cleared(&path); } pub fn load() -> Option { let path = session_path(); if !path.exists() { return None; } let content = match std::fs::read_to_string(&path) { Ok(content) => content, Err(err) => { warn!(err = %err, "failed to read session file"); return None; } }; match parse_snapshot(&content) { Ok(snapshot) => Some(snapshot), Err(err) => { if let Some(version) = snapshot_file_version(&content) { if version > SNAPSHOT_VERSION { warn!( file_version = version, supported = SNAPSHOT_VERSION, "session file is from a newer herdr version, ignoring" ); return None; } } warn!(err = %err, "failed to parse session file, ignoring"); None } } } #[cfg(test)] mod tests { use super::*; use crate::app::state::AgentPanelScope; fn temp_session_path(name: &str) -> PathBuf { let unique = format!( "herdr-session-tests-{}-{}-{}", name, std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() ); std::env::temp_dir().join(unique).join("session.json") } fn empty_snapshot() -> SessionSnapshot { SessionSnapshot { version: SNAPSHOT_VERSION, workspaces: vec![], active: None, selected: 0, agent_panel_scope: AgentPanelScope::CurrentWorkspace, sidebar_width: Some(26), sidebar_section_split: Some(0.5), } } #[test] fn clear_path_removes_existing_session_file() { let path = temp_session_path("clear-existing"); save_to_path(&path, &empty_snapshot()).unwrap(); clear_path(&path).unwrap(); assert!(!path.exists()); } #[test] fn clear_path_ignores_missing_session_file() { let path = temp_session_path("clear-missing"); clear_path(&path).unwrap(); assert!(!path.exists()); } }