Skip to main content

odds/persistence/
session.rs

1use serde::{Deserialize, Serialize};
2use std::{
3    path::PathBuf,
4    time::{SystemTime, UNIX_EPOCH},
5};
6
7use crate::{paths, persistence::persistable::Persistable};
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct SessionEntry {
11    pub path: PathBuf,
12}
13
14#[derive(Debug, Serialize, Deserialize)]
15pub struct Session {
16    max_size: usize,
17    pub entries: Vec<SessionEntry>,
18    saved_at: u64,
19}
20
21const SESSION_EXPIRY_SECS: u64 = 43200; // 12 hours
22const MAX_SIZE: usize = 10;
23
24impl Default for Session {
25    fn default() -> Self {
26        Self {
27            max_size: MAX_SIZE,
28            entries: Vec::new(),
29            saved_at: time_now(),
30        }
31    }
32}
33
34impl Session {
35    /// Push a directory onto the session stack.
36    pub fn push(&mut self, path: &PathBuf) {
37        let path = paths::normalize(path);
38
39        // If already current do nothing
40        if self.current() == Some(&path) {
41            return;
42        }
43
44        // Remove existing occurrences
45        self.entries.retain(|e| e.path != path);
46
47        // Insert at top
48        self.entries.insert(0, SessionEntry { path });
49
50        // Enforce max size
51        if self.entries.len() > self.max_size {
52            self.entries.truncate(self.max_size);
53        }
54    }
55
56    pub fn current(&self) -> Option<&PathBuf> {
57        self.entries.first().map(|e| &e.path)
58    }
59
60    pub fn previous(&self) -> Option<&PathBuf> {
61        self.entries.get(1).map(|e| &e.path)
62    }
63
64    /// List all directories (most recent first).
65    pub fn list(&self) -> &[SessionEntry] {
66        &self.entries
67    }
68
69    pub fn contains(&self, path: &PathBuf) -> bool {
70        self.entries.iter().any(|e| e.path == *path)
71    }
72
73    /// Human-readable session (for `o session`).
74    pub fn formatted(&self) -> Vec<String> {
75        self.entries
76            .iter()
77            .enumerate()
78            .map(|(i, e)| {
79                if i == 0 {
80                    format!("{} {} <-- current", i + 1, e.path.display())
81                } else {
82                    format!("{} {}", i + 1, e.path.display())
83                }
84            })
85            .collect()
86    }
87
88    /// Load the existing session or create and return a new one if the old one is expired or it doesn't exist yet.
89    pub fn load_or_new() -> Self {
90        if let Ok(session) = Self::load() {
91            // Expire sessions older than SESSION_EXPIRY_SECS; saturating_sub guards against clock skew.
92            if time_now().saturating_sub(session.saved_at) < SESSION_EXPIRY_SECS {
93                return session;
94            }
95        }
96
97        let mut new_session = Self::default();
98
99        if let Err(e) = new_session.save() {
100            eprintln!("Error saving session: {e}");
101        }
102
103        new_session
104    }
105}
106
107fn time_now() -> u64 {
108    SystemTime::now()
109        .duration_since(UNIX_EPOCH)
110        .unwrap_or_default()
111        .as_secs()
112}
113
114impl Persistable for Session {
115    const FILE: &'static str = "session.json";
116
117    fn before_save(&mut self) {
118        self.saved_at = time_now();
119    }
120}