Skip to content

Commit 81fd0f8

Browse files
replace osascript polling with direct AX API for zoom mute detection
Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
1 parent 23259e4 commit 81fd0f8

File tree

2 files changed

+48
-49
lines changed

2 files changed

+48
-49
lines changed

crates/detect/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ hypr-bundle = { workspace = true }
3131
hypr-language = { workspace = true }
3232

3333
plist = "1.7"
34-
cidre = { workspace = true }
34+
cidre = { workspace = true, features = ["ax"] }
3535
block2 = { workspace = true }
3636

3737
objc2-foundation = { workspace = true, features = ["NSLocale", "NSArray", "NSString"] }

crates/detect/src/zoom.rs

Lines changed: 47 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use std::process::Command;
21
use std::time::{Duration, Instant};
32

3+
use cidre::{ax, cf, ns};
4+
45
use crate::{BackgroundTask, DetectCallback, DetectEvent};
56

67
const ZOOM_BUNDLE_ID: &str = "us.zoom.xos";
@@ -33,57 +34,55 @@ impl WatcherState {
3334
}
3435
}
3536

36-
fn check_zoom_mute_state() -> Option<bool> {
37-
let script = r#"
38-
tell application "System Events"
39-
if (get name of every application process) contains "zoom.us" then
40-
tell application process "zoom.us"
41-
if exists (menu item "Mute audio" of menu 1 of menu bar item "Meeting" of menu bar 1) then
42-
return "unmuted"
43-
else if exists (menu item "Unmute audio" of menu 1 of menu bar item "Meeting" of menu bar 1) then
44-
return "muted"
45-
else
46-
return "unknown"
47-
end if
48-
end tell
49-
else
50-
return "not_running"
51-
end if
52-
end tell
53-
"#;
54-
55-
let output = Command::new("osascript")
56-
.arg("-e")
57-
.arg(script)
58-
.output()
59-
.ok()?;
60-
61-
if !output.status.success() {
62-
tracing::warn!(
63-
"osascript failed: {:?}",
64-
String::from_utf8_lossy(&output.stderr)
65-
);
66-
return None;
67-
}
37+
fn find_zoom_pid() -> Option<i32> {
38+
let bundle_id = ns::String::with_str(ZOOM_BUNDLE_ID);
39+
let apps = ns::RunningApp::with_bundle_id(&bundle_id);
40+
let app = apps.get(0)?;
41+
Some(app.pid())
42+
}
6843

69-
let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
44+
fn ax_element_title(elem: &ax::UiElement) -> Option<String> {
45+
let value = elem.attr_value(ax::attr::title()).ok()?;
46+
let s: &cf::String = unsafe { std::mem::transmute(&*value) };
47+
Some(s.to_string())
48+
}
7049

71-
match result.as_str() {
72-
"muted" => Some(true),
73-
"unmuted" => Some(false),
74-
"unknown" => {
75-
tracing::debug!("zoom state unknown (likely not in meeting)");
76-
None
77-
}
78-
"not_running" => {
79-
tracing::debug!("zoom not running");
80-
None
81-
}
82-
other => {
83-
tracing::warn!("unexpected osascript output: {}", other);
84-
None
50+
fn check_zoom_mute_state() -> Option<bool> {
51+
let pid = find_zoom_pid()?;
52+
let app = ax::UiElement::with_app_pid(pid);
53+
54+
let children = app.children().ok()?;
55+
let menu_bar = children.iter().find(|child| {
56+
child
57+
.role()
58+
.ok()
59+
.map(|r| r.equal(ax::role::menu_bar()))
60+
.unwrap_or(false)
61+
})?;
62+
63+
let menu_bar_items = menu_bar.children().ok()?;
64+
let meeting_item = menu_bar_items.iter().find(|item| {
65+
ax_element_title(item)
66+
.map(|t| t == "Meeting")
67+
.unwrap_or(false)
68+
})?;
69+
70+
let menu_children = meeting_item.children().ok()?;
71+
let meeting_menu = menu_children.get(0)?;
72+
73+
let menu_items = meeting_menu.children().ok()?;
74+
for item in menu_items.iter() {
75+
if let Some(title) = ax_element_title(item) {
76+
match title.as_str() {
77+
"Mute Audio" | "Mute audio" => return Some(false),
78+
"Unmute Audio" | "Unmute audio" => return Some(true),
79+
_ => continue,
80+
}
8581
}
8682
}
83+
84+
tracing::debug!("zoom mute state unknown (likely not in meeting)");
85+
None
8786
}
8887

8988
fn is_zoom_using_mic() -> bool {

0 commit comments

Comments
 (0)