Skip to content

Commit d8f2ebc

Browse files
committed
not gonna comback to this
1 parent ba00f3a commit d8f2ebc

File tree

5 files changed

+129
-62
lines changed

5 files changed

+129
-62
lines changed

rust/client/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use gauntlet_component_model::PropertyType;
1313
use gauntlet_component_model::create_component_model;
1414

1515
fn main() -> anyhow::Result<()> {
16-
#[cfg(target_os = "macos")] // needed for window focus stuff, specifically SkyLight framework
16+
#[cfg(target_os = "macos")]
1717
println!(
1818
"cargo:rustc-link-search=framework={}",
1919
"/System/Library/PrivateFrameworks"

rust/client/src/ui/window_tracking/macos/apps.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ use accessibility::AXUIElement;
77
use accessibility_sys::kAXRaiseAction;
88
use accessibility_sys::pid_t;
99
use anyhow::Context;
10+
use core_foundation::base::CFType;
11+
use core_foundation::base::FromVoid;
12+
use core_foundation::dictionary::CFDictionary;
1013
use core_foundation::string::CFString;
14+
use core_graphics::display::CGDisplay;
15+
use core_graphics::window::kCGWindowListExcludeDesktopElements;
1116
use gauntlet_server::plugins::ApplicationManager;
1217
use objc2::AnyThread;
1318
use objc2::DefinedClass;
@@ -24,7 +29,8 @@ use objc2_app_kit::NSWorkspaceDidLaunchApplicationNotification;
2429
use objc2_app_kit::NSWorkspaceDidTerminateApplicationNotification;
2530
use objc2_foundation::NSNotification;
2631

27-
use super::window::WindowNotificationDelegate;
32+
use super::window::{WindowNotificationDelegate, WindowType};
33+
use crate::ui::window_tracking::macos::sys::ax_window_id;
2834
use crate::ui::window_tracking::macos::sys::make_key_window;
2935

3036
pub struct MacosWindowTracker {
@@ -84,7 +90,6 @@ impl ApplicationNotificationDelegate {
8490
let state = ApplicationNotificationDelegateState {
8591
application_manager,
8692
applications: RefCell::new(HashMap::new()),
87-
windows: Rc::new(RefCell::new(vec![])),
8893
};
8994

9095
let delegate = ApplicationNotificationDelegate::alloc().set_ivars(state);
@@ -117,9 +122,8 @@ impl ApplicationNotificationDelegate {
117122

118123
fn create_window_notification_delegate(&self, pid: pid_t) -> anyhow::Result<()> {
119124
let application_manager = self.ivars().application_manager.clone();
120-
let windows = self.ivars().windows.clone();
121125

122-
let delegate = WindowNotificationDelegate::new(pid, application_manager, windows)
126+
let delegate = WindowNotificationDelegate::new(pid, application_manager)
123127
.context("Error creating window notification delegate")?;
124128

125129
delegate
@@ -153,7 +157,16 @@ impl ApplicationNotificationDelegate {
153157

154158
println!("Focusing window: {}, {:?}, {}", window_uuid, window, pid);
155159

156-
make_key_window(*pid, window).context("Failed to make window key")?;
160+
if let Some(windows) = CGDisplay::window_list_info(kCGWindowListExcludeDesktopElements, None) {
161+
for item in windows.into_iter() {
162+
let item: CFDictionary<CFString, CFType> = unsafe { CFDictionary::from_void(item.clone()) }.clone();
163+
println!("CFDictionary: {:?}", item);
164+
}
165+
};
166+
167+
let window_id = ax_window_id(window).context("Failed to get window id")?;
168+
169+
make_key_window(*pid, window_id).context("Failed to make window key")?;
157170

158171
// some apps seem to also require additional raise action
159172
window
@@ -167,7 +180,6 @@ impl ApplicationNotificationDelegate {
167180
struct ApplicationNotificationDelegateState {
168181
application_manager: Arc<ApplicationManager>,
169182
applications: RefCell<HashMap<pid_t, WindowNotificationDelegate>>,
170-
windows: Rc<RefCell<Vec<(String, pid_t, AXUIElement)>>>,
171183
}
172184

173185
define_class!(

rust/client/src/ui/window_tracking/macos/mod.rs

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,35 +21,29 @@ pub fn request_macos_accessibility_permissions() -> bool {
2121
unsafe { AXIsProcessTrustedWithOptions(options.as_concrete_TypeRef()) }
2222
}
2323

24-
// todo
25-
// on each ax notification + non-ax destroy event
26-
// get app pid using AXUIElementGetPid
27-
// run bruteforce search (+ regular) for axuielement using _AXUIElementCreateWithRemoteToken
28-
// from each found window axuielement
29-
// filter based on window role/subrole
30-
// get window using _AXUIElementGetWindow
31-
// get title
32-
// to focus use window id and private api
24+
// only active space(s) and fullscreen apps (only single item per app will be shown) are supported
25+
// tabs are supported only on active "desktop" space, not supported in fullscreen window
26+
// show warning when:
27+
// there are non-active spaces
28+
// except fullscreen windows
29+
// there multiple fullscreen windows for specific app
3330

31+
// warning should say:
32+
// gauntlet doesn't show windows on non-active spaces except fullscreen applications
33+
// gauntlet doesn't support fullscreen applications on multiple spaces
34+
// gauntlet doesn't support native tabs for fullscreen applications
3435

35-
// do not support hidden windows
36-
// do not support multiple "desktop" spaces unless they are all visible
37-
// what about multiple fullscreen windows of the same app?
38-
// what about tabs in visible apps in non-visible spaces?
39-
// i.e., only non-hidden windows in visible spaces and maybe(?) fullscreen apps
40-
// support minimized windows but only on visible spaces
36+
// refresh window list when space switches
37+
// if current space is fullscreen do not scan for tabs? show warning?
4138

42-
// support fullscreen applications?
43-
// support tabs on the visible windows in visible spaces only
39+
// CGSSpaceGetType to get type of given space
40+
// CGSGetWindowWorkspace to get list of spaces for specific window
41+
// ? to get space for given
42+
// ? to get current space
4443

45-
// I think ignoring existence of spaces is fine???
44+
// todo what if there are 2 monitors. is it same space or multiple? what does "separate spaces" setting do?
45+
// todo what if gauntlet started on fullscreen space?
4646

47-
// is the private function for focusing a window needed?
48-
49-
// todo support for windows on separate spaces
50-
// todo multiple desktop spaces ("Desktop" vs. "Desktop 1" and "Desktop 2")
51-
// todo sometimes the window state seems to be lost, clearing the list of windows
52-
// todo when starting the gauntlet, tabbed windows only show single one
5347
// todo implement this https://github.com/glide-wm/glide/issues/10
5448
// todo how to handle system apps and settings wrt window tracking?
5549
// todo add all github issue links and appreciations to the commit message
@@ -63,5 +57,6 @@ pub fn request_macos_accessibility_permissions() -> bool {
6357
// https://github.com/koekeishiya/yabai/issues/68
6458
// https://github.com/koekeishiya/yabai/issues/199#issuecomment-519152388
6559
// https://github.com/lwouis/alt-tab-macos/issues/1324#issuecomment-2631035482
60+
// https://github.com/glide-wm/glide/issues/10
6661

6762

rust/client/src/ui/window_tracking/macos/sys.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,10 @@ impl ProcessSerialNumber {
5757
}
5858
}
5959

60-
pub fn make_key_window(pid: pid_t, window: &AXUIElement) -> anyhow::Result<()> {
60+
pub fn make_key_window(pid: pid_t, window_id: CGWindowID) -> anyhow::Result<()> {
6161
// See https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468.
6262
// god bless all the wizards in that thread that worked on it, thank you
6363

64-
let window_id = ax_window_id(window)?;
65-
6664
println!("window id: {}", window_id);
6765

6866
#[allow(non_upper_case_globals)]
@@ -95,7 +93,9 @@ pub fn make_key_window(pid: pid_t, window: &AXUIElement) -> anyhow::Result<()> {
9593
Ok(())
9694
}
9795

96+
#[allow(unused)]
9897
pub fn bruteforce_windows_for_app(app_pid: pid_t) -> Vec<AXUIElement> {
98+
// this whole thing can take more than a second, do not run on the main thread
9999
unsafe {
100100
let mut result = vec![];
101101
let mut data = [0; 0x14];

rust/client/src/ui/window_tracking/macos/window.rs

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::cell::RefCell;
2+
use std::collections::HashMap;
23
use std::ffi::c_void;
34
use std::mem::MaybeUninit;
45
use std::rc::Rc;
56
use std::sync::Arc;
67

8+
use accessibility::AXAttribute;
79
use accessibility::AXUIElement;
810
use accessibility::AXUIElementAttributes;
911
use accessibility::Error;
@@ -15,12 +17,17 @@ use accessibility_sys::AXUIElementRef;
1517
use accessibility_sys::kAXDialogSubrole;
1618
use accessibility_sys::kAXErrorSuccess;
1719
use accessibility_sys::kAXStandardWindowSubrole;
20+
use accessibility_sys::kAXTabGroupRole;
21+
use accessibility_sys::kAXTabsAttribute;
1822
use accessibility_sys::kAXTitleChangedNotification;
1923
use accessibility_sys::kAXUIElementDestroyedNotification;
2024
use accessibility_sys::kAXWindowCreatedNotification;
2125
use accessibility_sys::kAXWindowRole;
2226
use accessibility_sys::pid_t;
2327
use anyhow::Context;
28+
use core_foundation::array::CFArray;
29+
use core_foundation::base::CFType;
30+
use core_foundation::base::FromVoid;
2431
use core_foundation::base::TCFType;
2532
use core_foundation::runloop::CFRunLoop;
2633
use core_foundation::runloop::kCFRunLoopDefaultMode;
@@ -32,17 +39,28 @@ use objc2_app_kit::NSRunningApplication;
3239
use uuid::Uuid;
3340

3441
use super::sys::AXObserver;
35-
use super::sys::bruteforce_windows_for_app;
42+
use super::sys::ax_window_id;
3643

3744
pub struct WindowNotificationDelegate {
3845
app_element: AXUIElement,
3946
observer: AXObserver,
4047
inner: Rc<WindowNotificationDelegateInner>,
4148
}
4249

50+
pub enum WindowType {
51+
Window,
52+
Tab,
53+
}
54+
55+
struct WindowData {
56+
app_pid: pid_t,
57+
window_type: WindowType,
58+
element: AXUIElement,
59+
}
60+
4361
struct WindowNotificationDelegateInner {
4462
app_pid: pid_t,
45-
windows: Rc<RefCell<Vec<(String, pid_t, AXUIElement)>>>,
63+
windows: Rc<RefCell<HashMap<String, WindowData>>>,
4664
application_manager: Arc<ApplicationManager>,
4765
}
4866

@@ -52,14 +70,8 @@ const WINDOW_EVENTS: [&str; 3] = [
5270
kAXTitleChangedNotification,
5371
];
5472

55-
const MESSAGING_TIMEOUT_SEC: f32 = 1.0;
56-
5773
impl WindowNotificationDelegate {
58-
pub fn new(
59-
pid: pid_t,
60-
application_manager: Arc<ApplicationManager>,
61-
windows: Rc<RefCell<Vec<(String, pid_t, AXUIElement)>>>,
62-
) -> anyhow::Result<Self> {
74+
pub fn new(pid: pid_t, application_manager: Arc<ApplicationManager>) -> anyhow::Result<Self> {
6375
let observer = unsafe {
6476
let mut result = MaybeUninit::uninit();
6577

@@ -76,7 +88,7 @@ impl WindowNotificationDelegate {
7688
let element = AXUIElement::application(pid);
7789

7890
element
79-
.set_messaging_timeout(MESSAGING_TIMEOUT_SEC)
91+
.set_messaging_timeout(1.0)
8092
.context("Failed to set messaging timeout")?;
8193

8294
element
@@ -87,7 +99,7 @@ impl WindowNotificationDelegate {
8799
observer,
88100
inner: Rc::new(WindowNotificationDelegateInner {
89101
app_pid: pid,
90-
windows,
102+
windows: Rc::new(RefCell::new(HashMap::new())),
91103
application_manager,
92104
}),
93105
})
@@ -142,7 +154,7 @@ impl WindowNotificationDelegate {
142154

143155
let windows = self.inner.windows.borrow();
144156

145-
for (window_id, _, _) in windows.iter() {
157+
for (window_id, _) in windows.iter() {
146158
let event = MacosWindowTrackingEvent::WindowClosed {
147159
window_id: window_id.clone(),
148160
};
@@ -205,40 +217,51 @@ fn get_bundle_path(pid: pid_t) -> Option<String> {
205217
impl WindowNotificationDelegateInner {
206218
fn refresh_windows(&self) -> anyhow::Result<()> {
207219
tracing::debug!("Refreshing windows for app: {}", self.app_pid);
220+
208221
let mut retrieved_windows: Vec<_> = AXUIElement::application(self.app_pid)
209222
.windows()?
210223
.into_iter()
211-
.map(|item| item.clone())
212-
.collect();
224+
.map(|window| window.clone())
225+
.flat_map(|window| {
226+
let tabs = list_tabs(window.clone());
213227

214-
for window in bruteforce_windows_for_app(self.app_pid) {
215-
if !retrieved_windows.contains(&window) {
216-
retrieved_windows.push(window);
217-
};
218-
}
228+
if !tabs.is_empty() {
229+
return tabs.into_iter().map(|tab| (WindowType::Tab, tab)).collect::<Vec<_>>();
230+
}
231+
232+
return vec![(WindowType::Window, window)];
233+
})
234+
.collect();
219235

220-
tracing::debug!("Retrieved {} windows", retrieved_windows.len());
236+
tracing::debug!("Retrieved windows: {}", retrieved_windows.len());
221237

222238
let stored_windows = self
223239
.windows
224240
.borrow()
225241
.iter()
226-
.map(|(_, _, window)| window.clone())
242+
.map(|(_, window)| (window.app_pid.clone(), window.clone()))
227243
.collect::<Vec<_>>();
228244

229-
tracing::debug!("Stored {} windows", stored_windows.len());
245+
tracing::debug!("Stored windows: {}", stored_windows.len());
246+
247+
let mut destroyed_windows = 0;
248+
for (pid, window) in stored_windows.iter() {
249+
if pid != &self.app_pid {
250+
continue;
251+
}
230252

231-
for window in stored_windows.into_iter() {
232-
let Some(index) = retrieved_windows.iter().position(|el| el == &window) else {
253+
let Some(index) = retrieved_windows.iter().position(|el| el == window) else {
233254
// doesn't exist anymore, destroy it
234-
self.window_destroyed(window);
255+
self.window_destroyed(window.clone());
235256
continue;
236257
};
237258

259+
destroyed_windows += 1;
238260
retrieved_windows.swap_remove(index);
239261
}
240262

241-
tracing::debug!("left retrieved {} windows", retrieved_windows.len());
263+
tracing::debug!("Destroyed windows: {}", retrieved_windows.len());
264+
tracing::debug!("New windows: {}", retrieved_windows.len());
242265

243266
for window in retrieved_windows.into_iter() {
244267
self.window_opened(window);
@@ -264,7 +287,7 @@ impl WindowNotificationDelegateInner {
264287
}
265288

266289
let window_id = Uuid::new_v4().to_string();
267-
windows.push((window_id.clone(), self.app_pid, window.clone()));
290+
windows.insert(window_id.clone(), (self.app_pid, window.clone()));
268291

269292
let title = window.title().map(|title| title.to_string()).ok();
270293
let bundle_path = get_bundle_path(self.app_pid);
@@ -285,7 +308,7 @@ impl WindowNotificationDelegateInner {
285308
return;
286309
};
287310

288-
let (window_id, _, _) = windows.swap_remove(index);
311+
let (window_id, _, _) = windows.remove(index);
289312

290313
let event = MacosWindowTrackingEvent::WindowClosed { window_id };
291314

@@ -309,3 +332,40 @@ impl WindowNotificationDelegateInner {
309332
self.application_manager.send_macos_window_tracking_event(event);
310333
}
311334
}
335+
336+
fn list_tabs(window: AXUIElement) -> Vec<AXUIElement> {
337+
let Ok(children) = window.children() else {
338+
return vec![];
339+
};
340+
341+
let tab_group = children.into_iter().find(|child| {
342+
let role = child.role().map(|val| val.to_string()).ok();
343+
if role.as_deref() != Some(kAXTabGroupRole) {
344+
return false;
345+
}
346+
347+
let title = child.title().map(|val| val.to_string()).ok();
348+
if title.as_deref() != Some("tab bar") {
349+
return false;
350+
}
351+
352+
return true;
353+
});
354+
355+
let Some(tab_group) = tab_group else {
356+
return vec![];
357+
};
358+
359+
let tabs_attribute = AXAttribute::<CFType>::new(&CFString::from_static_string(kAXTabsAttribute));
360+
let Some(tabs) = tab_group.attribute(&tabs_attribute).ok() else {
361+
return vec![];
362+
};
363+
364+
let Some(tabs) = tabs.downcast::<CFArray>() else {
365+
return vec![];
366+
};
367+
368+
tabs.into_iter()
369+
.map(|item| unsafe { AXUIElement::from_void(item.clone()) }.clone())
370+
.collect()
371+
}

0 commit comments

Comments
 (0)