aurora_context/
session.rs1use 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}