ghost_core/learning/
mod.rs1use serde::{Deserialize, Serialize};
5use std::sync::Mutex;
6use std::time::{Duration, Instant};
7
8pub const MAX_SESSION_DURATION: Duration = Duration::from_secs(10 * 60); #[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct LearnedEvent {
14 pub ts_ms: u64,
16 pub event_type: String,
18 pub x: Option<i32>,
20 pub y: Option<i32>,
21 pub key: Option<String>,
23 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#[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
45pub 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 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}