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>, }
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 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}