Skip to main content

actl_core/
activity.rs

1//! Display-only action facts. No input contents, platform handles or execution policy.
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum ClipboardEffect {
7    #[default]
8    Untouched,
9    Read,
10    Write,
11}
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
14pub struct InputEffects {
15    pub keyboard: bool,
16    pub pointer: bool,
17    pub clipboard: ClipboardEffect,
18    pub focus: bool,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum ActionPhase {
24    Preparing,
25    Executing,
26    Delivered,
27    Failed,
28    Cancelled,
29    Partial,
30}
31
32/// Physical virtual-desktop pixels; a rectangle is [left, top, right, bottom].
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct ActionTarget {
35    pub window: Option<String>,
36    pub point: Option<[i32; 2]>,
37    pub bounds: Option<[i32; 4]>,
38    pub destination: Option<[i32; 2]>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ActionState {
43    pub id: u64,
44    pub seq: u64,
45    pub kind: String,
46    pub phase: ActionPhase,
47    pub effects: InputEffects,
48    pub target: ActionTarget,
49    pub ts_ms: u64,
50    pub expires_ms: u64,
51}
52
53impl ActionState {
54    pub fn active(&self) -> bool {
55        matches!(self.phase, ActionPhase::Preparing | ActionPhase::Executing)
56    }
57}
58
59/// A consumer cursor starts at launch time: reconnects never replay old clicks.
60#[derive(Default)]
61pub struct ActionCursor {
62    since_ms: u64,
63    seen: std::collections::HashMap<String, u64>,
64}
65
66impl ActionCursor {
67    pub fn new(since_ms: u64) -> Self {
68        Self {
69            since_ms,
70            seen: Default::default(),
71        }
72    }
73
74    pub fn take(&mut self, states: &[crate::state::SessionState], now: u64) -> Vec<ActionState> {
75        let mut out = Vec::new();
76        self.seen
77            .retain(|id, _| states.iter().any(|s| &s.call_id == id));
78        for state in states {
79            let seq = self.seen.entry(state.call_id.clone()).or_default();
80            for action in &state.recent_actions {
81                if action.seq > *seq
82                    && action.ts_ms >= self.since_ms
83                    && action.expires_ms > now
84                    && action.phase == ActionPhase::Delivered
85                {
86                    out.push(action.clone());
87                }
88                *seq = (*seq).max(action.seq);
89            }
90        }
91        out.sort_by_key(|a| a.ts_ms);
92        out
93    }
94}