Skip to main content

aurora_context/
session.rs

1use std::fs;
2use std::path::PathBuf;
3
4use aurora_core::{AuroraError, AuroraResult, Pipeline, Value};
5use chrono::{DateTime, Duration, Utc};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SessionEntry {
10    pub timestamp: DateTime<Utc>,
11    pub command: String,
12    pub directory: String,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct SessionStore {
17    pub entries: Vec<SessionEntry>,
18    path: PathBuf,
19}
20
21impl SessionStore {
22    pub fn new() -> AuroraResult<Self> {
23        let home = std::env::var("HOME")
24            .map_err(|_| AuroraError::ContextError("HOME not set".to_string()))?;
25        let dir = PathBuf::from(home).join(".cache/aurora");
26        let path = dir.join("session.json");
27
28        if path.exists() {
29            let content =
30                fs::read_to_string(&path).map_err(|e| AuroraError::Io(e))?;
31            let entries: Vec<SessionEntry> =
32                serde_json::from_str(&content).unwrap_or_default();
33            Ok(Self { entries, path })
34        } else {
35            fs::create_dir_all(&dir).map_err(|e| AuroraError::Io(e))?;
36            Ok(Self {
37                entries: Vec::new(),
38                path,
39            })
40        }
41    }
42
43    pub fn record(&mut self, cmd: &str) -> AuroraResult<()> {
44        let cwd = std::env::current_dir()
45            .map(|p| p.to_string_lossy().to_string())
46            .unwrap_or_default();
47
48        self.entries.push(SessionEntry {
49            timestamp: Utc::now(),
50            command: cmd.to_string(),
51            directory: cwd,
52        });
53
54        let json =
55            serde_json::to_string_pretty(&self.entries).map_err(|e| {
56                AuroraError::ContextError(format!("serialize: {e}"))
57            })?;
58        fs::write(&self.path, &json).map_err(|e| AuroraError::Io(e))?;
59        Ok(())
60    }
61
62    pub fn recent(&self, ago: &str) -> AuroraResult<Pipeline> {
63        let ago = ago.trim();
64        let duration = parse_duration(ago)?;
65        let cutoff = Utc::now() - duration;
66
67        let recent_entries: Vec<&SessionEntry> = self
68            .entries
69            .iter()
70            .filter(|e| e.timestamp >= cutoff)
71            .collect();
72
73        if recent_entries.is_empty() {
74            return Ok(Pipeline::new());
75        }
76
77        let headers = vec![
78            "time".to_string(),
79            "command".to_string(),
80            "directory".to_string(),
81        ];
82
83        let mut rows: Vec<Vec<Value>> = Vec::new();
84        for entry in &recent_entries {
85            let time_str = entry.timestamp.format("%Y-%m-%d %H:%M:%S").to_string();
86            rows.push(vec![
87                Value::String(time_str),
88                Value::String(entry.command.clone()),
89                Value::String(entry.directory.clone()),
90            ]);
91        }
92
93        Ok(Pipeline::table(headers, rows))
94    }
95}
96
97fn parse_duration(s: &str) -> AuroraResult<Duration> {
98    let parts: Vec<&str> = s.splitn(2, |c: char| c.is_whitespace()).collect();
99    if parts.len() != 2 {
100        return Err(AuroraError::ContextError(format!(
101            "Invalid duration format: '{s}'. Expected e.g. '30 min'"
102        )));
103    }
104
105    let amount: i64 = parts[0]
106        .parse()
107        .map_err(|_| AuroraError::ContextError(format!("Invalid number: '{}'", parts[0])))?;
108
109    match parts[1] {
110        "min" | "minute" | "minutes" => Ok(Duration::minutes(amount)),
111        "hour" | "hours" => Ok(Duration::hours(amount)),
112        "day" | "days" => Ok(Duration::days(amount)),
113        "sec" | "second" | "seconds" => Ok(Duration::seconds(amount)),
114        unit => Err(AuroraError::ContextError(format!(
115            "Unknown time unit: '{unit}'. Use min, hours, days"
116        ))),
117    }
118}