Skip to main content

ghost_core/learning/
mod.rs

1// Learning session: tracks recording state and collected action events.
2// Platform hook management is handled by the caller (apps/ghost or apps/shadow).
3
4use serde::{Deserialize, Serialize};
5use std::sync::Mutex;
6use std::time::{Duration, Instant};
7
8/// Maximum learning session duration.
9pub const MAX_SESSION_DURATION: Duration = Duration::from_secs(10 * 60); // 10 minutes
10
11/// A single learned action event.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct LearnedEvent {
14    /// Milliseconds since session start.
15    pub ts_ms: u64,
16    /// Event type: click, type, hotkey, scroll, app_switch.
17    pub event_type: String,
18    /// For clicks: screen coordinates.
19    pub x: Option<i32>,
20    pub y: Option<i32>,
21    /// For key events: the key or text typed.
22    pub key: Option<String>,
23    /// AX element info enriched at capture time.
24    pub element_role: Option<String>,
25    pub element_name: Option<String>,
26    pub element_id: Option<String>,
27    pub app_name: Option<String>,
28}
29
30/// Learning session status.
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32pub enum SessionStatus {
33    Idle,
34    Recording,
35    Stopped,
36}
37
38struct SessionInner {
39    status: SessionStatus,
40    task_description: Option<String>,
41    started_at: Option<Instant>,
42    events: Vec<LearnedEvent>,
43}
44
45/// Thread-safe learning session.
46pub struct LearningSession {
47    inner: Mutex<SessionInner>,
48}
49
50impl Default for LearningSession {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl LearningSession {
57    pub fn new() -> Self {
58        Self {
59            inner: Mutex::new(SessionInner {
60                status: SessionStatus::Idle,
61                task_description: None,
62                started_at: None,
63                events: vec![],
64            }),
65        }
66    }
67
68    pub fn start(&self, task_description: Option<String>) -> Result<(), String> {
69        let mut g = self.inner.lock().unwrap();
70        if g.status == SessionStatus::Recording {
71            return Err("Already recording".to_string());
72        }
73        g.status = SessionStatus::Recording;
74        g.task_description = task_description;
75        g.started_at = Some(Instant::now());
76        g.events.clear();
77        Ok(())
78    }
79
80    pub fn stop(&self) -> Result<Vec<LearnedEvent>, String> {
81        let mut g = self.inner.lock().unwrap();
82        if g.status != SessionStatus::Recording {
83            return Err("Not recording".to_string());
84        }
85        g.status = SessionStatus::Stopped;
86        Ok(g.events.clone())
87    }
88
89    pub fn push_event(&self, event: LearnedEvent) {
90        let mut g = self.inner.lock().unwrap();
91        if g.status != SessionStatus::Recording {
92            return;
93        }
94        // Hard limit: stop if session has been running too long
95        if let Some(started) = g.started_at {
96            if started.elapsed() > MAX_SESSION_DURATION {
97                g.status = SessionStatus::Stopped;
98                return;
99            }
100        }
101        g.events.push(event);
102    }
103
104    pub fn status(&self) -> SessionStatus {
105        self.inner.lock().unwrap().status.clone()
106    }
107
108    pub fn event_count(&self) -> usize {
109        self.inner.lock().unwrap().events.len()
110    }
111
112    pub fn elapsed_secs(&self) -> u64 {
113        let g = self.inner.lock().unwrap();
114        g.started_at.map(|t| t.elapsed().as_secs()).unwrap_or(0)
115    }
116
117    pub fn task_description(&self) -> Option<String> {
118        self.inner.lock().unwrap().task_description.clone()
119    }
120}