#![allow(dead_code)]
use chrono::{DateTime, Utc, Duration as ChronoDuration};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tokio::fs;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivitySession {
pub id: String,
pub start_time: DateTime<Utc>,
pub end_time: Option<DateTime<Utc>>,
pub project: Option<String>,
pub language: Option<String>,
pub files_changed: usize,
pub lines_added: usize,
pub lines_removed: usize,
pub commits: Vec<String>,
pub idle_time_seconds: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivitySummary {
pub total_sessions: usize,
pub total_coding_time_seconds: u64,
pub total_idle_time_seconds: u64,
pub projects_worked: Vec<String>,
pub languages_used: HashMap<String, u64>,
pub average_session_length_seconds: u64,
pub most_productive_hour: Option<u32>,
pub commit_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkPattern {
pub day_of_week: u8,
pub hour: u8,
pub activity_level: f64,
pub typical_duration_minutes: u32,
}
pub struct ActivityTracker {
pub sessions: Vec<ActivitySession>,
data_dir: PathBuf,
}
impl ActivityTracker {
pub fn new(data_dir: PathBuf) -> Self {
Self {
sessions: Vec::new(),
data_dir,
}
}
pub async fn load(&mut self) -> anyhow::Result<()> {
let sessions_file = self.data_dir.join("sessions.json");
if sessions_file.exists() {
let content = fs::read_to_string(&sessions_file).await?;
self.sessions = serde_json::from_str(&content).unwrap_or_default();
}
Ok(())
}
pub async fn save(&self) -> anyhow::Result<()> {
let sessions_file = self.data_dir.join("sessions.json");
let content = serde_json::to_string_pretty(&self.sessions)?;
fs::write(sessions_file, content).await?;
Ok(())
}
pub fn start_session(&mut self, project: Option<String>, language: Option<String>) -> &ActivitySession {
let session = ActivitySession {
id: uuid::Uuid::new_v4().to_string(),
start_time: Utc::now(),
end_time: None,
project,
language,
files_changed: 0,
lines_added: 0,
lines_removed: 0,
commits: Vec::new(),
idle_time_seconds: 0,
};
self.sessions.push(session);
self.sessions.last().unwrap()
}
pub fn end_session(&mut self, session_id: &str) -> Option<&ActivitySession> {
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
session.end_time = Some(Utc::now());
return Some(session);
}
None
}
pub fn record_file_change(&mut self, session_id: &str, lines_added: usize, lines_removed: usize) {
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
session.files_changed += 1;
session.lines_added += lines_added;
session.lines_removed += lines_removed;
}
}
pub fn record_commit(&mut self, session_id: &str, commit_hash: String) {
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
session.commits.push(commit_hash);
}
}
pub fn record_idle_time(&mut self, session_id: &str, seconds: u64) {
if let Some(session) = self.sessions.iter_mut().find(|s| s.id == session_id) {
session.idle_time_seconds += seconds;
}
}
pub fn get_summary(&self, days: u32) -> ActivitySummary {
let cutoff = Utc::now() - ChronoDuration::days(days as i64);
let recent_sessions: Vec<&ActivitySession> = self.sessions
.iter()
.filter(|s| s.start_time > cutoff)
.collect();
let total_coding_time: u64 = recent_sessions
.iter()
.map(|s| {
let end = s.end_time.unwrap_or(Utc::now());
(end - s.start_time).num_seconds() as u64
})
.sum();
let total_idle: u64 = recent_sessions.iter().map(|s| s.idle_time_seconds).sum();
let mut projects: HashMap<String, u64> = HashMap::new();
let mut languages: HashMap<String, u64> = HashMap::new();
let mut hourly_activity: HashMap<u32, u64> = HashMap::new();
for session in &recent_sessions {
if let Some(ref project) = session.project {
*projects.entry(project.clone()).or_insert(0) += 1;
}
if let Some(ref lang) = session.language {
*languages.entry(lang.clone()).or_insert(0) += 1;
}
let naive = session.start_time.naive_utc();
let hour = naive.format("%H").to_string().parse::<u32>().unwrap_or(0);
*hourly_activity.entry(hour).or_insert(0) += 1;
}
let most_productive_hour = hourly_activity
.iter()
.max_by_key(|(_, &count)| count)
.map(|(&hour, _)| hour);
let avg_session = if !recent_sessions.is_empty() {
total_coding_time / recent_sessions.len() as u64
} else {
0
};
ActivitySummary {
total_sessions: recent_sessions.len(),
total_coding_time_seconds: total_coding_time,
total_idle_time_seconds: total_idle,
projects_worked: projects.keys().cloned().collect(),
languages_used: languages,
average_session_length_seconds: avg_session,
most_productive_hour,
commit_count: recent_sessions.iter().map(|s| s.commits.len()).sum(),
}
}
pub fn get_work_patterns(&self, days: u32) -> Vec<WorkPattern> {
let cutoff = Utc::now() - ChronoDuration::days(days as i64);
let recent_sessions: Vec<&ActivitySession> = self.sessions
.iter()
.filter(|s| s.start_time > cutoff)
.collect();
let mut patterns: HashMap<(u8, u8), (u64, u32)> = HashMap::new();
for session in &recent_sessions {
let naive = session.start_time.naive_utc();
let day: u8 = naive.format("%w").to_string().parse().unwrap_or(0);
let hour: u8 = naive.format("%H").to_string().parse().unwrap_or(0);
let duration = session.end_time
.map(|e| (e - session.start_time).num_minutes() as u32)
.unwrap_or(0);
let entry = patterns.entry((day, hour)).or_insert((0, 0));
entry.0 += 1;
entry.1 = entry.1.saturating_add(duration);
}
patterns.iter().map(|((day, hour), (count, duration))| {
WorkPattern {
day_of_week: *day,
hour: *hour,
activity_level: *count as f64,
typical_duration_minutes: if *count > 0 { duration / (*count as u32) } else { 0 },
}
}).collect()
}
}