1use crate::challenge::ChallengeManager;
4use crate::stats::UserStats;
5use crate::types::{Error, Result};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Session {
13 pub id: String,
14 pub state: SessionState,
15 pub history: Vec<HistoryEntry>,
16 pub env_vars: HashMap<String, String>,
17 pub aliases: HashMap<String, String>,
18 pub statistics: SessionStatistics,
19 pub stats: UserStats,
20 pub challenge_manager: ChallengeManager,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct SessionState {
26 pub working_directory: PathBuf,
27 pub previous_directory: Option<PathBuf>,
28 pub exit_code: i32,
29 pub started_at: chrono::DateTime<chrono::Utc>,
30 pub last_activity: chrono::DateTime<chrono::Utc>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct HistoryEntry {
36 pub command: String,
37 pub executed_at: chrono::DateTime<chrono::Utc>,
38 pub exit_code: Option<i32>,
39 pub duration_ms: Option<u64>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SessionStatistics {
45 pub total_commands: usize,
46 pub unique_commands: usize,
47 pub command_counts: HashMap<String, usize>,
48 pub errors: usize,
49 pub warnings_shown: usize,
50}
51
52impl Session {
53 pub fn new() -> Self {
55 let now = chrono::Utc::now();
56 let working_directory = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
57
58 Self {
59 id: uuid::Uuid::new_v4().to_string(),
60 state: SessionState {
61 working_directory,
62 previous_directory: None,
63 exit_code: 0,
64 started_at: now,
65 last_activity: now,
66 },
67 history: Vec::new(),
68 env_vars: HashMap::new(),
69 aliases: HashMap::new(),
70 statistics: SessionStatistics {
71 total_commands: 0,
72 unique_commands: 0,
73 command_counts: HashMap::new(),
74 errors: 0,
75 warnings_shown: 0,
76 },
77 stats: UserStats::new(),
78 challenge_manager: ChallengeManager::new(),
79 }
80 }
81
82 pub fn record_command(&mut self, command: String, exit_code: Option<i32>, duration_ms: Option<u64>) {
84 let now = chrono::Utc::now();
85
86 self.history.push(HistoryEntry {
88 command: command.clone(),
89 executed_at: now,
90 exit_code,
91 duration_ms,
92 });
93
94 self.statistics.total_commands += 1;
96 let program = command.split_whitespace().next().unwrap_or("").to_string();
97 *self.statistics.command_counts.entry(program.clone()).or_insert(0) += 1;
98 self.statistics.unique_commands = self.statistics.command_counts.len();
99
100 if let Some(code) = exit_code {
101 if code != 0 {
102 self.statistics.errors += 1;
103 }
104 }
105
106 self.stats.record_command_use(program);
108 self.stats.update_streak();
109
110 self.state.last_activity = now;
111 self.state.exit_code = exit_code.unwrap_or(0);
112 }
113
114 pub fn set_env_var(&mut self, key: String, value: String) {
116 self.env_vars.insert(key, value);
117 }
118
119 pub fn get_env_var(&self, key: &str) -> Option<&String> {
121 self.env_vars.get(key)
122 }
123
124 pub fn set_alias(&mut self, name: String, command: String) {
126 self.aliases.insert(name, command);
127 }
128
129 pub fn get_alias(&self, name: &str) -> Option<&String> {
131 self.aliases.get(name)
132 }
133
134 pub fn expand_aliases(&self, input: &str) -> String {
136 if let Some(first_word) = input.split_whitespace().next() {
137 if let Some(alias_cmd) = self.get_alias(first_word) {
138 let remaining: Vec<&str> = input.split_whitespace().skip(1).collect();
139 if remaining.is_empty() {
140 return alias_cmd.clone();
141 } else {
142 return format!("{} {}", alias_cmd, remaining.join(" "));
143 }
144 }
145 }
146 input.to_string()
147 }
148
149 pub fn change_directory(&mut self, path: PathBuf) -> Result<()> {
151 let current = std::env::current_dir().ok();
152
153 std::env::set_current_dir(&path)
154 .map_err(|e| Error::SessionError(format!("Failed to change directory: {}", e)))?;
155
156 self.state.previous_directory = current;
157 self.state.working_directory = path;
158 Ok(())
159 }
160
161 pub fn get_top_commands(&self, limit: usize) -> Vec<(String, usize)> {
163 let mut commands: Vec<(String, usize)> = self
164 .statistics
165 .command_counts
166 .iter()
167 .map(|(cmd, count)| (cmd.clone(), *count))
168 .collect();
169
170 commands.sort_by(|a, b| b.1.cmp(&a.1));
171 commands.truncate(limit);
172 commands
173 }
174
175 pub fn duration(&self) -> chrono::Duration {
177 self.state.last_activity - self.state.started_at
178 }
179}
180
181impl Default for Session {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_new_session() {
193 let session = Session::new();
194 assert_eq!(session.statistics.total_commands, 0);
195 assert!(session.history.is_empty());
196 }
197
198 #[test]
199 fn test_record_command() {
200 let mut session = Session::new();
201 session.record_command("ls -la".to_string(), Some(0), Some(100));
202
203 assert_eq!(session.statistics.total_commands, 1);
204 assert_eq!(session.history.len(), 1);
205 assert_eq!(session.statistics.command_counts.get("ls"), Some(&1));
206 }
207
208 #[test]
209 fn test_aliases() {
210 let mut session = Session::new();
211 session.set_alias("ll".to_string(), "ls -la".to_string());
212
213 assert_eq!(session.get_alias("ll"), Some(&"ls -la".to_string()));
214 assert_eq!(session.expand_aliases("ll"), "ls -la");
215 assert_eq!(session.expand_aliases("ll /tmp"), "ls -la /tmp");
216 }
217
218 #[test]
219 fn test_top_commands() {
220 let mut session = Session::new();
221 session.record_command("ls".to_string(), Some(0), None);
222 session.record_command("ls".to_string(), Some(0), None);
223 session.record_command("cd".to_string(), Some(0), None);
224
225 let top = session.get_top_commands(2);
226 assert_eq!(top[0].0, "ls");
227 assert_eq!(top[0].1, 2);
228 }
229}