use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
#[derive(Debug)]
pub struct TransparencyLog {
keyboard_events: AtomicU64,
mouse_events: AtomicU64,
windows_completed: AtomicU64,
shortcut_events: AtomicU64,
snapshots_exported: AtomicU64,
session_start: DateTime<Utc>,
persist_path: Option<PathBuf>,
}
impl TransparencyLog {
pub fn new() -> Self {
Self {
keyboard_events: AtomicU64::new(0),
mouse_events: AtomicU64::new(0),
shortcut_events: AtomicU64::new(0),
windows_completed: AtomicU64::new(0),
snapshots_exported: AtomicU64::new(0),
session_start: Utc::now(),
persist_path: None,
}
}
pub fn with_persistence(path: PathBuf) -> Self {
let mut log = Self::new();
log.persist_path = Some(path);
if let Err(e) = log.load() {
eprintln!("Note: Could not load previous transparency stats: {e}");
}
log
}
pub fn record_keyboard_event(&self) {
self.keyboard_events.fetch_add(1, Ordering::Relaxed);
}
pub fn record_keyboard_events(&self, count: u64) {
self.keyboard_events.fetch_add(count, Ordering::Relaxed);
}
pub fn record_mouse_event(&self) {
self.mouse_events.fetch_add(1, Ordering::Relaxed);
}
pub fn record_mouse_events(&self, count: u64) {
self.mouse_events.fetch_add(count, Ordering::Relaxed);
}
pub fn record_shortcut_event(&self) {
self.shortcut_events.fetch_add(1, Ordering::Relaxed);
}
pub fn record_window_completed(&self) {
self.windows_completed.fetch_add(1, Ordering::Relaxed);
}
pub fn record_snapshot_exported(&self) {
self.snapshots_exported.fetch_add(1, Ordering::Relaxed);
}
pub fn stats(&self) -> TransparencyStats {
TransparencyStats {
keyboard_events: self.keyboard_events.load(Ordering::Relaxed),
mouse_events: self.mouse_events.load(Ordering::Relaxed),
shortcut_events: self.shortcut_events.load(Ordering::Relaxed),
windows_completed: self.windows_completed.load(Ordering::Relaxed),
snapshots_exported: self.snapshots_exported.load(Ordering::Relaxed),
session_start: self.session_start,
session_duration_secs: (Utc::now() - self.session_start).num_seconds() as u64,
}
}
pub fn summary(&self) -> String {
let stats = self.stats();
format!(
"Session Statistics:\n\
- Keyboard events processed: {}\n\
- Mouse events processed: {}\n\
- Shortcut events processed: {}\n\
- Windows completed: {}\n\
- Snapshots exported: {}\n\
- Session duration: {} seconds\n\
\n\
Privacy Guarantee:\n\
- No key content captured\n\
- No cursor coordinates captured\n\
- Only timing, categories, and magnitude data retained",
stats.keyboard_events,
stats.mouse_events,
stats.shortcut_events,
stats.windows_completed,
stats.snapshots_exported,
stats.session_duration_secs
)
}
pub fn save(&self) -> Result<(), std::io::Error> {
if let Some(ref path) = self.persist_path {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let stats = self.stats();
let persisted = PersistedStats {
keyboard_events: stats.keyboard_events,
mouse_events: stats.mouse_events,
shortcut_events: stats.shortcut_events,
windows_completed: stats.windows_completed,
snapshots_exported: stats.snapshots_exported,
last_updated: Utc::now(),
};
let json = serde_json::to_string_pretty(&persisted).map_err(std::io::Error::other)?;
std::fs::write(path, json)?;
}
Ok(())
}
fn load(&mut self) -> Result<(), std::io::Error> {
if let Some(ref path) = self.persist_path {
if path.exists() {
let content = std::fs::read_to_string(path)?;
let persisted: PersistedStats =
serde_json::from_str(&content).map_err(std::io::Error::other)?;
self.keyboard_events
.store(persisted.keyboard_events, Ordering::Relaxed);
self.mouse_events
.store(persisted.mouse_events, Ordering::Relaxed);
self.shortcut_events
.store(persisted.shortcut_events, Ordering::Relaxed);
self.windows_completed
.store(persisted.windows_completed, Ordering::Relaxed);
self.snapshots_exported
.store(persisted.snapshots_exported, Ordering::Relaxed);
}
}
Ok(())
}
pub fn reset(&self) {
self.keyboard_events.store(0, Ordering::Relaxed);
self.mouse_events.store(0, Ordering::Relaxed);
self.shortcut_events.store(0, Ordering::Relaxed);
self.windows_completed.store(0, Ordering::Relaxed);
self.snapshots_exported.store(0, Ordering::Relaxed);
}
}
impl Default for TransparencyLog {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransparencyStats {
pub keyboard_events: u64,
pub mouse_events: u64,
pub shortcut_events: u64,
pub windows_completed: u64,
pub snapshots_exported: u64,
pub session_start: DateTime<Utc>,
pub session_duration_secs: u64,
}
#[derive(Debug, Serialize, Deserialize)]
struct PersistedStats {
keyboard_events: u64,
mouse_events: u64,
#[serde(default)]
shortcut_events: u64,
windows_completed: u64,
snapshots_exported: u64,
last_updated: DateTime<Utc>,
}
pub type SharedTransparencyLog = Arc<TransparencyLog>;
pub fn create_shared_log() -> SharedTransparencyLog {
Arc::new(TransparencyLog::new())
}
pub fn create_shared_log_with_persistence(path: PathBuf) -> SharedTransparencyLog {
Arc::new(TransparencyLog::with_persistence(path))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transparency_log_counting() {
let log = TransparencyLog::new();
log.record_keyboard_event();
log.record_keyboard_event();
log.record_mouse_event();
let stats = log.stats();
assert_eq!(stats.keyboard_events, 2);
assert_eq!(stats.mouse_events, 1);
}
#[test]
fn test_transparency_log_reset() {
let log = TransparencyLog::new();
log.record_keyboard_events(100);
log.record_mouse_events(50);
log.reset();
let stats = log.stats();
assert_eq!(stats.keyboard_events, 0);
assert_eq!(stats.mouse_events, 0);
}
#[test]
fn test_summary_format() {
let log = TransparencyLog::new();
let summary = log.summary();
assert!(summary.contains("Keyboard events"));
assert!(summary.contains("Mouse events"));
assert!(summary.contains("Privacy Guarantee"));
assert!(summary.contains("No key content captured"));
}
}