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}