1use std::fs;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Default)]
7pub struct SessionFile {
8 pub path: String,
9 pub row: usize,
10 pub col: usize,
11}
12
13#[derive(Debug, Clone, Default)]
14pub struct Session {
15 pub files: Vec<SessionFile>,
16 pub active: usize,
17}
18
19fn session_path() -> PathBuf {
20 let home = std::env::var("HOME")
21 .or_else(|_| std::env::var("USERPROFILE"))
22 .unwrap_or_else(|_| ".".to_string());
23 PathBuf::from(home).join(".xei").join("session")
24}
25
26pub fn load() -> Session {
28 let mut session = Session::default();
29 let Ok(text) = fs::read_to_string(session_path()) else {
30 return session;
31 };
32 for line in text.lines() {
33 let line = line.trim();
34 if line.is_empty() || line.starts_with('#') {
35 continue;
36 }
37 if let Some(v) = line.strip_prefix("active=") {
38 session.active = v.trim().parse().unwrap_or(0);
39 continue;
40 }
41 let parts: Vec<&str> = line.split('|').collect();
43 if parts.is_empty() || parts[0].is_empty() {
44 continue;
45 }
46 let path = parts[0].to_string();
47 let row = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
48 let col = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
49 if !PathBuf::from(&path).exists() {
51 continue;
52 }
53 session.files.push(SessionFile { path, row, col });
54 }
55 if session.active >= session.files.len() && !session.files.is_empty() {
56 session.active = session.files.len() - 1;
57 }
58 session
59}
60
61pub fn save(session: &Session) {
63 let path = session_path();
64 if let Some(parent) = path.parent() {
65 let _ = fs::create_dir_all(parent);
66 }
67 let mut out = String::from("# xei session — paths and cursor positions\n");
68 out.push_str(&format!("active={}\n", session.active));
69 for f in &session.files {
70 if f.path.is_empty() {
71 continue;
72 }
73 out.push_str(&format!("{}|{}|{}\n", f.path, f.row, f.col));
74 }
75 let _ = fs::write(path, out);
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn roundtrip_format() {
84 let s = Session {
85 active: 1,
86 files: vec![
87 SessionFile {
88 path: "/tmp/a.rs".into(),
89 row: 2,
90 col: 3,
91 },
92 SessionFile {
93 path: "/tmp/b.rs".into(),
94 row: 0,
95 col: 0,
96 },
97 ],
98 };
99 let mut text = format!("active={}\n", s.active);
101 for f in &s.files {
102 text.push_str(&format!("{}|{}|{}\n", f.path, f.row, f.col));
103 }
104 let mut parsed = Session::default();
105 for line in text.lines() {
106 if let Some(v) = line.strip_prefix("active=") {
107 parsed.active = v.parse().unwrap();
108 } else {
109 let parts: Vec<&str> = line.split('|').collect();
110 parsed.files.push(SessionFile {
111 path: parts[0].into(),
112 row: parts[1].parse().unwrap(),
113 col: parts[2].parse().unwrap(),
114 });
115 }
116 }
117 assert_eq!(parsed.active, 1);
118 assert_eq!(parsed.files.len(), 2);
119 assert_eq!(parsed.files[0].row, 2);
120 }
121}