Skip to main content

synheart_sensor_agent/core/
windowing.rs

1//! Window management for collecting events into time-based windows.
2//!
3//! Events are collected into fixed-duration windows (default 10 seconds)
4//! for feature extraction. Session boundaries are detected based on gaps.
5
6use crate::collector::types::{KeyboardEvent, MouseEvent, SensorEvent, ShortcutEvent};
7use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9
10/// A time window containing collected events.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct EventWindow {
13    /// Start time of the window
14    pub start: DateTime<Utc>,
15    /// End time of the window
16    pub end: DateTime<Utc>,
17    /// Keyboard events in this window
18    pub keyboard_events: Vec<KeyboardEvent>,
19    /// Mouse events in this window
20    pub mouse_events: Vec<MouseEvent>,
21    /// Shortcut events in this window
22    pub shortcut_events: Vec<ShortcutEvent>,
23    /// Whether this window marks the start of a new session
24    pub is_session_start: bool,
25    /// Bundle identifier of the frontmost application when this window completed.
26    /// Only available on macOS. Privacy: no window titles or content captured.
27    pub app_id: Option<String>,
28}
29
30impl EventWindow {
31    /// Create a new empty window starting at the given time.
32    pub fn new(start: DateTime<Utc>, duration: Duration) -> Self {
33        Self {
34            start,
35            end: start + duration,
36            keyboard_events: Vec::new(),
37            mouse_events: Vec::new(),
38            shortcut_events: Vec::new(),
39            is_session_start: false,
40            app_id: None,
41        }
42    }
43
44    /// Check if a timestamp falls within this window.
45    pub fn contains(&self, timestamp: DateTime<Utc>) -> bool {
46        timestamp >= self.start && timestamp < self.end
47    }
48
49    /// Add an event to this window.
50    pub fn add_event(&mut self, event: SensorEvent) {
51        match event {
52            SensorEvent::Keyboard(e) => self.keyboard_events.push(e),
53            SensorEvent::Mouse(e) => self.mouse_events.push(e),
54            SensorEvent::Shortcut(e) => self.shortcut_events.push(e),
55        }
56    }
57
58    /// Check if the window has any events.
59    pub fn is_empty(&self) -> bool {
60        self.keyboard_events.is_empty()
61            && self.mouse_events.is_empty()
62            && self.shortcut_events.is_empty()
63    }
64
65    /// Get the total number of events in this window.
66    pub fn event_count(&self) -> usize {
67        self.keyboard_events.len() + self.mouse_events.len() + self.shortcut_events.len()
68    }
69
70    /// Get the duration of this window in seconds.
71    pub fn duration_secs(&self) -> f64 {
72        (self.end - self.start).num_milliseconds() as f64 / 1000.0
73    }
74}
75
76/// Manages the collection of events into time windows.
77pub struct WindowManager {
78    /// Duration of each window
79    window_duration: Duration,
80    /// Gap threshold for session boundaries
81    session_gap_threshold: Duration,
82    /// Current window being filled
83    current_window: Option<EventWindow>,
84    /// Completed windows ready for processing
85    completed_windows: Vec<EventWindow>,
86    /// Timestamp of the last event received
87    last_event_time: Option<DateTime<Utc>>,
88}
89
90impl WindowManager {
91    /// Create a new window manager with the given window duration.
92    pub fn new(window_duration_secs: u64, session_gap_threshold_secs: u64) -> Self {
93        Self {
94            window_duration: Duration::seconds(window_duration_secs as i64),
95            session_gap_threshold: Duration::seconds(session_gap_threshold_secs as i64),
96            current_window: None,
97            completed_windows: Vec::new(),
98            last_event_time: None,
99        }
100    }
101
102    /// Process an incoming event.
103    ///
104    /// This will:
105    /// 1. Detect session boundaries based on gaps
106    /// 2. Create new windows as needed
107    /// 3. Complete windows when their time expires
108    pub fn process_event(&mut self, event: SensorEvent) {
109        let event_time = event.timestamp();
110
111        // Check for session boundary (gap in events)
112        let is_new_session = if let Some(last_time) = self.last_event_time {
113            event_time - last_time > self.session_gap_threshold
114        } else {
115            true // First event starts a session
116        };
117
118        // If this is a new session, complete the current window
119        if is_new_session && self.current_window.is_some() {
120            self.complete_current_window();
121        }
122
123        // Ensure we have a current window
124        if self.current_window.is_none() {
125            let mut window = EventWindow::new(event_time, self.window_duration);
126            window.is_session_start = is_new_session;
127            self.current_window = Some(window);
128        }
129
130        // Check if the event falls outside the current window
131        let window = self.current_window.as_ref().unwrap();
132        if event_time >= window.end {
133            // Complete the current window and create a new one
134            self.complete_current_window();
135
136            // Align the new window to the event time
137            let mut window = EventWindow::new(event_time, self.window_duration);
138            window.is_session_start = is_new_session;
139            self.current_window = Some(window);
140        }
141
142        // Add the event to the current window
143        if let Some(ref mut window) = self.current_window {
144            window.add_event(event);
145        }
146
147        self.last_event_time = Some(event_time);
148    }
149
150    /// Force completion of the current window (e.g., on pause or stop).
151    pub fn flush(&mut self) {
152        self.complete_current_window();
153    }
154
155    /// Get and remove completed windows.
156    pub fn take_completed_windows(&mut self) -> Vec<EventWindow> {
157        std::mem::take(&mut self.completed_windows)
158    }
159
160    /// Check if there are completed windows available.
161    pub fn has_completed_windows(&self) -> bool {
162        !self.completed_windows.is_empty()
163    }
164
165    /// Get the number of completed windows.
166    pub fn completed_window_count(&self) -> usize {
167        self.completed_windows.len()
168    }
169
170    /// Complete the current window and move it to completed.
171    fn complete_current_window(&mut self) {
172        if let Some(mut window) = self.current_window.take() {
173            // Only keep non-empty windows
174            if !window.is_empty() {
175                window.app_id = crate::collector::get_frontmost_app_id();
176                self.completed_windows.push(window);
177            }
178        }
179    }
180
181    /// Check and complete the current window if it has expired.
182    pub fn check_window_expiry(&mut self) {
183        let now = Utc::now();
184        if let Some(ref window) = self.current_window {
185            if now >= window.end {
186                self.complete_current_window();
187            }
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_window_creation() {
198        let start = Utc::now();
199        let window = EventWindow::new(start, Duration::seconds(10));
200
201        assert_eq!(window.start, start);
202        assert_eq!(window.end, start + Duration::seconds(10));
203        assert!(window.is_empty());
204    }
205
206    #[test]
207    fn test_window_contains() {
208        let start = Utc::now();
209        let window = EventWindow::new(start, Duration::seconds(10));
210
211        assert!(window.contains(start));
212        assert!(window.contains(start + Duration::seconds(5)));
213        assert!(!window.contains(start + Duration::seconds(10)));
214        assert!(!window.contains(start - Duration::seconds(1)));
215    }
216
217    #[test]
218    fn test_shortcut_events_routed() {
219        let start = Utc::now();
220        let mut window = EventWindow::new(start, Duration::seconds(10));
221        assert!(window.is_empty());
222
223        let shortcut = SensorEvent::Shortcut(crate::collector::types::ShortcutEvent {
224            timestamp: start,
225            shortcut_type: crate::collector::types::ShortcutType::Copy,
226        });
227        window.add_event(shortcut);
228
229        assert!(!window.is_empty());
230        assert_eq!(window.event_count(), 1);
231        assert_eq!(window.shortcut_events.len(), 1);
232    }
233
234    #[test]
235    fn test_event_count_includes_shortcuts() {
236        let start = Utc::now();
237        let mut window = EventWindow::new(start, Duration::seconds(10));
238
239        window.add_event(SensorEvent::Keyboard(
240            crate::collector::types::KeyboardEvent::new(true),
241        ));
242        window.add_event(SensorEvent::Shortcut(
243            crate::collector::types::ShortcutEvent {
244                timestamp: start,
245                shortcut_type: crate::collector::types::ShortcutType::Paste,
246            },
247        ));
248
249        assert_eq!(window.event_count(), 2);
250        assert_eq!(window.keyboard_events.len(), 1);
251        assert_eq!(window.shortcut_events.len(), 1);
252    }
253
254    #[test]
255    fn test_window_manager_basic() {
256        let mut manager = WindowManager::new(10, 300);
257
258        // Process some keyboard events
259        for _ in 0..5 {
260            let event = SensorEvent::Keyboard(crate::collector::types::KeyboardEvent::new(true));
261            manager.process_event(event);
262        }
263
264        // Window shouldn't be complete yet
265        assert!(!manager.has_completed_windows());
266
267        // Flush to complete the current window
268        manager.flush();
269        assert!(manager.has_completed_windows());
270
271        let windows = manager.take_completed_windows();
272        assert_eq!(windows.len(), 1);
273        assert_eq!(windows[0].keyboard_events.len(), 5);
274    }
275}