par_term/session/
storage.rs1use super::SessionState;
6use anyhow::{Context, Result};
7use std::path::PathBuf;
8
9pub fn session_path() -> PathBuf {
11 dirs::config_dir()
12 .unwrap_or_else(|| PathBuf::from("."))
13 .join("par-term")
14 .join("last_session.yaml")
15}
16
17pub fn save_session(state: &SessionState) -> Result<()> {
19 save_session_to(state, session_path())
20}
21
22pub fn save_session_to(state: &SessionState, path: PathBuf) -> Result<()> {
24 if let Some(parent) = path.parent() {
26 std::fs::create_dir_all(parent)
27 .with_context(|| format!("Failed to create config directory {:?}", parent))?;
28 }
29
30 let contents = serde_yaml::to_string(state).context("Failed to serialize session state")?;
31
32 std::fs::write(&path, contents)
33 .with_context(|| format!("Failed to write session state to {:?}", path))?;
34
35 log::info!(
36 "Saved session state ({} windows) to {:?}",
37 state.windows.len(),
38 path
39 );
40 Ok(())
41}
42
43pub fn load_session() -> Result<Option<SessionState>> {
48 load_session_from(session_path())
49}
50
51pub fn load_session_from(path: PathBuf) -> Result<Option<SessionState>> {
53 if !path.exists() {
54 return Ok(None);
55 }
56
57 let contents = std::fs::read_to_string(&path)
58 .with_context(|| format!("Failed to read session state from {:?}", path))?;
59
60 if contents.trim().is_empty() {
61 return Ok(None);
62 }
63
64 let state: SessionState = serde_yaml::from_str(&contents)
65 .with_context(|| format!("Failed to parse session state from {:?}", path))?;
66
67 log::info!(
68 "Loaded session state ({} windows) from {:?}",
69 state.windows.len(),
70 path
71 );
72 Ok(Some(state))
73}
74
75pub fn clear_session() -> Result<()> {
77 let path = session_path();
78 if path.exists() {
79 std::fs::remove_file(&path)
80 .with_context(|| format!("Failed to remove session state file {:?}", path))?;
81 }
82 Ok(())
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::session::{SessionState, SessionTab, SessionWindow};
89 use tempfile::tempdir;
90
91 fn sample_session() -> SessionState {
92 SessionState {
93 saved_at: "2025-01-01T00:00:00Z".to_string(),
94 windows: vec![SessionWindow {
95 position: (100, 200),
96 size: (800, 600),
97 tabs: vec![SessionTab {
98 cwd: Some("/home/user/work".to_string()),
99 title: "work".to_string(),
100 pane_layout: None,
101 }],
102 active_tab_index: 0,
103 }],
104 }
105 }
106
107 #[test]
108 fn test_load_nonexistent_file() {
109 let temp = tempdir().unwrap();
110 let path = temp.path().join("nonexistent.yaml");
111 let result = load_session_from(path).unwrap();
112 assert!(result.is_none());
113 }
114
115 #[test]
116 fn test_load_empty_file() {
117 let temp = tempdir().unwrap();
118 let path = temp.path().join("empty.yaml");
119 std::fs::write(&path, "").unwrap();
120 let result = load_session_from(path).unwrap();
121 assert!(result.is_none());
122 }
123
124 #[test]
125 fn test_load_corrupt_file() {
126 let temp = tempdir().unwrap();
127 let path = temp.path().join("corrupt.yaml");
128 std::fs::write(&path, "not: valid: yaml: [[[").unwrap();
129 let result = load_session_from(path);
130 assert!(result.is_err());
131 }
132
133 #[test]
134 fn test_save_and_load_roundtrip() {
135 let temp = tempdir().unwrap();
136 let path = temp.path().join("session.yaml");
137
138 let state = sample_session();
139 save_session_to(&state, path.clone()).unwrap();
140
141 let loaded = load_session_from(path).unwrap().unwrap();
142 assert_eq!(loaded.windows.len(), 1);
143 assert_eq!(loaded.windows[0].position, (100, 200));
144 assert_eq!(loaded.windows[0].size, (800, 600));
145 assert_eq!(loaded.windows[0].tabs.len(), 1);
146 assert_eq!(
147 loaded.windows[0].tabs[0].cwd,
148 Some("/home/user/work".to_string())
149 );
150 assert_eq!(loaded.windows[0].tabs[0].title, "work");
151 }
152
153 #[test]
154 fn test_save_creates_parent_directory() {
155 let temp = tempdir().unwrap();
156 let path = temp.path().join("nested").join("dir").join("session.yaml");
157
158 let state = sample_session();
159 save_session_to(&state, path.clone()).unwrap();
160 assert!(path.exists());
161 }
162
163 #[test]
164 fn test_serialization_with_pane_layout() {
165 use crate::pane::SplitDirection;
166 use crate::session::SessionPaneNode;
167
168 let state = SessionState {
169 saved_at: "2025-01-01T00:00:00Z".to_string(),
170 windows: vec![SessionWindow {
171 position: (0, 0),
172 size: (1920, 1080),
173 tabs: vec![SessionTab {
174 cwd: Some("/home/user".to_string()),
175 title: "dev".to_string(),
176 pane_layout: Some(SessionPaneNode::Split {
177 direction: SplitDirection::Vertical,
178 ratio: 0.5,
179 first: Box::new(SessionPaneNode::Leaf {
180 cwd: Some("/home/user/code".to_string()),
181 }),
182 second: Box::new(SessionPaneNode::Split {
183 direction: SplitDirection::Horizontal,
184 ratio: 0.6,
185 first: Box::new(SessionPaneNode::Leaf {
186 cwd: Some("/home/user/logs".to_string()),
187 }),
188 second: Box::new(SessionPaneNode::Leaf {
189 cwd: Some("/home/user/tests".to_string()),
190 }),
191 }),
192 }),
193 }],
194 active_tab_index: 0,
195 }],
196 };
197
198 let temp = tempdir().unwrap();
199 let path = temp.path().join("pane_session.yaml");
200
201 save_session_to(&state, path.clone()).unwrap();
202 let loaded = load_session_from(path).unwrap().unwrap();
203
204 let tab = &loaded.windows[0].tabs[0];
206 assert!(tab.pane_layout.is_some());
207 match tab.pane_layout.as_ref().unwrap() {
208 SessionPaneNode::Split {
209 direction, ratio, ..
210 } => {
211 assert_eq!(*direction, SplitDirection::Vertical);
212 assert!((ratio - 0.5).abs() < f32::EPSILON);
213 }
214 _ => panic!("Expected Split at root"),
215 }
216 }
217
218 #[test]
219 fn test_split_direction_serde() {
220 use crate::pane::SplitDirection;
221
222 let h = SplitDirection::Horizontal;
223 let v = SplitDirection::Vertical;
224
225 let h_yaml = serde_yaml::to_string(&h).unwrap();
226 let v_yaml = serde_yaml::to_string(&v).unwrap();
227
228 let h_back: SplitDirection = serde_yaml::from_str(&h_yaml).unwrap();
229 let v_back: SplitDirection = serde_yaml::from_str(&v_yaml).unwrap();
230
231 assert_eq!(h, h_back);
232 assert_eq!(v, v_back);
233 }
234
235 #[test]
236 fn test_clear_session() {
237 let temp = tempdir().unwrap();
238 let path = temp.path().join("to_clear.yaml");
239 std::fs::write(&path, "test").unwrap();
240 assert!(path.exists());
241
242 std::fs::remove_file(&path).unwrap();
245 assert!(!path.exists());
246 }
247}