Skip to main content

ui_automata/
registry.rs

1/// Anchor declarations: tiers, definitions, and launch-wait strategies.
2use std::collections::HashSet;
3
4use crate::SelectorPath;
5
6// ── LaunchWait / LaunchContext ────────────────────────────────────────────────
7
8/// How to identify the launched application's window after calling `launch:`.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, schemars::JsonSchema, serde::Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum LaunchWait {
12    /// Wait until the anchor's selector resolves against any window of the process.
13    /// Use for apps that reuse an existing process (browsers opening a new tab).
14    #[default]
15    MatchAny,
16    /// Wait for a window owned by the exact PID returned by the OS launcher.
17    /// Use for normal multi-instance apps (Notepad, Word).
18    NewPid,
19    /// Snapshot existing windows before launch; wait for a new HWND to appear in
20    /// the process. Use for single-instance apps (Explorer, VS Code) where the
21    /// launched process hands off to an existing one and exits.
22    NewWindow,
23}
24
25/// Context stored after a successful `launch:` wait, used by `ShadowDom::resolve`
26/// to filter the first resolution of root anchors.
27#[derive(Debug, Clone)]
28pub struct LaunchContext {
29    pub wait: LaunchWait,
30    /// PID returned by `open_application`. Used for `NewPid` filtering.
31    pub pid: u32,
32    /// HWNDs that existed before `open_application` was called. Used for `NewWindow` filtering.
33    pub pre_hwnds: HashSet<u64>,
34    /// Lowercase process name derived from the launched exe (without `.exe`).
35    /// The launch filter is skipped for root anchors that explicitly target a
36    /// different process, so multiple-root-anchor workflows work correctly.
37    pub process_name: String,
38}
39
40// ── Tier / AnchorDef ─────────────────────────────────────────────────────────
41
42/// Lifetime tier of a registered anchor.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Tier {
45    /// Present for the entire process lifetime. Staleness = fatal error.
46    Root,
47    /// Can be opened and closed during a workflow. Dependents are invalidated
48    /// wholesale when the session window goes away.
49    Session,
50    /// Stable while its root/session parent is open. Re-queried on stale.
51    Stable,
52    /// Plan-scoped captures. Released explicitly at plan exit.
53    Ephemeral,
54    /// CDP browser session. Calls `ensure()` on mount; stored as a root UIA anchor.
55    Browser,
56    /// CDP tab within a Browser anchor. Stored in the ShadowDom tab-handle map.
57    Tab,
58}
59
60/// Declaration of a named anchor.
61#[derive(Debug, Clone)]
62pub struct AnchorDef {
63    /// Unique name (used as a key in plans, conditions, actions).
64    pub name: String,
65    /// Parent anchor to resolve the selector relative to.
66    /// `None` means the selector is applied to desktop application windows.
67    pub parent: Option<String>,
68    /// CSS-like path from the parent to this element.
69    pub selector: SelectorPath,
70    pub tier: Tier,
71    /// Optional PID to pin this anchor to a specific process.
72    /// When set, resolution filters application windows by PID before applying
73    /// the selector, preventing accidental attachment to a different process.
74    pub pid: Option<u32>,
75    /// Optional process name filter (case-insensitive, without .exe).
76    /// When set, resolution only considers windows whose owning process name
77    /// matches this string. Can be used instead of or alongside `pid`.
78    pub process_name: Option<String>,
79    /// Subflow depth at which this anchor was mounted. Set by `ShadowDom::mount()`
80    /// so `cleanup_depth` can remove anchors introduced by a subflow regardless
81    /// of their tier (including Root anchors that are not depth-prefixed).
82    pub mount_depth: usize,
83}
84
85impl AnchorDef {
86    pub fn root(name: impl Into<String>, selector: SelectorPath) -> Self {
87        AnchorDef {
88            name: name.into(),
89            parent: None,
90            selector,
91            tier: Tier::Root,
92            pid: None,
93            process_name: None,
94            mount_depth: 0,
95        }
96    }
97
98    pub fn session(name: impl Into<String>, selector: SelectorPath) -> Self {
99        AnchorDef {
100            name: name.into(),
101            parent: None,
102            selector,
103            tier: Tier::Session,
104            pid: None,
105            process_name: None,
106            mount_depth: 0,
107        }
108    }
109
110    /// Pin this anchor to a specific process. Resolution will only match windows
111    /// belonging to that PID, preventing accidental attachment to unrelated
112    /// windows with the same title. Can be chained onto any constructor:
113    /// `AnchorDef::session("notepad", sel).with_pid(notepad_pid)`
114    pub fn with_pid(mut self, pid: u32) -> Self {
115        self.pid = Some(pid);
116        self
117    }
118
119    pub fn stable(
120        name: impl Into<String>,
121        parent: impl Into<String>,
122        selector: SelectorPath,
123    ) -> Self {
124        AnchorDef {
125            name: name.into(),
126            parent: Some(parent.into()),
127            selector,
128            tier: Tier::Stable,
129            pid: None,
130            process_name: None,
131            mount_depth: 0,
132        }
133    }
134
135    pub fn ephemeral(
136        name: impl Into<String>,
137        parent: impl Into<String>,
138        selector: SelectorPath,
139    ) -> Self {
140        AnchorDef {
141            name: name.into(),
142            parent: Some(parent.into()),
143            selector,
144            tier: Tier::Ephemeral,
145            pid: None,
146            process_name: None,
147            mount_depth: 0,
148        }
149    }
150}