|
1 | | -use std::process::Command; |
2 | 1 | use std::time::{Duration, Instant}; |
3 | 2 |
|
| 3 | +use cidre::{ax, cf, ns}; |
| 4 | + |
4 | 5 | use crate::{BackgroundTask, DetectCallback, DetectEvent}; |
5 | 6 |
|
6 | 7 | const ZOOM_BUNDLE_ID: &str = "us.zoom.xos"; |
@@ -33,57 +34,55 @@ impl WatcherState { |
33 | 34 | } |
34 | 35 | } |
35 | 36 |
|
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 | +} |
68 | 43 |
|
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 | +} |
70 | 49 |
|
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 | + } |
85 | 81 | } |
86 | 82 | } |
| 83 | + |
| 84 | + tracing::debug!("zoom mute state unknown (likely not in meeting)"); |
| 85 | + None |
87 | 86 | } |
88 | 87 |
|
89 | 88 | fn is_zoom_using_mic() -> bool { |
|
0 commit comments