Skip to main content

xei_core/
session.rs

1//! Lightweight session restore: open files + cursor positions.
2
3use 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
26/// Load session from `~/.xei/session`. Returns empty session if missing.
27pub 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        // path|row|col
42        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        // Skip files that no longer exist
50        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
61/// Persist session to `~/.xei/session`.
62pub 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        // Manual serialize/deserialize of format without writing home
100        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}