#![allow(dead_code)]
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tokio::time::interval;
pub mod screenshot;
pub mod input;
pub mod notification;
pub use screenshot::ScreenshotCapture;
pub use input::{InputTracker, UserActivity};
pub use notification::NotificationManager;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitorConfig {
pub screenshot_interval_secs: u64,
pub screenshot_dir: PathBuf,
pub track_keyboard: bool,
pub track_mouse: bool,
pub track_window: bool,
pub idle_threshold_secs: u64,
pub enable_notifications: bool,
pub analysis_interval_secs: u64,
}
impl Default for MonitorConfig {
fn default() -> Self {
Self {
screenshot_interval_secs: 5,
screenshot_dir: dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("i-self")
.join("screenshots"),
track_keyboard: true,
track_mouse: true,
track_window: true,
idle_threshold_secs: 300,
enable_notifications: true,
analysis_interval_secs: 30,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivitySnapshot {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub active_window: Option<String>,
pub keyboard_events: u64,
pub mouse_clicks: u64,
pub mouse_moves: u64,
pub is_idle: bool,
pub screenshot_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Suggestion {
pub id: String,
pub title: String,
pub description: String,
pub category: SuggestionCategory,
pub confidence: f64,
pub screenshot_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum SuggestionCategory {
Automation,
BetterPractice,
Shortcut,
Refactoring,
Security,
Performance,
}
pub struct ActivityMonitor {
config: MonitorConfig,
input_tracker: Arc<RwLock<InputTracker>>,
screenshot_capture: Arc<RwLock<ScreenshotCapture>>,
notification_manager: NotificationManager,
is_running: Arc<RwLock<bool>>,
last_activity: Arc<RwLock<Instant>>,
suggestions: Arc<RwLock<Vec<Suggestion>>>,
}
impl ActivityMonitor {
pub fn new(config: MonitorConfig) -> Result<Self> {
let input_tracker = Arc::new(RwLock::new(InputTracker::new(config.clone())?));
let screenshot_capture = Arc::new(RwLock::new(ScreenshotCapture::new(
config.screenshot_dir.clone(),
config.screenshot_interval_secs,
)?));
let notification_manager = NotificationManager::new();
Ok(Self {
config,
input_tracker,
screenshot_capture,
notification_manager,
is_running: Arc::new(RwLock::new(false)),
last_activity: Arc::new(RwLock::new(Instant::now())),
suggestions: Arc::new(RwLock::new(Vec::new())),
})
}
pub async fn start(&self) -> Result<()> {
*self.is_running.write().await = true;
{
let input_tracker = self.input_tracker.clone();
let is_running = self.is_running.clone();
let track_keyboard = self.config.track_keyboard;
let track_mouse = self.config.track_mouse;
let track_window = self.config.track_window;
tokio::spawn(async move {
use device_query::{DeviceQuery, DeviceState, MouseState};
let device = DeviceState::new();
let mut last_keys = device.get_keys();
let initial_mouse: MouseState = device.get_mouse();
let mut last_coords = initial_mouse.coords;
let mut last_buttons: Vec<bool> = initial_mouse.button_pressed.clone();
let mut last_window: Option<String> = None;
let mut ticker = interval(Duration::from_millis(50));
while *is_running.read().await {
ticker.tick().await;
if track_keyboard {
let keys = device.get_keys();
let new_presses = keys.iter().filter(|k| !last_keys.contains(k)).count();
if new_presses > 0 {
let mut t = input_tracker.write().await;
for _ in 0..new_presses {
t.record_keyboard();
}
}
last_keys = keys;
}
if track_mouse {
let mouse = device.get_mouse();
if mouse.coords != last_coords {
input_tracker.write().await.record_mouse_move();
last_coords = mouse.coords;
}
for (i, pressed) in mouse.button_pressed.iter().enumerate() {
let was_pressed = last_buttons.get(i).copied().unwrap_or(false);
if *pressed && !was_pressed {
input_tracker.write().await.record_mouse_click();
}
}
last_buttons = mouse.button_pressed.clone();
}
if track_window {
let win = InputTracker::get_active_window();
if win.is_some() && win != last_window {
input_tracker
.write()
.await
.record_window_change(win.clone().unwrap_or_default());
last_window = win;
}
}
}
});
}
let is_running = self.is_running.clone();
let input_tracker = self.input_tracker.clone();
let screenshot_capture = self.screenshot_capture.clone();
let last_activity = self.last_activity.clone();
let config = self.config.clone();
let suggestions = self.suggestions.clone();
let notification_manager = self.notification_manager.clone();
tokio::spawn(async move {
let mut ticker = interval(Duration::from_secs(1));
while *is_running.read().await {
ticker.tick().await;
let activity = input_tracker.read().await.get_current_activity();
if !activity.is_idle {
*last_activity.write().await = Instant::now();
}
if let Ok(screenshot_path) = screenshot_capture.write().await.capture().await {
if let Some(suggestion) = Self::analyze_activity(&activity, &screenshot_path).await {
let mut suggs = suggestions.write().await;
if !suggs.iter().any(|s| s.id == suggestion.id) {
suggs.push(suggestion.clone());
if config.enable_notifications {
let _ = notification_manager.send(&suggestion.title, &suggestion.description);
}
}
}
}
}
});
Ok(())
}
pub async fn stop(&self) {
*self.is_running.write().await = false;
}
pub async fn is_running(&self) -> bool {
*self.is_running.read().await
}
pub async fn get_suggestions(&self) -> Vec<Suggestion> {
self.suggestions.read().await.clone()
}
pub async fn clear_suggestions(&self) {
self.suggestions.write().await.clear();
}
pub async fn get_current_snapshot(&self) -> Result<ActivitySnapshot> {
let activity = self.input_tracker.read().await.get_current_activity();
let screenshot = self.screenshot_capture.read().await.get_latest_path();
let idle_duration = self.last_activity.read().await.elapsed();
let is_idle = idle_duration.as_secs() > self.config.idle_threshold_secs;
Ok(ActivitySnapshot {
timestamp: chrono::Utc::now(),
active_window: activity.active_window,
keyboard_events: activity.keyboard_events,
mouse_clicks: activity.mouse_clicks,
mouse_moves: activity.mouse_moves,
is_idle,
screenshot_path: screenshot,
})
}
async fn analyze_activity(activity: &UserActivity, screenshot: &Option<PathBuf>) -> Option<Suggestion> {
if activity.is_idle {
return None;
}
if activity.keyboard_events > 100 && activity.mouse_clicks < 5 {
return Some(Suggestion {
id: uuid::Uuid::new_v4().to_string(),
title: "Consider using keyboard shortcuts".to_string(),
description: "High keyboard activity with few clicks. You might benefit from learning keyboard shortcuts for your IDE.".to_string(),
category: SuggestionCategory::Shortcut,
confidence: 0.7,
screenshot_path: screenshot.clone(),
});
}
if activity.mouse_moves > 500 && activity.keyboard_events < 20 {
return Some(Suggestion {
id: uuid::Uuid::new_v4().to_string(),
title: "Repetitive mouse usage detected".to_string(),
description: "Lots of mouse movements. Consider keyboard navigation or macros for repetitive tasks.".to_string(),
category: SuggestionCategory::Automation,
confidence: 0.6,
screenshot_path: screenshot.clone(),
});
}
if let Some(ref window) = activity.active_window {
let window_lower = window.to_lowercase();
if window_lower.contains("terminal") || window_lower.contains("bash") || window_lower.contains("powershell") {
if activity.keyboard_events > 50 {
return Some(Suggestion {
id: uuid::Uuid::new_v4().to_string(),
title: "Create a shell alias or script".to_string(),
description: "Repeated terminal commands detected. Consider creating an alias or script for automation.".to_string(),
category: SuggestionCategory::Automation,
confidence: 0.75,
screenshot_path: screenshot.clone(),
});
}
}
}
None
}
}