1use super::SessionState;
6use anyhow::{Context, Result};
7use par_term_config::Config;
8use std::path::PathBuf;
9
10pub fn session_path() -> PathBuf {
12 Config::config_dir().join("last_session.yaml")
13}
14
15pub fn save_session(state: &SessionState) -> Result<()> {
17 save_session_to(state, session_path())
18}
19
20pub fn save_session_to(state: &SessionState, path: PathBuf) -> Result<()> {
22 crate::atomic_save::save_yaml_atomic(&path, state)
28 .with_context(|| format!("Failed to write session state to {:?}", path))?;
29
30 log::info!(
31 "Saved session state ({} windows) to {:?}",
32 state.windows.len(),
33 path
34 );
35 Ok(())
36}
37
38pub fn load_session() -> Result<Option<SessionState>> {
43 load_session_from(session_path())
44}
45
46pub fn load_session_from(path: PathBuf) -> Result<Option<SessionState>> {
48 if !path.exists() {
49 return Ok(None);
50 }
51
52 let contents = std::fs::read_to_string(&path)
53 .with_context(|| format!("Failed to read session state from {:?}", path))?;
54
55 if contents.trim().is_empty() {
56 return Ok(None);
57 }
58
59 let state: SessionState = serde_yaml_ng::from_str(&contents)
72 .with_context(|| format!("Failed to parse session state from {:?}", path))?;
73
74 log::info!(
75 "Loaded session state ({} windows) from {:?}",
76 state.windows.len(),
77 path
78 );
79 Ok(Some(state))
80}
81
82pub fn clear_session() -> Result<()> {
84 let path = session_path();
85 if path.exists() {
86 std::fs::remove_file(&path)
87 .with_context(|| format!("Failed to remove session state file {:?}", path))?;
88 }
89 Ok(())
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::session::{SessionState, SessionTab, SessionWindow};
96 use par_term_config::snapshot_types::TabSnapshot;
97 use tempfile::tempdir;
98
99 fn sample_session() -> SessionState {
100 SessionState {
101 saved_at: "2025-01-01T00:00:00Z".to_string(),
102 windows: vec![SessionWindow {
103 position: (100, 200),
104 size: (800, 600),
105 tabs: vec![SessionTab {
106 snapshot: TabSnapshot {
107 cwd: Some("/home/user/work".to_string()),
108 title: "work".to_string(),
109 custom_color: None,
110 user_title: None,
111 custom_icon: None,
112 },
113 pane_layout: None,
114 }],
115 active_tab_index: 0,
116 tmux_session_name: None,
117 }],
118 }
119 }
120
121 #[test]
122 fn test_load_nonexistent_file() {
123 let temp = tempdir().unwrap();
124 let path = temp.path().join("nonexistent.yaml");
125 let result = load_session_from(path).unwrap();
126 assert!(result.is_none());
127 }
128
129 #[test]
130 fn test_load_empty_file() {
131 let temp = tempdir().unwrap();
132 let path = temp.path().join("empty.yaml");
133 std::fs::write(&path, "").unwrap();
134 let result = load_session_from(path).unwrap();
135 assert!(result.is_none());
136 }
137
138 #[test]
139 fn test_load_corrupt_file() {
140 let temp = tempdir().unwrap();
141 let path = temp.path().join("corrupt.yaml");
142 std::fs::write(&path, "not: valid: yaml: [[[").unwrap();
143 let result = load_session_from(path);
144 assert!(result.is_err());
145 }
146
147 #[test]
148 fn test_save_and_load_roundtrip() {
149 let temp = tempdir().unwrap();
150 let path = temp.path().join("session.yaml");
151
152 let state = sample_session();
153 save_session_to(&state, path.clone()).unwrap();
154
155 let loaded = load_session_from(path).unwrap().unwrap();
156 assert_eq!(loaded.windows.len(), 1);
157 assert_eq!(loaded.windows[0].position, (100, 200));
158 assert_eq!(loaded.windows[0].size, (800, 600));
159 assert_eq!(loaded.windows[0].tabs.len(), 1);
160 assert_eq!(
161 loaded.windows[0].tabs[0].snapshot.cwd,
162 Some("/home/user/work".to_string())
163 );
164 assert_eq!(loaded.windows[0].tabs[0].snapshot.title, "work");
165 }
166
167 #[test]
168 fn test_roundtrip_preserves_custom_tab_properties() {
169 let temp = tempdir().unwrap();
170 let path = temp.path().join("session.yaml");
171
172 let state = SessionState {
173 saved_at: "2025-01-01T00:00:00Z".to_string(),
174 windows: vec![SessionWindow {
175 position: (0, 0),
176 size: (1920, 1080),
177 tabs: vec![
178 SessionTab {
179 snapshot: TabSnapshot {
180 cwd: Some("/home/user".to_string()),
181 title: "My Custom Tab".to_string(),
182 custom_color: Some([255, 128, 0]),
183 user_title: Some("My Custom Tab".to_string()),
184 custom_icon: Some("🔥".to_string()),
185 },
186 pane_layout: None,
187 },
188 SessionTab {
189 snapshot: TabSnapshot {
190 cwd: Some("/tmp".to_string()),
191 title: "Tab 2".to_string(),
192 custom_color: None,
193 user_title: None,
194 custom_icon: Some("📁".to_string()),
195 },
196 pane_layout: None,
197 },
198 SessionTab {
199 snapshot: TabSnapshot {
200 cwd: None,
201 title: "Colored Only".to_string(),
202 custom_color: Some([0, 200, 100]),
203 user_title: None,
204 custom_icon: None,
205 },
206 pane_layout: None,
207 },
208 ],
209 active_tab_index: 1,
210 tmux_session_name: None,
211 }],
212 };
213
214 save_session_to(&state, path.clone()).unwrap();
215 let loaded = load_session_from(path).unwrap().unwrap();
216 let tabs = &loaded.windows[0].tabs;
217
218 assert_eq!(tabs[0].snapshot.custom_color, Some([255, 128, 0]));
220 assert_eq!(
221 tabs[0].snapshot.user_title,
222 Some("My Custom Tab".to_string())
223 );
224 assert_eq!(tabs[0].snapshot.custom_icon, Some("🔥".to_string()));
225
226 assert_eq!(tabs[1].snapshot.custom_color, None);
228 assert_eq!(tabs[1].snapshot.user_title, None);
229 assert_eq!(tabs[1].snapshot.custom_icon, Some("📁".to_string()));
230
231 assert_eq!(tabs[2].snapshot.custom_color, Some([0, 200, 100]));
233 assert_eq!(tabs[2].snapshot.user_title, None);
234 assert_eq!(tabs[2].snapshot.custom_icon, None);
235 }
236
237 #[test]
238 fn test_save_creates_parent_directory() {
239 let temp = tempdir().unwrap();
240 let path = temp.path().join("nested").join("dir").join("session.yaml");
241
242 let state = sample_session();
243 save_session_to(&state, path.clone()).unwrap();
244 assert!(path.exists());
245 }
246
247 #[cfg(unix)]
250 #[test]
251 fn test_saved_session_is_owner_only() {
252 use std::os::unix::fs::PermissionsExt;
253
254 let temp = tempdir().unwrap();
255 let path = temp.path().join("session.yaml");
256
257 save_session_to(&sample_session(), path.clone()).unwrap();
258
259 let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
260 assert_eq!(mode, 0o600, "expected 0600, got {mode:o}");
261 }
262
263 #[test]
264 fn test_serialization_with_pane_layout() {
265 use crate::pane::SplitDirection;
266 use crate::session::SessionPaneNode;
267
268 let state = SessionState {
269 saved_at: "2025-01-01T00:00:00Z".to_string(),
270 windows: vec![SessionWindow {
271 position: (0, 0),
272 size: (1920, 1080),
273 tabs: vec![SessionTab {
274 snapshot: TabSnapshot {
275 cwd: Some("/home/user".to_string()),
276 title: "dev".to_string(),
277 custom_color: None,
278 user_title: None,
279 custom_icon: None,
280 },
281 pane_layout: Some(SessionPaneNode::Split {
282 direction: SplitDirection::Vertical,
283 ratio: 0.5,
284 first: Box::new(SessionPaneNode::Leaf {
285 cwd: Some("/home/user/code".to_string()),
286 }),
287 second: Box::new(SessionPaneNode::Split {
288 direction: SplitDirection::Horizontal,
289 ratio: 0.6,
290 first: Box::new(SessionPaneNode::Leaf {
291 cwd: Some("/home/user/logs".to_string()),
292 }),
293 second: Box::new(SessionPaneNode::Leaf {
294 cwd: Some("/home/user/tests".to_string()),
295 }),
296 }),
297 }),
298 }],
299 active_tab_index: 0,
300 tmux_session_name: None,
301 }],
302 };
303
304 let temp = tempdir().unwrap();
305 let path = temp.path().join("pane_session.yaml");
306
307 save_session_to(&state, path.clone()).unwrap();
308 let loaded = load_session_from(path).unwrap().unwrap();
309
310 let tab = &loaded.windows[0].tabs[0];
312 assert!(tab.pane_layout.is_some());
313 match tab.pane_layout.as_ref().unwrap() {
314 SessionPaneNode::Split {
315 direction, ratio, ..
316 } => {
317 assert_eq!(*direction, SplitDirection::Vertical);
318 assert!((ratio - 0.5).abs() < f32::EPSILON);
319 }
320 _ => panic!("Expected Split at root"),
321 }
322 }
323
324 #[test]
325 fn test_split_direction_serde() {
326 use crate::pane::SplitDirection;
327
328 let h = SplitDirection::Horizontal;
329 let v = SplitDirection::Vertical;
330
331 let h_yaml = serde_yaml_ng::to_string(&h).unwrap();
332 let v_yaml = serde_yaml_ng::to_string(&v).unwrap();
333
334 let h_back: SplitDirection = serde_yaml_ng::from_str(&h_yaml).unwrap();
335 let v_back: SplitDirection = serde_yaml_ng::from_str(&v_yaml).unwrap();
336
337 assert_eq!(h, h_back);
338 assert_eq!(v, v_back);
339 }
340
341 #[test]
342 fn test_clear_session() {
343 let temp = tempdir().unwrap();
344 let path = temp.path().join("to_clear.yaml");
345 std::fs::write(&path, "test").unwrap();
346 assert!(path.exists());
347
348 std::fs::remove_file(&path).unwrap();
351 assert!(!path.exists());
352 }
353}