├── .github └── workflows │ └── build.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── appkit-nsworkspace-bindings ├── Cargo.toml ├── build.rs ├── build_mac.rs └── src │ └── lib.rs ├── examples ├── active-window.rs └── simple.rs └── src ├── common ├── active_window.rs ├── mod.rs ├── platform_api.rs └── window_position.rs ├── lib.rs ├── linux ├── mod.rs └── platform_api.rs ├── mac ├── core_graphics_patch.rs ├── mod.rs ├── platform_api.rs └── window_position.rs └── win ├── mod.rs ├── platform_api.rs └── window_position.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build_windows: 15 | runs-on: windows-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Build 19 | run: cargo build --verbose 20 | - name: Run tests 21 | run: cargo test --verbose 22 | 23 | build_macos: 24 | runs-on: macos-latest 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Build 28 | run: cargo build --verbose 29 | - name: Run tests 30 | run: cargo test --verbose 31 | 32 | build_linux: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - run: | 37 | sudo apt-get install libxcb-ewmh-dev libxcb-randr0-dev 38 | - name: Build 39 | run: cargo build --verbose 40 | - name: Run tests 41 | run: cargo test --verbose 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target -------------------------------------------------------------------------------- /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 = "active-win-pos-rs" 7 | version = "0.9.0" 8 | dependencies = [ 9 | "appkit-nsworkspace-bindings", 10 | "core-foundation", 11 | "core-graphics", 12 | "objc", 13 | "windows", 14 | "xcb", 15 | ] 16 | 17 | [[package]] 18 | name = "appkit-nsworkspace-bindings" 19 | version = "0.1.2" 20 | dependencies = [ 21 | "bindgen", 22 | "objc", 23 | ] 24 | 25 | [[package]] 26 | name = "bindgen" 27 | version = "0.68.1" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" 30 | dependencies = [ 31 | "bitflags 2.4.0", 32 | "cexpr", 33 | "clang-sys", 34 | "lazy_static", 35 | "lazycell", 36 | "log", 37 | "peeking_take_while", 38 | "prettyplease", 39 | "proc-macro2", 40 | "quote", 41 | "regex", 42 | "rustc-hash", 43 | "shlex", 44 | "syn", 45 | "which", 46 | ] 47 | 48 | [[package]] 49 | name = "bitflags" 50 | version = "1.3.2" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 53 | 54 | [[package]] 55 | name = "bitflags" 56 | version = "2.4.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 59 | 60 | [[package]] 61 | name = "cexpr" 62 | version = "0.6.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 65 | dependencies = [ 66 | "nom", 67 | ] 68 | 69 | [[package]] 70 | name = "cfg-if" 71 | version = "1.0.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 74 | 75 | [[package]] 76 | name = "clang-sys" 77 | version = "1.3.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" 80 | dependencies = [ 81 | "glob", 82 | "libc", 83 | "libloading", 84 | ] 85 | 86 | [[package]] 87 | name = "core-foundation" 88 | version = "0.9.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" 91 | dependencies = [ 92 | "core-foundation-sys", 93 | "libc", 94 | ] 95 | 96 | [[package]] 97 | name = "core-foundation-sys" 98 | version = "0.8.3" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 101 | 102 | [[package]] 103 | name = "core-graphics" 104 | version = "0.23.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" 107 | dependencies = [ 108 | "bitflags 1.3.2", 109 | "core-foundation", 110 | "core-graphics-types", 111 | "foreign-types 0.5.0", 112 | "libc", 113 | ] 114 | 115 | [[package]] 116 | name = "core-graphics-types" 117 | version = "0.1.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" 120 | dependencies = [ 121 | "bitflags 1.3.2", 122 | "core-foundation", 123 | "foreign-types 0.3.2", 124 | "libc", 125 | ] 126 | 127 | [[package]] 128 | name = "either" 129 | version = "1.6.1" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 132 | 133 | [[package]] 134 | name = "foreign-types" 135 | version = "0.3.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 138 | dependencies = [ 139 | "foreign-types-shared 0.1.1", 140 | ] 141 | 142 | [[package]] 143 | name = "foreign-types" 144 | version = "0.5.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" 147 | dependencies = [ 148 | "foreign-types-macros", 149 | "foreign-types-shared 0.3.1", 150 | ] 151 | 152 | [[package]] 153 | name = "foreign-types-macros" 154 | version = "0.2.3" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" 157 | dependencies = [ 158 | "proc-macro2", 159 | "quote", 160 | "syn", 161 | ] 162 | 163 | [[package]] 164 | name = "foreign-types-shared" 165 | version = "0.1.1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 168 | 169 | [[package]] 170 | name = "foreign-types-shared" 171 | version = "0.3.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" 174 | 175 | [[package]] 176 | name = "glob" 177 | version = "0.3.0" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 180 | 181 | [[package]] 182 | name = "lazy_static" 183 | version = "1.4.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 186 | 187 | [[package]] 188 | name = "lazycell" 189 | version = "1.3.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 192 | 193 | [[package]] 194 | name = "libc" 195 | version = "0.2.113" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" 198 | 199 | [[package]] 200 | name = "libloading" 201 | version = "0.7.3" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" 204 | dependencies = [ 205 | "cfg-if", 206 | "winapi", 207 | ] 208 | 209 | [[package]] 210 | name = "log" 211 | version = "0.4.14" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 214 | dependencies = [ 215 | "cfg-if", 216 | ] 217 | 218 | [[package]] 219 | name = "malloc_buf" 220 | version = "0.0.6" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 223 | dependencies = [ 224 | "libc", 225 | ] 226 | 227 | [[package]] 228 | name = "memchr" 229 | version = "2.4.1" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 232 | 233 | [[package]] 234 | name = "minimal-lexical" 235 | version = "0.2.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 238 | 239 | [[package]] 240 | name = "nom" 241 | version = "7.1.0" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" 244 | dependencies = [ 245 | "memchr", 246 | "minimal-lexical", 247 | "version_check", 248 | ] 249 | 250 | [[package]] 251 | name = "objc" 252 | version = "0.2.7" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 255 | dependencies = [ 256 | "malloc_buf", 257 | ] 258 | 259 | [[package]] 260 | name = "peeking_take_while" 261 | version = "0.1.2" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" 264 | 265 | [[package]] 266 | name = "prettyplease" 267 | version = "0.2.15" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" 270 | dependencies = [ 271 | "proc-macro2", 272 | "syn", 273 | ] 274 | 275 | [[package]] 276 | name = "proc-macro2" 277 | version = "1.0.67" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 280 | dependencies = [ 281 | "unicode-ident", 282 | ] 283 | 284 | [[package]] 285 | name = "quick-xml" 286 | version = "0.28.2" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" 289 | dependencies = [ 290 | "memchr", 291 | ] 292 | 293 | [[package]] 294 | name = "quote" 295 | version = "1.0.33" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 298 | dependencies = [ 299 | "proc-macro2", 300 | ] 301 | 302 | [[package]] 303 | name = "regex" 304 | version = "1.6.0" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 307 | dependencies = [ 308 | "regex-syntax", 309 | ] 310 | 311 | [[package]] 312 | name = "regex-syntax" 313 | version = "0.6.27" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 316 | 317 | [[package]] 318 | name = "rustc-hash" 319 | version = "1.1.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 322 | 323 | [[package]] 324 | name = "shlex" 325 | version = "1.1.0" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" 328 | 329 | [[package]] 330 | name = "syn" 331 | version = "2.0.37" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" 334 | dependencies = [ 335 | "proc-macro2", 336 | "quote", 337 | "unicode-ident", 338 | ] 339 | 340 | [[package]] 341 | name = "unicode-ident" 342 | version = "1.0.12" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 345 | 346 | [[package]] 347 | name = "version_check" 348 | version = "0.9.4" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 351 | 352 | [[package]] 353 | name = "which" 354 | version = "4.2.4" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" 357 | dependencies = [ 358 | "either", 359 | "lazy_static", 360 | "libc", 361 | ] 362 | 363 | [[package]] 364 | name = "winapi" 365 | version = "0.3.9" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 368 | dependencies = [ 369 | "winapi-i686-pc-windows-gnu", 370 | "winapi-x86_64-pc-windows-gnu", 371 | ] 372 | 373 | [[package]] 374 | name = "winapi-i686-pc-windows-gnu" 375 | version = "0.4.0" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 378 | 379 | [[package]] 380 | name = "winapi-x86_64-pc-windows-gnu" 381 | version = "0.4.0" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 384 | 385 | [[package]] 386 | name = "windows" 387 | version = "0.48.0" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 390 | dependencies = [ 391 | "windows-targets", 392 | ] 393 | 394 | [[package]] 395 | name = "windows-targets" 396 | version = "0.48.0" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 399 | dependencies = [ 400 | "windows_aarch64_gnullvm", 401 | "windows_aarch64_msvc", 402 | "windows_i686_gnu", 403 | "windows_i686_msvc", 404 | "windows_x86_64_gnu", 405 | "windows_x86_64_gnullvm", 406 | "windows_x86_64_msvc", 407 | ] 408 | 409 | [[package]] 410 | name = "windows_aarch64_gnullvm" 411 | version = "0.48.0" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 414 | 415 | [[package]] 416 | name = "windows_aarch64_msvc" 417 | version = "0.48.0" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 420 | 421 | [[package]] 422 | name = "windows_i686_gnu" 423 | version = "0.48.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 426 | 427 | [[package]] 428 | name = "windows_i686_msvc" 429 | version = "0.48.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 432 | 433 | [[package]] 434 | name = "windows_x86_64_gnu" 435 | version = "0.48.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 438 | 439 | [[package]] 440 | name = "windows_x86_64_gnullvm" 441 | version = "0.48.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 444 | 445 | [[package]] 446 | name = "windows_x86_64_msvc" 447 | version = "0.48.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 450 | 451 | [[package]] 452 | name = "xcb" 453 | version = "1.2.1" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "4b90c622d513012e7419594a2138953603c63848cb189041e7b5dc04d3895da5" 456 | dependencies = [ 457 | "bitflags 1.3.2", 458 | "libc", 459 | "quick-xml", 460 | ] 461 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "active-win-pos-rs" 3 | version = "0.9.0" 4 | authors = ["Dmitry Malkov "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["window", "active", "current", "position", "title"] 8 | description = "Get position, size, title and a few other properties of the active window on Windows, MacOS and Linux" 9 | repository = "https://github.com/dimusic/active-win-pos-rs" 10 | 11 | [workspace] 12 | members = [ 13 | "appkit-nsworkspace-bindings", 14 | ] 15 | 16 | [target.'cfg(target_os = "macos")'.dependencies] 17 | core-graphics = "0.23" 18 | core-foundation = "0.9" 19 | objc = "0.2" 20 | appkit-nsworkspace-bindings = { path = "./appkit-nsworkspace-bindings", version = "0.1" } 21 | 22 | [target.'cfg(target_os = "windows")'.dependencies] 23 | windows = { version = "0.48.0", features = [ 24 | "Win32_Foundation", 25 | "Win32_Graphics_Dwm", 26 | "Win32_UI_WindowsAndMessaging", 27 | "Win32_System_Threading", 28 | "Win32_Storage_FileSystem", 29 | "Win32_System_ProcessStatus" 30 | ]} 31 | 32 | [target.'cfg(target_os = "linux")'.dependencies] 33 | xcb = { version = "1.2.1", features = [ "randr" ] } 34 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # active-win-pos-rs 2 | 3 | ![Build status](https://github.com/dimusic/active-win-pos-rs/actions/workflows/build.yml/badge.svg) 4 | 5 | A small Rust library that lets you get position, size, title and a few other properties of the active window on Windows, MacOS and Linux 6 | 7 | ## Usage 8 | 9 | ### Add to Cargo.toml: 10 | ```toml 11 | [dependencies] 12 | active-win-pos-rs = "0.9" 13 | ``` 14 | 15 | ### Use: 16 | ```rust 17 | use active_win_pos_rs::get_active_window; 18 | 19 | fn main() { 20 | match get_active_window() { 21 | Ok(active_window) => { 22 | println!("active window: {:#?}", active_window); 23 | }, 24 | Err(()) => { 25 | println!("error occurred while getting the active window"); 26 | } 27 | } 28 | } 29 | ``` 30 | Would give you an instance of ```ActiveWindow``` struct with unique window id, process id, window position and window title. 31 | 32 | Or use ``` active_win_pos_rs::get_position ``` to get the ```WindowPosition``` only. 33 | 34 | ### Window title on MacOS 35 | On MacOS ```title``` property will always return an empty string 36 | unless you [Enable Screen Recording permission](https://support.apple.com/en-ca/guide/mac-help/mchld6aa7d23/mac) for your app. 37 | 38 | ## Build 39 | 40 | ```sh 41 | % git clone https://github.com/dimusic/active-win-pos-rs.git 42 | % cd active-win-pos-rs 43 | % cargo build 44 | ``` 45 | 46 | ## Example 47 | ```sh 48 | % cargo run --example active-window 49 | ``` 50 | Output: 51 | ``` 52 | active window: ActiveWindow { 53 | title: "cmd - cargo run --example active-window", 54 | process_path: "C:\\Program Files\\WindowsApps\\Microsoft.WindowsTerminal_1.16.10262.0_x64__8wekyb3d8bbwe\\WindowsTerminal.exe", 55 | app_name: "WindowsTerminal", 56 | window_id: "HWND(9700584)", 57 | process_id: 8460, 58 | position: WindowPosition { 59 | x: 6.0, 60 | y: 296.0, 61 | width: 1129.0, 62 | height: 635.0, 63 | }, 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /appkit-nsworkspace-bindings/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "appkit-nsworkspace-bindings" 3 | version = "0.1.2" 4 | edition = "2021" 5 | authors = ["Dmitry Malkov "] 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["appkit", "nsworkspace", "macos", "nsrunningapplication"] 8 | description = "AppKit NSWorkspace bindings" 9 | repository = "https://github.com/dimusic/active-win-pos-rs" 10 | 11 | [target.'cfg(target_os = "macos")'.build-dependencies] 12 | bindgen = "0.68.1" 13 | 14 | [target.'cfg(target_os = "macos")'.dependencies] 15 | objc = "0.2" 16 | -------------------------------------------------------------------------------- /appkit-nsworkspace-bindings/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | mod build_mac; 3 | #[cfg(target_os = "macos")] 4 | use crate::build_mac::build; 5 | 6 | #[cfg(target_os = "macos")] 7 | fn main() { 8 | build(); 9 | } 10 | 11 | #[cfg(not(target_os = "macos"))] 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /appkit-nsworkspace-bindings/build_mac.rs: -------------------------------------------------------------------------------- 1 | extern crate bindgen; 2 | 3 | use std::env; 4 | use std::io::Error; 5 | use std::path::Path; 6 | use std::process::Command; 7 | 8 | fn get_sdk_path() -> Result { 9 | let output = Command::new("xcrun") 10 | .args(["--sdk", "macosx", "--show-sdk-path"]) 11 | .output()? 12 | .stdout; 13 | 14 | let output_str = String::from_utf8(output).expect("Failed to convert xcrun output to string"); 15 | 16 | Ok(output_str.trim().to_string()) 17 | } 18 | 19 | pub fn build() { 20 | let target = std::env::var("TARGET").unwrap(); 21 | 22 | let default_sdk_path = "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.1.sdk"; 23 | 24 | let sdk_path: String = match get_sdk_path() { 25 | Ok(path) => path, 26 | Err(e) => { 27 | println!( 28 | "cargo:warning=Failed to get MacOSX SDK Path. Trying to use default one. {:?}", 29 | e 30 | ); 31 | String::from(default_sdk_path) 32 | } 33 | }; 34 | 35 | println!("cargo:rustc-link-lib=framework=AppKit"); 36 | 37 | let builder = bindgen::Builder::default() 38 | .rustfmt_bindings(true) 39 | .header_contents( 40 | "NSWorkspace.h", 41 | " 42 | #include 43 | #include 44 | ", 45 | ) 46 | .clang_arg(format!("--target={}", target)) 47 | .clang_args(&["-isysroot", sdk_path.as_ref()]) 48 | .block_extern_crate(true) 49 | .objc_extern_crate(true) 50 | .clang_arg("-ObjC") 51 | .blocklist_item("objc_object"); 52 | 53 | let bindings = builder.generate().expect("Failed to generate bindings"); 54 | 55 | let out_dir = env::var_os("OUT_DIR").unwrap(); 56 | 57 | bindings 58 | .write_to_file(Path::new(&out_dir).join("nsworkspace.rs")) 59 | .expect("Failed to write bindings to file"); 60 | } 61 | -------------------------------------------------------------------------------- /appkit-nsworkspace-bindings/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(improper_ctypes)] 5 | #![allow(clippy::all)] 6 | #[cfg(target_os = "macos")] 7 | include!(concat!(env!("OUT_DIR"), "/nsworkspace.rs")); 8 | -------------------------------------------------------------------------------- /examples/active-window.rs: -------------------------------------------------------------------------------- 1 | use active_win_pos_rs::get_active_window; 2 | 3 | fn main() { 4 | match get_active_window() { 5 | Ok(active_window) => { 6 | println!("active window: {:#?}", active_window); 7 | } 8 | Err(()) => { 9 | println!("error occurred while getting the active window"); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use active_win_pos_rs::get_position; 2 | 3 | fn main() { 4 | match get_position() { 5 | Ok(window_position) => { 6 | println!("window position: {:#?}", window_position); 7 | } 8 | Err(()) => { 9 | println!("error occurred while getting window position"); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/common/active_window.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use super::window_position::WindowPosition; 4 | 5 | #[derive(Debug, Clone, Default)] 6 | pub struct ActiveWindow { 7 | pub title: String, 8 | pub process_path: PathBuf, 9 | pub app_name: String, 10 | pub window_id: String, 11 | pub process_id: u64, 12 | pub position: WindowPosition, 13 | } 14 | 15 | impl PartialEq for ActiveWindow { 16 | fn eq(&self, other: &Self) -> bool { 17 | self.process_id == other.process_id && self.window_id == other.window_id 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod active_window; 2 | pub mod platform_api; 3 | pub mod window_position; 4 | -------------------------------------------------------------------------------- /src/common/platform_api.rs: -------------------------------------------------------------------------------- 1 | use super::active_window::ActiveWindow; 2 | use super::window_position::WindowPosition; 3 | 4 | pub trait PlatformApi { 5 | fn get_position(&self) -> Result; 6 | fn get_active_window(&self) -> Result; 7 | } 8 | -------------------------------------------------------------------------------- /src/common/window_position.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, PartialEq)] 2 | pub struct WindowPosition { 3 | pub x: f64, 4 | pub y: f64, 5 | pub width: f64, 6 | pub height: f64, 7 | } 8 | 9 | impl WindowPosition { 10 | pub fn new(x: f64, y: f64, w: f64, h: f64) -> Self { 11 | Self { 12 | x, 13 | y, 14 | width: w, 15 | height: h, 16 | } 17 | } 18 | } 19 | 20 | impl Default for WindowPosition { 21 | fn default() -> Self { 22 | Self::new(0.0, 0.0, 0.0, 0.0) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | #[macro_use] 3 | extern crate objc; 4 | 5 | mod common; 6 | #[cfg(target_os = "linux")] 7 | mod linux; 8 | #[cfg(target_os = "macos")] 9 | mod mac; 10 | #[cfg(target_os = "windows")] 11 | mod win; 12 | 13 | #[cfg(target_os = "linux")] 14 | use linux::init_platform_api; 15 | #[cfg(target_os = "macos")] 16 | use mac::init_platform_api; 17 | #[cfg(target_os = "windows")] 18 | use win::init_platform_api; 19 | 20 | pub use common::active_window::ActiveWindow; 21 | use common::platform_api::PlatformApi; 22 | pub use common::window_position::WindowPosition; 23 | 24 | pub fn get_position() -> Result { 25 | let api = init_platform_api(); 26 | api.get_position() 27 | } 28 | 29 | pub fn get_active_window() -> Result { 30 | let api = init_platform_api(); 31 | api.get_active_window() 32 | } 33 | -------------------------------------------------------------------------------- /src/linux/mod.rs: -------------------------------------------------------------------------------- 1 | mod platform_api; 2 | 3 | use crate::common::platform_api::PlatformApi; 4 | use platform_api::LinuxPlatformApi; 5 | 6 | pub fn init_platform_api() -> impl PlatformApi { 7 | LinuxPlatformApi {} 8 | } 9 | -------------------------------------------------------------------------------- /src/linux/platform_api.rs: -------------------------------------------------------------------------------- 1 | use std::fs::read_link; 2 | 3 | use xcb::{x, Xid}; 4 | 5 | use crate::{common::platform_api::PlatformApi, ActiveWindow, WindowPosition}; 6 | 7 | fn get_xcb_window_pid(conn: &xcb::Connection, window: x::Window) -> xcb::Result { 8 | let window_pid = conn.send_request(&x::InternAtom { 9 | only_if_exists: true, 10 | name: b"_NET_WM_PID", 11 | }); 12 | let window_pid = conn.wait_for_reply(window_pid)?.atom(); 13 | 14 | let window_pid = conn.send_request(&x::GetProperty { 15 | delete: false, 16 | window, 17 | property: window_pid, 18 | r#type: x::ATOM_ANY, 19 | long_offset: 0, 20 | long_length: 1, 21 | }); 22 | let window_pid = conn.wait_for_reply(window_pid)?; 23 | 24 | Ok(window_pid.value::().first().unwrap_or(&0).to_owned()) 25 | } 26 | 27 | //Uses _NET_WM_NAME to get window title in UTF-8 encoding 28 | fn get_ewmh_window_title(conn: &xcb::Connection, window: x::Window) -> xcb::Result { 29 | let wm_name_atom = conn.send_request(&x::InternAtom { 30 | only_if_exists: true, 31 | name: "_NET_WM_NAME".as_bytes(), 32 | }); 33 | let wm_name_atom = conn.wait_for_reply(wm_name_atom)?.atom(); 34 | 35 | let window_title = conn.send_request(&x::GetProperty { 36 | delete: false, 37 | window, 38 | property: wm_name_atom, 39 | r#type: x::ATOM_ANY, 40 | long_offset: 0, 41 | long_length: 1024, 42 | }); 43 | let window_title = conn.wait_for_reply(window_title)?; 44 | 45 | let window_title = String::from_utf8_lossy(window_title.value()); 46 | Ok(window_title.into_owned()) 47 | } 48 | 49 | fn get_xcb_window_title(conn: &xcb::Connection, window: x::Window) -> xcb::Result { 50 | let ewmh_window_title = get_ewmh_window_title(conn, window); 51 | 52 | if ewmh_window_title.is_ok() { 53 | return ewmh_window_title; 54 | } 55 | 56 | let window_title = conn.send_request(&x::GetProperty { 57 | delete: false, 58 | window, 59 | property: x::ATOM_WM_NAME, 60 | r#type: x::ATOM_ANY, 61 | long_offset: 0, 62 | long_length: 1024, 63 | }); 64 | let window_title = conn.wait_for_reply(window_title)?; 65 | let window_title = String::from_utf8_lossy(window_title.value()); 66 | Ok(window_title.into_owned()) 67 | } 68 | 69 | fn get_xcb_window_class(conn: &xcb::Connection, window: x::Window) -> xcb::Result { 70 | let window_class = conn.send_request(&x::GetProperty { 71 | delete: false, 72 | window, 73 | property: x::ATOM_WM_CLASS, 74 | r#type: x::ATOM_STRING, 75 | long_offset: 0, 76 | long_length: 1024, 77 | }); 78 | let window_class = conn.wait_for_reply(window_class)?; 79 | let window_class = window_class.value(); 80 | let window_class = std::str::from_utf8(window_class); 81 | Ok(window_class.unwrap_or("").to_owned()) 82 | } 83 | 84 | fn get_xcb_active_window_atom(conn: &xcb::Connection) -> xcb::Result { 85 | let active_window_id = conn.send_request(&x::InternAtom { 86 | only_if_exists: true, 87 | name: b"_NET_ACTIVE_WINDOW", 88 | }); 89 | 90 | Ok(conn.wait_for_reply(active_window_id)?.atom()) 91 | } 92 | 93 | fn get_xcb_translated_position( 94 | conn: &xcb::Connection, 95 | active_window: x::Window, 96 | ) -> xcb::Result { 97 | let window_geometry = conn.send_request(&x::GetGeometry { 98 | drawable: x::Drawable::Window(active_window), 99 | }); 100 | let window_geometry = conn.wait_for_reply(window_geometry)?; 101 | let window_geometry_x = window_geometry.x(); 102 | let window_geometry_y = window_geometry.y(); 103 | 104 | let translated_position = conn.send_request(&x::TranslateCoordinates { 105 | dst_window: window_geometry.root(), 106 | src_window: active_window, 107 | src_x: window_geometry_x, 108 | src_y: window_geometry_y, 109 | }); 110 | let translated_position = conn.wait_for_reply(translated_position)?; 111 | 112 | Ok(WindowPosition { 113 | x: (translated_position.dst_x() - window_geometry_x) 114 | .try_into() 115 | .unwrap(), 116 | y: (translated_position.dst_y() - window_geometry_y) 117 | .try_into() 118 | .unwrap(), 119 | width: window_geometry.width().try_into().unwrap(), 120 | height: window_geometry.height().try_into().unwrap(), 121 | }) 122 | } 123 | 124 | pub struct LinuxPlatformApi {} 125 | 126 | impl PlatformApi for LinuxPlatformApi { 127 | fn get_position(&self) -> Result { 128 | let active_winow = self.get_active_window()?; 129 | Ok(active_winow.position) 130 | } 131 | 132 | fn get_active_window(&self) -> Result { 133 | let (conn, _) = xcb::Connection::connect(None).map_err(|_| ())?; 134 | let setup = conn.get_setup(); 135 | 136 | let xcb_active_window_atom = get_xcb_active_window_atom(&conn).map_err(|_| ())?; 137 | if xcb_active_window_atom == x::ATOM_NONE { 138 | // EWMH not supported 139 | return Err(()); 140 | } 141 | 142 | let root_window = setup.roots().next(); 143 | if root_window.is_none() { 144 | return Err(()); 145 | } 146 | let root_window = root_window.unwrap().root(); 147 | 148 | let active_window = conn.send_request(&x::GetProperty { 149 | delete: false, 150 | window: root_window, 151 | property: xcb_active_window_atom, 152 | r#type: x::ATOM_WINDOW, 153 | long_offset: 0, 154 | long_length: 1, 155 | }); 156 | let active_window = conn.wait_for_reply(active_window).map_err(|_| ())?; 157 | let active_window = active_window.value::().get(0); 158 | if active_window.is_none() { 159 | return Err(()); 160 | } 161 | let active_window = active_window.unwrap(); 162 | 163 | let window_pid: u32 = get_xcb_window_pid(&conn, *active_window).map_err(|_| ())?; 164 | let position = get_xcb_translated_position(&conn, *active_window).map_err(|_| ())?; 165 | let title = get_xcb_window_title(&conn, *active_window).map_err(|_| ())?; 166 | let window_class = get_xcb_window_class(&conn, *active_window).map_err(|_| ())?; 167 | 168 | let mut process_name = window_class 169 | .split('\u{0}') 170 | .filter(|str| !str.is_empty()) 171 | .collect::>(); 172 | let process_name = process_name.pop().unwrap_or("").to_owned(); 173 | 174 | let process_path = read_link(format!("/proc/{}/exe", window_pid)); 175 | 176 | Ok(ActiveWindow { 177 | process_id: window_pid.try_into().unwrap(), 178 | window_id: active_window.resource_id().to_string(), 179 | app_name: process_name, 180 | position, 181 | title, 182 | process_path: process_path.unwrap_or_default(), 183 | }) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/mac/core_graphics_patch.rs: -------------------------------------------------------------------------------- 1 | use core_graphics::base::boolean_t; 2 | use core_graphics::display::CFDictionaryRef; 3 | use core_graphics::display::CGRect; 4 | 5 | #[link(name = "CoreGraphics", kind = "framework")] 6 | extern "C" { 7 | pub fn CGRectMakeWithDictionaryRepresentation( 8 | dict: CFDictionaryRef, 9 | rect: *mut CGRect, 10 | ) -> boolean_t; 11 | } 12 | -------------------------------------------------------------------------------- /src/mac/mod.rs: -------------------------------------------------------------------------------- 1 | mod core_graphics_patch; 2 | mod platform_api; 3 | mod window_position; 4 | 5 | use crate::common::platform_api::PlatformApi; 6 | use platform_api::MacPlatformApi; 7 | 8 | pub fn init_platform_api() -> impl PlatformApi { 9 | MacPlatformApi {} 10 | } 11 | -------------------------------------------------------------------------------- /src/mac/platform_api.rs: -------------------------------------------------------------------------------- 1 | use super::core_graphics_patch::CGRectMakeWithDictionaryRepresentation; 2 | use super::window_position::FromCgRect; 3 | use crate::common::{ 4 | active_window::ActiveWindow, platform_api::PlatformApi, window_position::WindowPosition, 5 | }; 6 | use appkit_nsworkspace_bindings::{INSRunningApplication, INSWorkspace, NSWorkspace, INSURL}; 7 | use core_foundation::{ 8 | base::{CFGetTypeID, ToVoid}, 9 | boolean::CFBooleanGetTypeID, 10 | dictionary::CFDictionaryGetTypeID, 11 | mach_port::CFTypeID, 12 | number::{ 13 | CFBooleanGetValue, CFNumberGetType, CFNumberGetTypeID, CFNumberGetValue, CFNumberRef, 14 | CFNumberType, 15 | }, 16 | string::{CFString, CFStringGetTypeID}, 17 | }; 18 | use core_graphics::display::*; 19 | use objc::runtime::Object; 20 | use std::{ffi::c_void, path::PathBuf}; 21 | 22 | #[allow(non_upper_case_globals)] 23 | pub const kCFNumberSInt32Type: CFNumberType = 3; 24 | #[allow(non_upper_case_globals)] 25 | pub const kCFNumberSInt64Type: CFNumberType = 4; 26 | 27 | #[derive(Debug)] 28 | enum DictEntryValue { 29 | _Number(i64), 30 | _Bool(bool), 31 | _String(String), 32 | _Rect(WindowPosition), 33 | _Unknown, 34 | } 35 | 36 | pub struct MacPlatformApi {} 37 | 38 | impl PlatformApi for MacPlatformApi { 39 | fn get_position(&self) -> Result { 40 | if let Ok(active_window) = self.get_active_window() { 41 | return Ok(active_window.position); 42 | } 43 | 44 | Err(()) 45 | } 46 | 47 | fn get_active_window(&self) -> Result { 48 | const OPTIONS: CGWindowListOption = 49 | kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements; 50 | let window_list_info = unsafe { CGWindowListCopyWindowInfo(OPTIONS, kCGNullWindowID) }; 51 | 52 | let windows_count: isize = unsafe { CFArrayGetCount(window_list_info) }; 53 | 54 | let active_app = unsafe { 55 | let workspace = NSWorkspace::sharedWorkspace(); 56 | workspace.frontmostApplication() 57 | }; 58 | 59 | let active_window_pid = unsafe { active_app.processIdentifier() as i64 }; 60 | 61 | let mut win_pos = WindowPosition::new(0., 0., 0., 0.); 62 | let mut win_title = String::from(""); 63 | let mut app_name = String::from(""); 64 | 65 | for i in 0..windows_count { 66 | let dic_ref = unsafe { CFArrayGetValueAtIndex(window_list_info, i) as CFDictionaryRef }; 67 | 68 | if dic_ref.is_null() { 69 | continue; 70 | } 71 | 72 | let window_pid = get_from_dict(dic_ref, "kCGWindowOwnerPID"); 73 | 74 | if let DictEntryValue::_Number(win_pid) = window_pid { 75 | if win_pid != active_window_pid { 76 | continue; 77 | } 78 | 79 | if let DictEntryValue::_Rect(window_bounds) = 80 | get_from_dict(dic_ref, "kCGWindowBounds") 81 | { 82 | if window_bounds.width < 50. || window_bounds.height < 50. { 83 | continue; 84 | } 85 | 86 | win_pos = window_bounds; 87 | } 88 | 89 | if let DictEntryValue::_String(window_title) = 90 | get_from_dict(dic_ref, "kCGWindowName") 91 | { 92 | win_title = window_title; 93 | } 94 | 95 | if let DictEntryValue::_String(owner_name) = 96 | get_from_dict(dic_ref, "kCGWindowOwnerName") 97 | { 98 | app_name = owner_name; 99 | } 100 | 101 | let process_path: PathBuf = unsafe { 102 | let bundle_url = active_app.bundleURL().path(); 103 | PathBuf::from(nsstring_to_rust_string(bundle_url.0)) 104 | }; 105 | 106 | if let DictEntryValue::_Number(window_id) = 107 | get_from_dict(dic_ref, "kCGWindowNumber") 108 | { 109 | let active_window = ActiveWindow { 110 | window_id: window_id.to_string(), 111 | process_id: active_window_pid as u64, 112 | app_name, 113 | position: win_pos, 114 | title: win_title, 115 | process_path, 116 | }; 117 | 118 | unsafe { CFRelease(window_list_info as CFTypeRef) } 119 | 120 | return Ok(active_window); 121 | } 122 | } 123 | } 124 | 125 | unsafe { CFRelease(window_list_info as CFTypeRef) } 126 | 127 | Err(()) 128 | } 129 | } 130 | 131 | // Taken from https://github.com/sassman/t-rec-rs/blob/v0.7.0/src/macos/window_id.rs#L73 132 | // Modified to support dictionary type id for kCGWindowBounds 133 | fn get_from_dict(dict: CFDictionaryRef, key: &str) -> DictEntryValue { 134 | let cf_key: CFString = key.into(); 135 | let mut value: *const c_void = std::ptr::null(); 136 | if unsafe { CFDictionaryGetValueIfPresent(dict, cf_key.to_void(), &mut value) } != 0 { 137 | let type_id: CFTypeID = unsafe { CFGetTypeID(value) }; 138 | if type_id == unsafe { CFNumberGetTypeID() } { 139 | let value = value as CFNumberRef; 140 | 141 | #[allow(non_upper_case_globals)] 142 | match unsafe { CFNumberGetType(value) } { 143 | kCFNumberSInt64Type => { 144 | let mut value_i64 = 0_i64; 145 | let out_value: *mut i64 = &mut value_i64; 146 | let converted = 147 | unsafe { CFNumberGetValue(value, kCFNumberSInt64Type, out_value.cast()) }; 148 | if converted { 149 | return DictEntryValue::_Number(value_i64); 150 | } 151 | } 152 | kCFNumberSInt32Type => { 153 | let mut value_i32 = 0_i32; 154 | let out_value: *mut i32 = &mut value_i32; 155 | let converted = 156 | unsafe { CFNumberGetValue(value, kCFNumberSInt32Type, out_value.cast()) }; 157 | if converted { 158 | return DictEntryValue::_Number(value_i32 as i64); 159 | } 160 | } 161 | n => { 162 | eprintln!("Unsupported Number of typeId: {}", n); 163 | } 164 | } 165 | } else if type_id == unsafe { CFBooleanGetTypeID() } { 166 | return DictEntryValue::_Bool(unsafe { CFBooleanGetValue(value.cast()) }); 167 | } else if type_id == unsafe { CFStringGetTypeID() } { 168 | let str = nsstring_to_rust_string(value as *mut Object); 169 | return DictEntryValue::_String(str); 170 | } else if type_id == unsafe { CFDictionaryGetTypeID() } && key == "kCGWindowBounds" { 171 | let rect: CGRect = unsafe { 172 | let mut rect = std::mem::zeroed(); 173 | CGRectMakeWithDictionaryRepresentation(value.cast(), &mut rect); 174 | rect 175 | }; 176 | 177 | return DictEntryValue::_Rect(WindowPosition::from_cg_rect(&rect)); 178 | } else { 179 | eprintln!("Unexpected type: {}", type_id); 180 | } 181 | } 182 | 183 | DictEntryValue::_Unknown 184 | } 185 | 186 | pub fn nsstring_to_rust_string(nsstring: *mut Object) -> String { 187 | unsafe { 188 | let cstr: *const i8 = msg_send![nsstring, UTF8String]; 189 | if !cstr.is_null() { 190 | std::ffi::CStr::from_ptr(cstr) 191 | .to_string_lossy() 192 | .into_owned() 193 | } else { 194 | "".into() 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/mac/window_position.rs: -------------------------------------------------------------------------------- 1 | use crate::common::window_position::WindowPosition; 2 | use core_graphics::display::CGRect; 3 | 4 | pub trait FromCgRect { 5 | fn from_cg_rect(cgrect: &CGRect) -> WindowPosition; 6 | } 7 | 8 | impl FromCgRect for WindowPosition { 9 | fn from_cg_rect(cgrect: &CGRect) -> Self { 10 | Self::new( 11 | cgrect.origin.x, 12 | cgrect.origin.y, 13 | cgrect.size.width, 14 | cgrect.size.height, 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/win/mod.rs: -------------------------------------------------------------------------------- 1 | mod platform_api; 2 | mod window_position; 3 | 4 | use crate::common::platform_api::PlatformApi; 5 | use platform_api::WindowsPlatformApi; 6 | 7 | pub fn init_platform_api() -> impl PlatformApi { 8 | WindowsPlatformApi {} 9 | } 10 | -------------------------------------------------------------------------------- /src/win/platform_api.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use windows::core::{HSTRING, PCWSTR, PWSTR}; 4 | use windows::w; 5 | use windows::Win32::Foundation::{CloseHandle, HANDLE, MAX_PATH}; 6 | use windows::Win32::Graphics::Dwm::{DwmGetWindowAttribute, DWMWA_EXTENDED_FRAME_BOUNDS}; 7 | use windows::Win32::Storage::FileSystem::{ 8 | GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW, 9 | }; 10 | use windows::Win32::System::Threading::{ 11 | OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, 12 | }; 13 | use windows::Win32::UI::WindowsAndMessaging::{GetGUIThreadInfo, GUITHREADINFO}; 14 | use windows::Win32::{ 15 | Foundation::{HWND, RECT}, 16 | UI::WindowsAndMessaging::{ 17 | GetForegroundWindow, GetWindowRect, GetWindowTextW, GetWindowThreadProcessId, 18 | }, 19 | }; 20 | 21 | use crate::{common::platform_api::PlatformApi, ActiveWindow, WindowPosition}; 22 | 23 | use super::window_position::FromWinRect; 24 | 25 | #[derive(Debug)] 26 | struct LangCodePage { 27 | pub w_language: u16, 28 | pub w_code_page: u16, 29 | } 30 | 31 | pub struct WindowsPlatformApi {} 32 | 33 | impl PlatformApi for WindowsPlatformApi { 34 | fn get_position(&self) -> Result { 35 | let active_window = get_foreground_window(); 36 | 37 | if let Ok(win_position) = get_foreground_window_position(active_window) { 38 | return Ok(WindowPosition::from_win_rect(&win_position)); 39 | } 40 | 41 | Ok(WindowPosition::new(0 as f64, 0 as f64, 0 as f64, 0 as f64)) 42 | } 43 | 44 | fn get_active_window(&self) -> Result { 45 | let active_window_hwnd = get_foreground_window(); 46 | 47 | let win_position = get_foreground_window_position(active_window_hwnd)?; 48 | let active_window_position = WindowPosition::from_win_rect(&win_position); 49 | let active_window_title = get_window_title(active_window_hwnd)?; 50 | let mut process_id: u32 = 0; 51 | unsafe { GetWindowThreadProcessId(active_window_hwnd, Some(&mut process_id as *mut u32)) }; 52 | 53 | let process_path = get_process_path(process_id)?; 54 | let app_name = get_process_name(&process_path)?; 55 | 56 | let active_window = ActiveWindow { 57 | title: active_window_title, 58 | process_path: process_path.clone(), 59 | app_name, 60 | position: active_window_position, 61 | process_id: process_id as u64, 62 | window_id: format!("{:?}", active_window_hwnd), 63 | }; 64 | 65 | //UWP app 66 | if let Some(file_name) = process_path.file_name() { 67 | if file_name == "ApplicationFrameHost.exe" { 68 | return Ok(get_uwp_window_info(active_window)); 69 | } 70 | } 71 | 72 | Ok(active_window) 73 | } 74 | } 75 | 76 | fn get_uwp_window_info(mut active_window: ActiveWindow) -> ActiveWindow { 77 | let mut gui_thread_info = GUITHREADINFO { 78 | cbSize: std::mem::size_of::() as u32, 79 | ..Default::default() 80 | }; 81 | 82 | unsafe { 83 | GetGUIThreadInfo(0, &mut gui_thread_info); 84 | }; 85 | 86 | let mut gui_thread_hwnd = gui_thread_info.hwndFocus; 87 | if (gui_thread_hwnd.0 as *mut HWND).is_null() { 88 | gui_thread_hwnd = gui_thread_info.hwndActive; 89 | } 90 | 91 | let mut gui_process_id: u32 = 0; 92 | unsafe { 93 | GetWindowThreadProcessId(gui_thread_hwnd, Some(&mut gui_process_id as *mut u32)); 94 | }; 95 | 96 | if let Ok(gui_process_path) = get_process_path(gui_process_id) { 97 | let gui_process_name = get_process_name(&gui_process_path).unwrap_or_default(); 98 | 99 | active_window.process_path = gui_process_path; 100 | active_window.process_id = gui_process_id as u64; 101 | active_window.app_name = gui_process_name; 102 | } 103 | 104 | active_window 105 | } 106 | 107 | fn get_foreground_window() -> HWND { 108 | unsafe { GetForegroundWindow() } 109 | } 110 | 111 | fn get_foreground_window_position(hwnd: HWND) -> Result { 112 | unsafe { 113 | let mut rect: RECT = std::mem::zeroed(); 114 | 115 | // Try DwmGetWindowAttribute first for more accurate bounds 116 | let result = DwmGetWindowAttribute( 117 | hwnd, 118 | DWMWA_EXTENDED_FRAME_BOUNDS, 119 | &mut rect as *mut RECT as *mut _, 120 | std::mem::size_of::() as u32, 121 | ); 122 | 123 | // Fall back to GetWindowRect if DwmGetWindowAttribute fails 124 | if result.is_err() && !GetWindowRect(hwnd, &mut rect).as_bool() { 125 | return Err(()); 126 | } 127 | 128 | Ok(rect) 129 | } 130 | } 131 | 132 | fn get_window_title(hwnd: HWND) -> Result { 133 | let title: String; 134 | unsafe { 135 | let mut v: Vec = vec![0; 255]; 136 | let title_len = GetWindowTextW(hwnd, &mut v); 137 | title = String::from_utf16_lossy(&v[0..(title_len as usize)]); 138 | }; 139 | 140 | Ok(title) 141 | } 142 | 143 | fn get_process_path(process_id: u32) -> Result { 144 | let process_handle = get_process_handle(process_id)?; 145 | let mut lpdw_size: u32 = MAX_PATH; 146 | let mut process_path_raw = vec![0; MAX_PATH as usize]; 147 | let process_path_pwstr = PWSTR::from_raw(process_path_raw.as_mut_ptr()); 148 | 149 | let process_path = unsafe { 150 | let success = QueryFullProcessImageNameW( 151 | process_handle, 152 | PROCESS_NAME_WIN32, 153 | process_path_pwstr, 154 | &mut lpdw_size, 155 | ); 156 | 157 | close_process_handle(process_handle); 158 | 159 | if !success.as_bool() { 160 | return Err(()); 161 | } 162 | 163 | process_path_pwstr.to_string().map_err(|_| ())? 164 | }; 165 | 166 | Ok(Path::new(&process_path).to_path_buf()) 167 | } 168 | 169 | fn get_process_name(process_path: &Path) -> Result { 170 | let file_description = get_file_description(process_path); 171 | if file_description.is_ok() && !file_description.as_ref().unwrap().is_empty() { 172 | return file_description; 173 | } 174 | 175 | let process_file_name = process_path 176 | .file_stem() 177 | .unwrap_or(std::ffi::OsStr::new("")) 178 | .to_str() 179 | .unwrap_or("") 180 | .to_owned(); 181 | 182 | Ok(process_file_name) 183 | } 184 | 185 | fn get_file_description(process_path: &Path) -> Result { 186 | let process_path_hstring: HSTRING = process_path.as_os_str().into(); 187 | 188 | let info_size = unsafe { GetFileVersionInfoSizeW(&process_path_hstring, None) }; 189 | 190 | if info_size == 0 { 191 | return Err(()); 192 | } 193 | 194 | let mut file_version_info = vec![0u8; info_size.try_into().unwrap()]; 195 | 196 | let file_info_query_success = unsafe { 197 | GetFileVersionInfoW( 198 | &process_path_hstring, 199 | 0, 200 | info_size, 201 | file_version_info.as_mut_ptr().cast(), 202 | ) 203 | }; 204 | if !file_info_query_success.as_bool() { 205 | return Err(()); 206 | } 207 | 208 | let mut lang_ptr = std::ptr::null_mut(); 209 | let mut len = 0; 210 | let lang_query_success = unsafe { 211 | VerQueryValueW( 212 | file_version_info.as_ptr().cast(), 213 | w!("\\VarFileInfo\\Translation"), 214 | &mut lang_ptr, 215 | &mut len, 216 | ) 217 | }; 218 | if !lang_query_success.as_bool() { 219 | return Err(()); 220 | } 221 | 222 | let lang: &[LangCodePage] = 223 | unsafe { std::slice::from_raw_parts(lang_ptr as *const LangCodePage, 1) }; 224 | 225 | if lang.is_empty() { 226 | return Err(()); 227 | } 228 | 229 | let mut query_len: u32 = 0; 230 | 231 | let lang = lang.first().unwrap(); 232 | let lang_code = format!( 233 | "\\StringFileInfo\\{:04x}{:04x}\\FileDescription", 234 | lang.w_language, lang.w_code_page 235 | ); 236 | let lang_code = PCWSTR(HSTRING::from(&lang_code).as_wide().as_ptr()); 237 | 238 | let mut file_description_ptr = std::ptr::null_mut(); 239 | 240 | let file_description_query_success = unsafe { 241 | VerQueryValueW( 242 | file_version_info.as_ptr().cast(), 243 | lang_code, 244 | &mut file_description_ptr, 245 | &mut query_len, 246 | ) 247 | }; 248 | 249 | if !file_description_query_success.as_bool() { 250 | return Err(()); 251 | } 252 | 253 | let file_description = 254 | unsafe { std::slice::from_raw_parts(file_description_ptr.cast(), query_len as usize) }; 255 | let file_description = String::from_utf16_lossy(file_description); 256 | let file_description = file_description.trim_matches(char::from(0)).to_owned(); 257 | 258 | Ok(file_description) 259 | } 260 | 261 | fn get_process_handle(process_id: u32) -> Result { 262 | let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, process_id) }; 263 | 264 | handle.map_err(|_| ()) 265 | } 266 | 267 | fn close_process_handle(process_handle: HANDLE) { 268 | unsafe { CloseHandle(process_handle) }; 269 | } 270 | -------------------------------------------------------------------------------- /src/win/window_position.rs: -------------------------------------------------------------------------------- 1 | use windows::Win32::Foundation::RECT; 2 | 3 | use crate::common::window_position::WindowPosition; 4 | 5 | pub trait FromWinRect { 6 | fn from_win_rect(rect: &RECT) -> WindowPosition; 7 | } 8 | 9 | impl FromWinRect for WindowPosition { 10 | fn from_win_rect(rect: &RECT) -> Self { 11 | WindowPosition { 12 | x: rect.left as f64, 13 | y: rect.top as f64, 14 | width: (rect.right - rect.left) as f64, 15 | height: (rect.bottom - rect.top) as f64, 16 | } 17 | } 18 | } 19 | --------------------------------------------------------------------------------