├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── assets └── pic.png ├── examples ├── adhoc.rs └── monitor.rs ├── src └── lib.rs └── swift ├── CurrentSpace-delegate.swift ├── CurrentSpace-main.swift ├── CurrentSpace-types.swift └── compile.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | swift/SpaceMonitor 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "1.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 10 | 11 | [[package]] 12 | name = "block" 13 | version = "0.1.6" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 16 | 17 | [[package]] 18 | name = "cocoa" 19 | version = "0.24.1" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" 22 | dependencies = [ 23 | "bitflags", 24 | "block", 25 | "cocoa-foundation", 26 | "core-foundation", 27 | "core-graphics", 28 | "foreign-types", 29 | "libc", 30 | "objc", 31 | ] 32 | 33 | [[package]] 34 | name = "cocoa-foundation" 35 | version = "0.1.2" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" 38 | dependencies = [ 39 | "bitflags", 40 | "block", 41 | "core-foundation", 42 | "core-graphics-types", 43 | "libc", 44 | "objc", 45 | ] 46 | 47 | [[package]] 48 | name = "core-foundation" 49 | version = "0.9.4" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 52 | dependencies = [ 53 | "core-foundation-sys", 54 | "libc", 55 | ] 56 | 57 | [[package]] 58 | name = "core-foundation-sys" 59 | version = "0.8.7" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 62 | 63 | [[package]] 64 | name = "core-graphics" 65 | version = "0.22.3" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" 68 | dependencies = [ 69 | "bitflags", 70 | "core-foundation", 71 | "core-graphics-types", 72 | "foreign-types", 73 | "libc", 74 | ] 75 | 76 | [[package]] 77 | name = "core-graphics-types" 78 | version = "0.1.3" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" 81 | dependencies = [ 82 | "bitflags", 83 | "core-foundation", 84 | "libc", 85 | ] 86 | 87 | [[package]] 88 | name = "foreign-types" 89 | version = "0.3.2" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 92 | dependencies = [ 93 | "foreign-types-shared", 94 | ] 95 | 96 | [[package]] 97 | name = "foreign-types-shared" 98 | version = "0.1.1" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 101 | 102 | [[package]] 103 | name = "libc" 104 | version = "0.2.161" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 107 | 108 | [[package]] 109 | name = "malloc_buf" 110 | version = "0.0.6" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 113 | dependencies = [ 114 | "libc", 115 | ] 116 | 117 | [[package]] 118 | name = "objc" 119 | version = "0.2.7" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 122 | dependencies = [ 123 | "malloc_buf", 124 | ] 125 | 126 | [[package]] 127 | name = "space-monitor" 128 | version = "0.1.0" 129 | dependencies = [ 130 | "cocoa", 131 | "core-foundation", 132 | "libc", 133 | "objc", 134 | ] 135 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "space-monitor" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Ryan Schachte "] 6 | description = "A library for monitoring macOS virtual desktop (space) changes" 7 | license = "MIT" 8 | repository = "https://github.com/Schachte/mac-space-monitor-rs" 9 | keywords = ["macos", "desktop", "monitor", "spaces", "virtual-desktop"] 10 | categories = ["os::macos-apis"] 11 | 12 | [dependencies] 13 | cocoa = "0.24.1" 14 | core-foundation = "0.9.3" 15 | objc = "0.2.7" 16 | libc = "0.2.147" 17 | 18 | [lib] 19 | name = "macos_space_monitor" 20 | path = "src/lib.rs" 21 | 22 | [[example]] 23 | name = "monitor" 24 | path = "examples/monitor.rs" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Space Monitor 2 | 3 | ![Spaces Demo](./assets/pic.png) 4 | 5 | Space Monitor is a Rust API for subscribing to real-time changes on Mac OS X to obtain the current active [space](https://support.apple.com/guide/mac-help/work-in-multiple-spaces-mh14112/mac) (virtual desktop) index. 6 | 7 | Heavily inspired by the great work of [George Christou](https://github.com/gechr) and his Swift project - [WhichSpace](https://github.com/gechr/WhichSpace). 8 | 9 | ## 📚 Examples 10 | 11 | Check usage in the [examples](./examples/) directory 12 | 13 | ### Async retrieval (event listener) 14 | 15 | ```rust 16 | use std::thread; 17 | 18 | use macos_space_monitor::{MonitorEvent, SpaceMonitor}; 19 | 20 | fn main() { 21 | let (monitor, rx) = SpaceMonitor::new(); 22 | let _monitoring_thread = thread::spawn(move || { 23 | while let Ok(event) = rx.recv() { 24 | match event { 25 | MonitorEvent::SpaceChange(space) => { 26 | println!("Space change detected! Active space is: {}", space); 27 | } 28 | } 29 | } 30 | }); 31 | 32 | monitor.start_listening(); 33 | } 34 | ``` 35 | 36 | ### Sync retrieval 37 | 38 | ```rust 39 | use macos_space_monitor::SpaceMonitor; 40 | 41 | fn main() { 42 | let space = SpaceMonitor::get_current_space_number(); 43 | println!("Current space: {}", space); 44 | } 45 | ``` 46 | 47 | ## Why? 48 | 49 | This library was motivated by a fun project I am working on that deals with managing spaces in a more custom way on Mac OS X for more efficient space navigation. One of the core requirements when building space/window management tooling is to understand _where_ you are within your display. This is a key crate I rely on to enable real-time lookups to map a virtual display ID to a space index. 50 | 51 | ## Building 52 | 53 | If you don't know Rust or aren't using Rust and simply just want a binary you can invoke from your own code, you can build the example directly and embed the binary or add it to your `$PATH`. 54 | 55 | - Event Listener Version: 56 | 57 | - Build: `cargo build --release --example monitor` 58 | - Run: `./target/release/examples/monitor` 59 | 60 | - Adhoc Version: 61 | - Build: `cargo build --release --example adhoc` 62 | - Run: `./target/release/examples/adhoc` 63 | 64 | ## 🧠 How it works 65 | 66 | Surprisingly, obtaining the active virtual desktop index is a non-trivial task on Mac OS X and attempts in doing so have been breaking release after release as the method relies on undocumented Mac OS native APIs. 67 | 68 | This method relies on a few key ingredients: 69 | 70 | - Core Graphics (CG) 71 | 72 | - We use `CGSMainConnectionID` to get a connection to the main window server 73 | - The CGS (core graphics services) API is exploited to obtain this information 74 | 75 | - FFI (Foreign function interface) 76 | 77 | - Bridge for us to call the C APIs from Rust 78 | 79 | - Cocoa 80 | 81 | - Apple's native API for Mac OS apps 82 | - `NSApplication` for background app 83 | - Handle system notifications 84 | 85 | - Objective-C 86 | - Some message-passing invocations (`msg_send!`) 87 | - Used for receiving event notifications 88 | 89 | Space monitor is essentially a Rust binding to access lower-level mac OS internal APIs in an easy and efficient way. 90 | 91 | While you can occassionally deciper some esoteric plist files to derive the active screen via `defaults read com.apple.spaces SpacesDisplayConfiguration`, the contents are almost always incorrect and out of date, which makes it a non-starter for realtime change detection. 92 | 93 | ## 🐦 Swift 94 | 95 | When I designed this crate, I wanted a minimal example I could iterate off of in Swift to simplify the migration into Rust since I'm not a Swift developer. Mostly just committing this for posterity, but you can find a much simpler implementation of this lib in Swift underneath the [./swift](./swift) directory. Once again, this is heavily inspired by [WhichSpace](https://github.com/gechr/WhichSpace), but wanted to remove all the boilerplate. 96 | 97 | You can compile it via either of the following: 98 | 99 | - [./swift/compile.sh](./swift/compile.sh) 100 | - `swiftc -o SpaceMonitor CurrentSpace-types.swift CurrentSpace-main.swift CurrentSpace-delegate.swift` 101 | 102 | Then just run: 103 | 104 | - `./SpaceMonitor` 105 | 106 | ## ⚠️ Warning 107 | 108 | As this crate relies on private, undocumented native Mac OS APIs internally, I _believe_ your app would be rejected from the Apple app store if this crate is used within your application. However, users can still install the application externally. 109 | -------------------------------------------------------------------------------- /assets/pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schachte/space-monitor-rs/0038f1f5dda30fe36985134b43a7cfc372fed7a4/assets/pic.png -------------------------------------------------------------------------------- /examples/adhoc.rs: -------------------------------------------------------------------------------- 1 | use macos_space_monitor::SpaceMonitor; 2 | 3 | fn main() { 4 | let space = SpaceMonitor::get_current_space_number(); 5 | println!("Current space: {}", space); 6 | } 7 | -------------------------------------------------------------------------------- /examples/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::thread; 2 | 3 | use macos_space_monitor::{MonitorEvent, SpaceMonitor}; 4 | 5 | fn main() { 6 | let (monitor, rx) = SpaceMonitor::new(); 7 | let _monitoring_thread = thread::spawn(move || { 8 | while let Ok(event) = rx.recv() { 9 | match event { 10 | MonitorEvent::SpaceChange(space) => { 11 | println!("Space change detected! Active space is: {}", space); 12 | } 13 | } 14 | } 15 | }); 16 | 17 | monitor.start_listening(); 18 | } 19 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | 5 | use cocoa::appkit::NSApplicationActivationPolicy; 6 | use cocoa::base::{id, YES}; 7 | use core_foundation::array::CFArray; 8 | use core_foundation::string::CFString; 9 | use objc::runtime::{Object, BOOL}; 10 | use objc::{class, msg_send, sel, sel_impl}; 11 | use std::cell::Cell; 12 | use std::os::raw::{c_uint, c_void}; 13 | use std::sync::mpsc::{self, Receiver, Sender}; 14 | 15 | pub type CGConnectionID = c_uint; 16 | 17 | #[derive(Debug, Clone)] 18 | pub enum MonitorEvent { 19 | SpaceChange(i32), 20 | } 21 | 22 | pub struct SpaceMonitor { 23 | event_tx: Sender, 24 | } 25 | 26 | impl SpaceMonitor { 27 | pub fn new() -> (Self, Receiver) { 28 | let (tx, rx) = mpsc::channel(); 29 | (SpaceMonitor { event_tx: tx }, rx) 30 | } 31 | 32 | pub fn start_listening(self) { 33 | println!("Starting space monitor..."); 34 | println!("Press Ctrl+C to exit"); 35 | AppDelegate::new(self.event_tx).start_listening(); 36 | } 37 | 38 | pub fn get_current_space_number() -> i32 { 39 | unsafe { 40 | let conn = CGSMainConnectionID(); 41 | let displays = CGSCopyManagedDisplaySpaces(conn); 42 | let active_display = CGSCopyActiveMenuBarDisplayIdentifier(conn); 43 | let displays_array: id = msg_send![class!(NSArray), arrayWithArray:displays]; 44 | 45 | let mut active_space_id = -1; 46 | let mut all_spaces = Vec::new(); 47 | 48 | let count: usize = msg_send![displays_array, count]; 49 | for i in 0..count { 50 | let display: id = msg_send![displays_array, objectAtIndex:i]; 51 | 52 | let current_space = make_nsstring("Current Space"); 53 | let spaces = make_nsstring("Spaces"); 54 | let disp_id = make_nsstring("Display Identifier"); 55 | 56 | let current: id = msg_send![display, objectForKey:current_space]; 57 | let spaces_arr: id = msg_send![display, objectForKey:spaces]; 58 | let disp_identifier: id = msg_send![display, objectForKey:disp_id]; 59 | 60 | if current.is_null() || spaces_arr.is_null() || disp_identifier.is_null() { 61 | continue; 62 | } 63 | 64 | let disp_str: id = msg_send![disp_identifier, description]; 65 | let main_str = make_nsstring("Main"); 66 | let active_str = make_nsstring(&active_display.to_string()); 67 | let is_main: BOOL = msg_send![disp_str, isEqualToString:main_str]; 68 | let is_active: BOOL = msg_send![disp_str, isEqualToString:active_str]; 69 | 70 | if is_main == YES || is_active == YES { 71 | let space_id_key = make_nsstring("ManagedSpaceID"); 72 | active_space_id = msg_send![current, objectForKey:space_id_key]; 73 | } 74 | 75 | let spaces_count: usize = msg_send![spaces_arr, count]; 76 | for j in 0..spaces_count { 77 | let space: id = msg_send![spaces_arr, objectAtIndex:j]; 78 | let tile_key = make_nsstring("TileLayoutManager"); 79 | let tile_layout: id = msg_send![space, objectForKey:tile_key]; 80 | 81 | if tile_layout.is_null() { 82 | all_spaces.push(space); 83 | } 84 | } 85 | } 86 | 87 | if active_space_id == -1 { 88 | return -1; 89 | } 90 | 91 | for (index, space) in all_spaces.iter().enumerate() { 92 | let space_id_key = make_nsstring("ManagedSpaceID"); 93 | let space_id: i32 = msg_send![*space, objectForKey:space_id_key]; 94 | let space_number = index + 1; 95 | 96 | if space_id == active_space_id { 97 | return space_number as i32; 98 | } 99 | } 100 | -1 101 | } 102 | } 103 | } 104 | 105 | #[link(name = "CoreGraphics", kind = "framework")] 106 | extern "C" { 107 | fn CGSMainConnectionID() -> CGConnectionID; 108 | fn CGSCopyManagedDisplaySpaces(connection: CGConnectionID) -> CFArray; 109 | fn CGSCopyActiveMenuBarDisplayIdentifier(connection: CGConnectionID) -> CFString; 110 | } 111 | 112 | unsafe fn make_nsstring(string: &str) -> id { 113 | let cls = class!(NSString); 114 | let string = std::ffi::CString::new(string).unwrap(); 115 | msg_send![cls, stringWithUTF8String:string.as_ptr()] 116 | } 117 | 118 | struct AppState { 119 | conn: CGConnectionID, 120 | current_space: Cell, 121 | event_tx: Sender, 122 | } 123 | 124 | impl AppState { 125 | fn new(event_tx: Sender) -> Self { 126 | AppState { 127 | conn: unsafe { CGSMainConnectionID() }, 128 | current_space: Cell::new(0), 129 | event_tx, 130 | } 131 | } 132 | 133 | fn update_active_space_number(&self) -> i32 { 134 | unsafe { 135 | let displays = CGSCopyManagedDisplaySpaces(self.conn); 136 | let active_display = CGSCopyActiveMenuBarDisplayIdentifier(self.conn); 137 | let displays_array: id = msg_send![class!(NSArray), arrayWithArray:displays]; 138 | 139 | let mut active_space_id = -1; 140 | let mut all_spaces = Vec::new(); 141 | 142 | let count: usize = msg_send![displays_array, count]; 143 | for i in 0..count { 144 | let display: id = msg_send![displays_array, objectAtIndex:i]; 145 | 146 | let current_space = make_nsstring("Current Space"); 147 | let spaces = make_nsstring("Spaces"); 148 | let disp_id = make_nsstring("Display Identifier"); 149 | 150 | let current: id = msg_send![display, objectForKey:current_space]; 151 | let spaces_arr: id = msg_send![display, objectForKey:spaces]; 152 | let disp_identifier: id = msg_send![display, objectForKey:disp_id]; 153 | 154 | if current.is_null() || spaces_arr.is_null() || disp_identifier.is_null() { 155 | continue; 156 | } 157 | 158 | let disp_str: id = msg_send![disp_identifier, description]; 159 | let main_str = make_nsstring("Main"); 160 | let active_str = make_nsstring(&active_display.to_string()); 161 | let is_main: BOOL = msg_send![disp_str, isEqualToString:main_str]; 162 | let is_active: BOOL = msg_send![disp_str, isEqualToString:active_str]; 163 | 164 | if is_main == YES || is_active == YES { 165 | let space_id_key = make_nsstring("ManagedSpaceID"); 166 | active_space_id = msg_send![current, objectForKey:space_id_key]; 167 | } 168 | 169 | let spaces_count: usize = msg_send![spaces_arr, count]; 170 | for j in 0..spaces_count { 171 | let space: id = msg_send![spaces_arr, objectAtIndex:j]; 172 | let tile_key = make_nsstring("TileLayoutManager"); 173 | let tile_layout: id = msg_send![space, objectForKey:tile_key]; 174 | 175 | if tile_layout.is_null() { 176 | all_spaces.push(space); 177 | } 178 | } 179 | } 180 | 181 | if active_space_id == -1 { 182 | println!("No active space found."); 183 | return -1; 184 | } 185 | 186 | for (index, space) in all_spaces.iter().enumerate() { 187 | let space_id_key = make_nsstring("ManagedSpaceID"); 188 | let space_id: i32 = msg_send![*space, objectForKey:space_id_key]; 189 | let space_number = index + 1; 190 | 191 | if space_id == active_space_id { 192 | let prev_space = self.current_space.get(); 193 | self.current_space.set(space_number as i32); 194 | 195 | if prev_space != space_number as i32 && prev_space != 0 { 196 | let _ = self 197 | .event_tx 198 | .send(MonitorEvent::SpaceChange(space_number as i32)); 199 | } 200 | return space_number as i32; 201 | } 202 | } 203 | -1 204 | } 205 | } 206 | } 207 | 208 | struct AppDelegate { 209 | state: AppState, 210 | _delegate: id, 211 | } 212 | 213 | impl AppDelegate { 214 | fn new(event_tx: Sender) -> Self { 215 | let state = AppState::new(event_tx); 216 | 217 | unsafe { 218 | let mut decl = 219 | objc::declare::ClassDecl::new("RustAppDelegate", class!(NSObject)).unwrap(); 220 | 221 | decl.add_ivar::<*mut c_void>("_rustState"); 222 | 223 | extern "C" fn update_active_space_number( 224 | this: &Object, 225 | _sel: objc::runtime::Sel, 226 | _notification: id, 227 | ) { 228 | unsafe { 229 | let state_ptr: *mut c_void = *this.get_ivar("_rustState"); 230 | let state = &*(state_ptr as *const AppState); 231 | state.update_active_space_number(); 232 | } 233 | } 234 | 235 | decl.add_method( 236 | sel!(updateActiveSpaceNumber:), 237 | update_active_space_number as extern "C" fn(&Object, _, _), 238 | ); 239 | 240 | decl.register(); 241 | 242 | let delegate_class = class!(RustAppDelegate); 243 | let delegate: id = msg_send![delegate_class, new]; 244 | 245 | let app_delegate = AppDelegate { 246 | state, 247 | _delegate: delegate, 248 | }; 249 | 250 | let state_ptr = &app_delegate.state as *const _ as *mut c_void; 251 | (*delegate).set_ivar("_rustState", state_ptr); 252 | 253 | app_delegate 254 | } 255 | } 256 | 257 | fn setup_application(&self) { 258 | unsafe { 259 | let app: id = msg_send![class!(NSApplication), sharedApplication]; 260 | let _: () = msg_send![app, setActivationPolicy: 261 | NSApplicationActivationPolicy::NSApplicationActivationPolicyAccessory]; 262 | } 263 | } 264 | 265 | fn setup_observers(&self) { 266 | unsafe { 267 | let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace]; 268 | let notification_center: id = msg_send![workspace, notificationCenter]; 269 | let active_space_name = make_nsstring("NSWorkspaceActiveSpaceDidChangeNotification"); 270 | 271 | let _: () = msg_send![notification_center, 272 | addObserver:self._delegate 273 | selector:sel!(updateActiveSpaceNumber:) 274 | name:active_space_name 275 | object:workspace]; 276 | } 277 | } 278 | 279 | fn start_listening(self) { 280 | self.setup_application(); 281 | self.setup_observers(); 282 | self.state.update_active_space_number(); 283 | 284 | unsafe { 285 | let app: id = msg_send![class!(NSApplication), sharedApplication]; 286 | let _: () = msg_send![app, setDelegate:self._delegate]; 287 | let _: () = msg_send![app, run]; 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /swift/CurrentSpace-delegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { 4 | let conn: CGConnectionID = CGSMainConnectionID() 5 | static var darkModeEnabled = false 6 | 7 | override init() { 8 | super.init() 9 | setupApplication() 10 | setupObservers() 11 | updateActiveSpaceNumber() 12 | } 13 | 14 | private func setupApplication() { 15 | NSApplication.shared.setActivationPolicy(.accessory) 16 | updateDarkModeStatus() 17 | } 18 | 19 | private func setupObservers() { 20 | NSWorkspace.shared.notificationCenter.addObserver( 21 | self, 22 | selector: #selector(updateActiveSpaceNumber), 23 | name: NSWorkspace.activeSpaceDidChangeNotification, 24 | object: NSWorkspace.shared 25 | ) 26 | 27 | DistributedNotificationCenter.default().addObserver( 28 | self, 29 | selector: #selector(updateDarkModeStatus), 30 | name: NSNotification.Name("AppleInterfaceThemeChangedNotification"), 31 | object: nil 32 | ) 33 | 34 | NotificationCenter.default.addObserver( 35 | self, 36 | selector: #selector(updateActiveSpaceNumber), 37 | name: NSApplication.didUpdateNotification, 38 | object: nil 39 | ) 40 | } 41 | 42 | @objc func updateDarkModeStatus(_ sender: AnyObject? = nil) { 43 | let dictionary = UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain) 44 | if let interfaceStyle = dictionary?["AppleInterfaceStyle"] as? NSString { 45 | AppDelegate.darkModeEnabled = interfaceStyle.localizedCaseInsensitiveContains("dark") 46 | } else { 47 | AppDelegate.darkModeEnabled = false 48 | } 49 | } 50 | 51 | @objc func updateActiveSpaceNumber() { 52 | let displays = CGSCopyManagedDisplaySpaces(conn) as! [NSDictionary] 53 | let activeDisplay = CGSCopyActiveMenuBarDisplayIdentifier(conn) as! String 54 | let allSpaces: NSMutableArray = [] 55 | var activeSpaceID = -1 56 | 57 | for d in displays { 58 | guard 59 | let current = d["Current Space"] as? [String: Any], 60 | let spaces = d["Spaces"] as? [[String: Any]], 61 | let dispID = d["Display Identifier"] as? String 62 | else { 63 | continue 64 | } 65 | 66 | switch dispID { 67 | case "Main", activeDisplay: 68 | activeSpaceID = current["ManagedSpaceID"] as! Int 69 | default: 70 | break 71 | } 72 | 73 | for s in spaces { 74 | let isFullscreen = s["TileLayoutManager"] as? [String: Any] != nil 75 | if isFullscreen { 76 | continue 77 | } 78 | allSpaces.add(s) 79 | } 80 | } 81 | 82 | if activeSpaceID == -1 { 83 | print("No active space found.") 84 | return 85 | } 86 | 87 | for (index, space) in allSpaces.enumerated() { 88 | let spaceID = (space as! NSDictionary)["ManagedSpaceID"] as! Int 89 | let spaceNumber = index + 1 90 | if spaceID == activeSpaceID { 91 | print("Active Space Number: \(spaceNumber)") 92 | return 93 | } 94 | } 95 | } 96 | 97 | @objc func quitClicked(_ sender: NSMenuItem) { 98 | NSApplication.shared.terminate(self) 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /swift/CurrentSpace-main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @main 4 | struct MainApp { 5 | static func main() { 6 | autoreleasepool { 7 | let app = NSApplication.shared 8 | let delegate = AppDelegate() 9 | app.delegate = delegate 10 | app.run() 11 | } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /swift/CurrentSpace-types.swift: -------------------------------------------------------------------------------- 1 | // CurrentSpace-types.swift 2 | import Cocoa 3 | 4 | // Private CGS API declarations 5 | @_silgen_name("CGSMainConnectionID") 6 | public func CGSMainConnectionID() -> CGConnectionID 7 | 8 | @_silgen_name("CGSCopyManagedDisplaySpaces") 9 | public func CGSCopyManagedDisplaySpaces(_ connection: CGConnectionID) -> CFArray 10 | 11 | @_silgen_name("CGSCopyActiveMenuBarDisplayIdentifier") 12 | public func CGSCopyActiveMenuBarDisplayIdentifier(_ connection: CGConnectionID) -> CFString 13 | 14 | // Type aliases for CGS types 15 | public typealias CGConnectionID = UInt32 16 | -------------------------------------------------------------------------------- /swift/compile.sh: -------------------------------------------------------------------------------- 1 | swiftc -o SpaceMonitor CurrentSpace-types.swift CurrentSpace-main.swift CurrentSpace-delegate.swift --------------------------------------------------------------------------------