Skip to main content

victauri_core/
event.rs

1//! Application event types and a thread-safe ring-buffer event log.
2
3use std::collections::VecDeque;
4use std::fmt;
5use std::sync::{Arc, Mutex};
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// A single Tauri IPC call with timing, result, and source webview.
11#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
12pub struct IpcCall {
13    /// Unique call identifier for correlation.
14    pub id: String,
15    /// Name of the Tauri command that was invoked.
16    pub command: String,
17    /// When the call was initiated.
18    pub timestamp: DateTime<Utc>,
19    /// Round-trip duration in milliseconds, if completed.
20    pub duration_ms: Option<u64>,
21    /// Current outcome of the call (pending, ok, or error).
22    pub result: IpcResult,
23    /// Size of the serialized arguments in bytes.
24    pub arg_size_bytes: usize,
25    /// Label of the webview that initiated the call.
26    pub webview_label: String,
27}
28
29/// Outcome of an IPC call: pending, success with a JSON value, or error.
30#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
31#[non_exhaustive]
32pub enum IpcResult {
33    /// Call is still in flight, awaiting a response.
34    Pending,
35    /// Call completed successfully with a JSON return value.
36    Ok(serde_json::Value),
37    /// Call failed with an error message.
38    Err(String),
39}
40
41impl fmt::Display for IpcResult {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::Pending => f.write_str("pending"),
45            Self::Ok(_) => f.write_str("ok"),
46            Self::Err(msg) => write!(f, "error: {msg}"),
47        }
48    }
49}
50
51impl fmt::Display for IpcCall {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "{} [{}] \u{2192} {}", self.command, self.id, self.result)
54    }
55}
56
57impl From<IpcCall> for AppEvent {
58    fn from(call: IpcCall) -> Self {
59        Self::Ipc(call)
60    }
61}
62
63/// The kind of user interaction captured from the DOM.
64#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
65pub enum InteractionKind {
66    /// Mouse click on an element.
67    Click,
68    /// Double-click on an element.
69    DoubleClick,
70    /// Text typed into an input field.
71    Fill,
72    /// Individual key press event.
73    KeyPress,
74    /// Option selected from a dropdown.
75    Select,
76    /// Page navigation (URL change).
77    Navigate,
78    /// Scroll to element or position.
79    Scroll,
80}
81
82impl fmt::Display for InteractionKind {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::Click => f.write_str("click"),
86            Self::DoubleClick => f.write_str("double_click"),
87            Self::Fill => f.write_str("fill"),
88            Self::KeyPress => f.write_str("key_press"),
89            Self::Select => f.write_str("select"),
90            Self::Navigate => f.write_str("navigate"),
91            Self::Scroll => f.write_str("scroll"),
92        }
93    }
94}
95
96/// Application event captured by the introspection layer.
97#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
98#[serde(tag = "type")]
99#[non_exhaustive]
100pub enum AppEvent {
101    /// An IPC call between webview and Rust backend.
102    Ipc(IpcCall),
103    /// A change to application state in the backend.
104    StateChange {
105        /// State key that changed.
106        key: String,
107        /// When the change occurred.
108        timestamp: DateTime<Utc>,
109        /// Command or action that triggered the change, if known.
110        caused_by: Option<String>,
111    },
112    /// A batch of DOM mutations observed in a webview.
113    DomMutation {
114        /// Webview where the mutations were observed.
115        webview_label: String,
116        /// When the mutations were observed.
117        timestamp: DateTime<Utc>,
118        /// Number of individual DOM mutations in this batch.
119        mutation_count: u32,
120    },
121    /// A user interaction captured from the DOM during recording.
122    DomInteraction {
123        /// What kind of interaction occurred.
124        action: InteractionKind,
125        /// Best available selector for the target element (data-testid, id, CSS path).
126        selector: String,
127        /// Value associated with the interaction (typed text, selected option, URL, key name).
128        value: Option<String>,
129        /// When the interaction occurred.
130        timestamp: DateTime<Utc>,
131        /// Label of the webview where the interaction happened.
132        webview_label: String,
133    },
134    /// A native window lifecycle event (e.g. focus, resize, close).
135    WindowEvent {
136        /// Tauri window label that emitted the event.
137        label: String,
138        /// Event name (e.g. "focus", "resize").
139        event: String,
140        /// When the event occurred.
141        timestamp: DateTime<Utc>,
142    },
143    /// A console log/warn/error message captured from the webview.
144    Console {
145        /// Severity level: "log", "warn", or "error".
146        level: String,
147        /// The log message text.
148        message: String,
149        /// When the message was captured.
150        timestamp: DateTime<Utc>,
151    },
152}
153
154impl AppEvent {
155    /// Returns the timestamp of this event, regardless of variant.
156    #[must_use]
157    pub fn timestamp(&self) -> DateTime<Utc> {
158        match self {
159            Self::Ipc(call) => call.timestamp,
160            Self::StateChange { timestamp, .. }
161            | Self::DomMutation { timestamp, .. }
162            | Self::DomInteraction { timestamp, .. }
163            | Self::WindowEvent { timestamp, .. }
164            | Self::Console { timestamp, .. } => *timestamp,
165        }
166    }
167
168    /// Returns true if this event was generated by Victauri's own infrastructure
169    /// rather than the application under observation.
170    #[must_use]
171    pub fn is_internal(&self) -> bool {
172        match self {
173            Self::Ipc(call) => call.command.starts_with("plugin:victauri|"),
174            _ => false,
175        }
176    }
177}
178
179/// Thread-safe ring-buffer event log. Automatically evicts the oldest events
180/// when capacity is reached. All operations recover from mutex poisoning.
181#[derive(Debug, Clone)]
182pub struct EventLog {
183    events: Arc<Mutex<VecDeque<AppEvent>>>,
184    max_capacity: usize,
185}
186
187impl EventLog {
188    /// Creates a new event log with the given maximum capacity.
189    ///
190    /// ```
191    /// use victauri_core::EventLog;
192    ///
193    /// let log = EventLog::new(100);
194    /// assert!(log.is_empty());
195    /// assert_eq!(log.capacity(), 100);
196    /// ```
197    #[must_use]
198    pub fn new(max_capacity: usize) -> Self {
199        Self {
200            events: Arc::new(Mutex::new(VecDeque::with_capacity(max_capacity))),
201            max_capacity,
202        }
203    }
204
205    /// Returns the maximum number of events this log can hold.
206    #[must_use]
207    pub fn capacity(&self) -> usize {
208        self.max_capacity
209    }
210
211    /// Appends an event, evicting the oldest if at capacity.
212    ///
213    /// # Examples
214    ///
215    /// ```
216    /// use victauri_core::{EventLog, AppEvent};
217    /// use chrono::Utc;
218    ///
219    /// let log = EventLog::new(100);
220    /// log.push(AppEvent::StateChange {
221    ///     key: "theme".to_string(),
222    ///     timestamp: Utc::now(),
223    ///     caused_by: None,
224    /// });
225    /// assert_eq!(log.len(), 1);
226    /// assert_eq!(log.snapshot().len(), 1);
227    /// ```
228    pub fn push(&self, event: AppEvent) {
229        let mut events = crate::acquire_lock(&self.events, "EventLog");
230        if events.len() >= self.max_capacity {
231            events.pop_front();
232        }
233        events.push_back(event);
234    }
235
236    /// Returns a clone of all events currently in the log.
237    #[must_use]
238    pub fn snapshot(&self) -> Vec<AppEvent> {
239        crate::acquire_lock(&self.events, "EventLog")
240            .iter()
241            .cloned()
242            .collect()
243    }
244
245    /// Returns a paginated slice of events starting at `offset`, up to `limit` items.
246    #[must_use]
247    pub fn snapshot_range(&self, offset: usize, limit: usize) -> Vec<AppEvent> {
248        crate::acquire_lock(&self.events, "EventLog")
249            .iter()
250            .skip(offset)
251            .take(limit)
252            .cloned()
253            .collect()
254    }
255
256    /// Returns all events with a timestamp at or after the given time.
257    #[must_use]
258    pub fn since(&self, timestamp: DateTime<Utc>) -> Vec<AppEvent> {
259        crate::acquire_lock(&self.events, "EventLog")
260            .iter()
261            .filter(|e| e.timestamp() >= timestamp)
262            .cloned()
263            .collect()
264    }
265
266    /// Returns all IPC call events, filtering out non-IPC events.
267    ///
268    /// # Examples
269    ///
270    /// ```
271    /// use victauri_core::{EventLog, AppEvent, IpcCall, IpcResult};
272    /// use chrono::Utc;
273    ///
274    /// let log = EventLog::new(100);
275    /// log.push(AppEvent::Ipc(IpcCall {
276    ///     id: "c1".to_string(),
277    ///     command: "greet".to_string(),
278    ///     timestamp: Utc::now(),
279    ///     duration_ms: Some(5),
280    ///     result: IpcResult::Ok(serde_json::json!("hi")),
281    ///     arg_size_bytes: 0,
282    ///     webview_label: "main".to_string(),
283    /// }));
284    /// assert_eq!(log.ipc_calls().len(), 1);
285    /// ```
286    #[must_use]
287    pub fn ipc_calls(&self) -> Vec<IpcCall> {
288        crate::acquire_lock(&self.events, "EventLog")
289            .iter()
290            .filter_map(|e| match e {
291                AppEvent::Ipc(call) => Some(call.clone()),
292                _ => None,
293            })
294            .collect()
295    }
296
297    /// Returns IPC calls with a timestamp at or after the given time.
298    #[must_use]
299    pub fn ipc_calls_since(&self, timestamp: DateTime<Utc>) -> Vec<IpcCall> {
300        crate::acquire_lock(&self.events, "EventLog")
301            .iter()
302            .filter_map(|e| match e {
303                AppEvent::Ipc(call) if call.timestamp >= timestamp => Some(call.clone()),
304                _ => None,
305            })
306            .collect()
307    }
308
309    /// Returns the number of events currently in the log.
310    #[must_use]
311    pub fn len(&self) -> usize {
312        crate::acquire_lock(&self.events, "EventLog").len()
313    }
314
315    /// Returns true if the log contains no events.
316    #[must_use]
317    pub fn is_empty(&self) -> bool {
318        crate::acquire_lock(&self.events, "EventLog").is_empty()
319    }
320
321    /// Removes all events from the log.
322    pub fn clear(&self) {
323        crate::acquire_lock(&self.events, "EventLog").clear();
324    }
325}