arct_core/
session.rs

1//! Session management and state tracking
2
3use 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/// Represents a user session
11#[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/// Current state of the session
24#[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/// Entry in command history
34#[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/// Statistics about the session
43#[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    /// Create a new session
54    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    /// Record a command in history
83    pub fn record_command(&mut self, command: String, exit_code: Option<i32>, duration_ms: Option<u64>) {
84        let now = chrono::Utc::now();
85
86        // Add to history
87        self.history.push(HistoryEntry {
88            command: command.clone(),
89            executed_at: now,
90            exit_code,
91            duration_ms,
92        });
93
94        // Update statistics
95        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        // Update user stats
107        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    /// Set an environment variable
115    pub fn set_env_var(&mut self, key: String, value: String) {
116        self.env_vars.insert(key, value);
117    }
118
119    /// Get an environment variable
120    pub fn get_env_var(&self, key: &str) -> Option<&String> {
121        self.env_vars.get(key)
122    }
123
124    /// Create an alias
125    pub fn set_alias(&mut self, name: String, command: String) {
126        self.aliases.insert(name, command);
127    }
128
129    /// Get an alias
130    pub fn get_alias(&self, name: &str) -> Option<&String> {
131        self.aliases.get(name)
132    }
133
134    /// Expand aliases in a command
135    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    /// Change directory and update state
150    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    /// Get the most used commands
162    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    /// Get session duration
176    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}