#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AXNotification {
ValueChanged,
UIElementDestroyed,
WindowCreated,
WindowMoved,
WindowResized,
FocusedWindowChanged,
FocusedUIElementChanged,
SelectedChildrenChanged,
TitleChanged,
}
impl AXNotification {
#[must_use]
pub fn as_cf_str(self) -> &'static str {
match self {
Self::ValueChanged => "AXValueChanged",
Self::UIElementDestroyed => "AXUIElementDestroyed",
Self::WindowCreated => "AXWindowCreated",
Self::WindowMoved => "AXWindowMoved",
Self::WindowResized => "AXWindowResized",
Self::FocusedWindowChanged => "AXFocusedWindowChanged",
Self::FocusedUIElementChanged => "AXFocusedUIElementChanged",
Self::SelectedChildrenChanged => "AXSelectedChildrenChanged",
Self::TitleChanged => "AXTitleChanged",
}
}
}
pub struct AXObserverHandle {
active: bool,
}
impl AXObserverHandle {
#[must_use]
pub fn new(_pid: i32) -> Self {
tracing::debug!("AX Observer created (stub — synchronous mode, no CFRunLoop)");
Self { active: false }
}
pub fn subscribe(&mut self, notification: AXNotification) -> Result<(), String> {
tracing::debug!(
notification = notification.as_cf_str(),
"AX Observer subscribe (stub — no CFRunLoop, notification discarded)"
);
Ok(())
}
#[must_use]
pub fn is_active(&self) -> bool {
self.active
}
}
impl Drop for AXObserverHandle {
fn drop(&mut self) {
tracing::debug!("AX Observer dropped");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value_changed_maps_to_correct_cf_str() {
assert_eq!(AXNotification::ValueChanged.as_cf_str(), "AXValueChanged");
}
#[test]
fn ui_element_destroyed_maps_to_correct_cf_str() {
assert_eq!(
AXNotification::UIElementDestroyed.as_cf_str(),
"AXUIElementDestroyed"
);
}
#[test]
fn window_created_maps_to_correct_cf_str() {
assert_eq!(AXNotification::WindowCreated.as_cf_str(), "AXWindowCreated");
}
#[test]
fn window_moved_maps_to_correct_cf_str() {
assert_eq!(AXNotification::WindowMoved.as_cf_str(), "AXWindowMoved");
}
#[test]
fn window_resized_maps_to_correct_cf_str() {
assert_eq!(AXNotification::WindowResized.as_cf_str(), "AXWindowResized");
}
#[test]
fn focused_window_changed_maps_to_correct_cf_str() {
assert_eq!(
AXNotification::FocusedWindowChanged.as_cf_str(),
"AXFocusedWindowChanged"
);
}
#[test]
fn focused_ui_element_changed_maps_to_correct_cf_str() {
assert_eq!(
AXNotification::FocusedUIElementChanged.as_cf_str(),
"AXFocusedUIElementChanged"
);
}
#[test]
fn selected_children_changed_maps_to_correct_cf_str() {
assert_eq!(
AXNotification::SelectedChildrenChanged.as_cf_str(),
"AXSelectedChildrenChanged"
);
}
#[test]
fn title_changed_maps_to_correct_cf_str() {
assert_eq!(AXNotification::TitleChanged.as_cf_str(), "AXTitleChanged");
}
#[test]
fn all_notifications_produce_non_empty_cf_str() {
let variants = [
AXNotification::ValueChanged,
AXNotification::UIElementDestroyed,
AXNotification::WindowCreated,
AXNotification::WindowMoved,
AXNotification::WindowResized,
AXNotification::FocusedWindowChanged,
AXNotification::FocusedUIElementChanged,
AXNotification::SelectedChildrenChanged,
AXNotification::TitleChanged,
];
for n in variants {
assert!(
!n.as_cf_str().is_empty(),
"variant {n:?} produced empty cf_str"
);
}
}
#[test]
fn all_cf_strs_start_with_ax_prefix() {
let variants = [
AXNotification::ValueChanged,
AXNotification::UIElementDestroyed,
AXNotification::WindowCreated,
AXNotification::WindowMoved,
AXNotification::WindowResized,
AXNotification::FocusedWindowChanged,
AXNotification::FocusedUIElementChanged,
AXNotification::SelectedChildrenChanged,
AXNotification::TitleChanged,
];
for n in variants {
assert!(
n.as_cf_str().starts_with("AX"),
"expected 'AX' prefix on {:?} -> '{}'",
n,
n.as_cf_str()
);
}
}
#[test]
fn new_observer_is_not_active() {
let handle = AXObserverHandle::new(1234);
assert!(!handle.is_active());
}
#[test]
fn subscribe_returns_ok_in_stub_mode() {
let mut handle = AXObserverHandle::new(0);
let result = handle.subscribe(AXNotification::FocusedWindowChanged);
assert!(result.is_ok());
}
#[test]
fn subscribe_all_notification_types_without_error() {
let mut handle = AXObserverHandle::new(0);
let results = [
handle.subscribe(AXNotification::ValueChanged),
handle.subscribe(AXNotification::UIElementDestroyed),
handle.subscribe(AXNotification::WindowCreated),
handle.subscribe(AXNotification::WindowMoved),
handle.subscribe(AXNotification::WindowResized),
handle.subscribe(AXNotification::FocusedWindowChanged),
handle.subscribe(AXNotification::FocusedUIElementChanged),
handle.subscribe(AXNotification::SelectedChildrenChanged),
handle.subscribe(AXNotification::TitleChanged),
];
for r in results {
assert!(r.is_ok());
}
}
#[test]
fn is_active_remains_false_after_subscribe() {
let mut handle = AXObserverHandle::new(0);
handle.subscribe(AXNotification::TitleChanged).unwrap();
assert!(!handle.is_active());
}
#[test]
fn observer_drops_without_panic() {
{
let _handle = AXObserverHandle::new(0);
}
}
}