use std::fs;
use std::path::PathBuf;
use aurora_core::{AuroraError, AuroraResult, Pipeline, Value};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionEntry {
pub timestamp: DateTime<Utc>,
pub command: String,
pub directory: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionStore {
pub entries: Vec<SessionEntry>,
path: PathBuf,
}
impl SessionStore {
pub fn new() -> AuroraResult<Self> {
let home = std::env::var("HOME")
.map_err(|_| AuroraError::ContextError("HOME not set".to_string()))?;
let dir = PathBuf::from(home).join(".cache/aurora");
let path = dir.join("session.json");
if path.exists() {
let content =
fs::read_to_string(&path).map_err(|e| AuroraError::Io(e))?;
let entries: Vec<SessionEntry> =
serde_json::from_str(&content).unwrap_or_default();
Ok(Self { entries, path })
} else {
fs::create_dir_all(&dir).map_err(|e| AuroraError::Io(e))?;
Ok(Self {
entries: Vec::new(),
path,
})
}
}
pub fn record(&mut self, cmd: &str) -> AuroraResult<()> {
let cwd = std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
self.entries.push(SessionEntry {
timestamp: Utc::now(),
command: cmd.to_string(),
directory: cwd,
});
let json =
serde_json::to_string_pretty(&self.entries).map_err(|e| {
AuroraError::ContextError(format!("serialize: {e}"))
})?;
fs::write(&self.path, &json).map_err(|e| AuroraError::Io(e))?;
Ok(())
}
pub fn recent(&self, ago: &str) -> AuroraResult<Pipeline> {
let ago = ago.trim();
let duration = parse_duration(ago)?;
let cutoff = Utc::now() - duration;
let recent_entries: Vec<&SessionEntry> = self
.entries
.iter()
.filter(|e| e.timestamp >= cutoff)
.collect();
if recent_entries.is_empty() {
return Ok(Pipeline::new());
}
let headers = vec![
"time".to_string(),
"command".to_string(),
"directory".to_string(),
];
let mut rows: Vec<Vec<Value>> = Vec::new();
for entry in &recent_entries {
let time_str = entry.timestamp.format("%Y-%m-%d %H:%M:%S").to_string();
rows.push(vec![
Value::String(time_str),
Value::String(entry.command.clone()),
Value::String(entry.directory.clone()),
]);
}
Ok(Pipeline::table(headers, rows))
}
}
fn parse_duration(s: &str) -> AuroraResult<Duration> {
let parts: Vec<&str> = s.splitn(2, |c: char| c.is_whitespace()).collect();
if parts.len() != 2 {
return Err(AuroraError::ContextError(format!(
"Invalid duration format: '{s}'. Expected e.g. '30 min'"
)));
}
let amount: i64 = parts[0]
.parse()
.map_err(|_| AuroraError::ContextError(format!("Invalid number: '{}'", parts[0])))?;
match parts[1] {
"min" | "minute" | "minutes" => Ok(Duration::minutes(amount)),
"hour" | "hours" => Ok(Duration::hours(amount)),
"day" | "days" => Ok(Duration::days(amount)),
"sec" | "second" | "seconds" => Ok(Duration::seconds(amount)),
unit => Err(AuroraError::ContextError(format!(
"Unknown time unit: '{unit}'. Use min, hours, days"
))),
}
}