detached_shell/
history.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5
6use crate::error::{NdsError, Result};
7use crate::session::Session;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub enum SessionEvent {
11    Created,
12    Attached,
13    Detached,
14    Killed,
15    Crashed,
16    Renamed { from: Option<String>, to: String },
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct HistoryEntry {
21    pub session_id: String,
22    pub session_name: Option<String>,
23    pub event: SessionEvent,
24    pub timestamp: DateTime<Utc>,
25    pub pid: i32,
26    pub shell: String,
27    pub working_dir: String,
28    pub duration_seconds: Option<i64>, // For Killed/Crashed events
29}
30
31#[derive(Debug, Serialize, Deserialize)]
32pub struct SessionHistory {
33    pub entries: Vec<HistoryEntry>,
34}
35
36impl SessionHistory {
37    pub fn new() -> Self {
38        SessionHistory {
39            entries: Vec::new(),
40        }
41    }
42
43    pub fn history_file() -> Result<PathBuf> {
44        let dir = directories::BaseDirs::new()
45            .ok_or_else(|| {
46                NdsError::DirectoryCreationError("Could not find home directory".to_string())
47            })?
48            .home_dir()
49            .join(".nds");
50
51        if !dir.exists() {
52            fs::create_dir_all(&dir)
53                .map_err(|e| NdsError::DirectoryCreationError(e.to_string()))?;
54        }
55
56        Ok(dir.join("history.json"))
57    }
58
59    pub fn load() -> Result<Self> {
60        let path = Self::history_file()?;
61
62        if !path.exists() {
63            // Create empty history if file doesn't exist
64            let history = Self::new();
65            history.save()?;
66            return Ok(history);
67        }
68
69        let content = fs::read_to_string(&path)?;
70        let history: SessionHistory =
71            serde_json::from_str(&content).unwrap_or_else(|_| Self::new());
72
73        Ok(history)
74    }
75
76    pub fn save(&self) -> Result<()> {
77        let path = Self::history_file()?;
78        let json = serde_json::to_string_pretty(self)?;
79        fs::write(path, json)?;
80        Ok(())
81    }
82
83    pub fn add_entry(&mut self, entry: HistoryEntry) -> Result<()> {
84        self.entries.push(entry);
85        self.save()
86    }
87
88    pub fn record_session_created(session: &Session) -> Result<()> {
89        let mut history = Self::load()?;
90        let entry = HistoryEntry {
91            session_id: session.id.clone(),
92            session_name: session.name.clone(),
93            event: SessionEvent::Created,
94            timestamp: session.created_at,
95            pid: session.pid,
96            shell: session.shell.clone(),
97            working_dir: session.working_dir.clone(),
98            duration_seconds: None,
99        };
100        history.add_entry(entry)?;
101        Ok(())
102    }
103
104    pub fn record_session_attached(session: &Session) -> Result<()> {
105        let mut history = Self::load()?;
106        let entry = HistoryEntry {
107            session_id: session.id.clone(),
108            session_name: session.name.clone(),
109            event: SessionEvent::Attached,
110            timestamp: Utc::now(),
111            pid: session.pid,
112            shell: session.shell.clone(),
113            working_dir: session.working_dir.clone(),
114            duration_seconds: None,
115        };
116        history.add_entry(entry)?;
117        Ok(())
118    }
119
120    pub fn record_session_detached(session: &Session) -> Result<()> {
121        let mut history = Self::load()?;
122        let entry = HistoryEntry {
123            session_id: session.id.clone(),
124            session_name: session.name.clone(),
125            event: SessionEvent::Detached,
126            timestamp: Utc::now(),
127            pid: session.pid,
128            shell: session.shell.clone(),
129            working_dir: session.working_dir.clone(),
130            duration_seconds: None,
131        };
132        history.add_entry(entry)?;
133        Ok(())
134    }
135
136    pub fn record_session_killed(session: &Session) -> Result<()> {
137        let mut history = Self::load()?;
138        let duration = (Utc::now() - session.created_at).num_seconds();
139        let entry = HistoryEntry {
140            session_id: session.id.clone(),
141            session_name: session.name.clone(),
142            event: SessionEvent::Killed,
143            timestamp: Utc::now(),
144            pid: session.pid,
145            shell: session.shell.clone(),
146            working_dir: session.working_dir.clone(),
147            duration_seconds: Some(duration),
148        };
149        history.add_entry(entry)?;
150        Ok(())
151    }
152
153    pub fn record_session_crashed(session: &Session) -> Result<()> {
154        let mut history = Self::load()?;
155        let duration = (Utc::now() - session.created_at).num_seconds();
156        let entry = HistoryEntry {
157            session_id: session.id.clone(),
158            session_name: session.name.clone(),
159            event: SessionEvent::Crashed,
160            timestamp: Utc::now(),
161            pid: session.pid,
162            shell: session.shell.clone(),
163            working_dir: session.working_dir.clone(),
164            duration_seconds: Some(duration),
165        };
166        history.add_entry(entry)?;
167        Ok(())
168    }
169
170    pub fn record_session_renamed(
171        session: &Session,
172        old_name: Option<String>,
173        new_name: String,
174    ) -> Result<()> {
175        let mut history = Self::load()?;
176        let entry = HistoryEntry {
177            session_id: session.id.clone(),
178            session_name: Some(new_name.clone()),
179            event: SessionEvent::Renamed {
180                from: old_name,
181                to: new_name,
182            },
183            timestamp: Utc::now(),
184            pid: session.pid,
185            shell: session.shell.clone(),
186            working_dir: session.working_dir.clone(),
187            duration_seconds: None,
188        };
189        history.add_entry(entry)?;
190        Ok(())
191    }
192
193    pub fn get_session_history(&self, session_id: &str) -> Vec<&HistoryEntry> {
194        self.entries
195            .iter()
196            .filter(|e| e.session_id.starts_with(session_id))
197            .collect()
198    }
199
200    pub fn get_all_sessions(&self) -> Vec<String> {
201        let mut sessions = Vec::new();
202        let mut seen = std::collections::HashSet::new();
203
204        for entry in &self.entries {
205            if seen.insert(entry.session_id.clone()) {
206                sessions.push(entry.session_id.clone());
207            }
208        }
209
210        sessions
211    }
212
213    pub fn format_duration(seconds: i64) -> String {
214        let hours = seconds / 3600;
215        let minutes = (seconds % 3600) / 60;
216        let secs = seconds % 60;
217
218        if hours > 0 {
219            format!("{}h {}m {}s", hours, minutes, secs)
220        } else if minutes > 0 {
221            format!("{}m {}s", minutes, secs)
222        } else {
223            format!("{}s", secs)
224        }
225    }
226}