├── .github └── workflows │ └── rust.yml ├── .gitignore ├── COPYRIGHT ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── build.rs ├── examples └── devtool.rs ├── nimbus.default.toml ├── rustfmt.toml └── src ├── actor.rs ├── actor ├── app.rs ├── layout.rs ├── mouse.rs ├── notification_center.rs ├── reactor.rs ├── reactor │ ├── animation.rs │ ├── main_window.rs │ ├── replay.rs │ └── testing.rs └── wm_controller.rs ├── bin └── nimbus.rs ├── collections.rs ├── config.rs ├── lib.rs ├── log.rs ├── model.rs ├── model ├── layout.rs ├── layout_tree.rs ├── selection.rs ├── tree.rs └── window.rs ├── sys.rs └── sys ├── app.rs ├── event.rs ├── executor.rs ├── geometry.rs ├── observer.rs ├── run_loop.rs ├── screen.rs └── window_server.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Install Apple targets 18 | run: rustup target add aarch64-apple-darwin 19 | - name: Formatting 20 | run: cargo fmt --check --verbose 21 | - name: Check 22 | run: cargo check --verbose --locked --target aarch64-apple-darwin 23 | 24 | build: 25 | runs-on: macos-13 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Build 29 | run: cargo build --verbose --locked 30 | - name: Run tests 31 | run: cargo test --verbose --locked 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cargo 2 | /target/ 3 | .cargo 4 | 5 | # Vim 6 | *~ 7 | *.swp 8 | *.swo 9 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | The Nimbus Window Manager is dual-licensed under Apache 2.0 and MIT 2 | terms. 3 | 4 | Copyrights in the Nimbus project are retained by their contributors. No 5 | copyright assignment is required to contribute to the Nimbus project. 6 | 7 | Some files include explicit copyright notices and/or license notices. 8 | For full authorship information, see the version control history. 9 | 10 | Except as otherwise noted (below and/or in individual files), Nimbus is 11 | licensed under the Apache License, Version 2.0 or 12 | or the MIT license 13 | or , at your option. 14 | 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nimbus-wm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [[bin]] 9 | name = "nimbus" 10 | test = false 11 | 12 | # We don't have a need for unwinding, and aborting means we don't have to worry 13 | # about propagating panics to the main CFRunLoop thread. 14 | # 15 | # However, since panic-abort-tests is not stable yet, we will always build test 16 | # deps with panic=unwind. Therefore dev builds continue to use panic=unwind 17 | # with a panic hook that aborts so we can reuse dependency builds. 18 | [profile] 19 | release.panic = "abort" 20 | 21 | [dependencies] 22 | accessibility = "0.1.6" 23 | accessibility-sys = "0.1.3" 24 | anyhow = "1.0.83" 25 | ascii_tree = "0.1.1" 26 | bitflags = "2.4.1" 27 | clap = { version = "4.5.4", features = ["derive"] } 28 | core-foundation = "0.9.4" 29 | core-graphics = "0.23.2" 30 | core-graphics-types = "0.1.3" 31 | dirs = "5.0.1" 32 | icrate = { version = "0.1.0", features = [ 33 | "Foundation_NSString", 34 | "AppKit_NSRunningApplication", 35 | "Foundation_NSArray", 36 | "AppKit_NSWorkspace", 37 | "AppKit", 38 | "Foundation_NSNotificationCenter", 39 | "Foundation_NSNotification", 40 | "Foundation_NSThread", 41 | "AppKit_NSScreen", 42 | "Foundation_NSNumber", 43 | "AppKit_NSWindow", 44 | "AppKit_NSEvent", 45 | ] } 46 | livesplit-hotkey = "0.7.0" 47 | rand = "0.8.5" 48 | ron = "0.9.0-alpha.1" 49 | rustc-hash = "2.0.0" 50 | serde = { version = "1.0.201", features = ["derive"] } 51 | serde_with = "3.9.0" 52 | slotmap = { version = "1.0.7", features = ["serde"] } 53 | static_assertions = "1.1.0" 54 | tokio = { version = "1.35.1", features = ["macros", "sync"] } 55 | tokio-stream = "0.1.16" 56 | toml = "0.8.20" 57 | tracing = "0.1.40" 58 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 59 | tracing-timing = { version = "0.6.0", features = ["layer"] } 60 | tracing-tree = { version = "0.3.0", features = ["time"] } 61 | 62 | [dev-dependencies] 63 | pretty_assertions = "1.4.0" 64 | tokio = { version = "1.35.1", features = ["rt", "sync", "macros"] } 65 | test-log = { version = "0.2.16", default-features = false, features = [ 66 | "trace", 67 | ] } 68 | test_bin = "0.4.0" 69 | tempfile = { version = "3.17.1", default-features = false } 70 | 71 | [patch.crates-io] 72 | core-foundation = { git = "https://github.com/tmandry/core-foundation-rs", branch = "master" } 73 | core-foundation-sys = { git = "https://github.com/tmandry/core-foundation-rs", branch = "master" } 74 | core-graphics = { git = "https://github.com/tmandry/core-foundation-rs", branch = "master" } 75 | core-graphics-types = { git = "https://github.com/tmandry/core-foundation-rs", branch = "master" } 76 | accessibility = { git = "https://github.com/tmandry/accessibility", branch = "master" } 77 | accessibility-sys = { git = "https://github.com/tmandry/accessibility", branch = "master" } 78 | livesplit-hotkey = { git = "https://github.com/LiveSplit/livesplit-core", branch = "master" } 79 | -------------------------------------------------------------------------------- /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 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /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 | [![GHA Status]][GitHub Actions] 2 | 3 | # Nimbus 4 | 5 | Nimbus is a tiling window manager for macOS. It takes inspiration from window 6 | managers like i3, Sway, and Hyprland. 7 | 8 | #### Status 9 | 10 | Nimbus is in early development and is not recommended for use. 11 | 12 | ## Quick start 13 | 14 | Optional: Copy [nimbus.default.toml](./nimbus.default.toml) to 15 | `$HOME/.nimbus.toml` and customize it to your needs. 16 | 17 | ``` 18 | git clone https://github.com/nimbus-wm/nimbus 19 | cd nimbus 20 | cargo run --release 21 | ``` 22 | 23 | Press Alt+Z to start managing the current space. Note: This will resize all your 24 | windows! 25 | 26 | ## Save and restore 27 | 28 | If you need to update Nimbus or restart it for any reason, exit with the 29 | `save_and_exit` key binding (default Alt+Shift+E). Then, when starting again, 30 | run it with the `--restore` flag: 31 | 32 | ``` 33 | cargo run --release -- --restore 34 | ``` 35 | 36 | Note that this does not work across machine restarts. 37 | 38 | #### License and usage notes 39 | 40 | Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or 41 | [MIT license](LICENSE-MIT) at your option. 42 | 43 | [GitHub Actions]: https://github.com/nimbus-wm/nimbus/actions 44 | [GHA Status]: https://github.com/nimbus-wm/nimbus/actions/workflows/rust.yml/badge.svg 45 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!( 3 | "cargo:rustc-link-search=framework={}", 4 | "/System/Library/PrivateFrameworks" 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /examples/devtool.rs: -------------------------------------------------------------------------------- 1 | //! This tool is used to exercise nimbus and system APIs during development. 2 | 3 | use std::{future::Future, path::PathBuf, ptr, time::Instant}; 4 | 5 | use accessibility::{AXUIElement, AXUIElementAttributes}; 6 | use accessibility_sys::{pid_t, AXUIElementCopyElementAtPosition, AXUIElementRef}; 7 | use anyhow::Context; 8 | use clap::{Parser, Subcommand}; 9 | use core_foundation::{ 10 | array::CFArray, 11 | base::{FromMutVoid, TCFType}, 12 | dictionary::CFDictionaryRef, 13 | }; 14 | use core_graphics::{ 15 | display::{CGDisplayBounds, CGMainDisplayID}, 16 | window::{ 17 | kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowID, CGWindowListCopyWindowInfo, 18 | }, 19 | }; 20 | use icrate::{ 21 | AppKit::{NSScreen, NSWindow, NSWindowNumberListAllApplications}, 22 | Foundation::MainThreadMarker, 23 | }; 24 | use livesplit_hotkey::{ConsumePreference, Modifiers}; 25 | use nimbus_wm::{ 26 | actor::reactor, 27 | sys::{ 28 | self, 29 | app::WindowInfo, 30 | event::{self, get_mouse_pos}, 31 | executor::Executor, 32 | screen::{self, ScreenCache}, 33 | window_server::{self, get_window, WindowServerId}, 34 | }, 35 | }; 36 | use tokio::sync::mpsc::{self, unbounded_channel, UnboundedReceiver}; 37 | use tracing::info; 38 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; 39 | 40 | #[derive(Parser)] 41 | struct Opt { 42 | #[arg(long)] 43 | bundle: Option, 44 | #[command(subcommand)] 45 | command: Command, 46 | #[arg(long)] 47 | verbose: bool, 48 | } 49 | 50 | #[derive(Subcommand, Clone)] 51 | enum Command { 52 | #[command(subcommand)] 53 | List(List), 54 | #[command(subcommand)] 55 | App(App), 56 | #[command(subcommand)] 57 | WindowServer(WindowServer), 58 | #[command()] 59 | Replay(Replay), 60 | #[command(subcommand)] 61 | Mouse(Mouse), 62 | #[command()] 63 | Inspect, 64 | } 65 | 66 | #[derive(Subcommand, Clone)] 67 | enum List { 68 | All, 69 | Apps, 70 | Ax, 71 | Cg, 72 | Ns, 73 | Spaces, 74 | } 75 | 76 | #[derive(Subcommand, Clone)] 77 | enum App { 78 | #[command()] 79 | SetMainWindow { 80 | pid: pid_t, 81 | window_server_id: CGWindowID, 82 | #[arg(long)] 83 | wait: bool, 84 | }, 85 | #[command()] 86 | ReadMainWindow { 87 | pid: pid_t, 88 | #[arg(long)] 89 | wait: bool, 90 | }, 91 | } 92 | 93 | #[derive(Subcommand, Clone)] 94 | enum WindowServer { 95 | #[command()] 96 | List { 97 | #[arg(short, long)] 98 | all: bool, 99 | /// Whether to show the raw window dictionaries. Implies --all. 100 | #[arg(short, long)] 101 | raw: bool, 102 | }, 103 | #[command()] 104 | Get { id: u32 }, 105 | } 106 | 107 | #[derive(Parser, Clone)] 108 | struct Replay { 109 | path: PathBuf, 110 | } 111 | 112 | #[derive(Subcommand, Clone)] 113 | enum Mouse { 114 | #[command()] 115 | Clicks, 116 | #[command()] 117 | Hide, 118 | } 119 | 120 | #[tokio::main(flavor = "current_thread")] 121 | async fn main() -> anyhow::Result<()> { 122 | tracing_subscriber::registry() 123 | .with(nimbus_wm::log::tree_layer()) 124 | .with(EnvFilter::from_default_env()) 125 | .init(); 126 | let opt: Opt = Parser::parse(); 127 | 128 | match opt.command { 129 | Command::List(List::Ax) => { 130 | time("accessibility", || get_windows_with_ax(&opt, false, true)).await 131 | } 132 | Command::List(List::Cg) => time("core-graphics", || get_windows_with_cg(&opt, true)).await, 133 | Command::List(List::Ns) => time("ns-window", || get_windows_with_ns(&opt, true)).await, 134 | Command::List(List::Apps) => get_apps(&opt), 135 | Command::List(List::All) => { 136 | //time("accessibility serial", || get_windows_with_ax(&opt, true)).await; 137 | time("core-graphics", || get_windows_with_cg(&opt, true)).await; 138 | time("ns-window", || get_windows_with_ns(&opt, true)).await; 139 | time("accessibility", || get_windows_with_ax(&opt, false, true)).await; 140 | time("core-graphics second time", || get_windows_with_cg(&opt, false)).await; 141 | time("ns-window second time", || get_windows_with_ns(&opt, false)).await; 142 | time("accessibility second time", || { 143 | get_windows_with_ax(&opt, false, false) 144 | }) 145 | .await; 146 | } 147 | Command::List(List::Spaces) => { 148 | println!("Current space: {:?}", screen::diagnostic::cur_space()); 149 | println!("Visible spaces: {:?}", screen::diagnostic::visible_spaces()); 150 | println!("All spaces: {:?}", screen::diagnostic::all_spaces()); 151 | println!( 152 | "Managed display spaces: {:?}", 153 | screen::diagnostic::managed_display_spaces() 154 | ); 155 | 156 | dbg!(screen::diagnostic::managed_displays()); 157 | let screens = NSScreen::screens(MainThreadMarker::new().unwrap()); 158 | let frames: Vec<_> = screens.iter().map(|screen| screen.visibleFrame()).collect(); 159 | println!("NSScreen sizes: {frames:?}"); 160 | 161 | println!(); 162 | let mut sc = ScreenCache::new(MainThreadMarker::new().unwrap()); 163 | println!("Frames: {:?}", sc.update_screen_config()); 164 | println!("Spaces: {:?}", sc.get_screen_spaces()); 165 | } 166 | Command::App(App::SetMainWindow { pid, window_server_id, wait }) => { 167 | let app = AXUIElement::application(pid); 168 | let windows = app.windows()?; 169 | let window = windows 170 | .iter() 171 | .filter(|w| { 172 | let id: Result = (&**w).try_into(); 173 | id.is_ok_and(|id| id.as_u32() == window_server_id) 174 | }) 175 | .next() 176 | .context("Could not find matching window")?; 177 | if wait { 178 | println!("Press enter to complete action"); 179 | std::io::stdin().read_line(&mut String::new())?; 180 | window.set_messaging_timeout(3600.0)?; 181 | } 182 | window.set_main(true).context("Failed to set window as main")?; 183 | } 184 | Command::App(App::ReadMainWindow { pid, wait }) => { 185 | let app = AXUIElement::application(pid); 186 | println!("frontmost = {:?}", app.frontmost()?); 187 | let main_window = if opt.verbose { 188 | let main_window = dbg!(app.main_window()?); 189 | let main_window_id: WindowServerId = (&app.main_window()?).try_into()?; 190 | dbg!(main_window_id); 191 | main_window 192 | } else { 193 | let main_window = app.main_window(); 194 | println!("main_window = {:?}", main_window.as_ref().map(|_| ())); 195 | // let main_window_id: WindowServerId = (&app.main_window()?).try_into()?; 196 | // dbg!(main_window_id); 197 | main_window? 198 | }; 199 | if wait { 200 | println!("Press enter to complete action"); 201 | std::io::stdin().read_line(&mut String::new())?; 202 | app.set_messaging_timeout(3600.0)?; 203 | } 204 | dbg!(main_window.main()?); 205 | } 206 | Command::WindowServer(WindowServer::List { all, raw }) => { 207 | if raw { 208 | for window in window_server::get_visible_windows_raw().iter() { 209 | println!("{window:?}"); 210 | } 211 | } else { 212 | let layer = if all { None } else { Some(0) }; 213 | for window in window_server::get_visible_windows_with_layer(layer) { 214 | println!("{window:?}"); 215 | } 216 | } 217 | } 218 | Command::WindowServer(WindowServer::Get { id }) => { 219 | match window_server::get_window(WindowServerId(id)) { 220 | Some(win) => println!("{win:?}"), 221 | None => println!("Could not find window {id}"), 222 | } 223 | } 224 | Command::Replay(Replay { path }) => { 225 | // We have to spawn a thread because the reactor uses a blocking receive. 226 | tokio::task::spawn_blocking(move || { 227 | reactor::replay(&path, |_span, request| { 228 | info!(?request); 229 | match request { 230 | nimbus_wm::actor::app::Request::Raise(_, _, Some(ch), _) => _ = ch.send(()), 231 | _ => (), 232 | } 233 | }) 234 | }) 235 | .await 236 | .unwrap()?; 237 | } 238 | Command::Mouse(Mouse::Clicks) => { 239 | use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop}; 240 | use core_graphics::event::{ 241 | CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType, 242 | }; 243 | let current = CFRunLoop::get_current(); 244 | match CGEventTap::new( 245 | CGEventTapLocation::HID, 246 | CGEventTapPlacement::HeadInsertEventTap, 247 | CGEventTapOptions::Default, 248 | vec![CGEventType::LeftMouseUp], 249 | |_a, _b, d| { 250 | println!("{:?}", d.location()); 251 | None 252 | }, 253 | ) { 254 | Ok(tap) => unsafe { 255 | let loop_source = tap.mach_port().create_runloop_source(0).unwrap(); 256 | current.add_source(&loop_source, kCFRunLoopCommonModes); 257 | tap.enable(); 258 | CFRunLoop::run_current(); 259 | }, 260 | Err(_) => assert!(false), 261 | } 262 | } 263 | Command::Mouse(Mouse::Hide) => { 264 | window_server::allow_hide_mouse().unwrap(); 265 | event::hide_mouse().unwrap(); 266 | 267 | println!("Press enter to show"); 268 | std::io::stdin().read_line(&mut String::new())?; 269 | event::show_mouse().unwrap(); 270 | 271 | println!("Press enter to exit"); 272 | std::io::stdin().read_line(&mut String::new())?; 273 | } 274 | Command::Inspect => inspect(MainThreadMarker::new().unwrap()), 275 | } 276 | Ok(()) 277 | } 278 | 279 | fn inspect(mtm: MainThreadMarker) { 280 | let (tx, rx) = unbounded_channel(); 281 | let hook = 282 | livesplit_hotkey::Hook::with_consume_preference(ConsumePreference::MustConsume).unwrap(); 283 | let key = event::Hotkey { 284 | key_code: event::KeyCode::KeyI, 285 | modifiers: Modifiers::ALT | Modifiers::SHIFT, 286 | }; 287 | hook.register(key, move || _ = tx.send(())).unwrap(); 288 | println!("Press {key:?} to inspect the window under the mouse"); 289 | Executor::run(inspect_inner(rx, mtm)); 290 | } 291 | 292 | async fn inspect_inner(mut rx: UnboundedReceiver<()>, mtm: MainThreadMarker) { 293 | let mut screen_cache = ScreenCache::new(mtm); 294 | let (_, _, converter) = screen_cache.update_screen_config(); 295 | while let Some(()) = rx.recv().await { 296 | let Some(pos) = get_mouse_pos(converter) else { continue }; 297 | // This API doesn't always work, but for some reason get_window_at_point 298 | // *never* works from devtool. 299 | let mut element: AXUIElementRef = ptr::null_mut(); 300 | let err = unsafe { 301 | AXUIElementCopyElementAtPosition( 302 | AXUIElement::system_wide().as_CFTypeRef() as _, 303 | pos.x as f32, 304 | pos.y as f32, 305 | &raw mut element, 306 | ) 307 | }; 308 | if err != 0 { 309 | println!("Failed to get element under cursor: {err:?}"); 310 | continue; 311 | } 312 | let Ok(ax_window) = unsafe { AXUIElement::from_mut_void(element as *mut _) }.window() 313 | else { 314 | println!("No window for element {element:#?}"); 315 | continue; 316 | }; 317 | println!("{ax_window:#?}"); 318 | let Some(info) = 319 | WindowServerId::try_from(&ax_window).ok().and_then(|wsid| get_window(wsid)) 320 | else { 321 | println!("Couldn't get window server info for {element:?}"); 322 | continue; 323 | }; 324 | println!("{info:#?}"); 325 | } 326 | } 327 | 328 | async fn get_windows_with_cg(opt: &Opt, print: bool) { 329 | let windows: CFArray = unsafe { 330 | CFArray::wrap_under_get_rule(CGWindowListCopyWindowInfo( 331 | kCGWindowListOptionOnScreenOnly, 332 | kCGNullWindowID, 333 | )) 334 | }; 335 | if print && opt.verbose { 336 | println!("{windows:?}"); 337 | } 338 | if print { 339 | println!("visible window ids:"); 340 | for window in window_server::get_visible_windows_with_layer(None) { 341 | if opt.verbose { 342 | println!("- {window:?}"); 343 | } else { 344 | println!("- {id:?}, pid={pid:?}", id = window.id, pid = window.pid); 345 | } 346 | } 347 | } 348 | let display_id = unsafe { CGMainDisplayID() }; 349 | let screen = unsafe { CGDisplayBounds(display_id) }; 350 | if print { 351 | println!("main display = {screen:?}"); 352 | } 353 | } 354 | 355 | async fn get_windows_with_ns(_opt: &Opt, print: bool) { 356 | let mtm = MainThreadMarker::new().unwrap(); 357 | let windows = 358 | unsafe { NSWindow::windowNumbersWithOptions(NSWindowNumberListAllApplications, mtm) }; 359 | if print { 360 | println!("{windows:?}"); 361 | } 362 | } 363 | 364 | async fn get_windows_with_ax(opt: &Opt, serial: bool, print: bool) { 365 | let (sender, mut receiver) = mpsc::unbounded_channel(); 366 | for (pid, bundle_id) in sys::app::running_apps(opt.bundle.clone()) { 367 | let sender = sender.clone(); 368 | let verbose = opt.verbose; 369 | let task = move || { 370 | let app = AXUIElement::application(pid); 371 | let windows = get_windows_for_app(app, verbose); 372 | sender.send((bundle_id, windows)).unwrap() 373 | }; 374 | if serial { 375 | task(); 376 | } else { 377 | tokio::task::spawn_blocking(task); 378 | } 379 | } 380 | drop(sender); 381 | while let Some((info, windows)) = receiver.recv().await { 382 | //println!("{info:?}"); 383 | match windows { 384 | Ok(windows) => { 385 | if print { 386 | for (win, dbg) in windows { 387 | println!("{win:?} from {}", info.bundle_id.as_deref().unwrap_or("?")); 388 | if opt.verbose { 389 | println!("=> {dbg}"); 390 | } 391 | } 392 | } 393 | } 394 | Err(_) => (), //println!(" * Error reading windows: {err:?}"), 395 | } 396 | } 397 | } 398 | 399 | fn get_windows_for_app( 400 | app: AXUIElement, 401 | verbose: bool, 402 | ) -> Result, accessibility::Error> { 403 | let Ok(windows) = &app.windows() else { 404 | return Err(accessibility::Error::NotFound); 405 | }; 406 | windows 407 | .iter() 408 | .map(|win| { 409 | Ok(( 410 | WindowInfo::try_from(&*win)?, 411 | verbose.then(|| format!("{:#?}", &*win)).unwrap_or_default(), 412 | )) 413 | }) 414 | .collect() 415 | } 416 | 417 | fn get_apps(opt: &Opt) { 418 | for (pid, _bundle_id) in sys::app::running_apps(opt.bundle.clone()) { 419 | let app = AXUIElement::application(pid); 420 | println!("{app:#?}"); 421 | } 422 | } 423 | 424 | async fn time>(desc: &str, f: impl FnOnce() -> F) -> O { 425 | let start = Instant::now(); 426 | let out = f().await; 427 | let end = Instant::now(); 428 | println!("{desc} took {:?}", end - start); 429 | out 430 | } 431 | -------------------------------------------------------------------------------- /nimbus.default.toml: -------------------------------------------------------------------------------- 1 | # This is the default configuration. Nimbus will load this file if you do not 2 | # have a .nimbus.toml file in your home directory. You should copy this file 3 | # to $HOME and modify it to suit your needs. 4 | 5 | [settings] 6 | 7 | # Enable animations. 8 | animate = true 9 | 10 | # Disable each space by default. When this is set, Use the 11 | # toggle_space_activated command to enable a space. 12 | default_disable = true 13 | 14 | # Focus the window under the mouse as it moves. 15 | focus_follows_mouse = true 16 | 17 | # Move the mouse to the middle of the window each time the focus changes. 18 | mouse_follows_focus = true 19 | 20 | # Hide the mouse each time a new window is focused. Ignored if 21 | # mouse_follows_focus is false. 22 | mouse_hides_on_focus = true 23 | 24 | [keys] 25 | # Modifier and key names must be capitalized. 26 | 27 | # Toggle whether the current space is managed by Nimbus. 28 | "Alt + Z" = "toggle_space_activated" 29 | 30 | # Focus the window in the specified direction. 31 | "Alt + H" = { move_focus = "left" } 32 | "Alt + J" = { move_focus = "down" } 33 | "Alt + K" = { move_focus = "up" } 34 | "Alt + L" = { move_focus = "right" } 35 | 36 | # Move the focused window in the specified direction. 37 | "Alt + Shift + H" = { move_node = "left" } 38 | "Alt + Shift + J" = { move_node = "down" } 39 | "Alt + Shift + K" = { move_node = "up" } 40 | "Alt + Shift + L" = { move_node = "right" } 41 | 42 | # Move up or down the tree hieararchy, selecting a parent or child node 43 | # respectively. 44 | "Alt + A" = "ascend" 45 | "Alt + D" = "descend" 46 | 47 | # Create a container above the current node in the specified orientation. 48 | # This has the effect of "splitting" the current window/node once a new node 49 | # is added. 50 | "Alt + Backslash" = { split = "horizontal" } 51 | "Alt + Equal" = { split = "vertical" } 52 | 53 | # Change the parent node to a horizontal or vertical group, also known as 54 | # "tabbed" and "stacked" respectively. 55 | "Alt + T" = { group = "horizontal" } 56 | "Alt + S" = { group = "vertical" } 57 | "Alt + E" = "ungroup" 58 | 59 | # Float the current node. Floating windows are allowed to overlap and keep 60 | # whatever size and position you give them. Note that they do not actually 61 | # float on top of other windows, as that would require disabling security 62 | # protection on your machine. 63 | "Alt + Shift + Space" = "toggle_window_floating" 64 | 65 | # Toggle between focusing floating nodes. When switching to floating mode 66 | # this will put all floating windows on top, and when switching away it will 67 | # hide floating windows. 68 | "Alt + Space" = "toggle_focus_floating" 69 | 70 | # Toggle whether the focused node takes up the whole screen. 71 | "Alt + F" = "toggle_fullscreen" 72 | 73 | # 74 | # Developer commands 75 | # 76 | 77 | # Save the current state to the restore file and exit. 78 | # Restore this with the --restore option when starting nimbus. 79 | # Note that the restore file is only useful when upgrading or restarting 80 | # nimbus itself; it is not valid after the user logs out or restarts. 81 | "Alt + Shift + E" = "save_and_exit" 82 | 83 | # Print the current layout in the logs. 84 | "Alt + Shift + D" = "debug" 85 | 86 | "Alt + M" = "show_timing" 87 | "Alt + Shift + S" = "serialize" 88 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | group_imports = "StdExternalCrate" 3 | struct_lit_width = 30 4 | single_line_let_else_max_width = 60 5 | chain_width = 80 6 | fn_call_width = 65 7 | -------------------------------------------------------------------------------- /src/actor.rs: -------------------------------------------------------------------------------- 1 | //! Actors in the window manager. 2 | //! 3 | //! Each actor manages some important resource, like an external application or 4 | //! the layout state. The flow of events between these actors defines the 5 | //! overall behavior of the window manager. 6 | 7 | pub mod app; 8 | pub mod layout; 9 | pub mod mouse; 10 | pub mod notification_center; 11 | pub mod reactor; 12 | pub mod wm_controller; 13 | -------------------------------------------------------------------------------- /src/actor/app.rs: -------------------------------------------------------------------------------- 1 | //! The app actor manages messaging to an application using the system 2 | //! accessibility APIs. 3 | //! 4 | //! These APIs support reading and writing window states like position and size. 5 | 6 | use std::{ 7 | fmt::Debug, 8 | num::NonZeroU32, 9 | sync::{ 10 | atomic::{AtomicI32, Ordering}, 11 | Arc, Mutex, 12 | }, 13 | thread, 14 | time::{Duration, Instant}, 15 | }; 16 | 17 | use accessibility::{AXAttribute, AXUIElement, AXUIElementActions, AXUIElementAttributes}; 18 | use accessibility_sys::{ 19 | kAXApplicationActivatedNotification, kAXApplicationDeactivatedNotification, 20 | kAXMainWindowChangedNotification, kAXStandardWindowSubrole, kAXTitleChangedNotification, 21 | kAXUIElementDestroyedNotification, kAXWindowCreatedNotification, 22 | kAXWindowDeminiaturizedNotification, kAXWindowMiniaturizedNotification, 23 | kAXWindowMovedNotification, kAXWindowResizedNotification, kAXWindowRole, 24 | }; 25 | use core_foundation::{runloop::CFRunLoop, string::CFString}; 26 | use icrate::{ 27 | objc2::rc::Id, 28 | AppKit::NSRunningApplication, 29 | Foundation::{CGPoint, CGRect}, 30 | }; 31 | use serde::{Deserialize, Serialize}; 32 | use tokio::sync::{ 33 | mpsc::{ 34 | unbounded_channel as channel, UnboundedReceiver as Receiver, UnboundedSender as Sender, 35 | }, 36 | oneshot, 37 | }; 38 | use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; 39 | use tracing::{debug, error, info, instrument, trace, warn, Span}; 40 | 41 | pub use crate::sys::app::{pid_t, AppInfo, WindowInfo}; 42 | use crate::{ 43 | actor::reactor::{self, Event, Requested, TransactionId}, 44 | collections::HashMap, 45 | sys::{ 46 | app::NSRunningApplicationExt, 47 | event, 48 | executor::Executor, 49 | geometry::{ToCGType, ToICrate}, 50 | observer::Observer, 51 | window_server::{self, WindowServerId}, 52 | }, 53 | }; 54 | 55 | /// An identifier representing a window. 56 | /// 57 | /// This identifier is only valid for the lifetime of the process that owns it. 58 | /// It is not stable across restarts of the window manager. 59 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] 60 | pub struct WindowId { 61 | pub pid: pid_t, 62 | idx: NonZeroU32, 63 | } 64 | 65 | impl WindowId { 66 | #[cfg(test)] 67 | pub(crate) fn new(pid: pid_t, idx: u32) -> WindowId { 68 | WindowId { 69 | pid, 70 | idx: NonZeroU32::new(idx).unwrap(), 71 | } 72 | } 73 | } 74 | 75 | #[derive(Clone)] 76 | pub struct AppThreadHandle { 77 | requests_tx: Sender<(Span, Request)>, 78 | } 79 | 80 | impl AppThreadHandle { 81 | pub(crate) fn new_for_test(requests_tx: Sender<(Span, Request)>) -> Self { 82 | let this = AppThreadHandle { requests_tx }; 83 | this 84 | } 85 | 86 | pub fn send(&self, req: Request) -> anyhow::Result<()> { 87 | self.requests_tx.send((Span::current(), req))?; 88 | Ok(()) 89 | } 90 | } 91 | 92 | impl Debug for AppThreadHandle { 93 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 94 | f.debug_struct("ThreadHandle").finish() 95 | } 96 | } 97 | 98 | #[derive(Debug)] 99 | pub enum Request { 100 | Terminate, 101 | GetVisibleWindows, 102 | 103 | SetWindowFrame(WindowId, CGRect, TransactionId), 104 | SetWindowPos(WindowId, CGPoint, TransactionId), 105 | 106 | /// Temporarily suspend position and size update events for this window. 107 | BeginWindowAnimation(WindowId), 108 | /// Resume position and size events for the window. One position and size 109 | /// event are sent immediately upon receiving the request. 110 | EndWindowAnimation(WindowId), 111 | 112 | /// Raise the window by making it the main window of this app and then 113 | /// making this app frontmost. 114 | /// 115 | /// Events attributed to this request will have the [`Quiet`] parameter 116 | /// attached to them. 117 | Raise(WindowId, RaiseToken, Option>, Quiet), 118 | } 119 | 120 | #[derive(Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize)] 121 | pub enum Quiet { 122 | Yes, 123 | #[default] 124 | No, 125 | } 126 | 127 | /// Prevents stale activation requests from happening after more recent ones. 128 | /// 129 | /// This token holds the pid of the latest activation request from the reactor, 130 | /// and provides synchronization between the app threads to ensure that multiple 131 | /// requests aren't handled simultaneously. 132 | /// 133 | /// It is also designed not to block the main reactor thread. 134 | #[derive(Clone, Debug, Default)] 135 | pub struct RaiseToken(Arc<(Mutex<()>, AtomicI32)>); 136 | 137 | impl RaiseToken { 138 | /// Checks if the most recent activation request was for `pid`. Calls the 139 | /// supplied closure if it was. 140 | pub fn with(&self, pid: pid_t, f: impl FnOnce() -> R) -> Option { 141 | let _lock = trace_misc("RT lock", || self.0 .0.lock()).unwrap(); 142 | if pid == self.0 .1.load(Ordering::SeqCst) { 143 | Some(f()) 144 | } else { 145 | None 146 | } 147 | } 148 | 149 | pub fn set_pid(&self, pid: pid_t) { 150 | // Even though we don't hold the lock, we know that the app servicing 151 | // the Raise request will have to hold it while it activates itself. 152 | // This means any apps that are first in the queue have either completed 153 | // their activation request or timed out. 154 | self.0 .1.store(pid, Ordering::SeqCst) 155 | } 156 | } 157 | 158 | pub fn spawn_app_thread(pid: pid_t, info: AppInfo, events_tx: reactor::Sender) { 159 | thread::Builder::new() 160 | .name(format!("{}({pid})", info.bundle_id.as_deref().unwrap_or(""))) 161 | .spawn(move || app_thread_main(pid, info, events_tx)) 162 | .unwrap(); 163 | } 164 | 165 | struct State { 166 | pid: pid_t, 167 | bundle_id: Option, 168 | #[expect(dead_code, reason = "unused for now")] 169 | running_app: Id, 170 | app: AXUIElement, 171 | observer: Observer, 172 | events_tx: reactor::Sender, 173 | windows: HashMap, 174 | last_window_idx: u32, 175 | main_window: Option, 176 | last_activated: Option<(Instant, Quiet, Option>)>, 177 | is_frontmost: bool, 178 | } 179 | 180 | struct WindowState { 181 | elem: AXUIElement, 182 | last_seen_txid: TransactionId, 183 | } 184 | 185 | const APP_NOTIFICATIONS: &[&str] = &[ 186 | kAXApplicationActivatedNotification, 187 | kAXApplicationDeactivatedNotification, 188 | kAXMainWindowChangedNotification, 189 | kAXWindowCreatedNotification, 190 | ]; 191 | 192 | const WINDOW_NOTIFICATIONS: &[&str] = &[ 193 | kAXUIElementDestroyedNotification, 194 | kAXWindowMovedNotification, 195 | kAXWindowResizedNotification, 196 | kAXWindowMiniaturizedNotification, 197 | kAXWindowDeminiaturizedNotification, 198 | kAXTitleChangedNotification, 199 | ]; 200 | 201 | const WINDOW_ANIMATION_NOTIFICATIONS: &[&str] = 202 | &[kAXWindowMovedNotification, kAXWindowResizedNotification]; 203 | 204 | impl State { 205 | async fn run( 206 | mut self, 207 | info: AppInfo, 208 | requests_tx: Sender<(Span, Request)>, 209 | requests_rx: Receiver<(Span, Request)>, 210 | notifications_rx: Receiver<(AXUIElement, String)>, 211 | ) { 212 | let handle = AppThreadHandle { requests_tx }; 213 | if !self.init(handle, info) { 214 | return; 215 | } 216 | 217 | pub enum Incoming { 218 | Notification((AXUIElement, String)), 219 | Request((Span, Request)), 220 | } 221 | 222 | let mut merged = StreamExt::merge( 223 | UnboundedReceiverStream::new(requests_rx).map(Incoming::Request), 224 | UnboundedReceiverStream::new(notifications_rx).map(Incoming::Notification), 225 | ); 226 | 227 | while let Some(incoming) = merged.next().await { 228 | match incoming { 229 | Incoming::Request((span, mut request)) => { 230 | let _guard = span.enter(); 231 | debug!(?self.bundle_id, ?self.pid, ?request, "Got request"); 232 | match self.handle_request(&mut request) { 233 | Ok(should_terminate) if should_terminate => break, 234 | Ok(_) => (), 235 | Err(err) => { 236 | error!(?self.bundle_id, ?self.pid, ?request, "Error handling request: {err}"); 237 | } 238 | } 239 | } 240 | Incoming::Notification((elem, notif)) => { 241 | self.handle_notification(elem, ¬if); 242 | } 243 | } 244 | } 245 | } 246 | 247 | #[instrument(skip_all, fields(?info))] 248 | #[must_use] 249 | fn init(&mut self, handle: AppThreadHandle, info: AppInfo) -> bool { 250 | // Register for notifications on the application element. 251 | for notif in APP_NOTIFICATIONS { 252 | let res = self.observer.add_notification(&self.app, notif); 253 | if let Err(err) = res { 254 | debug!(pid = ?self.pid, ?err, "Watching app failed"); 255 | return false; 256 | } 257 | } 258 | 259 | // Now that we will observe new window events, read the list of windows. 260 | let Ok(initial_window_elements) = self.app.windows() else { 261 | // This is probably not a normal application, or it has exited. 262 | return false; 263 | }; 264 | 265 | // Process the list and register notifications on all windows. 266 | self.windows.reserve(initial_window_elements.len() as usize); 267 | let mut windows = Vec::with_capacity(initial_window_elements.len() as usize); 268 | let mut wsids = Vec::with_capacity(initial_window_elements.len() as usize); 269 | for elem in initial_window_elements.iter() { 270 | let elem = elem.clone(); 271 | let wsid = WindowServerId::try_from(&elem).ok(); 272 | let Some((info, wid)) = self.register_window(elem) else { 273 | continue; 274 | }; 275 | if let Some(wsid) = wsid { 276 | wsids.push(wsid); 277 | } 278 | windows.push((wid, info)); 279 | } 280 | let window_server_info = window_server::get_windows(&wsids); 281 | 282 | self.main_window = self.app.main_window().ok().and_then(|w| self.id(&w).ok()); 283 | self.is_frontmost = self.app.frontmost().map(|b| b.into()).unwrap_or(false); 284 | 285 | // Send the ApplicationLaunched event. 286 | if self 287 | .events_tx 288 | .send(( 289 | Span::current(), 290 | Event::ApplicationLaunched { 291 | pid: self.pid, 292 | handle, 293 | info, 294 | is_frontmost: self.is_frontmost, 295 | main_window: self.main_window, 296 | visible_windows: windows, 297 | window_server_info, 298 | }, 299 | )) 300 | .is_err() 301 | { 302 | debug!(pid = ?self.pid, "Failed to send ApplicationLaunched event, exiting thread"); 303 | return false; 304 | }; 305 | 306 | true 307 | } 308 | 309 | /// Handles a request. Returns whether the actor should terminate. 310 | #[instrument(skip_all, fields(app = ?self.app, ?request))] 311 | fn handle_request(&mut self, request: &mut Request) -> Result { 312 | match request { 313 | Request::Terminate => { 314 | CFRunLoop::get_current().stop(); 315 | self.send_event(Event::ApplicationThreadTerminated(self.pid)); 316 | return Ok(true); 317 | } 318 | Request::GetVisibleWindows => { 319 | let window_elems = match self.app.windows() { 320 | Ok(elems) => elems, 321 | Err(e) => { 322 | // Send an empty event so that any previously known 323 | // windows for this app are cleared. 324 | self.send_event(Event::WindowsDiscovered { 325 | pid: self.pid, 326 | new: Default::default(), 327 | known_visible: Default::default(), 328 | }); 329 | return Err(e); 330 | } 331 | }; 332 | let mut new = Vec::with_capacity(window_elems.len() as usize); 333 | let mut known_visible = Vec::with_capacity(window_elems.len() as usize); 334 | for elem in window_elems.iter() { 335 | let elem = elem.clone(); 336 | if let Ok(id) = self.id(&elem) { 337 | known_visible.push(id); 338 | continue; 339 | } 340 | let Some((info, wid)) = self.register_window(elem) else { 341 | continue; 342 | }; 343 | new.push((wid, info)); 344 | } 345 | self.send_event(Event::WindowsDiscovered { 346 | pid: self.pid, 347 | new, 348 | known_visible, 349 | }); 350 | } 351 | &mut Request::SetWindowPos(wid, pos, txid) => { 352 | let window = self.window_mut(wid)?; 353 | window.last_seen_txid = txid; 354 | trace("set_position", &window.elem, || { 355 | window.elem.set_position(pos.to_cgtype()) 356 | })?; 357 | let frame = trace("frame", &window.elem, || window.elem.frame())?; 358 | self.send_event(Event::WindowFrameChanged( 359 | wid, 360 | frame.to_icrate(), 361 | txid, 362 | Requested(true), 363 | // We don't need to check the mouse state since we know this 364 | // change was requested by the reactor. 365 | None, 366 | )); 367 | } 368 | &mut Request::SetWindowFrame(wid, frame, txid) => { 369 | let window = self.window_mut(wid)?; 370 | window.last_seen_txid = txid; 371 | trace("set_position", &window.elem, || { 372 | window.elem.set_position(frame.origin.to_cgtype()) 373 | })?; 374 | trace("set_size", &window.elem, || { 375 | window.elem.set_size(frame.size.to_cgtype()) 376 | })?; 377 | let frame = trace("frame", &window.elem, || window.elem.frame())?; 378 | self.send_event(Event::WindowFrameChanged( 379 | wid, 380 | frame.to_icrate(), 381 | txid, 382 | Requested(true), 383 | None, 384 | )); 385 | } 386 | &mut Request::BeginWindowAnimation(wid) => { 387 | let window = self.window(wid)?; 388 | self.stop_notifications_for_animation(&window.elem); 389 | } 390 | &mut Request::EndWindowAnimation(wid) => { 391 | let &WindowState { ref elem, last_seen_txid } = self.window(wid)?; 392 | self.restart_notifications_after_animation(elem); 393 | let frame = trace("frame", elem, || elem.frame())?; 394 | self.send_event(Event::WindowFrameChanged( 395 | wid, 396 | frame.to_icrate(), 397 | last_seen_txid, 398 | Requested(true), 399 | None, 400 | )); 401 | } 402 | &mut Request::Raise(wid, ref token, ref mut done, quiet) => { 403 | self.handle_raise_request(wid, token, done, quiet)?; 404 | } 405 | } 406 | Ok(false) 407 | } 408 | 409 | #[instrument(skip_all, fields(app = ?self.app, ?notif))] 410 | fn handle_notification(&mut self, elem: AXUIElement, notif: &str) { 411 | trace!(?notif, ?elem, "Got notification"); 412 | #[allow(non_upper_case_globals)] 413 | #[forbid(non_snake_case)] 414 | // TODO: Handle all of these. 415 | match notif { 416 | kAXApplicationActivatedNotification | kAXApplicationDeactivatedNotification => { 417 | _ = self.on_activation_changed(); 418 | } 419 | kAXMainWindowChangedNotification => { 420 | self.on_main_window_changed(None); 421 | } 422 | kAXWindowCreatedNotification => { 423 | if self.id(&elem).is_ok() { 424 | // We already registered this window because of an earlier event. 425 | return; 426 | } 427 | let Some((window, wid)) = self.register_window(elem) else { 428 | return; 429 | }; 430 | let window_server_info = window_server::get_window(WindowServerId(wid.idx.into())); 431 | self.send_event(Event::WindowCreated( 432 | wid, 433 | window, 434 | window_server_info, 435 | event::get_mouse_state(), 436 | )); 437 | } 438 | kAXUIElementDestroyedNotification => { 439 | let Some((&wid, _)) = self.windows.iter().find(|(_, w)| w.elem == elem) else { 440 | return; 441 | }; 442 | self.windows.remove(&wid); 443 | self.send_event(Event::WindowDestroyed(wid)); 444 | } 445 | kAXWindowMovedNotification | kAXWindowResizedNotification => { 446 | // The difference between these two events isn't very useful to 447 | // expose. Anytime there's a resize we'll want to check the 448 | // position to see which corner the window was resized from. So 449 | // we always read and send the full frame since it's a single 450 | // request anyway. 451 | let Ok(wid) = self.id(&elem) else { 452 | return; 453 | }; 454 | let last_seen = self.window(wid).unwrap().last_seen_txid; 455 | let Ok(frame) = elem.frame() else { 456 | return; 457 | }; 458 | self.send_event(Event::WindowFrameChanged( 459 | wid, 460 | frame.to_icrate(), 461 | last_seen, 462 | Requested(false), 463 | Some(event::get_mouse_state()), 464 | )); 465 | } 466 | kAXWindowMiniaturizedNotification => {} 467 | kAXWindowDeminiaturizedNotification => {} 468 | kAXTitleChangedNotification => {} 469 | _ => { 470 | error!("Unhandled notification {notif:?} on {elem:#?}"); 471 | } 472 | } 473 | } 474 | 475 | fn handle_raise_request( 476 | &mut self, 477 | wid: WindowId, 478 | token: &RaiseToken, 479 | done: &mut Option>, 480 | quiet: Quiet, 481 | ) -> Result<(), accessibility::Error> { 482 | // This request could be handled out of order with respect to 483 | // later requests sent to other apps by the reactor. To avoid 484 | // raising ourselves after a later request was processed to 485 | // raise a different app, we check the last-raised pid while 486 | // holding a lock that ensures no other apps are executing a 487 | // raise request at the same time. 488 | // 489 | // FIXME: Unfonrtunately this is still very racy in that we now 490 | // use the unsynchronized NSRunningApplication API to raise the 491 | // application, which still relies on the application itself to 492 | // see and respond to a request, and there is no apparent 493 | // ordering between this and the accessibility messaging. The 494 | // only way to know whether a raise request was processed is 495 | // to wait for an event telling us the app has been activated. 496 | // We can hold the token until then, but will need to time out 497 | // just in case the activation silently fails for some reason. 498 | token 499 | .with(self.pid, || { 500 | // Check whether the app thinks it is frontmost. If it 501 | // does we won't get an activated event. 502 | // 503 | // We read the value directly instead of using the 504 | // cached value because it's possible the cache is 505 | // outdated and the app is no longer frontmost. If that 506 | // happens, it's important that we activate the app or 507 | // the window may never be raised. 508 | // 509 | // Note that it is possible for the app to be outdated 510 | // since the window server is the source of truth. For 511 | // now we don't handle this case. We could handle it 512 | // by looking at the window list and seeing whether 513 | // the requested window is indeed frontmost. 514 | let is_frontmost: bool = 515 | trace("is_frontmost", &self.app, || self.app.frontmost())?.into(); 516 | let window = self.window(wid)?; 517 | let is_standard = 518 | window.elem.subrole().map(|s| s == kAXStandardWindowSubrole).unwrap_or(false); 519 | // Make this the key window regardless of is_frontmost. This 520 | // ensures that the window has focus and can receive keyboard events. 521 | let result = window_server::make_key_window( 522 | self.pid, 523 | WindowServerId::try_from(&self.window(wid)?.elem)?, 524 | ); 525 | if result.is_err() { 526 | warn!(?self.pid, "Failed to activate app"); 527 | } else if !is_frontmost { 528 | // We should be getting an activation event from make_key_window. 529 | // Record the activation so we can match against its 530 | // notification and correctly mark it as quiet. 531 | // 532 | // FIXME: We might not get the activation event, and 533 | // this will deadlock the reactor. 534 | // 535 | // As a temporary workaround, don't expect activation events 536 | // for non-standard windows or we will hit the deadlock 537 | // mentioned above. 538 | if is_standard { 539 | self.last_activated = Some((Instant::now(), quiet, done.take())); 540 | } 541 | } 542 | Ok(()) 543 | }) 544 | .unwrap_or(Ok(()))?; 545 | let window = self.window(wid)?; 546 | trace("raise", &window.elem, || window.elem.raise())?; 547 | let quiet_if = (quiet == Quiet::Yes).then_some(wid); 548 | let main_window = self.on_main_window_changed(quiet_if); 549 | if main_window != Some(wid) { 550 | warn!( 551 | "Raise request failed to raise {desired:?}; instead got {main_window:?}", 552 | desired = self.window(wid)?.elem 553 | ); 554 | } 555 | Ok(()) 556 | } 557 | 558 | fn on_main_window_changed(&mut self, quiet_if: Option) -> Option { 559 | // Always read back the main window instead of getting it from an event, 560 | // in case the event is stale. This is necessary because we sometimes 561 | // manufacture events and don't want them to be incorrectly interleaved. 562 | let elem = match trace("main_window", &self.app, || self.app.main_window()) { 563 | Ok(elem) => elem, 564 | Err(e) => { 565 | error!("Failed to read main window: {e:?}"); 566 | return None; 567 | } 568 | }; 569 | // Often we get this event for new windows before the WindowCreated 570 | // notification. If that happens, register it and send the corresponding 571 | // event here. 572 | // FIXME: This can happen ahead of a space change and result in us adding 573 | // a window to the wrong space. 574 | let wid = match self.id(&elem).ok() { 575 | Some(wid) => wid, 576 | None => { 577 | let Some((info, wid)) = self.register_window(elem) else { 578 | warn!(?self.pid, "Got MainWindowChanged on unknown window"); 579 | return None; 580 | }; 581 | let window_server_info = window_server::get_window(WindowServerId(wid.idx.into())); 582 | self.send_event(Event::WindowCreated( 583 | wid, 584 | info, 585 | window_server_info, 586 | event::get_mouse_state(), 587 | )); 588 | wid 589 | } 590 | }; 591 | // Suppress redundant events. This is so we don't repeat an event that 592 | // was manufactured as a quiet event before. 593 | if self.main_window == Some(wid) { 594 | return Some(wid); 595 | } 596 | self.main_window = Some(wid); 597 | let quiet = match quiet_if { 598 | Some(id) if id == wid => Quiet::Yes, 599 | _ => Quiet::No, 600 | }; 601 | self.send_event(Event::ApplicationMainWindowChanged(self.pid, Some(wid), quiet)); 602 | Some(wid) 603 | } 604 | 605 | fn on_activation_changed(&mut self) -> Result<(), accessibility::Error> { 606 | // Regardless of the notification we received, read the current activation 607 | // and base our event on that. This has the effect of "collapsing" old 608 | // stale events, ensuring that they don't interfere with the matching 609 | // we do below. 610 | let is_frontmost: bool = trace("is_frontmost", &self.app, || self.app.frontmost())?.into(); 611 | if is_frontmost == self.is_frontmost { 612 | return Ok(()); 613 | } 614 | self.is_frontmost = is_frontmost; 615 | 616 | if is_frontmost { 617 | // Suppress events from our own activation by attempting to match up 618 | // the event with `self.last_activated`. 619 | // 620 | // Since it is possible for an activation to not happen for some reason, 621 | // we are stuck with using a timeout so we don't suppress real events 622 | // in the future. 623 | let quiet = match self.last_activated.take() { 624 | // Idea: Maybe we can control the timeout in client and use the send 625 | // result to decide whether `quiet` applies. 626 | Some((ts, quiet, done)) if ts.elapsed() < Duration::from_millis(1000) => { 627 | // Initiated by us. 628 | _ = done.map(|s| s.send(())); 629 | quiet 630 | } 631 | _ => { 632 | // Initiated by the user or system. 633 | // Unfortunately, if the user clicks on a new main window to 634 | // activate this app, we get this notification before getting 635 | // the main window changed notification. First read the main 636 | // window and send a notification if it changed. 637 | self.on_main_window_changed(None); 638 | Quiet::No 639 | } 640 | }; 641 | self.send_event(Event::ApplicationActivated(self.pid, quiet)); 642 | } else { 643 | self.send_event(Event::ApplicationDeactivated(self.pid)); 644 | } 645 | Ok(()) 646 | } 647 | 648 | #[must_use] 649 | fn register_window(&mut self, elem: AXUIElement) -> Option<(WindowInfo, WindowId)> { 650 | let Ok(mut info) = WindowInfo::try_from(&elem) else { 651 | return None; 652 | }; 653 | 654 | // HACK: Ignore hotkey iTerm2 windows. 655 | // Obviously this should be done with some configurable feature. 656 | if self.bundle_id.as_deref() == Some("com.googlecode.iterm2") 657 | && elem 658 | .attribute(&AXAttribute::new(&CFString::from_static_string( 659 | "AXTitleUIElement", 660 | ))) 661 | .is_err() 662 | { 663 | info.is_standard = false; 664 | } 665 | 666 | if !register_notifs(&elem, self) { 667 | return None; 668 | } 669 | let idx = WindowServerId::try_from(&elem) 670 | .or_else(|e| { 671 | info!("Could not get window server id for {elem:?}: {e}"); 672 | Err(e) 673 | }) 674 | .ok() 675 | .map(|id| NonZeroU32::new(id.as_u32()).expect("Window server id was 0")) 676 | .unwrap_or_else(|| { 677 | self.last_window_idx += 1; 678 | NonZeroU32::new(self.last_window_idx).unwrap() 679 | }); 680 | let wid = WindowId { pid: self.pid, idx }; 681 | let old = self.windows.insert( 682 | wid, 683 | WindowState { 684 | elem, 685 | last_seen_txid: TransactionId::default(), 686 | }, 687 | ); 688 | assert!(old.is_none(), "Duplicate window id {wid:?}"); 689 | return Some((info, wid)); 690 | 691 | fn register_notifs(win: &AXUIElement, state: &State) -> bool { 692 | // Filter out elements that aren't regular windows. 693 | match win.role() { 694 | Ok(role) if role == kAXWindowRole => (), 695 | _ => return false, 696 | } 697 | for notif in WINDOW_NOTIFICATIONS { 698 | let res = state.observer.add_notification(win, notif); 699 | if let Err(err) = res { 700 | trace!("Watching failed with error {err:?} on window {win:#?}"); 701 | return false; 702 | } 703 | } 704 | true 705 | } 706 | } 707 | 708 | fn send_event(&self, event: Event) { 709 | self.events_tx.send((Span::current(), event)).unwrap(); 710 | } 711 | 712 | fn window(&self, wid: WindowId) -> Result<&WindowState, accessibility::Error> { 713 | assert_eq!(wid.pid, self.pid); 714 | self.windows.get(&wid).ok_or(accessibility::Error::NotFound) 715 | } 716 | 717 | fn window_mut(&mut self, wid: WindowId) -> Result<&mut WindowState, accessibility::Error> { 718 | assert_eq!(wid.pid, self.pid); 719 | self.windows.get_mut(&wid).ok_or(accessibility::Error::NotFound) 720 | } 721 | 722 | fn id(&self, elem: &AXUIElement) -> Result { 723 | if let Ok(id) = WindowServerId::try_from(elem) { 724 | let wid = WindowId { 725 | pid: self.pid, 726 | idx: NonZeroU32::new(id.as_u32()).expect("Window server id was 0"), 727 | }; 728 | if self.windows.contains_key(&wid) { 729 | return Ok(wid); 730 | } 731 | } else if let Some((&wid, _)) = self.windows.iter().find(|(_, w)| &w.elem == elem) { 732 | return Ok(wid); 733 | } 734 | Err(accessibility::Error::NotFound) 735 | } 736 | 737 | fn stop_notifications_for_animation(&self, elem: &AXUIElement) { 738 | for notif in WINDOW_ANIMATION_NOTIFICATIONS { 739 | let res = self.observer.remove_notification(elem, notif); 740 | if let Err(err) = res { 741 | // There isn't much we can do here except log and keep going. 742 | debug!(?notif, ?elem, "Removing notification failed with error {err}"); 743 | } 744 | } 745 | } 746 | 747 | fn restart_notifications_after_animation(&self, elem: &AXUIElement) { 748 | for notif in WINDOW_ANIMATION_NOTIFICATIONS { 749 | let res = self.observer.add_notification(elem, notif); 750 | if let Err(err) = res { 751 | // There isn't much we can do here except log and keep going. 752 | debug!(?notif, ?elem, "Adding notification failed with error {err}"); 753 | } 754 | } 755 | } 756 | } 757 | 758 | fn app_thread_main(pid: pid_t, info: AppInfo, events_tx: reactor::Sender) { 759 | let app = AXUIElement::application(pid); 760 | let Some(running_app) = NSRunningApplication::with_process_id(pid) else { 761 | debug!(?pid, "Making NSRunningApplication failed; exiting app thread"); 762 | return; 763 | }; 764 | 765 | // Set up the observer callback. 766 | let Ok(observer) = Observer::new(pid) else { 767 | debug!(?pid, "Making observer failed; exiting app thread"); 768 | return; 769 | }; 770 | let (notifications_tx, notifications_rx) = channel(); 771 | let observer = 772 | observer.install(move |elem, notif| _ = notifications_tx.send((elem, notif.to_owned()))); 773 | 774 | // Create our app state. 775 | let state = State { 776 | pid, 777 | running_app, 778 | bundle_id: info.bundle_id.clone(), 779 | app: app.clone(), 780 | observer, 781 | events_tx, 782 | windows: HashMap::default(), 783 | last_window_idx: 0, 784 | main_window: None, 785 | last_activated: None, 786 | is_frontmost: false, 787 | }; 788 | 789 | let (requests_tx, requests_rx) = channel(); 790 | Executor::run(state.run(info, requests_tx, requests_rx, notifications_rx)); 791 | } 792 | 793 | fn trace( 794 | desc: &str, 795 | elem: &AXUIElement, 796 | f: impl FnOnce() -> Result, 797 | ) -> Result { 798 | let start = Instant::now(); 799 | let out = f(); 800 | let end = Instant::now(); 801 | trace!(time = ?(end - start), ?elem, "{desc:12}"); 802 | if let Err(err) = &out { 803 | let app = elem.parent(); 804 | debug!("{desc} failed with {err} for element {elem:#?} with parent {app:#?}"); 805 | } 806 | out 807 | } 808 | 809 | fn trace_misc(desc: &str, f: impl FnOnce() -> T) -> T { 810 | let start = Instant::now(); 811 | let out = f(); 812 | let end = Instant::now(); 813 | trace!(time = ?(end - start), "{desc:12}"); 814 | out 815 | } 816 | -------------------------------------------------------------------------------- /src/actor/mouse.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, mem::replace, rc::Rc, sync::Arc, time::Instant}; 2 | 3 | use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop}; 4 | use core_graphics::event::{ 5 | CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType, 6 | }; 7 | use icrate::Foundation::{CGPoint, MainThreadMarker, NSInteger}; 8 | use tracing::{debug, error, trace, warn, Span}; 9 | 10 | use super::reactor::{self, Event}; 11 | use crate::{ 12 | config::Config, 13 | sys::{ 14 | event, 15 | geometry::ToICrate, 16 | screen::CoordinateConverter, 17 | window_server::{self, get_window, WindowServerId}, 18 | }, 19 | }; 20 | 21 | #[derive(Debug)] 22 | pub enum Request { 23 | Warp(CGPoint), 24 | /// The system resets the hidden state of the mouse each time the focused 25 | /// application changes. WmController sends us this request when that 26 | /// happens, so we can re-hide the mouse if it is supposed to be hidden. 27 | EnforceHidden, 28 | ScreenParametersChanged(CoordinateConverter), 29 | } 30 | 31 | pub struct Mouse { 32 | config: Arc, 33 | events_tx: reactor::Sender, 34 | requests_rx: Option, 35 | state: RefCell, 36 | } 37 | 38 | #[derive(Default)] 39 | struct State { 40 | hidden: bool, 41 | above_window: Option, 42 | above_window_level: NSWindowLevel, 43 | converter: CoordinateConverter, 44 | } 45 | 46 | pub type Sender = tokio::sync::mpsc::UnboundedSender<(Span, Request)>; 47 | pub type Receiver = tokio::sync::mpsc::UnboundedReceiver<(Span, Request)>; 48 | 49 | pub fn channel() -> (Sender, Receiver) { 50 | tokio::sync::mpsc::unbounded_channel() 51 | } 52 | 53 | impl Mouse { 54 | pub fn new(config: Arc, events_tx: reactor::Sender, requests_rx: Receiver) -> Self { 55 | Mouse { 56 | config, 57 | events_tx, 58 | requests_rx: Some(requests_rx), 59 | state: RefCell::new(State::default()), 60 | } 61 | } 62 | 63 | pub async fn run(mut self) { 64 | let mut requests_rx = self.requests_rx.take().unwrap(); 65 | 66 | let this = Rc::new(self); 67 | let this_ = Rc::clone(&this); 68 | let current = CFRunLoop::get_current(); 69 | let mtm = MainThreadMarker::new().unwrap(); 70 | let tap = CGEventTap::new( 71 | CGEventTapLocation::Session, 72 | CGEventTapPlacement::HeadInsertEventTap, 73 | CGEventTapOptions::ListenOnly, 74 | vec![ 75 | // Any event we want the mouse to be shown for. 76 | // Note that this does not include scroll events. 77 | CGEventType::LeftMouseDown, 78 | CGEventType::LeftMouseUp, 79 | CGEventType::RightMouseDown, 80 | CGEventType::RightMouseUp, 81 | CGEventType::MouseMoved, 82 | CGEventType::LeftMouseDragged, 83 | CGEventType::RightMouseDragged, 84 | ], 85 | move |_, event_type, event| { 86 | this_.on_event(event_type, event, mtm); 87 | None 88 | }, 89 | ) 90 | .expect("Could not create event tap"); 91 | 92 | let loop_source = tap.mach_port().create_runloop_source(0).unwrap(); 93 | current.add_source(&loop_source, unsafe { kCFRunLoopCommonModes }); 94 | 95 | // Callbacks will be dispatched by the run loop, which we assume is 96 | // running by the time this function is awaited. 97 | tap.enable(); 98 | 99 | if this.config.settings.mouse_hides_on_focus { 100 | if let Err(e) = window_server::allow_hide_mouse() { 101 | error!( 102 | "Could not enable mouse hiding: {e:?}. \ 103 | mouse_hides_on_focus will have no effect." 104 | ); 105 | } 106 | } 107 | 108 | while let Some((_span, request)) = requests_rx.recv().await { 109 | this.on_request(request); 110 | } 111 | } 112 | 113 | fn on_request(self: &Rc, request: Request) { 114 | let mut state = self.state.borrow_mut(); 115 | match request { 116 | Request::Warp(point) => { 117 | if let Err(e) = event::warp_mouse(point) { 118 | warn!("Failed to warp mouse: {e:?}"); 119 | } 120 | if self.config.settings.mouse_hides_on_focus && !state.hidden { 121 | debug!("Hiding mouse"); 122 | if let Err(e) = event::hide_mouse() { 123 | warn!("Failed to hide mouse: {e:?}"); 124 | } 125 | state.hidden = true; 126 | } 127 | } 128 | Request::EnforceHidden => { 129 | if state.hidden { 130 | if let Err(e) = event::hide_mouse() { 131 | warn!("Failed to hide mouse: {e:?}"); 132 | } 133 | } 134 | } 135 | Request::ScreenParametersChanged(converter) => state.converter = converter, 136 | } 137 | } 138 | 139 | fn on_event(self: &Rc, event_type: CGEventType, event: &CGEvent, mtm: MainThreadMarker) { 140 | let mut state = self.state.borrow_mut(); 141 | if state.hidden { 142 | debug!("Showing mouse"); 143 | if let Err(e) = event::show_mouse() { 144 | warn!("Failed to show mouse: {e:?}"); 145 | } 146 | state.hidden = false; 147 | } 148 | match event_type { 149 | CGEventType::LeftMouseUp => { 150 | _ = self.events_tx.send((Span::current().clone(), Event::MouseUp)); 151 | } 152 | CGEventType::MouseMoved if self.config.settings.focus_follows_mouse => { 153 | let loc = event.location(); 154 | trace!("Mouse moved {loc:?}"); 155 | if let Some(wsid) = state.track_mouse_move(loc.to_icrate(), mtm) { 156 | _ = self.events_tx.send((Span::current(), Event::MouseMovedOverWindow(wsid))); 157 | } 158 | } 159 | _ => (), 160 | } 161 | } 162 | } 163 | 164 | impl State { 165 | fn track_mouse_move(&mut self, loc: CGPoint, mtm: MainThreadMarker) -> Option { 166 | // This takes on the order of 200µs, which can be a while for something 167 | // that may run many times a second on the main thread. For now this 168 | // isn't a problem, but when we start doing anything with UI we might 169 | // want to compute this internally. 170 | let new_window = trace_misc("get_window_at_point", || { 171 | window_server::get_window_at_point(loc, self.converter, mtm) 172 | }); 173 | if self.above_window == new_window { 174 | return None; 175 | } 176 | 177 | debug!("Mouse is now above window {new_window:?}"); 178 | let old_window = replace(&mut self.above_window, new_window); 179 | let new_window_level = new_window 180 | .and_then(|id| trace_misc("get_window", || get_window(id))) 181 | .map(|info| info.layer as NSWindowLevel) 182 | .unwrap_or(NSWindowLevel::MIN); 183 | let old_window_level = replace(&mut self.above_window_level, new_window_level); 184 | 185 | debug!(?old_window, ?old_window_level, ?new_window, ?new_window_level); 186 | 187 | // Don't dismiss popups when the mouse moves off them. 188 | // 189 | // The only reason this is NSMainMenuWindowLevel and not 190 | // NSPopUpMenuWindowLevel is that there seems to be a gap between the 191 | // menu bar and the actual menu pop-ups when a menu is opened. When the 192 | // mouse goes over this gap, the system reports it to be over whatever 193 | // window happens to be below the menu bar and behind the pop-up, and so 194 | // we would dismiss the pop-up. First observed on 13.5.2. 195 | if old_window_level >= NSMainMenuWindowLevel { 196 | return None; 197 | } 198 | 199 | // Don't focus windows outside the "normal" range. 200 | if !(0..NSPopUpMenuWindowLevel).contains(&new_window_level) { 201 | return None; 202 | } 203 | 204 | new_window 205 | } 206 | } 207 | 208 | fn trace_misc(desc: &str, f: impl FnOnce() -> T) -> T { 209 | let start = Instant::now(); 210 | let out = f(); 211 | let end = Instant::now(); 212 | trace!(time = ?(end - start), "{desc}"); 213 | out 214 | } 215 | 216 | /// https://developer.apple.com/documentation/appkit/nswindowlevel?language=objc 217 | pub type NSWindowLevel = NSInteger; 218 | #[allow(non_upper_case_globals)] 219 | pub const NSMainMenuWindowLevel: NSWindowLevel = 24; 220 | #[allow(non_upper_case_globals)] 221 | pub const NSPopUpMenuWindowLevel: NSWindowLevel = 101; 222 | -------------------------------------------------------------------------------- /src/actor/notification_center.rs: -------------------------------------------------------------------------------- 1 | //! This actor manages the global notification queue, which tells us when an 2 | //! application is launched or focused or the screen state changes. 3 | 4 | use std::{cell::RefCell, future, mem}; 5 | 6 | use icrate::{ 7 | objc2::{ 8 | declare_class, msg_send_id, mutability, 9 | rc::{Allocated, Id}, 10 | sel, ClassType, DeclaredClass, Encode, Encoding, 11 | }, 12 | AppKit::{self, NSApplication, NSRunningApplication, NSWorkspace, NSWorkspaceApplicationKey}, 13 | Foundation::{MainThreadMarker, NSNotification, NSNotificationCenter, NSObject}, 14 | }; 15 | use tracing::{info_span, trace, warn, Span}; 16 | 17 | use super::wm_controller::{self, WmEvent}; 18 | use crate::{ 19 | actor::app::AppInfo, 20 | sys::{app::NSRunningApplicationExt, screen::ScreenCache}, 21 | }; 22 | 23 | #[repr(C)] 24 | struct Instance { 25 | screen_cache: RefCell, 26 | events_tx: wm_controller::Sender, 27 | } 28 | 29 | unsafe impl Encode for Instance { 30 | const ENCODING: Encoding = Encoding::Object; 31 | } 32 | 33 | declare_class! { 34 | struct NotificationCenterInner; 35 | 36 | // SAFETY: 37 | // - The superclass NSObject does not have any subclassing requirements. 38 | // - Interior mutability is a safe default. 39 | // - `NotificationHandler` does not implement `Drop`. 40 | unsafe impl ClassType for NotificationCenterInner { 41 | type Super = NSObject; 42 | type Mutability = mutability::InteriorMutable; 43 | const NAME: &'static str = "NotificationHandler"; 44 | } 45 | 46 | impl DeclaredClass for NotificationCenterInner { 47 | type Ivars = Box; 48 | } 49 | 50 | // SAFETY: Each of these method signatures must match their invocations. 51 | unsafe impl NotificationCenterInner { 52 | #[method_id(initWith:)] 53 | fn init(this: Allocated, instance: Instance) -> Option> { 54 | let this = this.set_ivars(Box::new(instance)); 55 | unsafe { msg_send_id![super(this), init] } 56 | } 57 | 58 | #[method(recvScreenChangedEvent:)] 59 | fn recv_screen_changed_event(&self, notif: &NSNotification) { 60 | trace!("{notif:#?}"); 61 | self.handle_screen_changed_event(notif); 62 | } 63 | 64 | #[method(recvAppEvent:)] 65 | fn recv_app_event(&self, notif: &NSNotification) { 66 | trace!("{notif:#?}"); 67 | self.handle_app_event(notif); 68 | } 69 | } 70 | } 71 | 72 | impl NotificationCenterInner { 73 | fn new(events_tx: wm_controller::Sender) -> Id { 74 | let instance = Instance { 75 | screen_cache: RefCell::new(ScreenCache::new(MainThreadMarker::new().unwrap())), 76 | events_tx, 77 | }; 78 | unsafe { msg_send_id![Self::alloc(), initWith: instance] } 79 | } 80 | 81 | fn handle_screen_changed_event(&self, notif: &NSNotification) { 82 | use AppKit::*; 83 | let name = unsafe { &*notif.name() }; 84 | let span = info_span!("notification_center::handle_screen_changed_event", ?name); 85 | let _s = span.enter(); 86 | if unsafe { NSWorkspaceActiveSpaceDidChangeNotification } == name { 87 | self.send_current_space(); 88 | } else if unsafe { NSApplicationDidChangeScreenParametersNotification } == name { 89 | self.send_screen_parameters(); 90 | } else { 91 | panic!("Unexpected screen changed event: {notif:?}"); 92 | } 93 | } 94 | 95 | fn send_screen_parameters(&self) { 96 | let mut screen_cache = self.ivars().screen_cache.borrow_mut(); 97 | let (frames, ids, converter) = screen_cache.update_screen_config(); 98 | let spaces = screen_cache.get_screen_spaces(); 99 | self.send_event(WmEvent::ScreenParametersChanged(frames, ids, converter, spaces)); 100 | } 101 | 102 | fn send_current_space(&self) { 103 | let spaces = self.ivars().screen_cache.borrow().get_screen_spaces(); 104 | self.send_event(WmEvent::SpaceChanged(spaces)); 105 | } 106 | 107 | fn handle_app_event(&self, notif: &NSNotification) { 108 | use AppKit::*; 109 | let Some(app) = self.running_application(notif) else { 110 | return; 111 | }; 112 | let pid = app.pid(); 113 | let name = unsafe { &*notif.name() }; 114 | let span = info_span!("notification_center::handle_app_event", ?name); 115 | let _guard = span.enter(); 116 | if unsafe { NSWorkspaceDidLaunchApplicationNotification } == name { 117 | self.send_event(WmEvent::AppLaunch(pid, AppInfo::from(&*app))); 118 | } else if unsafe { NSWorkspaceDidActivateApplicationNotification } == name { 119 | self.send_event(WmEvent::AppGloballyActivated(pid)); 120 | } else if unsafe { NSWorkspaceDidDeactivateApplicationNotification } == name { 121 | self.send_event(WmEvent::AppGloballyDeactivated(pid)); 122 | } else if unsafe { NSWorkspaceDidTerminateApplicationNotification } == name { 123 | self.send_event(WmEvent::AppTerminated(pid)); 124 | } else if unsafe { NSWorkspaceActiveSpaceDidChangeNotification } == name { 125 | self.send_current_space(); 126 | } else { 127 | panic!("Unexpected application event: {notif:?}"); 128 | } 129 | } 130 | 131 | fn send_event(&self, event: WmEvent) { 132 | // Errors only happen during shutdown, so we can ignore them. 133 | _ = self.ivars().events_tx.send((Span::current().clone(), event)); 134 | } 135 | 136 | fn running_application(&self, notif: &NSNotification) -> Option> { 137 | let info = unsafe { notif.userInfo() }; 138 | let Some(info) = info else { 139 | warn!("Got app notification without user info: {notif:?}"); 140 | return None; 141 | }; 142 | let app = unsafe { info.valueForKey(NSWorkspaceApplicationKey) }; 143 | let Some(app) = app else { 144 | warn!("Got app notification without app object: {notif:?}"); 145 | return None; 146 | }; 147 | assert!(app.class() == NSRunningApplication::class()); 148 | let app: Id = unsafe { mem::transmute(app) }; 149 | Some(app) 150 | } 151 | } 152 | 153 | pub struct NotificationCenter { 154 | #[allow(dead_code)] 155 | inner: Id, 156 | } 157 | 158 | impl NotificationCenter { 159 | pub fn new(events_tx: wm_controller::Sender) -> Self { 160 | let handler = NotificationCenterInner::new(events_tx); 161 | 162 | // SAFETY: Selector must have signature fn(&self, &NSNotification) 163 | let register_unsafe = |selector, notif_name, center: &Id, object| unsafe { 164 | center.addObserver_selector_name_object( 165 | &handler, 166 | selector, 167 | Some(notif_name), 168 | Some(object), 169 | ); 170 | }; 171 | 172 | let workspace = &unsafe { NSWorkspace::sharedWorkspace() }; 173 | let workspace_center = &unsafe { workspace.notificationCenter() }; 174 | let default_center = &unsafe { NSNotificationCenter::defaultCenter() }; 175 | let shared_app = &NSApplication::sharedApplication(MainThreadMarker::new().unwrap()); 176 | unsafe { 177 | use AppKit::*; 178 | register_unsafe( 179 | sel!(recvScreenChangedEvent:), 180 | NSApplicationDidChangeScreenParametersNotification, 181 | default_center, 182 | shared_app, 183 | ); 184 | register_unsafe( 185 | sel!(recvScreenChangedEvent:), 186 | NSWorkspaceActiveSpaceDidChangeNotification, 187 | workspace_center, 188 | workspace, 189 | ); 190 | register_unsafe( 191 | sel!(recvAppEvent:), 192 | NSWorkspaceDidLaunchApplicationNotification, 193 | workspace_center, 194 | workspace, 195 | ); 196 | register_unsafe( 197 | sel!(recvAppEvent:), 198 | NSWorkspaceDidActivateApplicationNotification, 199 | workspace_center, 200 | workspace, 201 | ); 202 | register_unsafe( 203 | sel!(recvAppEvent:), 204 | NSWorkspaceDidDeactivateApplicationNotification, 205 | workspace_center, 206 | workspace, 207 | ); 208 | register_unsafe( 209 | sel!(recvAppEvent:), 210 | NSWorkspaceDidTerminateApplicationNotification, 211 | workspace_center, 212 | workspace, 213 | ); 214 | }; 215 | 216 | NotificationCenter { inner: handler } 217 | } 218 | 219 | pub async fn watch_for_notifications(self) { 220 | let workspace = &unsafe { NSWorkspace::sharedWorkspace() }; 221 | 222 | self.inner.send_screen_parameters(); 223 | self.inner.send_event(WmEvent::AppEventsRegistered); 224 | if let Some(app) = unsafe { workspace.frontmostApplication() } { 225 | self.inner.send_event(WmEvent::AppGloballyActivated(app.pid())); 226 | } 227 | 228 | // All the work is done in callbacks dispatched by the run loop, which 229 | // we assume is running once this function is awaited. 230 | future::pending().await 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/actor/reactor/animation.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | thread, 3 | time::{Duration, Instant}, 4 | }; 5 | 6 | use icrate::Foundation::{CGPoint, CGRect, CGSize}; 7 | 8 | use super::TransactionId; 9 | use crate::actor::app::{AppThreadHandle, Request, WindowId}; 10 | 11 | #[derive(Debug)] 12 | pub struct Animation<'a> { 13 | //start: CFAbsoluteTime, 14 | //interval: CFTimeInterval, 15 | start: Instant, 16 | interval: Duration, 17 | frames: u32, 18 | 19 | windows: Vec<( 20 | &'a AppThreadHandle, 21 | WindowId, 22 | CGRect, 23 | CGRect, 24 | bool, 25 | TransactionId, 26 | )>, 27 | } 28 | 29 | impl<'a> Animation<'a> { 30 | pub fn new() -> Self { 31 | const FPS: f64 = 100.0; 32 | const DURATION: f64 = 0.30; 33 | let interval = Duration::from_secs_f64(1.0 / FPS); 34 | // let now = unsafe { CFAbsoluteTimeGetCurrent() }; 35 | let now = Instant::now(); 36 | Animation { 37 | start: now, // + interval, // not necessary, provide one extra frame to get things going 38 | interval, 39 | frames: (DURATION * FPS).round() as u32, 40 | windows: vec![], 41 | } 42 | } 43 | 44 | pub fn add_window( 45 | &mut self, 46 | handle: &'a AppThreadHandle, 47 | wid: WindowId, 48 | start: CGRect, 49 | finish: CGRect, 50 | is_focus: bool, 51 | txid: TransactionId, 52 | ) { 53 | self.windows.push((handle, wid, start, finish, is_focus, txid)) 54 | } 55 | 56 | pub fn run(self) { 57 | if self.windows.is_empty() { 58 | return; 59 | } 60 | 61 | for &(handle, wid, from, to, is_focus, txid) in &self.windows { 62 | handle.send(Request::BeginWindowAnimation(wid)).unwrap(); 63 | // Resize new windows immediately. 64 | if is_focus { 65 | let frame = CGRect { 66 | origin: from.origin, 67 | size: to.size, 68 | }; 69 | handle.send(Request::SetWindowFrame(wid, frame, txid)).unwrap(); 70 | } 71 | } 72 | 73 | let mut next_frames = Vec::with_capacity(self.windows.len()); 74 | for frame in 1..=self.frames { 75 | let t: f64 = f64::from(frame) / f64::from(self.frames); 76 | 77 | next_frames.clear(); 78 | for (_, _, from, to, _, _) in &self.windows { 79 | next_frames.push(get_frame(*from, *to, t)); 80 | } 81 | 82 | let deadline = self.start + frame * self.interval; 83 | let duration = deadline - Instant::now(); 84 | if duration < Duration::ZERO { 85 | continue; 86 | } 87 | thread::sleep(duration); 88 | 89 | for (&(handle, wid, _, to, _, txid), rect) in self.windows.iter().zip(&next_frames) { 90 | let mut rect = *rect; 91 | // Actually don't animate size, too slow. Resize halfway through 92 | // and then set the size again at the end, in case it got 93 | // clipped during the animation. 94 | if frame * 2 == self.frames || frame == self.frames { 95 | rect.size = to.size; 96 | handle.send(Request::SetWindowFrame(wid, rect, txid)).unwrap(); 97 | } else { 98 | handle.send(Request::SetWindowPos(wid, rect.origin, txid)).unwrap(); 99 | } 100 | } 101 | } 102 | 103 | for &(handle, wid, ..) in &self.windows { 104 | handle.send(Request::EndWindowAnimation(wid)).unwrap(); 105 | } 106 | } 107 | 108 | #[allow(dead_code)] 109 | pub fn skip_to_end(self) { 110 | for &(handle, wid, _from, to, _, txid) in &self.windows { 111 | handle.send(Request::SetWindowFrame(wid, to, txid)).unwrap(); 112 | } 113 | } 114 | } 115 | 116 | fn get_frame(a: CGRect, b: CGRect, t: f64) -> CGRect { 117 | let s = ease(t); 118 | CGRect { 119 | origin: CGPoint { 120 | x: blend(a.origin.x, b.origin.x, s), 121 | y: blend(a.origin.y, b.origin.y, s), 122 | }, 123 | size: CGSize { 124 | width: blend(a.size.width, b.size.width, s), 125 | height: blend(a.size.height, b.size.height, s), 126 | }, 127 | } 128 | } 129 | 130 | fn ease(t: f64) -> f64 { 131 | if t < 0.5 { 132 | (1.0 - f64::sqrt(1.0 - f64::powi(2.0 * t, 2))) / 2.0 133 | } else { 134 | (f64::sqrt(1.0 - f64::powi(-2.0 * t + 2.0, 2)) + 1.0) / 2.0 135 | } 136 | } 137 | 138 | fn blend(a: f64, b: f64, s: f64) -> f64 { 139 | (1.0 - s) * a + s * b 140 | } 141 | -------------------------------------------------------------------------------- /src/actor/reactor/main_window.rs: -------------------------------------------------------------------------------- 1 | use super::Event; 2 | use crate::{ 3 | actor::app::{pid_t, Quiet, WindowId}, 4 | collections::HashMap, 5 | }; 6 | 7 | /// Keeps track of the main window. 8 | #[derive(Default)] 9 | pub(crate) struct MainWindowTracker { 10 | apps: HashMap, 11 | global_frontmost: Option, 12 | } 13 | 14 | struct AppState { 15 | is_frontmost: bool, 16 | frontmost_is_quiet: Quiet, 17 | main_window: Option, 18 | } 19 | 20 | impl MainWindowTracker { 21 | /// Returns Some(wid) if a WindowFocused layout event should be produced. 22 | #[must_use] 23 | pub fn handle_event(&mut self, event: &Event) -> Option { 24 | // There are two kinds of edges that can transition from one main window 25 | // state to another. One is an app main window change and the other 26 | // is a frontmost app change. Either can be labelled with quiet; 27 | // in the case of the frontmost app, the quiet label from the most 28 | // recent frontmost update of that app applies (even if the actual 29 | // event was a global frontmost change). If the main window changes on 30 | // a non-quiet edge we will produce a layout event. 31 | let (event_pid, quiet_edge) = match event { 32 | &Event::ApplicationLaunched { 33 | pid, is_frontmost, main_window, .. 34 | } => { 35 | self.apps.insert( 36 | pid, 37 | AppState { 38 | is_frontmost, 39 | frontmost_is_quiet: Quiet::No, 40 | main_window, 41 | }, 42 | ); 43 | (pid, Quiet::No) 44 | } 45 | &Event::ApplicationThreadTerminated(pid) => { 46 | self.apps.remove(&pid); 47 | return None; 48 | } 49 | &Event::ApplicationActivated(pid, quiet) => { 50 | let app = self.apps.get_mut(&pid)?; 51 | app.is_frontmost = true; 52 | app.frontmost_is_quiet = quiet; 53 | (pid, quiet) 54 | } 55 | &Event::ApplicationDeactivated(pid) => { 56 | let app = self.apps.get_mut(&pid)?; 57 | app.is_frontmost = false; 58 | return None; 59 | } 60 | &Event::ApplicationGloballyActivated(pid) => { 61 | // See the comment in main_window() for the difference between 62 | // this and the ApplicationActivated event. 63 | self.global_frontmost = Some(pid); 64 | let Some(app) = self.apps.get(&pid) else { return None }; 65 | (pid, app.frontmost_is_quiet) 66 | } 67 | &Event::ApplicationGloballyDeactivated(pid) => { 68 | if self.global_frontmost == Some(pid) { 69 | self.global_frontmost = None; 70 | } 71 | return None; 72 | } 73 | &Event::ApplicationMainWindowChanged(pid, wid, quiet) => { 74 | let app = self.apps.get_mut(&pid)?; 75 | app.main_window = wid; 76 | (pid, quiet) 77 | } 78 | Event::ApplicationTerminated(..) 79 | | Event::WindowsDiscovered { .. } 80 | | Event::WindowCreated(..) 81 | | Event::WindowDestroyed(..) 82 | | Event::WindowFrameChanged(..) 83 | | Event::ScreenParametersChanged(..) 84 | | Event::SpaceChanged(..) 85 | | Event::MouseUp 86 | | Event::MouseMovedOverWindow(..) 87 | | Event::Command(..) => return None, 88 | }; 89 | if Some(event_pid) == self.global_frontmost && quiet_edge == Quiet::No { 90 | if let Some(wid) = self.main_window() { 91 | return Some(wid); 92 | } 93 | } 94 | None 95 | } 96 | 97 | /// The main window of the active app, if any. 98 | pub fn main_window(&self) -> Option { 99 | // Because apps self-report this event from their respective 100 | // threads, they can appear out of order. To mitigate this, we 101 | // require that the "global" view from NSNotificationCenter 102 | // agrees with the app about which is frontmost. This guarantees 103 | // eventual consistency. 104 | // 105 | // Since the global events provide an authoritative ordering, why 106 | // care about this event at all? The reason is that we want to 107 | // know what the main window of the app is upon activation. This 108 | // is important when the user clicks on a window of the app 109 | // that was not previously the main window: The frontmost app 110 | // and its main window can switch at the same time. In that case 111 | // we don't want to record the old main window as having focus, 112 | // since it never did. So we wait until both events are received. 113 | let Some(pid) = self.global_frontmost else { 114 | return None; 115 | }; 116 | match self.apps.get(&pid) { 117 | Some(&AppState { 118 | is_frontmost: true, 119 | main_window: Some(window), 120 | .. 121 | }) => Some(window), 122 | _ => None, 123 | } 124 | } 125 | } 126 | 127 | #[cfg(test)] 128 | mod tests { 129 | use icrate::Foundation::CGRect; 130 | use test_log::test; 131 | 132 | use super::super::{ 133 | testing::{make_windows, Apps}, 134 | Event, LayoutManager, Quiet, Reactor, SpaceId, WindowId, 135 | }; 136 | 137 | #[test] 138 | fn it_tracks_frontmost_app_and_main_window_correctly() { 139 | use Event::*; 140 | let mut apps = Apps::new(); 141 | let mut reactor = Reactor::new_for_test(LayoutManager::new()); 142 | let space = SpaceId::new(1); 143 | reactor.handle_event(ScreenParametersChanged( 144 | vec![CGRect::ZERO], 145 | vec![Some(space)], 146 | vec![], 147 | )); 148 | assert_eq!(None, reactor.main_window()); 149 | 150 | reactor.handle_event(ApplicationGloballyActivated(1)); 151 | reactor.handle_events(apps.make_app_with_opts( 152 | 1, 153 | make_windows(2), 154 | Some(WindowId::new(1, 1)), 155 | true, 156 | true, 157 | )); 158 | reactor.handle_events(apps.make_app_with_opts(2, make_windows(2), None, false, true)); 159 | assert_eq!(Some(WindowId::new(1, 1)), reactor.main_window()); 160 | assert_eq!(reactor.layout.selected_window(space), Some(WindowId::new(1, 1))); 161 | 162 | reactor.handle_event(ApplicationGloballyDeactivated(1)); 163 | assert_eq!(None, reactor.main_window()); 164 | reactor.handle_event(ApplicationActivated(2, Quiet::No)); 165 | reactor.handle_event(ApplicationGloballyActivated(2)); 166 | assert_eq!(None, reactor.main_window()); 167 | reactor.handle_event(ApplicationMainWindowChanged( 168 | 2, 169 | Some(WindowId::new(2, 2)), 170 | Quiet::No, 171 | )); 172 | assert_eq!(Some(WindowId::new(2, 2)), reactor.main_window()); 173 | assert_eq!(reactor.layout.selected_window(space), Some(WindowId::new(2, 2))); 174 | reactor.handle_event(ApplicationMainWindowChanged( 175 | 1, 176 | Some(WindowId::new(1, 2)), 177 | Quiet::No, 178 | )); 179 | assert_eq!(Some(WindowId::new(2, 2)), reactor.main_window()); 180 | reactor.handle_event(ApplicationDeactivated(1)); 181 | assert_eq!(Some(WindowId::new(2, 2)), reactor.main_window()); 182 | reactor.handle_event(ApplicationDeactivated(2)); 183 | assert_eq!(None, reactor.main_window()); 184 | 185 | reactor.handle_event(ApplicationGloballyActivated(3)); 186 | assert_eq!(None, reactor.main_window()); 187 | 188 | reactor.handle_events(apps.make_app_with_opts( 189 | 3, 190 | make_windows(2), 191 | Some(WindowId::new(3, 1)), 192 | true, 193 | true, 194 | )); 195 | assert_eq!(Some(WindowId::new(3, 1)), reactor.main_window()); 196 | assert_eq!(reactor.layout.selected_window(space), Some(WindowId::new(3, 1))); 197 | } 198 | 199 | #[test] 200 | fn it_does_not_update_layout_for_quiet_raises() { 201 | use Event::*; 202 | let mut apps = Apps::new(); 203 | let mut reactor = Reactor::new_for_test(LayoutManager::new()); 204 | let space = SpaceId::new(1); 205 | reactor.handle_event(ScreenParametersChanged( 206 | vec![CGRect::ZERO], 207 | vec![Some(space)], 208 | vec![], 209 | )); 210 | 211 | reactor.handle_event(ApplicationGloballyActivated(1)); 212 | reactor.handle_events(apps.make_app_with_opts( 213 | 1, 214 | make_windows(2), 215 | Some(WindowId::new(1, 1)), 216 | true, 217 | true, 218 | )); 219 | reactor.handle_events(apps.make_app_with_opts(2, make_windows(2), None, false, true)); 220 | assert_eq!(Some(WindowId::new(1, 1)), reactor.main_window()); 221 | assert_eq!(reactor.layout.selected_window(space), Some(WindowId::new(1, 1))); 222 | 223 | reactor.handle_event(ApplicationGloballyDeactivated(1)); 224 | assert_eq!(None, reactor.main_window()); 225 | reactor.handle_event(ApplicationGloballyActivated(2)); 226 | reactor.handle_event(ApplicationActivated(2, Quiet::Yes)); 227 | assert_eq!(None, reactor.main_window()); 228 | reactor.handle_event(ApplicationMainWindowChanged( 229 | 2, 230 | Some(WindowId::new(2, 2)), 231 | Quiet::Yes, 232 | )); 233 | assert_eq!(Some(WindowId::new(2, 2)), reactor.main_window()); 234 | assert_eq!(reactor.layout.selected_window(space), Some(WindowId::new(1, 1))); 235 | 236 | reactor.handle_event(ApplicationActivated(2, Quiet::No)); 237 | assert_eq!(reactor.layout.selected_window(space), Some(WindowId::new(2, 2))); 238 | 239 | reactor.handle_event(ApplicationMainWindowChanged( 240 | 2, 241 | Some(WindowId::new(2, 1)), 242 | Quiet::Yes, 243 | )); 244 | assert_eq!(Some(WindowId::new(2, 1)), reactor.main_window()); 245 | assert_eq!(reactor.layout.selected_window(space), Some(WindowId::new(2, 2))); 246 | 247 | reactor.handle_event(ApplicationActivated(1, Quiet::Yes)); 248 | reactor.handle_event(ApplicationGloballyActivated(1)); 249 | assert_eq!(Some(WindowId::new(1, 1)), reactor.main_window()); 250 | assert_eq!(reactor.layout.selected_window(space), Some(WindowId::new(2, 2))); 251 | 252 | reactor.handle_event(ApplicationMainWindowChanged( 253 | 1, 254 | Some(WindowId::new(1, 2)), 255 | Quiet::No, 256 | )); 257 | assert_eq!(Some(WindowId::new(1, 2)), reactor.main_window()); 258 | assert_eq!(reactor.layout.selected_window(space), Some(WindowId::new(1, 2))); 259 | } 260 | 261 | #[test] 262 | fn it_selects_main_window_when_space_is_enabled() { 263 | use Event::*; 264 | let mut apps = Apps::new(); 265 | let mut reactor = Reactor::new_for_test(LayoutManager::new()); 266 | let pid = 3; 267 | let windows = make_windows(2); 268 | let space = SpaceId::new(1); 269 | reactor.handle_event(ScreenParametersChanged( 270 | vec![CGRect::ZERO], 271 | vec![Some(space)], 272 | vec![], 273 | )); 274 | 275 | reactor.handle_events(apps.make_app_with_opts( 276 | pid, 277 | windows, 278 | Some(WindowId::new(3, 1)), 279 | false, 280 | true, 281 | )); 282 | 283 | reactor.handle_event(SpaceChanged(vec![None], vec![])); 284 | reactor.handle_event(ApplicationActivated(3, Quiet::No)); 285 | reactor.handle_event(ApplicationGloballyActivated(3)); 286 | reactor.handle_event(WindowsDiscovered { 287 | pid, 288 | new: vec![], 289 | known_visible: vec![WindowId::new(3, 1), WindowId::new(3, 2)], 290 | }); 291 | assert_eq!(Some(WindowId::new(3, 1)), reactor.main_window()); 292 | 293 | reactor.handle_event(SpaceChanged(vec![Some(space)], vec![])); 294 | assert_eq!(reactor.layout.selected_window(space), Some(WindowId::new(3, 1))); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/actor/reactor/replay.rs: -------------------------------------------------------------------------------- 1 | //! Support for recording reactor events to a file and replaying them later. 2 | //! 3 | //! This is used in development. 4 | 5 | use std::{ 6 | cell::RefCell, 7 | fs::File, 8 | io::{BufRead, BufReader, Write}, 9 | path::Path, 10 | sync::Arc, 11 | }; 12 | 13 | #[cfg(test)] 14 | use tempfile::NamedTempFile; 15 | use tokio::sync::mpsc::unbounded_channel; 16 | use tracing::Span; 17 | 18 | use super::{Event, Reactor}; 19 | use crate::{ 20 | actor::{ 21 | app::{AppThreadHandle, Request}, 22 | layout::LayoutManager, 23 | }, 24 | config::Config, 25 | }; 26 | 27 | thread_local! { 28 | static DESERIALIZE_THREAD_HANDLE: RefCell> = RefCell::new(None); 29 | } 30 | 31 | pub(super) fn deserialize_app_thread_handle() -> AppThreadHandle { 32 | DESERIALIZE_THREAD_HANDLE 33 | .with(|handle| handle.borrow().clone().expect("No deserialize thread handle set!")) 34 | } 35 | 36 | /// File to record incoming events. 37 | pub struct Record { 38 | file: Option, 39 | #[cfg(test)] 40 | temp: Option, 41 | } 42 | 43 | // The format is simple: 44 | // One line for the layout, followed by one line per event. 45 | 46 | impl Record { 47 | pub fn new(path: Option<&Path>) -> Self { 48 | Self { 49 | file: path.map(|path| File::create(path).unwrap()), 50 | #[cfg(test)] 51 | temp: None, 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | pub fn new_for_test(temp: NamedTempFile) -> Self { 57 | Self { file: None, temp: Some(temp) } 58 | } 59 | 60 | #[cfg(test)] 61 | pub(super) fn temp(&mut self) -> Option<&mut NamedTempFile> { 62 | self.temp.as_mut() 63 | } 64 | 65 | fn file(&mut self) -> Option<&mut File> { 66 | #[cfg(test)] 67 | return self.file.as_mut().or(self.temp.as_mut().map(|temp| temp.as_file_mut())); 68 | #[cfg(not(test))] 69 | self.file.as_mut() 70 | } 71 | 72 | pub(super) fn start(&mut self, config: &Config, layout: &LayoutManager) { 73 | let Some(file) = self.file() else { return }; 74 | let config = ron::ser::to_string(&config).unwrap(); 75 | let layout = ron::ser::to_string(&layout).unwrap(); 76 | write!(file, "{config}\n{layout}\n").unwrap(); 77 | } 78 | 79 | pub(super) fn on_event(&mut self, event: &Event) { 80 | let Some(file) = self.file() else { return }; 81 | let line = ron::ser::to_string(&event).unwrap(); 82 | write!(file, "{line}\n").unwrap(); 83 | } 84 | } 85 | 86 | pub fn replay( 87 | path: &Path, 88 | mut on_event: impl FnMut(Span, Request) + Send + 'static, 89 | ) -> anyhow::Result<()> { 90 | let file = BufReader::new(File::open(path)?); 91 | let (tx, mut rx) = unbounded_channel(); 92 | let handle = AppThreadHandle::new_for_test(tx); 93 | DESERIALIZE_THREAD_HANDLE.with(|h| h.borrow_mut().replace(handle)); 94 | let mut lines = file.lines(); 95 | let config = ron::de::from_str(&lines.next().expect("Empty restore file")?)?; 96 | let layout = ron::de::from_str(&lines.next().expect("Expected layout line")?)?; 97 | let mut reactor = Reactor::new(Arc::new(config), layout, Record::new(None)); 98 | std::thread::spawn(move || { 99 | // Unfortunately we have to spawn a thread because the reactor blocks 100 | // on raise requests currently. 101 | while let Some((span, request)) = rx.blocking_recv() { 102 | on_event(span, request); 103 | } 104 | }); 105 | for line in lines { 106 | reactor.handle_event(ron::de::from_str(&line?)?); 107 | } 108 | Ok(()) 109 | } 110 | -------------------------------------------------------------------------------- /src/actor/reactor/testing.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, io::Write, sync::Arc}; 2 | 3 | use accessibility_sys::pid_t; 4 | use icrate::Foundation::{CGPoint, CGRect, CGSize}; 5 | use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; 6 | use tracing::{debug, Span}; 7 | 8 | use super::{Event, Reactor, Record, Requested, TransactionId}; 9 | use crate::{ 10 | actor::{ 11 | app::{AppThreadHandle, Request, WindowId}, 12 | layout::LayoutManager, 13 | }, 14 | config::Config, 15 | sys::{ 16 | app::{AppInfo, WindowInfo}, 17 | geometry::SameAs, 18 | window_server::{WindowServerId, WindowServerInfo}, 19 | }, 20 | }; 21 | 22 | impl Reactor { 23 | pub fn new_for_test(layout: LayoutManager) -> Reactor { 24 | let mut config = Config::default(); 25 | config.settings.default_disable = false; 26 | config.settings.animate = false; 27 | let record = Record::new_for_test(tempfile::NamedTempFile::new().unwrap()); 28 | Reactor::new(Arc::new(config), layout, record) 29 | } 30 | 31 | pub fn handle_events(&mut self, events: Vec) { 32 | for event in events { 33 | self.handle_event(event); 34 | } 35 | } 36 | } 37 | 38 | impl Drop for Reactor { 39 | fn drop(&mut self) { 40 | if std::thread::panicking() { 41 | return; 42 | } 43 | // Replay the recorded data to make sure we can do so without crashing. 44 | if let Some(temp) = self.record.temp() { 45 | temp.as_file().flush().unwrap(); 46 | let mut cmd = test_bin::get_test_bin("examples/devtool"); 47 | cmd.arg("replay").arg(temp.path()); 48 | println!("Replaying recorded data:\n{cmd:?}"); 49 | assert!(cmd.spawn().unwrap().wait().unwrap().success(), "replay failed"); 50 | } 51 | } 52 | } 53 | 54 | pub fn make_window(idx: usize) -> WindowInfo { 55 | WindowInfo { 56 | is_standard: true, 57 | title: format!("Window{idx}"), 58 | frame: CGRect::new( 59 | CGPoint::new(100.0 * f64::from(idx as u32), 100.0), 60 | CGSize::new(50.0, 50.0), 61 | ), 62 | // TODO: This is wrong and conflicts with windows from other apps. 63 | sys_id: Some(WindowServerId::new(idx as u32)), 64 | } 65 | } 66 | 67 | pub fn make_windows(count: usize) -> Vec { 68 | (1..=count).map(make_window).collect() 69 | } 70 | 71 | pub struct Apps { 72 | tx: UnboundedSender<(Span, Request)>, 73 | rx: UnboundedReceiver<(Span, Request)>, 74 | pub windows: BTreeMap, 75 | } 76 | 77 | #[derive(Default, PartialEq, Debug, Clone)] 78 | pub struct WindowState { 79 | pub last_seen_txid: TransactionId, 80 | pub animating: bool, 81 | pub frame: CGRect, 82 | } 83 | 84 | impl Apps { 85 | pub fn new() -> Apps { 86 | let (tx, rx) = unbounded_channel(); 87 | Apps { 88 | tx, 89 | rx, 90 | windows: BTreeMap::new(), 91 | } 92 | } 93 | 94 | pub fn make_app(&mut self, pid: pid_t, windows: Vec) -> Vec { 95 | let frontmost = windows.first().map(|_| WindowId::new(pid, 1)); 96 | self.make_app_with_opts(pid, windows, frontmost, false, true) 97 | } 98 | 99 | pub fn make_app_with_opts( 100 | &mut self, 101 | pid: pid_t, 102 | windows: Vec, 103 | main_window: Option, 104 | is_frontmost: bool, 105 | with_ws_info: bool, 106 | ) -> Vec { 107 | for (id, info) in (1..).map(|idx| WindowId::new(pid, idx)).zip(&windows) { 108 | self.windows.insert( 109 | id, 110 | WindowState { 111 | frame: info.frame, 112 | ..Default::default() 113 | }, 114 | ); 115 | } 116 | let handle = AppThreadHandle::new_for_test(self.tx.clone()); 117 | vec![Event::ApplicationLaunched { 118 | pid, 119 | info: AppInfo { 120 | bundle_id: Some(format!("com.testapp{pid}")), 121 | localized_name: Some(format!("TestApp{pid}")), 122 | }, 123 | handle, 124 | is_frontmost, 125 | main_window, 126 | window_server_info: if with_ws_info { 127 | windows 128 | .iter() 129 | .map(|info| WindowServerInfo { 130 | pid, 131 | id: info.sys_id.unwrap(), 132 | layer: 0, 133 | frame: info.frame, 134 | }) 135 | .collect() 136 | } else { 137 | Default::default() 138 | }, 139 | visible_windows: (1..).map(|idx| WindowId::new(pid, idx)).zip(windows).collect(), 140 | }] 141 | } 142 | 143 | pub fn requests(&mut self) -> Vec { 144 | let mut requests = Vec::new(); 145 | while let Ok((_, req)) = self.rx.try_recv() { 146 | requests.push(req); 147 | } 148 | requests 149 | } 150 | 151 | pub fn simulate_until_quiet(&mut self, reactor: &mut Reactor) { 152 | let mut requests = self.requests(); 153 | while !requests.is_empty() { 154 | for event in self.simulate_events_for_requests(requests) { 155 | reactor.handle_event(event); 156 | } 157 | requests = self.requests(); 158 | } 159 | } 160 | 161 | pub fn simulate_events(&mut self) -> Vec { 162 | let requests = self.requests(); 163 | self.simulate_events_for_requests(requests) 164 | } 165 | 166 | pub fn simulate_events_for_requests(&mut self, requests: Vec) -> Vec { 167 | let mut events = vec![]; 168 | let mut got_visible_windows = false; 169 | for request in requests { 170 | debug!(?request); 171 | match request { 172 | Request::Terminate => break, 173 | Request::GetVisibleWindows => { 174 | // Only do this once per cycle, since we simulate responding 175 | // from all apps. 176 | if got_visible_windows { 177 | continue; 178 | } 179 | got_visible_windows = true; 180 | let mut app_windows = BTreeMap::>::new(); 181 | for &wid in self.windows.keys() { 182 | app_windows.entry(wid.pid).or_default().push(wid); 183 | } 184 | for (pid, windows) in app_windows { 185 | events.push(Event::WindowsDiscovered { 186 | pid, 187 | new: vec![], 188 | known_visible: windows, 189 | }); 190 | } 191 | } 192 | Request::SetWindowFrame(wid, frame, txid) => { 193 | let window = self.windows.entry(wid).or_default(); 194 | window.last_seen_txid = txid; 195 | let old_frame = window.frame; 196 | window.frame = frame; 197 | if !window.animating && !old_frame.same_as(frame) { 198 | events.push(Event::WindowFrameChanged( 199 | wid, 200 | frame, 201 | txid, 202 | Requested(true), 203 | None, 204 | )); 205 | } 206 | } 207 | Request::SetWindowPos(wid, pos, txid) => { 208 | let window = self.windows.entry(wid).or_default(); 209 | window.last_seen_txid = txid; 210 | let old_frame = window.frame; 211 | window.frame.origin = pos; 212 | if !window.animating && !old_frame.same_as(window.frame) { 213 | events.push(Event::WindowFrameChanged( 214 | wid, 215 | window.frame, 216 | txid, 217 | Requested(true), 218 | None, 219 | )); 220 | } 221 | } 222 | Request::BeginWindowAnimation(wid) => { 223 | self.windows.entry(wid).or_default().animating = true; 224 | } 225 | Request::EndWindowAnimation(wid) => { 226 | let window = self.windows.entry(wid).or_default(); 227 | window.animating = false; 228 | events.push(Event::WindowFrameChanged( 229 | wid, 230 | window.frame, 231 | window.last_seen_txid, 232 | Requested(true), 233 | None, 234 | )); 235 | } 236 | Request::Raise(..) => todo!(), 237 | } 238 | } 239 | debug!(?events); 240 | events 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/actor/wm_controller.rs: -------------------------------------------------------------------------------- 1 | //! The WM Controller handles major events like enabling and disabling the 2 | //! window manager on certain spaces and launching app threads. It also 3 | //! controls hotkey registration. 4 | 5 | use std::{path::PathBuf, sync::Arc}; 6 | 7 | use accessibility_sys::pid_t; 8 | use icrate::{ 9 | AppKit::NSScreen, 10 | Foundation::{CGRect, MainThreadMarker}, 11 | }; 12 | use serde::{Deserialize, Serialize}; 13 | use tracing::{debug, instrument, Span}; 14 | 15 | pub type Sender = tokio::sync::mpsc::UnboundedSender<(Span, WmEvent)>; 16 | type WeakSender = tokio::sync::mpsc::WeakUnboundedSender<(Span, WmEvent)>; 17 | type Receiver = tokio::sync::mpsc::UnboundedReceiver<(Span, WmEvent)>; 18 | 19 | use super::mouse; 20 | use crate::{ 21 | actor::{self, app::AppInfo, reactor}, 22 | collections::HashSet, 23 | sys::{ 24 | self, 25 | event::HotkeyManager, 26 | screen::{CoordinateConverter, NSScreenExt, ScreenId, SpaceId}, 27 | window_server::WindowServerInfo, 28 | }, 29 | }; 30 | 31 | #[derive(Debug)] 32 | pub enum WmEvent { 33 | AppEventsRegistered, 34 | AppLaunch(pid_t, AppInfo), 35 | AppGloballyActivated(pid_t), 36 | AppGloballyDeactivated(pid_t), 37 | AppTerminated(pid_t), 38 | SpaceChanged(Vec>), 39 | ScreenParametersChanged( 40 | Vec, 41 | Vec, 42 | CoordinateConverter, 43 | Vec>, 44 | ), 45 | Command(WmCommand), 46 | } 47 | 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | #[serde(untagged)] 50 | pub enum WmCommand { 51 | Wm(WmCmd), 52 | ReactorCommand(reactor::Command), 53 | } 54 | 55 | #[derive(Debug, Clone, Serialize, Deserialize)] 56 | #[serde(rename_all = "snake_case")] 57 | pub enum WmCmd { 58 | ToggleSpaceActivated, 59 | } 60 | 61 | pub struct Config { 62 | /// Only enables the WM on the starting space. On all other spaces, hotkeys are disabled. 63 | /// 64 | /// This can be useful for development. 65 | pub one_space: bool, 66 | pub restore_file: PathBuf, 67 | pub config: Arc, 68 | } 69 | 70 | pub struct WmController { 71 | config: Config, 72 | events_tx: reactor::Sender, 73 | mouse_tx: mouse::Sender, 74 | receiver: Receiver, 75 | sender: WeakSender, 76 | starting_space: Option, 77 | cur_space: Vec>, 78 | cur_screen_id: Vec, 79 | disabled_spaces: HashSet, 80 | enabled_spaces: HashSet, 81 | login_window_pid: Option, 82 | hotkeys: Option, 83 | mtm: MainThreadMarker, 84 | } 85 | 86 | impl WmController { 87 | pub fn new( 88 | config: Config, 89 | events_tx: reactor::Sender, 90 | mouse_tx: mouse::Sender, 91 | ) -> (Self, Sender) { 92 | let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); 93 | let this = Self { 94 | config, 95 | events_tx, 96 | mouse_tx, 97 | receiver, 98 | sender: sender.downgrade(), 99 | starting_space: None, 100 | cur_space: Vec::new(), 101 | cur_screen_id: Vec::new(), 102 | disabled_spaces: HashSet::default(), 103 | enabled_spaces: HashSet::default(), 104 | login_window_pid: None, 105 | hotkeys: None, 106 | mtm: MainThreadMarker::new().unwrap(), 107 | }; 108 | (this, sender) 109 | } 110 | 111 | pub async fn run(mut self) { 112 | while let Some((span, event)) = self.receiver.recv().await { 113 | let _guard = span.enter(); 114 | self.handle_event(event); 115 | } 116 | } 117 | 118 | #[instrument(skip(self))] 119 | pub fn handle_event(&mut self, event: WmEvent) { 120 | debug!("handle_event"); 121 | use reactor::Event; 122 | 123 | use self::{WmCmd::*, WmCommand::*, WmEvent::*}; 124 | match event { 125 | AppEventsRegistered => { 126 | for (pid, info) in sys::app::running_apps(None) { 127 | self.new_app(pid, info); 128 | } 129 | } 130 | AppLaunch(pid, info) => { 131 | self.new_app(pid, info); 132 | } 133 | AppGloballyActivated(pid) => { 134 | // Make sure the mouse cursor stays hidden after app switch. 135 | _ = self.mouse_tx.send((Span::current(), mouse::Request::EnforceHidden)); 136 | self.send_event(Event::ApplicationGloballyActivated(pid)); 137 | } 138 | AppGloballyDeactivated(pid) => { 139 | self.send_event(Event::ApplicationGloballyDeactivated(pid)); 140 | if self.login_window_pid == Some(pid) { 141 | // While the login screen is active AX APIs do not work. 142 | // When it's dismissed, simulate a space change to update 143 | // the set of visible windows on screen and their positions. 144 | let mut spaces = self.cur_space.clone(); 145 | self.apply_space_activation(&mut spaces); 146 | self.send_event(Event::SpaceChanged(spaces, self.get_windows())); 147 | } 148 | } 149 | AppTerminated(pid) => { 150 | self.send_event(Event::ApplicationTerminated(pid)); 151 | } 152 | ScreenParametersChanged(frames, ids, converter, mut spaces) => { 153 | self.cur_screen_id = ids; 154 | self.handle_space_changed(&spaces); 155 | self.apply_space_activation(&mut spaces); 156 | self.send_event(Event::ScreenParametersChanged( 157 | frames.clone(), 158 | spaces, 159 | self.get_windows(), 160 | )); 161 | _ = self.mouse_tx.send(( 162 | Span::current(), 163 | mouse::Request::ScreenParametersChanged(converter), 164 | )); 165 | } 166 | SpaceChanged(mut spaces) => { 167 | self.handle_space_changed(&spaces); 168 | self.apply_space_activation(&mut spaces); 169 | self.send_event(Event::SpaceChanged(spaces, self.get_windows())); 170 | } 171 | Command(Wm(ToggleSpaceActivated)) => { 172 | let Some(space) = self.get_focused_space() else { return }; 173 | let toggle_set = if self.config.config.settings.default_disable { 174 | &mut self.enabled_spaces 175 | } else { 176 | &mut self.disabled_spaces 177 | }; 178 | if !toggle_set.remove(&space) { 179 | toggle_set.insert(space); 180 | } 181 | let mut spaces = self.cur_space.clone(); 182 | self.apply_space_activation(&mut spaces); 183 | self.send_event(Event::SpaceChanged(spaces, self.get_windows())); 184 | } 185 | Command(ReactorCommand(cmd)) => { 186 | self.send_event(Event::Command(cmd)); 187 | } 188 | } 189 | } 190 | 191 | fn new_app(&mut self, pid: pid_t, info: AppInfo) { 192 | if info.bundle_id.as_deref() == Some("com.apple.loginwindow") { 193 | self.login_window_pid = Some(pid); 194 | } 195 | actor::app::spawn_app_thread(pid, info, self.events_tx.clone()); 196 | } 197 | 198 | fn get_focused_space(&self) -> Option { 199 | // The currently focused screen is what NSScreen calls the "main" screen. 200 | let screen = NSScreen::mainScreen(self.mtm)?; 201 | let number = screen.get_number().ok()?; 202 | *self.cur_screen_id.iter().zip(&self.cur_space).find(|(id, _)| **id == number)?.1 203 | } 204 | 205 | fn handle_space_changed(&mut self, spaces: &[Option]) { 206 | self.cur_space = spaces.iter().copied().collect(); 207 | let Some(&Some(space)) = spaces.first() else { return }; 208 | if self.starting_space.is_none() { 209 | self.starting_space = Some(space); 210 | self.register_hotkeys(); 211 | } else if self.config.one_space { 212 | if Some(space) == self.starting_space { 213 | self.register_hotkeys(); 214 | } else { 215 | self.unregister_hotkeys(); 216 | } 217 | } 218 | } 219 | 220 | fn apply_space_activation(&self, spaces: &mut [Option]) { 221 | for space in spaces { 222 | let enabled = match space { 223 | Some(_) if self.config.one_space && *space != self.starting_space => false, 224 | Some(sp) if self.disabled_spaces.contains(sp) => false, 225 | Some(sp) if self.enabled_spaces.contains(sp) => true, 226 | _ if self.config.config.settings.default_disable => false, 227 | _ => true, 228 | }; 229 | if !enabled { 230 | *space = None; 231 | } 232 | } 233 | } 234 | 235 | fn send_event(&mut self, event: reactor::Event) { 236 | _ = self.events_tx.send((Span::current().clone(), event)); 237 | } 238 | 239 | fn register_hotkeys(&mut self) { 240 | debug!("register_hotkeys"); 241 | let mgr = HotkeyManager::new(self.sender.upgrade().unwrap()); 242 | for (key, cmd) in &self.config.config.keys { 243 | mgr.register_wm(key.modifiers, key.key_code, cmd.clone()); 244 | } 245 | self.hotkeys = Some(mgr); 246 | } 247 | 248 | fn unregister_hotkeys(&mut self) { 249 | debug!("unregister_hotkeys"); 250 | self.hotkeys = None; 251 | } 252 | 253 | fn get_windows(&self) -> Vec { 254 | #[cfg(not(test))] 255 | return sys::window_server::get_visible_windows_with_layer(None); 256 | #[cfg(test)] 257 | vec![] 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/bin/nimbus.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, sync::Arc}; 2 | 3 | use clap::Parser; 4 | use nimbus_wm::{ 5 | actor::{ 6 | layout::LayoutManager, 7 | mouse::{self, Mouse}, 8 | notification_center::NotificationCenter, 9 | reactor::{self, Reactor}, 10 | wm_controller::{self, WmController}, 11 | }, 12 | config::{config_file, restore_file, Config}, 13 | log, 14 | sys::executor::Executor, 15 | }; 16 | use tokio::join; 17 | 18 | #[derive(Parser)] 19 | struct Cli { 20 | /// Only run the window manager on the current space. 21 | #[arg(long)] 22 | one: bool, 23 | 24 | /// Disable new spaces by default. 25 | /// 26 | /// Ignored if --one is used. 27 | #[arg(long)] 28 | default_disable: bool, 29 | 30 | /// Disable animations. 31 | #[arg(long)] 32 | no_animate: bool, 33 | 34 | /// Check whether the restore file can be loaded without actually starting 35 | /// the window manager. 36 | #[arg(long)] 37 | validate: bool, 38 | 39 | /// Restore the configuration saved with the save_and_exit command. This is 40 | /// only useful within the same session. 41 | #[arg(long)] 42 | restore: bool, 43 | 44 | /// Record reactor events to the specified file path. Overwrites the file if 45 | /// exists. 46 | #[arg(long)] 47 | record: Option, 48 | } 49 | 50 | fn main() { 51 | let opt: Cli = Parser::parse(); 52 | 53 | if std::env::var_os("RUST_BACKTRACE").is_none() { 54 | // SAFETY: We are single threaded at this point. 55 | unsafe { std::env::set_var("RUST_BACKTRACE", "1") }; 56 | } 57 | log::init_logging(); 58 | install_panic_hook(); 59 | 60 | let mut config = if config_file().exists() { 61 | Config::read(&config_file()).unwrap() 62 | } else { 63 | Config::default() 64 | }; 65 | config.settings.animate &= !opt.no_animate; 66 | config.settings.default_disable |= opt.default_disable; 67 | let config = Arc::new(config); 68 | 69 | if opt.validate { 70 | LayoutManager::load(restore_file()).unwrap(); 71 | return; 72 | } 73 | 74 | let layout = if opt.restore { 75 | LayoutManager::load(restore_file()).unwrap() 76 | } else { 77 | LayoutManager::new() 78 | }; 79 | let (mouse_tx, mouse_rx) = mouse::channel(); 80 | let events_tx = Reactor::spawn( 81 | config.clone(), 82 | layout, 83 | reactor::Record::new(opt.record.as_deref()), 84 | mouse_tx.clone(), 85 | ); 86 | 87 | let wm_config = wm_controller::Config { 88 | one_space: opt.one, 89 | restore_file: restore_file(), 90 | config: config.clone(), 91 | }; 92 | let (wm_controller, wm_controller_sender) = 93 | WmController::new(wm_config, events_tx.clone(), mouse_tx.clone()); 94 | let notification_center = NotificationCenter::new(wm_controller_sender); 95 | let mouse = Mouse::new(config.clone(), events_tx, mouse_rx); 96 | 97 | Executor::run(async move { 98 | join!( 99 | wm_controller.run(), 100 | notification_center.watch_for_notifications(), 101 | mouse.run(), 102 | ); 103 | }); 104 | } 105 | 106 | #[cfg(panic = "unwind")] 107 | fn install_panic_hook() { 108 | // Abort on panic instead of propagating panics to the main thread. 109 | // See Cargo.toml for why we don't use panic=abort everywhere. 110 | let original_hook = std::panic::take_hook(); 111 | std::panic::set_hook(Box::new(move |info| { 112 | original_hook(info); 113 | std::process::abort(); 114 | })); 115 | } 116 | 117 | #[cfg(not(panic = "unwind"))] 118 | fn install_panic_hook() {} 119 | -------------------------------------------------------------------------------- /src/collections.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | pub(crate) use std::collections::{hash_map, BTreeMap, BTreeSet}; 3 | 4 | // We don't need or want the random state of the default std collections. 5 | // We also don't need cryptographic hashing, and these are faster. 6 | pub(crate) use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; 7 | 8 | use crate::{actor::app::WindowId, sys::app::pid_t}; 9 | 10 | pub trait BTreeExt { 11 | fn remove_all_for_pid(&mut self, pid: pid_t) -> Self; 12 | } 13 | 14 | // There's not currently a stable way to remove only a range, so we have 15 | // to do this split/extend dance. Is it faster than scanning through all 16 | // the keys? Who knows! 17 | 18 | impl BTreeExt for BTreeSet { 19 | fn remove_all_for_pid(&mut self, pid: pid_t) -> Self { 20 | let mut split = self.split_off(&PidRange(pid)); 21 | self.extend(split.split_off(&PidRange(pid + 1))); 22 | split 23 | } 24 | } 25 | 26 | impl BTreeExt for BTreeMap { 27 | fn remove_all_for_pid(&mut self, pid: pid_t) -> Self { 28 | let mut split = self.split_off(&PidRange(pid)); 29 | self.extend(split.split_off(&PidRange(pid + 1))); 30 | split 31 | } 32 | } 33 | 34 | #[derive(Ord, PartialOrd, Eq, PartialEq)] 35 | #[repr(transparent)] 36 | struct PidRange(pid_t); 37 | 38 | // Technically this violates the Borrow requirements by having Ord/Eq 39 | // behave differently than the original type, but we are working around 40 | // API limitations and it should not matter for a reasonable implementation 41 | // of `split_off`. 42 | impl Borrow for WindowId { 43 | fn borrow(&self) -> &PidRange { 44 | // Safety: PidRange is repr(transparent). 45 | unsafe { &*std::ptr::addr_of!(self.pid).cast() } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::Read, 4 | path::{Path, PathBuf}, 5 | str::FromStr, 6 | }; 7 | 8 | use anyhow::bail; 9 | use livesplit_hotkey::Hotkey; 10 | use rustc_hash::FxHashMap; 11 | use serde::{Deserialize, Serialize}; 12 | 13 | use crate::actor::wm_controller::WmCommand; 14 | 15 | pub fn data_dir() -> PathBuf { 16 | dirs::home_dir().unwrap().join(".nimbus") 17 | } 18 | 19 | pub fn restore_file() -> PathBuf { 20 | data_dir().join("layout.ron") 21 | } 22 | 23 | pub fn config_file() -> PathBuf { 24 | dirs::home_dir().unwrap().join(".nimbus.toml") 25 | } 26 | 27 | #[derive(Serialize, Deserialize)] 28 | #[serde(deny_unknown_fields)] 29 | struct ConfigFile { 30 | settings: Settings, 31 | keys: FxHashMap, 32 | } 33 | 34 | #[derive(Serialize, Deserialize)] 35 | pub struct Config { 36 | pub settings: Settings, 37 | pub keys: Vec<(Hotkey, WmCommand)>, 38 | } 39 | 40 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 41 | #[serde(deny_unknown_fields)] 42 | pub struct Settings { 43 | #[serde(default = "yes")] 44 | pub animate: bool, 45 | #[serde(default = "yes")] 46 | pub default_disable: bool, 47 | #[serde(default = "yes")] 48 | pub mouse_follows_focus: bool, 49 | #[serde(default = "yes")] 50 | pub mouse_hides_on_focus: bool, 51 | #[serde(default = "yes")] 52 | pub focus_follows_mouse: bool, 53 | } 54 | 55 | fn yes() -> bool { 56 | true 57 | } 58 | #[allow(dead_code)] 59 | fn no() -> bool { 60 | false 61 | } 62 | 63 | impl Config { 64 | pub fn read(path: &Path) -> anyhow::Result { 65 | let mut buf = String::new(); 66 | File::open(path).unwrap().read_to_string(&mut buf)?; 67 | Self::parse(&buf) 68 | } 69 | 70 | pub fn default() -> Config { 71 | Self::parse(include_str!("../nimbus.default.toml")).unwrap() 72 | } 73 | 74 | fn parse(buf: &str) -> anyhow::Result { 75 | let c: ConfigFile = toml::from_str(&buf)?; 76 | let mut keys = Vec::new(); 77 | for (key, cmd) in c.keys { 78 | let Ok(key) = Hotkey::from_str(&key) else { 79 | bail!("Could not parse hotkey: {key}"); 80 | }; 81 | keys.push((key, cmd)); 82 | } 83 | Ok(Config { settings: c.settings, keys }) 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | #[test] 90 | fn default_config_parses() { 91 | super::Config::default(); 92 | } 93 | 94 | #[test] 95 | fn default_settings_match_unspecified_setting_values() { 96 | assert_eq!(super::Config::default().settings, toml::from_str("").unwrap()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod actor; 2 | pub mod config; 3 | pub mod log; 4 | pub mod model; 5 | pub mod sys; 6 | 7 | mod collections; 8 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use tracing_subscriber::{ 5 | layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry, 6 | }; 7 | use tracing_timing::{group, Histogram}; 8 | use tracing_tree::time::UtcDateTime; 9 | 10 | pub fn init_logging() { 11 | tracing_subscriber::registry() 12 | .with(tree_layer()) 13 | .with(timing_layer()) 14 | .with(EnvFilter::from_default_env()) 15 | .init(); 16 | } 17 | 18 | pub fn tree_layer() -> impl Layer { 19 | tracing_tree::HierarchicalLayer::default() 20 | .with_indent_amount(2) 21 | .with_indent_lines(true) 22 | .with_deferred_spans(true) 23 | .with_span_retrace(true) 24 | .with_targets(true) 25 | .with_timer(UtcDateTime::default()) 26 | } 27 | 28 | type TimingLayer = tracing_timing::TimingLayer; 29 | 30 | fn timing_layer() -> TimingLayer { 31 | tracing_timing::Builder::default() 32 | //.events(group::ByName) 33 | .layer(|| Histogram::new_with_max(100_000_000, 2).unwrap()) 34 | } 35 | 36 | #[derive(Serialize, Deserialize, Debug, Clone)] 37 | #[serde(rename_all = "snake_case")] 38 | pub enum MetricsCommand { 39 | ShowTiming, 40 | } 41 | 42 | pub fn handle_command(command: MetricsCommand) { 43 | match command { 44 | MetricsCommand::ShowTiming => show_timing(), 45 | } 46 | } 47 | 48 | pub fn show_timing() { 49 | tracing::dispatcher::get_default(|d| { 50 | let timing_layer = d.downcast_ref::().unwrap(); 51 | print_histograms(timing_layer); 52 | }) 53 | } 54 | 55 | fn print_histograms(timing_layer: &TimingLayer) { 56 | timing_layer.force_synchronize(); 57 | timing_layer.with_histograms(|hs| { 58 | println!("\nHistograms:\n"); 59 | for (span, hs) in hs { 60 | for (event, h) in hs { 61 | let ns = |nanos| Duration::from_nanos(nanos); 62 | println!("{span} -> {event} ({} events)", h.len()); 63 | println!(" mean: {:?}", ns(h.mean() as u64)); 64 | println!(" min: {:?}", ns(h.min())); 65 | println!(" p50: {:?}", ns(h.value_at_quantile(0.50))); 66 | println!(" p90: {:?}", ns(h.value_at_quantile(0.90))); 67 | println!(" p99: {:?}", ns(h.value_at_quantile(0.99))); 68 | println!(" max: {:?}", ns(h.max())); 69 | } 70 | } 71 | println!(); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the [`Tree`][tree::Tree] data structure, on which all 2 | //! layout logic is defined. 3 | 4 | mod layout; 5 | mod layout_tree; 6 | mod selection; 7 | mod tree; 8 | mod window; 9 | 10 | #[allow(unused_imports)] 11 | pub use layout::{Direction, LayoutKind, Orientation}; 12 | pub use layout_tree::{LayoutId, LayoutTree}; 13 | -------------------------------------------------------------------------------- /src/model/layout.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | use std::mem; 3 | 4 | use icrate::Foundation::{CGPoint, CGRect, CGSize}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use super::{ 8 | layout_tree::TreeEvent, 9 | tree::{NodeId, NodeMap}, 10 | }; 11 | use crate::{actor::app::WindowId, sys::geometry::Round}; 12 | 13 | #[derive(Default, Serialize, Deserialize)] 14 | pub struct Layout { 15 | info: slotmap::SecondaryMap, 16 | } 17 | 18 | #[allow(unused)] 19 | #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 20 | #[serde(rename_all = "snake_case")] 21 | pub enum LayoutKind { 22 | #[default] 23 | Horizontal, 24 | Vertical, 25 | Tabbed, 26 | Stacked, 27 | } 28 | 29 | impl LayoutKind { 30 | pub fn from(orientation: Orientation) -> Self { 31 | match orientation { 32 | Orientation::Horizontal => LayoutKind::Horizontal, 33 | Orientation::Vertical => LayoutKind::Vertical, 34 | } 35 | } 36 | 37 | pub fn group(orientation: Orientation) -> Self { 38 | match orientation { 39 | Orientation::Horizontal => LayoutKind::Tabbed, 40 | Orientation::Vertical => LayoutKind::Stacked, 41 | } 42 | } 43 | } 44 | 45 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 46 | #[serde(rename_all = "snake_case")] 47 | pub enum Orientation { 48 | Horizontal, 49 | Vertical, 50 | } 51 | 52 | impl LayoutKind { 53 | pub fn orientation(self) -> Orientation { 54 | use LayoutKind::*; 55 | match self { 56 | Horizontal | Tabbed => Orientation::Horizontal, 57 | Vertical | Stacked => Orientation::Vertical, 58 | } 59 | } 60 | 61 | pub fn is_group(self) -> bool { 62 | use LayoutKind::*; 63 | match self { 64 | Stacked | Tabbed => true, 65 | _ => false, 66 | } 67 | } 68 | } 69 | 70 | #[allow(dead_code)] 71 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] 72 | #[serde(rename_all = "snake_case")] 73 | pub enum Direction { 74 | Left, 75 | Right, 76 | Up, 77 | Down, 78 | } 79 | 80 | impl Direction { 81 | pub(super) fn orientation(self) -> Orientation { 82 | use Direction::*; 83 | match self { 84 | Left | Right => Orientation::Horizontal, 85 | Up | Down => Orientation::Vertical, 86 | } 87 | } 88 | } 89 | 90 | // TODO: 91 | // 92 | // It'd be much easier to only move specific edges if we keep the min edge 93 | // of each child (relative to the parent, from 0 to 1). Then we just need 94 | // to adjust this edge, and preserve the invariant that no edge is greater 95 | // than the following edge. 96 | // 97 | // Calculating the size of a single node is easy and just needs to look at the 98 | // next sibling. 99 | // 100 | // Proportional changes would no longer happen by default, but should still be 101 | // relatively easy. Just keep a count of children, and we can adjust each child's 102 | // size in a single scan. 103 | // 104 | // This seems *way* simpler than trying to fix up a proportionate representation 105 | // to create a single edge change. 106 | // 107 | // Actually, on second thought, this would still create proportional resizes of 108 | // children. To prevent that we would need the edges to be absolute (relative 109 | // to the root) and traverse *recursively* when one is modified, fixing up any 110 | // edges that violate our invariant. 111 | // 112 | // This might still be overall simpler than the resize logic would need to be 113 | // for the proportionate case, but it feels more like we are distributing the 114 | // complexity rather than reducing it. 115 | 116 | #[derive(Default, Debug, Serialize, Deserialize, Clone)] 117 | struct LayoutInfo { 118 | /// The share of the parent's size taken up by this node; 1.0 by default. 119 | size: f32, 120 | /// The total size of all children. 121 | total: f32, 122 | /// The orientation of this node. Not used for leaf nodes. 123 | kind: LayoutKind, 124 | /// The last ungrouped layout of this node. 125 | last_ungrouped_kind: LayoutKind, 126 | /// Whether the node is fullscreen. 127 | #[serde(default)] 128 | is_fullscreen: bool, 129 | } 130 | 131 | impl Layout { 132 | pub(super) fn handle_event(&mut self, map: &NodeMap, event: TreeEvent) { 133 | match event { 134 | TreeEvent::AddedToForest(node) => { 135 | self.info.insert(node, LayoutInfo::default()); 136 | } 137 | TreeEvent::AddedToParent(node) => { 138 | let parent = node.parent(map).unwrap(); 139 | self.info[node].size = 1.0; 140 | self.info[parent].total += 1.0; 141 | } 142 | TreeEvent::Copied { src, dest, .. } => { 143 | self.info.insert(dest, self.info[src].clone()); 144 | } 145 | TreeEvent::RemovingFromParent(node) => { 146 | self.info[node.parent(map).unwrap()].total -= self.info[node].size; 147 | } 148 | TreeEvent::RemovedFromForest(node) => { 149 | self.info.remove(node); 150 | } 151 | } 152 | } 153 | 154 | pub(super) fn assume_size_of(&mut self, new: NodeId, old: NodeId, map: &NodeMap) { 155 | assert_eq!(new.parent(map), old.parent(map)); 156 | let parent = new.parent(map).unwrap(); 157 | self.info[parent].total -= self.info[new].size; 158 | self.info[new].size = mem::replace(&mut self.info[old].size, 0.0); 159 | } 160 | 161 | pub(super) fn set_kind(&mut self, node: NodeId, kind: LayoutKind) { 162 | self.info[node].kind = kind; 163 | if !kind.is_group() { 164 | self.info[node].last_ungrouped_kind = kind; 165 | } 166 | } 167 | 168 | pub(super) fn kind(&self, node: NodeId) -> LayoutKind { 169 | self.info[node].kind 170 | } 171 | 172 | pub(super) fn last_ungrouped_kind(&self, node: NodeId) -> LayoutKind { 173 | self.info[node].last_ungrouped_kind 174 | } 175 | 176 | pub(super) fn proportion(&self, map: &NodeMap, node: NodeId) -> Option { 177 | let Some(parent) = node.parent(map) else { return None }; 178 | Some(f64::from(self.info[node].size) / f64::from(self.info[parent].total)) 179 | } 180 | 181 | pub(super) fn total(&self, node: NodeId) -> f64 { 182 | f64::from(self.info[node].total) 183 | } 184 | 185 | pub(super) fn take_share(&mut self, map: &NodeMap, node: NodeId, from: NodeId, share: f32) { 186 | assert_eq!(node.parent(map), from.parent(map)); 187 | let share = share.min(self.info[from].size); 188 | let share = share.max(-self.info[node].size); 189 | self.info[from].size -= share; 190 | self.info[node].size += share; 191 | } 192 | 193 | pub(super) fn set_fullscreen(&mut self, node: NodeId, is_fullscreen: bool) { 194 | self.info[node].is_fullscreen = is_fullscreen; 195 | } 196 | 197 | pub(super) fn toggle_fullscreen(&mut self, node: NodeId) -> bool { 198 | self.info[node].is_fullscreen = !self.info[node].is_fullscreen; 199 | self.info[node].is_fullscreen 200 | } 201 | 202 | pub(super) fn debug(&self, node: NodeId, is_container: bool) -> String { 203 | let info = &self.info[node]; 204 | if is_container { 205 | format!("{:?} [size {} total={}]", info.kind, info.size, info.total) 206 | } else { 207 | format!("[size {}]", info.size) 208 | } 209 | } 210 | 211 | pub(super) fn get_sizes( 212 | &self, 213 | map: &NodeMap, 214 | window: &super::window::Window, 215 | root: NodeId, 216 | screen: CGRect, 217 | ) -> Vec<(WindowId, CGRect)> { 218 | let mut sizes = vec![]; 219 | self.apply(map, window, root, screen, screen, &mut sizes); 220 | sizes 221 | } 222 | 223 | fn apply( 224 | &self, 225 | map: &NodeMap, 226 | window: &super::window::Window, 227 | node: NodeId, 228 | rect: CGRect, 229 | screen: CGRect, 230 | sizes: &mut Vec<(WindowId, CGRect)>, 231 | ) { 232 | let info = &self.info[node]; 233 | let rect = if info.is_fullscreen { screen } else { rect }; 234 | 235 | if let Some(wid) = window.at(node) { 236 | debug_assert!( 237 | node.children(map).next().is_none(), 238 | "non-leaf node with window id" 239 | ); 240 | sizes.push((wid, rect)); 241 | return; 242 | } 243 | 244 | use LayoutKind::*; 245 | match info.kind { 246 | Tabbed | Stacked => { 247 | for child in node.children(map) { 248 | self.apply(map, window, child, rect, screen, sizes); 249 | } 250 | } 251 | Horizontal => { 252 | let mut x = rect.origin.x; 253 | let total = self.info[node].total; 254 | for child in node.children(map) { 255 | let ratio = f64::from(self.info[child].size) / f64::from(total); 256 | let rect = CGRect { 257 | origin: CGPoint { x, y: rect.origin.y }, 258 | size: CGSize { 259 | width: rect.size.width * ratio, 260 | height: rect.size.height, 261 | }, 262 | } 263 | .round(); 264 | self.apply(map, window, child, rect, screen, sizes); 265 | x = rect.max().x; 266 | } 267 | } 268 | Vertical => { 269 | let mut y = rect.origin.y; 270 | let total = self.info[node].total; 271 | for child in node.children(map) { 272 | let ratio = f64::from(self.info[child].size) / f64::from(total); 273 | let rect = CGRect { 274 | origin: CGPoint { x: rect.origin.x, y }, 275 | size: CGSize { 276 | width: rect.size.width, 277 | height: rect.size.height * ratio, 278 | }, 279 | } 280 | .round(); 281 | self.apply(map, window, child, rect, screen, sizes); 282 | y = rect.max().y; 283 | } 284 | } 285 | } 286 | } 287 | } 288 | 289 | #[cfg(test)] 290 | mod tests { 291 | use pretty_assertions::assert_eq; 292 | 293 | use super::*; 294 | use crate::model::LayoutTree; 295 | 296 | fn rect(x: i32, y: i32, w: i32, h: i32) -> CGRect { 297 | CGRect::new( 298 | CGPoint::new(f64::from(x), f64::from(y)), 299 | CGSize::new(f64::from(w), f64::from(h)), 300 | ) 301 | } 302 | 303 | #[test] 304 | fn it_lays_out_windows_proportionally() { 305 | let mut tree = LayoutTree::new(); 306 | let layout = tree.create_layout(); 307 | let root = tree.root(layout); 308 | let _a1 = tree.add_window_under(layout, root, WindowId::new(1, 1)); 309 | let a2 = tree.add_container(root, LayoutKind::Vertical); 310 | let _b1 = tree.add_window_under(layout, a2, WindowId::new(1, 2)); 311 | let _b2 = tree.add_window_under(layout, a2, WindowId::new(1, 3)); 312 | let _a3 = tree.add_window_under(layout, root, WindowId::new(1, 4)); 313 | 314 | let screen = rect(0, 0, 3000, 1000); 315 | let mut frames = tree.calculate_layout(layout, screen); 316 | frames.sort_by_key(|&(wid, _)| wid); 317 | assert_eq!( 318 | frames, 319 | vec![ 320 | (WindowId::new(1, 1), rect(0, 0, 1000, 1000)), 321 | (WindowId::new(1, 2), rect(1000, 0, 1000, 500)), 322 | (WindowId::new(1, 3), rect(1000, 500, 1000, 500)), 323 | (WindowId::new(1, 4), rect(2000, 0, 1000, 1000)), 324 | ] 325 | ); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/model/selection.rs: -------------------------------------------------------------------------------- 1 | use std::iter; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::{ 6 | layout_tree::TreeEvent, 7 | tree::{NodeId, NodeMap}, 8 | }; 9 | 10 | #[derive(Default, Serialize, Deserialize)] 11 | pub struct Selection { 12 | nodes: slotmap::SecondaryMap, 13 | } 14 | 15 | #[derive(Serialize, Deserialize)] 16 | struct SelectionInfo { 17 | selected_child: NodeId, 18 | stop_here: bool, 19 | } 20 | 21 | impl Selection { 22 | pub(super) fn current_selection(&self, root: NodeId) -> NodeId { 23 | let mut node = root; 24 | while let Some(info) = self.nodes.get(node) { 25 | if info.stop_here { 26 | break; 27 | } 28 | node = info.selected_child; 29 | } 30 | node 31 | } 32 | 33 | pub(super) fn last_selection(&self, _map: &NodeMap, node: NodeId) -> Option { 34 | self.nodes.get(node).map(|info| info.selected_child) 35 | } 36 | 37 | pub(super) fn local_selection(&self, map: &NodeMap, node: NodeId) -> Option { 38 | let result = self.nodes.get(node); 39 | if let Some(result) = result { 40 | debug_assert_eq!(result.selected_child.parent(map), Some(node)); 41 | } 42 | result.filter(|info| !info.stop_here).map(|info| info.selected_child) 43 | } 44 | 45 | pub(super) fn select_locally(&mut self, map: &NodeMap, node: NodeId) { 46 | if let Some(parent) = node.parent(map) { 47 | self.nodes.insert( 48 | parent, 49 | SelectionInfo { 50 | selected_child: node, 51 | stop_here: false, 52 | }, 53 | ); 54 | } 55 | } 56 | 57 | pub(super) fn select(&mut self, map: &NodeMap, selection: NodeId) { 58 | if let Some(info) = self.nodes.get_mut(selection) { 59 | info.stop_here = true; 60 | } 61 | let mut node = selection; 62 | while let Some(parent) = node.parent(map) { 63 | self.nodes.insert( 64 | parent, 65 | SelectionInfo { 66 | selected_child: node, 67 | stop_here: false, 68 | }, 69 | ); 70 | node = parent; 71 | } 72 | } 73 | 74 | pub(super) fn handle_event(&mut self, map: &NodeMap, event: TreeEvent) { 75 | use TreeEvent::*; 76 | match event { 77 | AddedToForest(_node) => {} 78 | AddedToParent(_node) => {} 79 | Copied { src, dest, .. } => { 80 | let Some(info) = self.nodes.get(src) else { 81 | return; 82 | }; 83 | let selected_child = iter::zip(src.children(map), dest.children(map)) 84 | .filter(|(src_child, _)| *src_child == info.selected_child) 85 | .map(|(_, dest_child)| dest_child) 86 | .next() 87 | .unwrap_or_else(|| { 88 | panic!( 89 | "Dest tree had different structure, or source node \ 90 | had nonexistent selection: {src:?}, {dest:?}" 91 | ) 92 | }); 93 | self.nodes.insert( 94 | dest, 95 | SelectionInfo { 96 | selected_child, 97 | stop_here: self.nodes[src].stop_here, 98 | }, 99 | ); 100 | } 101 | RemovingFromParent(node) => { 102 | let parent = node.parent(map).unwrap(); 103 | if self.nodes.get(parent).map(|n| n.selected_child) == Some(node) { 104 | if let Some(new_selection) = node.next_sibling(map).or(node.prev_sibling(map)) { 105 | self.nodes[parent].selected_child = new_selection; 106 | } else { 107 | self.nodes.remove(parent); 108 | } 109 | } 110 | } 111 | RemovedFromForest(node) => { 112 | self.nodes.remove(node); 113 | } 114 | } 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use crate::{ 121 | actor::app::WindowId, 122 | model::{layout::LayoutKind, layout_tree::LayoutTree, Direction}, 123 | }; 124 | 125 | #[test] 126 | fn it_moves_as_nodes_are_added_and_removed() { 127 | let mut tree = LayoutTree::new(); 128 | let layout = tree.create_layout(); 129 | let root = tree.root(layout); 130 | let n1 = tree.add_window_under(layout, root, WindowId::new(1, 1)); 131 | let n2 = tree.add_window_under(layout, root, WindowId::new(1, 2)); 132 | let n3 = tree.add_window_under(layout, root, WindowId::new(1, 3)); 133 | assert_eq!(tree.selection(layout), root); 134 | tree.select(n2); 135 | assert_eq!(tree.selection(layout), n2); 136 | tree.remove_window(WindowId::new(1, 2)); 137 | assert_eq!(tree.selection(layout), n3); 138 | tree.remove_window(WindowId::new(1, 3)); 139 | assert_eq!(tree.selection(layout), n1); 140 | tree.remove_window(WindowId::new(1, 1)); 141 | assert_eq!(tree.selection(layout), root); 142 | } 143 | 144 | #[test] 145 | fn remembers_nested_paths() { 146 | let mut tree = LayoutTree::new(); 147 | let layout = tree.create_layout(); 148 | let root = tree.root(layout); 149 | let a1 = tree.add_window_under(layout, root, WindowId::new(1, 1)); 150 | let a2 = tree.add_container(root, LayoutKind::Horizontal); 151 | let _b1 = tree.add_window_under(layout, a2, WindowId::new(1, 2)); 152 | let b2 = tree.add_window_under(layout, a2, WindowId::new(1, 3)); 153 | let _b3 = tree.add_window_under(layout, a2, WindowId::new(1, 4)); 154 | let a3 = tree.add_window_under(layout, root, WindowId::new(1, 5)); 155 | 156 | tree.select(b2); 157 | assert_eq!(tree.selection(layout), b2); 158 | tree.select(a1); 159 | assert_eq!(tree.selection(layout), a1); 160 | tree.select(a3); 161 | assert_eq!(tree.selection(layout), a3); 162 | tree.remove_window(WindowId::new(1, 5)); 163 | assert_eq!(tree.selection(layout), b2); 164 | } 165 | 166 | #[test] 167 | fn preserves_selection_after_move_within_parent() { 168 | let mut tree = LayoutTree::new(); 169 | let layout = tree.create_layout(); 170 | let root = tree.root(layout); 171 | let _n1 = tree.add_window_under(layout, root, WindowId::new(1, 1)); 172 | let n2 = tree.add_window_under(layout, root, WindowId::new(1, 2)); 173 | let _n3 = tree.add_window_under(layout, root, WindowId::new(1, 3)); 174 | tree.select(n2); 175 | assert_eq!(tree.selection(layout), n2); 176 | tree.move_node(layout, n2, Direction::Left); 177 | assert_eq!(tree.selection(layout), n2); 178 | } 179 | 180 | #[test] 181 | fn allows_parent_selection() { 182 | let mut tree = LayoutTree::new(); 183 | let layout = tree.create_layout(); 184 | let root = tree.root(layout); 185 | let _a1 = tree.add_window_under(layout, root, WindowId::new(1, 1)); 186 | let a2 = tree.add_container(root, LayoutKind::Horizontal); 187 | let b1 = tree.add_window_under(layout, a2, WindowId::new(1, 2)); 188 | tree.select(b1); 189 | assert_eq!(tree.selection(layout), b1); 190 | tree.select(a2); 191 | assert_eq!(tree.selection(layout), a2); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/model/window.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use accessibility_sys::pid_t; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use super::{ 7 | tree::{NodeId, NodeMap}, 8 | LayoutId, 9 | }; 10 | use crate::{actor::app::WindowId, collections::BTreeExt, model::layout_tree::TreeEvent}; 11 | 12 | /// Maintains a two-way mapping between leaf nodes and window ids. 13 | #[derive(Default, Serialize, Deserialize)] 14 | pub struct Window { 15 | windows: slotmap::SecondaryMap, 16 | window_nodes: BTreeMap>, 17 | } 18 | 19 | #[derive(Serialize, Deserialize)] 20 | struct WindowNodeInfo { 21 | layout: LayoutId, 22 | node: NodeId, 23 | } 24 | 25 | impl Window { 26 | pub fn at(&self, node: NodeId) -> Option { 27 | self.windows.get(node).copied() 28 | } 29 | 30 | pub fn node_for(&self, layout: LayoutId, wid: WindowId) -> Option { 31 | self.window_nodes 32 | .get(&wid) 33 | .into_iter() 34 | .flat_map(|nodes| nodes.iter().filter(|info| info.layout == layout)) 35 | .next() 36 | .map(|info| info.node) 37 | } 38 | 39 | pub fn set_window(&mut self, layout: LayoutId, node: NodeId, wid: WindowId) { 40 | let existing = self.windows.insert(node, wid); 41 | assert!( 42 | existing.is_none(), 43 | "Attempted to overwrite window for node {node:?} from {existing:?} to {wid:?}" 44 | ); 45 | self.window_nodes.entry(wid).or_default().push(WindowNodeInfo { layout, node }); 46 | } 47 | 48 | pub fn set_capacity(&mut self, capacity: usize) { 49 | self.windows.set_capacity(capacity); 50 | // There's not currently a stable way to do this for BTreeMap. 51 | } 52 | 53 | pub(super) fn take_nodes_for( 54 | &mut self, 55 | wid: WindowId, 56 | ) -> impl Iterator + use<> { 57 | self.window_nodes 58 | .remove(&wid) 59 | .unwrap_or_default() 60 | .into_iter() 61 | .map(|info| (info.layout, info.node)) 62 | } 63 | 64 | pub(super) fn take_nodes_for_app( 65 | &mut self, 66 | pid: pid_t, 67 | ) -> impl Iterator + use<> { 68 | let removed = self.window_nodes.remove_all_for_pid(pid); 69 | removed.into_iter().flat_map(|(wid, infos)| { 70 | infos.into_iter().map(move |info| (wid, info.layout, info.node)) 71 | }) 72 | } 73 | 74 | pub(super) fn handle_event(&mut self, map: &NodeMap, event: TreeEvent) { 75 | use TreeEvent::*; 76 | match event { 77 | AddedToForest(_) => (), 78 | AddedToParent(node) => debug_assert!( 79 | self.windows.get(node.parent(map).unwrap()).is_none(), 80 | "Window nodes are not allowed to have children: {:?}/{:?}", 81 | node.parent(map).unwrap(), 82 | node 83 | ), 84 | Copied { src, dest, dest_layout } => { 85 | if let Some(&wid) = self.windows.get(src) { 86 | self.set_window(dest_layout, dest, wid); 87 | } 88 | } 89 | RemovingFromParent(_) => (), 90 | RemovedFromForest(node) => { 91 | if let Some(wid) = self.windows.remove(node) { 92 | if let Some(window_nodes) = self.window_nodes.get_mut(&wid) { 93 | window_nodes.retain(|info| info.node != node); 94 | if window_nodes.is_empty() { 95 | self.window_nodes.remove(&wid); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/sys.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for interfacing with OS-specific APIs. 2 | 3 | pub mod app; 4 | pub mod event; 5 | pub mod executor; 6 | pub mod geometry; 7 | pub mod observer; 8 | pub mod run_loop; 9 | pub mod screen; 10 | pub mod window_server; 11 | -------------------------------------------------------------------------------- /src/sys/app.rs: -------------------------------------------------------------------------------- 1 | use accessibility::{AXUIElement, AXUIElementAttributes}; 2 | pub use accessibility_sys::pid_t; 3 | use accessibility_sys::{kAXStandardWindowSubrole, kAXWindowRole}; 4 | use icrate::{ 5 | objc2::{class, msg_send, msg_send_id, rc::Id}, 6 | AppKit::{NSRunningApplication, NSWorkspace}, 7 | Foundation::{CGRect, NSString}, 8 | }; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use super::{ 12 | geometry::{CGRectDef, ToICrate}, 13 | window_server::WindowServerId, 14 | }; 15 | 16 | pub fn running_apps(bundle: Option) -> impl Iterator { 17 | unsafe { NSWorkspace::sharedWorkspace().runningApplications() } 18 | .into_iter() 19 | .flat_map(move |app| { 20 | let bundle_id = app.bundle_id()?.to_string(); 21 | if let Some(filter) = &bundle { 22 | if !bundle_id.contains(filter) { 23 | return None; 24 | } 25 | } 26 | Some((app.pid(), AppInfo::from(&*app))) 27 | }) 28 | } 29 | 30 | pub trait NSRunningApplicationExt { 31 | fn with_process_id(pid: pid_t) -> Option>; 32 | fn pid(&self) -> pid_t; 33 | fn bundle_id(&self) -> Option>; 34 | fn localized_name(&self) -> Option>; 35 | } 36 | 37 | impl NSRunningApplicationExt for NSRunningApplication { 38 | fn with_process_id(pid: pid_t) -> Option> { 39 | unsafe { 40 | // For some reason this binding isn't generated in icrate. 41 | msg_send_id![class!(NSRunningApplication), runningApplicationWithProcessIdentifier:pid] 42 | } 43 | } 44 | fn pid(&self) -> pid_t { 45 | unsafe { msg_send![self, processIdentifier] } 46 | } 47 | fn bundle_id(&self) -> Option> { 48 | unsafe { self.bundleIdentifier() } 49 | } 50 | fn localized_name(&self) -> Option> { 51 | unsafe { self.localizedName() } 52 | } 53 | } 54 | 55 | #[derive(Serialize, Deserialize, Debug)] 56 | #[allow(dead_code)] 57 | pub struct AppInfo { 58 | pub bundle_id: Option, 59 | pub localized_name: Option, 60 | } 61 | 62 | impl From<&NSRunningApplication> for AppInfo { 63 | fn from(app: &NSRunningApplication) -> Self { 64 | AppInfo { 65 | bundle_id: app.bundle_id().as_deref().map(ToString::to_string), 66 | localized_name: app.localized_name().as_deref().map(ToString::to_string), 67 | } 68 | } 69 | } 70 | 71 | #[derive(Serialize, Deserialize, Debug)] 72 | pub struct WindowInfo { 73 | pub is_standard: bool, 74 | pub title: String, 75 | #[serde(with = "CGRectDef")] 76 | pub frame: CGRect, 77 | pub sys_id: Option, 78 | } 79 | 80 | impl TryFrom<&AXUIElement> for WindowInfo { 81 | type Error = accessibility::Error; 82 | fn try_from(element: &AXUIElement) -> Result { 83 | Ok(WindowInfo { 84 | is_standard: element.role()? == kAXWindowRole 85 | && element.subrole()? == kAXStandardWindowSubrole, 86 | title: element.title().map(|t| t.to_string()).unwrap_or_default(), 87 | frame: element.frame()?.to_icrate(), 88 | sys_id: WindowServerId::try_from(element).ok(), 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/sys/event.rs: -------------------------------------------------------------------------------- 1 | use core_graphics::{ 2 | base::CGError, 3 | display::{ 4 | kCGNullDirectDisplayID, CGDisplayHideCursor, CGDisplayShowCursor, CGWarpMouseCursorPosition, 5 | }, 6 | }; 7 | use icrate::{AppKit::NSEvent, Foundation::CGPoint}; 8 | use livesplit_hotkey::{ConsumePreference, Hook}; 9 | pub use livesplit_hotkey::{Hotkey, KeyCode, Modifiers}; 10 | use serde::{Deserialize, Serialize}; 11 | use tracing::info_span; 12 | 13 | use super::{geometry::ToCGType, screen::CoordinateConverter}; 14 | use crate::actor::{ 15 | reactor::Command, 16 | wm_controller::{Sender, WmCommand, WmEvent}, 17 | }; 18 | 19 | pub struct HotkeyManager { 20 | hook: Hook, 21 | events_tx: Sender, 22 | } 23 | 24 | impl HotkeyManager { 25 | pub fn new(events_tx: Sender) -> Self { 26 | let hook = Hook::with_consume_preference(ConsumePreference::MustConsume).unwrap(); 27 | HotkeyManager { hook, events_tx } 28 | } 29 | 30 | pub fn register(&self, modifiers: Modifiers, key_code: KeyCode, cmd: Command) { 31 | self.register_wm(modifiers, key_code, WmCommand::ReactorCommand(cmd)) 32 | } 33 | 34 | pub fn register_wm(&self, modifiers: Modifiers, key_code: KeyCode, cmd: WmCommand) { 35 | let events_tx = self.events_tx.clone(); 36 | let mut seq = 0; 37 | self.hook 38 | .register(Hotkey { modifiers, key_code }, move || { 39 | seq += 1; 40 | let span = info_span!("hotkey::press", ?key_code, ?seq); 41 | events_tx.send((span, WmEvent::Command(cmd.clone()))).unwrap() 42 | }) 43 | .unwrap(); 44 | } 45 | } 46 | 47 | /// The state of the left mouse button. 48 | #[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq)] 49 | pub enum MouseState { 50 | Down, 51 | Up, 52 | } 53 | 54 | pub fn get_mouse_state() -> MouseState { 55 | let left_button = unsafe { NSEvent::pressedMouseButtons() } & 0x1 != 0; 56 | if left_button { 57 | MouseState::Down 58 | } else { 59 | MouseState::Up 60 | } 61 | } 62 | 63 | pub fn get_mouse_pos(converter: CoordinateConverter) -> Option { 64 | let ns_loc = unsafe { NSEvent::mouseLocation() }; 65 | converter.convert_point(ns_loc) 66 | } 67 | 68 | pub fn warp_mouse(point: CGPoint) -> Result<(), CGError> { 69 | cg_result(unsafe { CGWarpMouseCursorPosition(point.to_cgtype()) }) 70 | } 71 | 72 | /// Hide the mouse. Note that this will have no effect unless 73 | /// [`window_server::allow_hide_mouse`] was called or this application is 74 | /// focused. 75 | pub fn hide_mouse() -> Result<(), CGError> { 76 | cg_result(unsafe { CGDisplayHideCursor(kCGNullDirectDisplayID) }) 77 | } 78 | 79 | pub fn show_mouse() -> Result<(), CGError> { 80 | cg_result(unsafe { CGDisplayShowCursor(kCGNullDirectDisplayID) }) 81 | } 82 | 83 | fn cg_result(err: CGError) -> Result<(), CGError> { 84 | if err == 0 { 85 | Ok(()) 86 | } else { 87 | Err(err) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/sys/executor.rs: -------------------------------------------------------------------------------- 1 | //! A simple async executor that integrates with CFRunLoop. 2 | 3 | use std::{ 4 | cell::RefCell, 5 | future::Future, 6 | mem, 7 | pin::Pin, 8 | rc::{Rc, Weak}, 9 | sync::{Arc, Mutex}, 10 | task::{Context, Poll, Wake}, 11 | }; 12 | 13 | use core_foundation::runloop::CFRunLoop; 14 | 15 | use super::run_loop::WakeupHandle; 16 | 17 | thread_local! { 18 | static HANDLE: Handle = Handle::new(); 19 | } 20 | 21 | #[allow(dead_code)] 22 | pub struct Executor; 23 | 24 | impl Executor { 25 | #[allow(dead_code)] 26 | pub fn run(task: impl Future) { 27 | let task: Pin + '_>> = Box::pin(task); 28 | // Extend the lifetime. 29 | // Safety: We only poll the task within this function, then it is dropped. 30 | let task: Pin + 'static>> = unsafe { mem::transmute(task) }; 31 | 32 | HANDLE.with(move |handle| { 33 | // Ensure we drop the main task, even on unwind. 34 | struct Guard; 35 | impl Drop for Guard { 36 | fn drop(&mut self) { 37 | HANDLE.with(|handle| { 38 | handle.0.borrow_mut().main_task.take(); 39 | }) 40 | } 41 | } 42 | let _guard = Guard; 43 | 44 | { 45 | let mut state = handle.0.borrow_mut(); 46 | state.main_task.replace(task); 47 | state.wakeup.wake_by_ref(); 48 | } 49 | 50 | while handle.0.borrow().main_task.is_some() { 51 | // Run the loop until it is stopped by process_tasks below. 52 | // We do this in a loop just in case there were "spurious" 53 | // stops by some other code. 54 | CFRunLoop::run_current(); 55 | } 56 | }) 57 | } 58 | } 59 | 60 | struct Handle(Rc>); 61 | 62 | impl Handle { 63 | fn new() -> Self { 64 | Handle(Rc::new_cyclic(|weak: &Weak>| { 65 | let weak = weak.clone(); 66 | let wakeup = WakeupHandle::for_current_thread(0, move || { 67 | if let Some(this) = weak.upgrade() { 68 | this.borrow_mut().process_tasks(); 69 | } 70 | }); 71 | let state = State { 72 | wakeup: Arc::new(WakerImpl(Mutex::new(wakeup))), 73 | main_task: None, 74 | }; 75 | RefCell::new(state) 76 | })) 77 | } 78 | } 79 | 80 | struct State { 81 | wakeup: Arc, 82 | main_task: Option>>>, 83 | } 84 | 85 | impl State { 86 | fn process_tasks(&mut self) { 87 | let waker = self.wakeup.clone().into(); 88 | let mut context = Context::from_waker(&waker); 89 | if self.main_task.as_mut().unwrap().as_mut().poll(&mut context) == Poll::Ready(()) { 90 | self.main_task.take(); 91 | CFRunLoop::get_current().stop(); 92 | } 93 | } 94 | } 95 | 96 | struct WakerImpl(Mutex); 97 | 98 | impl Wake for WakerImpl { 99 | fn wake(self: Arc) { 100 | self.0.lock().unwrap().wake(); 101 | } 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use std::{ 107 | cell::Cell, 108 | future, 109 | panic::{catch_unwind, AssertUnwindSafe}, 110 | thread, 111 | time::Duration, 112 | }; 113 | 114 | use super::*; 115 | 116 | #[derive(Default)] 117 | struct PendingThenReady(bool); 118 | 119 | impl Future for PendingThenReady { 120 | type Output = (); 121 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 122 | if self.0 { 123 | return Poll::Ready(()); 124 | } 125 | self.0 = true; 126 | cx.waker().wake_by_ref(); 127 | Poll::Pending 128 | } 129 | } 130 | 131 | #[test] 132 | fn executor_runs() { 133 | Executor::run(future::ready(())); 134 | Executor::run(PendingThenReady::default()); 135 | 136 | let mut x = 0; 137 | Executor::run(async { 138 | x += 1; 139 | PendingThenReady::default().await; 140 | x += 1; 141 | }); 142 | assert_eq!(2, x); 143 | } 144 | 145 | #[test] 146 | fn executor_drops_main_task_on_unwind() { 147 | struct SignallingDrop(AssertUnwindSafe>>); 148 | impl Drop for SignallingDrop { 149 | fn drop(&mut self) { 150 | self.0.replace(true); 151 | } 152 | } 153 | 154 | let dropped = Rc::new(Cell::new(false)); 155 | 156 | let dropper = SignallingDrop(AssertUnwindSafe(dropped.clone())); 157 | let result = catch_unwind(|| { 158 | Executor::run(async move { 159 | let _dropper = dropper; 160 | PendingThenReady::default().await; 161 | panic!("oh no"); 162 | }); 163 | }); 164 | 165 | assert!(result.is_err()); 166 | assert_eq!(true, dropped.take()); 167 | } 168 | 169 | #[test] 170 | fn channel_works() { 171 | use tokio::sync::mpsc; 172 | 173 | let (tx, mut rx) = mpsc::unbounded_channel(); 174 | 175 | thread::spawn(move || { 176 | thread::sleep(Duration::from_millis(25)); 177 | _ = tx.send(()); 178 | _ = tx.send(()); 179 | drop(tx); 180 | }); 181 | 182 | let mut msgs = 0; 183 | Executor::run(async { 184 | while let Some(_msg) = rx.recv().await { 185 | msgs += 1; 186 | PendingThenReady::default().await; 187 | } 188 | }); 189 | 190 | assert_eq!(2, msgs); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/sys/geometry.rs: -------------------------------------------------------------------------------- 1 | use core_graphics_types::geometry as cg; 2 | use icrate::Foundation as ic; 3 | use serde::{Deserialize, Deserializer, Serialize}; 4 | use serde_with::{DeserializeAs, SerializeAs}; 5 | 6 | pub trait ToICrate { 7 | fn to_icrate(&self) -> T; 8 | } 9 | 10 | impl ToICrate for cg::CGPoint { 11 | fn to_icrate(&self) -> ic::CGPoint { 12 | ic::CGPoint { x: self.x, y: self.y } 13 | } 14 | } 15 | 16 | impl ToICrate for cg::CGSize { 17 | fn to_icrate(&self) -> ic::CGSize { 18 | ic::CGSize { 19 | width: self.width, 20 | height: self.height, 21 | } 22 | } 23 | } 24 | 25 | impl ToICrate for cg::CGRect { 26 | fn to_icrate(&self) -> ic::CGRect { 27 | ic::CGRect { 28 | origin: self.origin.to_icrate(), 29 | size: self.size.to_icrate(), 30 | } 31 | } 32 | } 33 | 34 | pub trait ToCGType { 35 | fn to_cgtype(&self) -> T; 36 | } 37 | 38 | impl ToCGType for ic::CGPoint { 39 | fn to_cgtype(&self) -> cg::CGPoint { 40 | cg::CGPoint { x: self.x, y: self.y } 41 | } 42 | } 43 | 44 | impl ToCGType for ic::CGSize { 45 | fn to_cgtype(&self) -> cg::CGSize { 46 | cg::CGSize { 47 | width: self.width, 48 | height: self.height, 49 | } 50 | } 51 | } 52 | 53 | impl ToCGType for ic::CGRect { 54 | fn to_cgtype(&self) -> cg::CGRect { 55 | cg::CGRect { 56 | origin: self.origin.to_cgtype(), 57 | size: self.size.to_cgtype(), 58 | } 59 | } 60 | } 61 | 62 | pub trait Round { 63 | fn round(&self) -> Self; 64 | } 65 | 66 | impl Round for ic::CGRect { 67 | fn round(&self) -> Self { 68 | // Round each corner to pixel boundaries, then use that to calculate the size. 69 | let min_rounded = self.min().round(); 70 | let max_rounded = self.max().round(); 71 | ic::CGRect { 72 | origin: min_rounded, 73 | size: ic::CGSize { 74 | width: max_rounded.x - min_rounded.x, 75 | height: max_rounded.y - min_rounded.y, 76 | }, 77 | } 78 | } 79 | } 80 | 81 | impl Round for ic::CGPoint { 82 | fn round(&self) -> Self { 83 | ic::CGPoint { 84 | x: self.x.round(), 85 | y: self.y.round(), 86 | } 87 | } 88 | } 89 | 90 | impl Round for ic::CGSize { 91 | fn round(&self) -> Self { 92 | ic::CGSize { 93 | width: self.width.round(), 94 | height: self.height.round(), 95 | } 96 | } 97 | } 98 | 99 | pub trait IsWithin { 100 | fn is_within(&self, how_much: f64, other: Self) -> bool; 101 | } 102 | 103 | impl IsWithin for ic::CGRect { 104 | fn is_within(&self, how_much: f64, other: Self) -> bool { 105 | self.origin.is_within(how_much, other.origin) && self.size.is_within(how_much, other.size) 106 | } 107 | } 108 | 109 | impl IsWithin for ic::CGPoint { 110 | fn is_within(&self, how_much: f64, other: Self) -> bool { 111 | self.x.is_within(how_much, other.x) && self.y.is_within(how_much, other.y) 112 | } 113 | } 114 | 115 | impl IsWithin for ic::CGSize { 116 | fn is_within(&self, how_much: f64, other: Self) -> bool { 117 | self.width.is_within(how_much, other.width) && self.height.is_within(how_much, other.height) 118 | } 119 | } 120 | 121 | impl IsWithin for f64 { 122 | fn is_within(&self, how_much: f64, other: Self) -> bool { 123 | (self - other).abs() < how_much 124 | } 125 | } 126 | 127 | pub trait SameAs: IsWithin + Sized { 128 | fn same_as(&self, other: Self) -> bool { 129 | self.is_within(0.1, other) 130 | } 131 | } 132 | 133 | impl SameAs for ic::CGRect {} 134 | impl SameAs for ic::CGPoint {} 135 | impl SameAs for ic::CGSize {} 136 | 137 | pub trait CGRectExt { 138 | fn intersection(&self, other: &Self) -> Self; 139 | fn area(&self) -> f64; 140 | } 141 | 142 | impl CGRectExt for ic::CGRect { 143 | fn intersection(&self, other: &Self) -> Self { 144 | let min_x = f64::max(self.min().x, other.min().x); 145 | let max_x = f64::min(self.max().x, other.max().x); 146 | let min_y = f64::max(self.min().y, other.min().y); 147 | let max_y = f64::min(self.max().y, other.max().y); 148 | ic::CGRect { 149 | origin: ic::CGPoint::new(min_x, min_y), 150 | size: ic::CGSize::new(f64::max(max_x - min_x, 0.), f64::max(max_y - min_y, 0.)), 151 | } 152 | } 153 | 154 | fn area(&self) -> f64 { 155 | self.size.width * self.size.height 156 | } 157 | } 158 | 159 | #[derive(Serialize, Deserialize)] 160 | #[serde(remote = "ic::CGRect")] 161 | pub struct CGRectDef { 162 | #[serde(with = "CGPointDef")] 163 | pub origin: ic::CGPoint, 164 | #[serde(with = "CGSizeDef")] 165 | pub size: ic::CGSize, 166 | } 167 | 168 | #[derive(Serialize, Deserialize)] 169 | #[serde(remote = "ic::CGPoint")] 170 | pub struct CGPointDef { 171 | pub x: f64, 172 | pub y: f64, 173 | } 174 | 175 | #[derive(Serialize, Deserialize)] 176 | #[serde(remote = "ic::CGSize")] 177 | pub struct CGSizeDef { 178 | pub width: f64, 179 | pub height: f64, 180 | } 181 | 182 | impl SerializeAs for CGRectDef { 183 | fn serialize_as(value: &ic::CGRect, serializer: S) -> Result 184 | where 185 | S: serde::Serializer, 186 | { 187 | CGRectDef::serialize(value, serializer) 188 | } 189 | } 190 | 191 | impl<'de> DeserializeAs<'de, ic::CGRect> for CGRectDef { 192 | fn deserialize_as(deserializer: D) -> Result 193 | where 194 | D: Deserializer<'de>, 195 | { 196 | CGRectDef::deserialize(deserializer) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/sys/observer.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, ffi::c_void, marker::PhantomData, mem::ManuallyDrop, ptr}; 2 | 3 | use accessibility::AXUIElement; 4 | use accessibility_sys::{ 5 | kAXErrorSuccess, pid_t, AXError, AXObserverAddNotification, AXObserverCreate, 6 | AXObserverGetRunLoopSource, AXObserverGetTypeID, AXObserverRef, AXObserverRemoveNotification, 7 | AXUIElementRef, 8 | }; 9 | use core_foundation::{ 10 | base::TCFType, 11 | declare_TCFType, impl_TCFType, 12 | runloop::{kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetCurrent}, 13 | string::{CFString, CFStringRef}, 14 | }; 15 | 16 | declare_TCFType!(AXObserver, AXObserverRef); 17 | impl_TCFType!(AXObserver, AXObserverRef, AXObserverGetTypeID); 18 | 19 | /// An observer for accessibility events. 20 | pub struct Observer { 21 | callback: *mut (), 22 | dtor: unsafe fn(*mut ()), 23 | observer: ManuallyDrop, 24 | } 25 | 26 | static_assertions::assert_not_impl_any!(Observer: Send); 27 | 28 | /// Helper type for building an [`Observer`]. 29 | // 30 | // This type exists to carry type information about our callback `F` to the call 31 | // to `new` from the call to `install`. It exists because of the following 32 | // constraints: 33 | // 34 | // * Creating the observer object can fail, e.g. if the app in question is no 35 | // longer running. 36 | // * The `Observer` often needs to go inside an object that is also referenced 37 | // by the callback. This necessitates the use of APIs like 38 | // [`std::rc::Rc::make_cyclic`], which unfortunately is not fallible. 39 | // * `Observer` should not know about the type of its callback, both because 40 | // that type usually cannot be named and for convenience. 41 | // * We want to avoid double indirection on calls to the callback, which 42 | // necessitates knowing the type of `F` when creating the system observer 43 | // object during the call to `new`. 44 | // 45 | // This means we make creation of the Observer a two-step process. `new` can 46 | // fail and can be called before the call to `make_cyclic`. `install` is 47 | // infallible and can be called inside, meaning the callback passed to it can 48 | // capture a weak pointer to our object. 49 | pub struct ObserverBuilder(AXObserver, PhantomData); 50 | 51 | impl Observer { 52 | /// Creates a new observer for an app, given its `pid`. 53 | /// 54 | /// Note that you must call [`ObserverBuilder::install`] on the result of 55 | /// this function and supply a callback for the observer to have any effect. 56 | pub fn new( 57 | pid: pid_t, 58 | ) -> Result, accessibility::Error> { 59 | // SAFETY: We just create an observer here, and check the return code. 60 | // The callback cannot be called yet. The API guarantees that F will be 61 | // supplied as the callback in the call to install (and the 'static 62 | // bound on F means we don't need to worry about variance). 63 | let mut observer: AXObserverRef = ptr::null_mut(); 64 | unsafe { 65 | make_result(AXObserverCreate(pid, internal_callback::, &mut observer))?; 66 | } 67 | Ok(ObserverBuilder( 68 | unsafe { AXObserver::wrap_under_create_rule(observer) }, 69 | PhantomData, 70 | )) 71 | } 72 | } 73 | 74 | impl ObserverBuilder { 75 | /// Installs the observer with the supplied callback into the current 76 | /// thread's run loop. 77 | pub fn install(self, callback: F) -> Observer { 78 | // SAFETY: We know from typestate that the observer will call 79 | // internal_callback::. F is 'static, so even if our destructor is 80 | // not run it will remain valid to call. 81 | unsafe { 82 | let source = AXObserverGetRunLoopSource(self.0.as_concrete_TypeRef()); 83 | CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes); 84 | } 85 | Observer { 86 | callback: Box::into_raw(Box::new(callback)) as *mut (), 87 | dtor: destruct::, 88 | observer: ManuallyDrop::new(self.0), 89 | } 90 | } 91 | } 92 | 93 | unsafe fn destruct(ptr: *mut ()) { 94 | let _ = unsafe { Box::from_raw(ptr as *mut T) }; 95 | } 96 | 97 | impl Drop for Observer { 98 | fn drop(&mut self) { 99 | unsafe { 100 | ManuallyDrop::drop(&mut self.observer); 101 | (self.dtor)(self.callback); 102 | } 103 | } 104 | } 105 | 106 | impl Observer { 107 | pub fn add_notification( 108 | &self, 109 | elem: &AXUIElement, 110 | notification: &'static str, 111 | ) -> Result<(), accessibility::Error> { 112 | make_result(unsafe { 113 | AXObserverAddNotification( 114 | self.observer.as_concrete_TypeRef(), 115 | elem.as_concrete_TypeRef(), 116 | CFString::from_static_string(notification).as_concrete_TypeRef(), 117 | self.callback as *mut c_void, 118 | ) 119 | }) 120 | } 121 | 122 | pub fn remove_notification( 123 | &self, 124 | elem: &AXUIElement, 125 | notification: &'static str, 126 | ) -> Result<(), accessibility::Error> { 127 | make_result(unsafe { 128 | AXObserverRemoveNotification( 129 | self.observer.as_concrete_TypeRef(), 130 | elem.as_concrete_TypeRef(), 131 | CFString::from_static_string(notification).as_concrete_TypeRef(), 132 | ) 133 | }) 134 | } 135 | } 136 | 137 | unsafe extern "C" fn internal_callback( 138 | _observer: AXObserverRef, 139 | elem: AXUIElementRef, 140 | notif: CFStringRef, 141 | data: *mut c_void, 142 | ) { 143 | let callback = unsafe { &*(data as *const F) }; 144 | let elem = unsafe { AXUIElement::wrap_under_get_rule(elem) }; 145 | let notif = unsafe { CFString::wrap_under_get_rule(notif) }; 146 | let notif = Cow::::from(¬if); 147 | callback(elem, &*notif); 148 | } 149 | 150 | fn make_result(err: AXError) -> Result<(), accessibility::Error> { 151 | if err != kAXErrorSuccess { 152 | return Err(accessibility::Error::Ax(err)); 153 | } 154 | Ok(()) 155 | } 156 | -------------------------------------------------------------------------------- /src/sys/run_loop.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for managing run loops. 2 | 3 | use std::{ffi::c_void, mem, ptr}; 4 | 5 | use core_foundation::{ 6 | base::TCFType, 7 | mach_port::CFIndex, 8 | runloop::{ 9 | kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource, CFRunLoopSourceContext, 10 | CFRunLoopSourceCreate, CFRunLoopSourceSignal, CFRunLoopWakeUp, 11 | }, 12 | }; 13 | 14 | /// A core foundation run loop source. 15 | /// 16 | /// This type primarily exists for the purpose of managing manual sources, which 17 | /// can be used for signaling code that blocks on a run loop. 18 | /// 19 | /// More information is available in the Apple documentation at 20 | /// https://developer.apple.com/documentation/corefoundation/cfrunloopsource-rhr. 21 | #[derive(Clone, PartialEq)] 22 | pub struct WakeupHandle(CFRunLoopSource, CFRunLoop); 23 | 24 | // SAFETY: 25 | // - CFRunLoopSource and CFRunLoop are ObjC objects which are allowed to be used 26 | // from multiple threads. 27 | // - We only allow signaling the source from this handle. No access to the 28 | // underlying handler is given, so it does not need to be Send or Sync. 29 | unsafe impl Send for WakeupHandle {} 30 | 31 | struct Handler { 32 | ref_count: isize, 33 | func: F, 34 | } 35 | 36 | impl WakeupHandle { 37 | /// Creates and adds a manual source for the current [`CFRunLoop`]. 38 | /// 39 | /// The supplied function `handler` is called inside the run loop when this 40 | /// handle has been woken and the run loop is running. 41 | /// 42 | /// The handler is run in all common modes. `order` controls the order it is 43 | /// run in relative to other run loop sources, and should normally be set to 44 | /// 0. 45 | pub fn for_current_thread(order: CFIndex, handler: F) -> WakeupHandle { 46 | let handler = Box::into_raw(Box::new(Handler { ref_count: 0, func: handler })); 47 | 48 | extern "C-unwind" fn perform(info: *const c_void) { 49 | // SAFETY: Only one thread may call these functions, and the mutable 50 | // reference lives only during the function call. No other code has 51 | // access to the handler. 52 | let handler = unsafe { &mut *(info as *mut Handler) }; 53 | (handler.func)(); 54 | } 55 | extern "C" fn retain(info: *const c_void) -> *const c_void { 56 | // SAFETY: As above. 57 | let handler = unsafe { &mut *(info as *mut Handler) }; 58 | handler.ref_count += 1; 59 | info 60 | } 61 | extern "C-unwind" fn release(info: *const c_void) { 62 | // SAFETY: As above. 63 | let handler = unsafe { &mut *(info as *mut Handler) }; 64 | handler.ref_count -= 1; 65 | if handler.ref_count == 0 { 66 | mem::drop(unsafe { Box::from_raw(info as *mut Handler) }); 67 | } 68 | } 69 | 70 | // SAFETY: Strip the C-unwind ABI from the function pointer types since 71 | // the core-foundation crate hasn't been updated with this ABI yet. This 72 | // should be sound as long as we don't call the transmuted function 73 | // pointer from Rust. 74 | let release = unsafe { 75 | mem::transmute::( 76 | release::, 77 | ) 78 | }; 79 | let perform = unsafe { 80 | mem::transmute::( 81 | perform::, 82 | ) 83 | }; 84 | 85 | let mut context = CFRunLoopSourceContext { 86 | version: 0, 87 | info: handler as *mut c_void, 88 | retain: Some(retain::), 89 | release: Some(release), 90 | copyDescription: None, 91 | equal: None, 92 | hash: None, 93 | schedule: None, 94 | cancel: None, 95 | perform, 96 | }; 97 | 98 | let source = unsafe { 99 | let source = CFRunLoopSourceCreate(ptr::null(), order, &mut context as *mut _); 100 | CFRunLoopSource::wrap_under_create_rule(source) 101 | }; 102 | let run_loop = CFRunLoop::get_current(); 103 | run_loop.add_source(&source, unsafe { kCFRunLoopCommonModes }); 104 | 105 | WakeupHandle(source, run_loop) 106 | } 107 | 108 | /// Wakes the run loop that owns the target of this handle and schedules its 109 | /// handler to be called. 110 | /// 111 | /// Multiple signals may be collapsed into a single call of the handler. 112 | pub fn wake(&self) { 113 | unsafe { 114 | CFRunLoopSourceSignal(self.0.as_concrete_TypeRef()); 115 | CFRunLoopWakeUp(self.1.as_concrete_TypeRef()); 116 | } 117 | } 118 | } 119 | 120 | #[cfg(test)] 121 | mod tests { 122 | use std::{ 123 | sync::{ 124 | atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering}, 125 | mpsc::{channel, Receiver, Sender}, 126 | Arc, 127 | }, 128 | thread::JoinHandle, 129 | }; 130 | 131 | use core_foundation::runloop::CFRunLoop; 132 | 133 | use super::WakeupHandle; 134 | 135 | struct RunLoopThread { 136 | num_wakeups: Arc, 137 | shutdown: Arc, 138 | channel: Receiver>, 139 | drop_tracker: DropTracker, 140 | thread: JoinHandle<()>, 141 | } 142 | 143 | fn spawn_run_loop_thread(run: bool) -> RunLoopThread { 144 | let num_wakeups = Arc::new(AtomicI32::new(0)); 145 | let shutdown = Arc::new(AtomicBool::new(false)); 146 | let (handler_wakeups, handler_shutdown) = (num_wakeups.clone(), shutdown.clone()); 147 | let (tx, rx) = channel(); 148 | let (drop_tracker, drop_signaler) = DropTracker::new(); 149 | let thread = std::thread::spawn(move || { 150 | let handler_tx = tx.clone(); 151 | let wakeup = WakeupHandle::for_current_thread(0, move || { 152 | println!("handler"); 153 | let _signaler = &drop_signaler; 154 | handler_wakeups.fetch_add(1, Ordering::SeqCst); 155 | handler_tx.send(None).unwrap(); 156 | if handler_shutdown.load(Ordering::SeqCst) { 157 | CFRunLoop::get_current().stop(); 158 | } 159 | println!("done"); 160 | }); 161 | tx.send(Some(wakeup)).unwrap(); 162 | if run { 163 | CFRunLoop::run_current(); 164 | } 165 | }); 166 | RunLoopThread { 167 | num_wakeups, 168 | shutdown, 169 | channel: rx, 170 | drop_tracker, 171 | thread, 172 | } 173 | } 174 | 175 | #[test] 176 | fn it_works_without_wakeups() { 177 | let RunLoopThread { 178 | num_wakeups, 179 | channel: rx, 180 | drop_tracker, 181 | thread, 182 | .. 183 | } = spawn_run_loop_thread(false); 184 | let wakeup = rx.recv().unwrap().expect("should receive a wakeup handle"); 185 | thread.join().unwrap(); 186 | assert_eq!(0, num_wakeups.load(Ordering::SeqCst)); 187 | drop(wakeup); 188 | drop_tracker.wait_for_drop(); 189 | } 190 | 191 | #[test] 192 | fn it_wakes() { 193 | let RunLoopThread { 194 | num_wakeups, 195 | shutdown, 196 | channel: rx, 197 | drop_tracker, 198 | thread, 199 | } = spawn_run_loop_thread(true); 200 | let wakeup = rx.recv().unwrap().expect("should receive a wakeup handle"); 201 | assert_eq!(0, num_wakeups.load(Ordering::SeqCst)); 202 | shutdown.store(true, Ordering::SeqCst); 203 | wakeup.wake(); 204 | thread.join().unwrap(); 205 | assert_eq!(1, num_wakeups.load(Ordering::SeqCst)); 206 | drop(wakeup); 207 | drop_tracker.wait_for_drop(); 208 | } 209 | 210 | #[test] 211 | fn it_can_wake_from_multiple_threads() { 212 | let RunLoopThread { 213 | num_wakeups, 214 | shutdown, 215 | channel: rx, 216 | drop_tracker, 217 | thread, 218 | } = spawn_run_loop_thread(true); 219 | let wakeup = rx.recv().unwrap().expect("should receive a wakeup handle"); 220 | assert_eq!(0, num_wakeups.load(Ordering::SeqCst)); 221 | let thread_wakeup = wakeup.clone(); 222 | std::thread::spawn(move || thread_wakeup.wake()).join().unwrap(); 223 | let _ = rx.recv().unwrap(); 224 | assert_eq!(1, num_wakeups.load(Ordering::SeqCst)); 225 | shutdown.store(true, Ordering::SeqCst); 226 | wakeup.wake(); 227 | thread.join().unwrap(); 228 | assert_eq!(2, num_wakeups.load(Ordering::SeqCst)); 229 | drop(wakeup); 230 | drop_tracker.wait_for_drop(); 231 | } 232 | 233 | struct DropTracker(Arc, Receiver<()>); 234 | impl DropTracker { 235 | fn new() -> (DropTracker, DropSignaller) { 236 | let (tx, rx) = channel(); 237 | let tracker = DropTracker(Default::default(), rx); 238 | let signaller = DropSignaller(tracker.0.clone(), tx); 239 | (tracker, signaller) 240 | } 241 | fn wait_for_drop(self) { 242 | self.1.recv().unwrap(); 243 | assert_eq!(1, self.0.load(Ordering::SeqCst)); 244 | assert!( 245 | Arc::into_inner(self.0).is_some(), 246 | "Another clone of our Arc exists somewhere!" 247 | ); 248 | assert!( 249 | self.1.recv().is_err(), 250 | "Another clone of our sender exists somewhere!", 251 | ); 252 | } 253 | } 254 | 255 | struct DropSignaller(Arc, Sender<()>); 256 | impl Drop for DropSignaller { 257 | fn drop(&mut self) { 258 | self.0.fetch_add(1, Ordering::SeqCst); 259 | self.1.send(()).unwrap(); 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/sys/screen.rs: -------------------------------------------------------------------------------- 1 | use std::{f64, ffi::c_int, mem::MaybeUninit, num::NonZeroU64}; 2 | 3 | use bitflags::bitflags; 4 | use core_foundation::{ 5 | array::{CFArray, CFArrayRef}, 6 | base::TCFType, 7 | string::{CFString, CFStringRef}, 8 | }; 9 | use core_graphics::display::{CGDisplayBounds, CGGetActiveDisplayList}; 10 | use core_graphics_types::base::{kCGErrorSuccess, CGError}; 11 | use icrate::{ 12 | objc2::{msg_send, ClassType}, 13 | AppKit::NSScreen, 14 | Foundation::{ns_string, CGPoint, CGRect, MainThreadMarker, NSNumber}, 15 | }; 16 | use serde::{Deserialize, Serialize}; 17 | use tracing::{debug, warn}; 18 | 19 | use crate::sys::geometry::ToICrate; 20 | 21 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 22 | #[repr(transparent)] 23 | pub struct SpaceId(NonZeroU64); 24 | 25 | #[cfg(test)] 26 | impl SpaceId { 27 | pub fn new(id: u64) -> SpaceId { 28 | SpaceId(NonZeroU64::new(id).unwrap()) 29 | } 30 | } 31 | 32 | /// Calculates the screen and space configuration. 33 | pub struct ScreenCache { 34 | system: S, 35 | uuids: Vec, 36 | } 37 | 38 | impl ScreenCache { 39 | pub fn new(mtm: MainThreadMarker) -> Self { 40 | Self::new_with(Actual { mtm }) 41 | } 42 | } 43 | 44 | impl ScreenCache { 45 | fn new_with(system: S) -> ScreenCache { 46 | ScreenCache { uuids: vec![], system } 47 | } 48 | 49 | /// Returns a list containing the usable frame for each screen. 50 | /// 51 | /// This method must be called when there is an update to the screen 52 | /// configuration. It updates the internal cache so that calls to 53 | /// screen_spaces are fast. 54 | /// 55 | /// The main screen (if any) is always first. Note that there may be no 56 | /// screens. 57 | #[forbid(unsafe_code)] // called from test 58 | pub fn update_screen_config(&mut self) -> (Vec, Vec, CoordinateConverter) { 59 | let mut cg_screens = self.system.cg_screens().unwrap(); 60 | debug!("cg_screens={cg_screens:?}"); 61 | if cg_screens.is_empty() { 62 | return (vec![], vec![], CoordinateConverter::default()); 63 | }; 64 | 65 | // Ensure that the main screen is always first. 66 | let main_screen_idx = cg_screens 67 | .iter() 68 | .position(|s| s.bounds.origin == CGPoint::ZERO) 69 | .expect("Could not find the main screen"); 70 | cg_screens.swap(0, main_screen_idx); 71 | 72 | self.uuids = cg_screens 73 | .iter() 74 | .map(|screen| self.system.uuid_for_rect(screen.bounds)) 75 | .collect(); 76 | 77 | // We want to get the visible_frame of the NSScreenInfo, but in CG's 78 | // top-left coordinates from NSScreen's bottom-left. 79 | let ns_screens = self.system.ns_screens(); 80 | debug!("ns_screens={ns_screens:?}"); 81 | 82 | // The main screen has origin (0, 0) in both coordinate systems. 83 | let converter = CoordinateConverter { 84 | screen_height: cg_screens[0].bounds.max().y, 85 | }; 86 | 87 | let (visible_frames, ids) = cg_screens 88 | .iter() 89 | .flat_map(|&CGScreenInfo { cg_id, .. }| { 90 | let Some(ns_screen) = ns_screens.iter().find(|s| s.cg_id == cg_id) else { 91 | warn!("Can't find NSScreen corresponding to {cg_id:?}"); 92 | return None; 93 | }; 94 | let converted = converter.convert_rect(ns_screen.visible_frame).unwrap(); 95 | Some((converted, cg_id)) 96 | }) 97 | .unzip(); 98 | (visible_frames, ids, converter) 99 | } 100 | 101 | /// Returns a list of the active spaces on each screen. The order 102 | /// corresponds to the screens returned by `screen_frames`. 103 | pub fn get_screen_spaces(&self) -> Vec> { 104 | self.uuids 105 | .iter() 106 | .map(|screen| unsafe { 107 | CGSManagedDisplayGetCurrentSpace( 108 | CGSMainConnectionID(), 109 | screen.as_concrete_TypeRef(), 110 | ) 111 | }) 112 | .map(|id| Some(SpaceId(NonZeroU64::new(id)?))) 113 | .collect() 114 | } 115 | } 116 | 117 | /// Converts between Quartz and Cocoa coordinate systems. 118 | #[derive(Clone, Copy, Debug)] 119 | pub struct CoordinateConverter { 120 | /// The y offset of the Cocoa origin in the Quartz coordinate system, and 121 | /// vice versa. This is the height of the first screen. The origins 122 | /// are the bottom left and top left of the screen, respectively. 123 | screen_height: f64, 124 | } 125 | 126 | /// Creates a `CoordinateConverter` that returns None for any conversion. 127 | impl Default for CoordinateConverter { 128 | fn default() -> Self { 129 | Self { screen_height: f64::NAN } 130 | } 131 | } 132 | 133 | impl CoordinateConverter { 134 | pub fn convert_point(&self, point: CGPoint) -> Option { 135 | if self.screen_height.is_nan() { 136 | return None; 137 | } 138 | Some(CGPoint::new(point.x, self.screen_height - point.y)) 139 | } 140 | 141 | pub fn convert_rect(&self, rect: CGRect) -> Option { 142 | if self.screen_height.is_nan() { 143 | return None; 144 | } 145 | Some(CGRect::new( 146 | CGPoint::new(rect.origin.x, self.screen_height - rect.max().y), 147 | rect.size, 148 | )) 149 | } 150 | } 151 | 152 | #[allow(private_interfaces)] 153 | pub trait System { 154 | fn cg_screens(&self) -> Result, CGError>; 155 | fn uuid_for_rect(&self, rect: CGRect) -> CFString; 156 | fn ns_screens(&self) -> Vec; 157 | } 158 | 159 | #[derive(Debug, Clone)] 160 | struct CGScreenInfo { 161 | cg_id: ScreenId, 162 | bounds: CGRect, 163 | } 164 | 165 | #[derive(Debug, Clone)] 166 | #[allow(dead_code)] 167 | struct NSScreenInfo { 168 | frame: CGRect, 169 | visible_frame: CGRect, 170 | cg_id: ScreenId, 171 | } 172 | 173 | pub struct Actual { 174 | mtm: MainThreadMarker, 175 | } 176 | #[allow(private_interfaces)] 177 | impl System for Actual { 178 | fn cg_screens(&self) -> Result, CGError> { 179 | const MAX_SCREENS: usize = 64; 180 | let mut ids: MaybeUninit<[CGDirectDisplayID; MAX_SCREENS]> = MaybeUninit::uninit(); 181 | let mut count: u32 = 0; 182 | let ids = unsafe { 183 | let err = CGGetActiveDisplayList( 184 | MAX_SCREENS as u32, 185 | ids.as_mut_ptr() as *mut CGDirectDisplayID, 186 | &mut count, 187 | ); 188 | if err != kCGErrorSuccess { 189 | return Err(err); 190 | } 191 | std::slice::from_raw_parts(ids.as_ptr() as *const u32, count as usize) 192 | }; 193 | Ok(ids 194 | .iter() 195 | .map(|&cg_id| CGScreenInfo { 196 | cg_id: ScreenId(cg_id), 197 | bounds: unsafe { CGDisplayBounds(cg_id).to_icrate() }, 198 | }) 199 | .collect()) 200 | } 201 | 202 | fn uuid_for_rect(&self, rect: CGRect) -> CFString { 203 | unsafe { 204 | CFString::wrap_under_create_rule(CGSCopyBestManagedDisplayForRect( 205 | CGSMainConnectionID(), 206 | rect, 207 | )) 208 | } 209 | } 210 | 211 | fn ns_screens(&self) -> Vec { 212 | NSScreen::screens(self.mtm) 213 | .iter() 214 | .flat_map(|s| { 215 | Some(NSScreenInfo { 216 | frame: s.frame(), 217 | visible_frame: s.visibleFrame(), 218 | cg_id: s.get_number().ok()?, 219 | }) 220 | }) 221 | .collect() 222 | } 223 | } 224 | 225 | type CGDirectDisplayID = u32; 226 | 227 | #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] 228 | pub struct ScreenId(CGDirectDisplayID); 229 | 230 | pub trait NSScreenExt { 231 | fn get_number(&self) -> Result; 232 | } 233 | impl NSScreenExt for NSScreen { 234 | fn get_number(&self) -> Result { 235 | let desc = self.deviceDescription(); 236 | match desc.get(ns_string!("NSScreenNumber")) { 237 | Some(val) if unsafe { msg_send![val, isKindOfClass:NSNumber::class() ] } => { 238 | let number: &NSNumber = unsafe { std::mem::transmute(val) }; 239 | Ok(ScreenId(number.as_u32())) 240 | } 241 | val => { 242 | warn!( 243 | "Could not get NSScreenNumber for screen with name {:?}: {:?}", 244 | unsafe { self.localizedName() }, 245 | val, 246 | ); 247 | Err(()) 248 | } 249 | } 250 | } 251 | } 252 | 253 | /// Utilities for querying the current system configuration. For diagnostic purposes only. 254 | #[allow(dead_code)] 255 | pub mod diagnostic { 256 | use super::*; 257 | 258 | pub fn cur_space() -> SpaceId { 259 | SpaceId(NonZeroU64::new(unsafe { CGSGetActiveSpace(CGSMainConnectionID()) }).unwrap()) 260 | } 261 | 262 | pub fn visible_spaces() -> CFArray { 263 | unsafe { 264 | let arr = CGSCopySpaces(CGSMainConnectionID(), CGSSpaceMask::ALL_VISIBLE_SPACES); 265 | CFArray::wrap_under_create_rule(arr) 266 | } 267 | } 268 | 269 | pub fn all_spaces() -> CFArray { 270 | unsafe { 271 | let arr = CGSCopySpaces(CGSMainConnectionID(), CGSSpaceMask::ALL_SPACES); 272 | CFArray::wrap_under_create_rule(arr) 273 | } 274 | } 275 | 276 | pub fn managed_displays() -> CFArray { 277 | unsafe { CFArray::wrap_under_create_rule(CGSCopyManagedDisplays(CGSMainConnectionID())) } 278 | } 279 | 280 | pub fn managed_display_spaces() -> CFArray { 281 | unsafe { 282 | CFArray::wrap_under_create_rule(CGSCopyManagedDisplaySpaces(CGSMainConnectionID())) 283 | } 284 | } 285 | } 286 | 287 | // Based on https://github.com/asmagill/hs._asm.undocumented.spaces/blob/master/CGSSpace.h. 288 | // Also see https://github.com/koekeishiya/yabai/blob/d55a647913ab72d8d8b348bee2d3e59e52ce4a5d/src/misc/extern.h. 289 | 290 | #[link(name = "CoreGraphics", kind = "framework")] 291 | unsafe extern "C" { 292 | fn CGSMainConnectionID() -> c_int; 293 | fn CGSGetActiveSpace(cid: c_int) -> u64; 294 | fn CGSCopySpaces(cid: c_int, mask: CGSSpaceMask) -> CFArrayRef; 295 | fn CGSCopyManagedDisplays(cid: c_int) -> CFArrayRef; 296 | fn CGSCopyManagedDisplaySpaces(cid: c_int) -> CFArrayRef; 297 | fn CGSManagedDisplayGetCurrentSpace(cid: c_int, uuid: CFStringRef) -> u64; 298 | fn CGSCopyBestManagedDisplayForRect(cid: c_int, rect: CGRect) -> CFStringRef; 299 | } 300 | 301 | bitflags! { 302 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 303 | #[repr(transparent)] 304 | struct CGSSpaceMask: c_int { 305 | const INCLUDE_CURRENT = 1 << 0; 306 | const INCLUDE_OTHERS = 1 << 1; 307 | 308 | const INCLUDE_USER = 1 << 2; 309 | const INCLUDE_OS = 1 << 3; 310 | 311 | const VISIBLE = 1 << 16; 312 | 313 | const CURRENT_SPACES = Self::INCLUDE_USER.bits() | Self::INCLUDE_CURRENT.bits(); 314 | const OTHER_SPACES = Self::INCLUDE_USER.bits() | Self::INCLUDE_OTHERS.bits(); 315 | const ALL_SPACES = 316 | Self::INCLUDE_USER.bits() | Self::INCLUDE_OTHERS.bits() | Self::INCLUDE_CURRENT.bits(); 317 | 318 | const ALL_VISIBLE_SPACES = Self::ALL_SPACES.bits() | Self::VISIBLE.bits(); 319 | 320 | const CURRENT_OS_SPACES = Self::INCLUDE_OS.bits() | Self::INCLUDE_CURRENT.bits(); 321 | const OTHER_OS_SPACES = Self::INCLUDE_OS.bits() | Self::INCLUDE_OTHERS.bits(); 322 | const ALL_OS_SPACES = 323 | Self::INCLUDE_OS.bits() | Self::INCLUDE_OTHERS.bits() | Self::INCLUDE_CURRENT.bits(); 324 | } 325 | } 326 | 327 | #[cfg(test)] 328 | mod test { 329 | use core_foundation::string::CFString; 330 | use icrate::Foundation::{CGPoint, CGRect, CGSize}; 331 | 332 | use super::{CGScreenInfo, NSScreenInfo, ScreenCache, ScreenId, System}; 333 | 334 | struct Stub { 335 | cg_screens: Vec, 336 | ns_screens: Vec, 337 | } 338 | impl System for Stub { 339 | fn cg_screens(&self) -> Result, core_graphics_types::base::CGError> { 340 | Ok(self.cg_screens.clone()) 341 | } 342 | fn ns_screens(&self) -> Vec { 343 | self.ns_screens.clone() 344 | } 345 | fn uuid_for_rect(&self, _rect: CGRect) -> CFString { 346 | CFString::new("stub") 347 | } 348 | } 349 | 350 | #[test] 351 | fn it_calculates_the_visible_frame() { 352 | println!("test"); 353 | let stub = Stub { 354 | cg_screens: vec![ 355 | CGScreenInfo { 356 | cg_id: ScreenId(1), 357 | bounds: CGRect::new(CGPoint::new(3840.0, 1080.0), CGSize::new(1512.0, 982.0)), 358 | }, 359 | CGScreenInfo { 360 | cg_id: ScreenId(3), 361 | bounds: CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(3840.0, 2160.0)), 362 | }, 363 | ], 364 | ns_screens: vec![ 365 | NSScreenInfo { 366 | cg_id: ScreenId(3), 367 | frame: CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(3840.0, 2160.0)), 368 | visible_frame: CGRect::new( 369 | CGPoint::new(0.0, 76.0), 370 | CGSize::new(3840.0, 2059.0), 371 | ), 372 | }, 373 | NSScreenInfo { 374 | cg_id: ScreenId(1), 375 | frame: CGRect::new(CGPoint::new(3840.0, 98.0), CGSize::new(1512.0, 982.0)), 376 | visible_frame: CGRect::new( 377 | CGPoint::new(3840.0, 98.0), 378 | CGSize::new(1512.0, 950.0), 379 | ), 380 | }, 381 | ], 382 | }; 383 | let mut sc = ScreenCache::new_with(stub); 384 | assert_eq!( 385 | vec![ 386 | CGRect::new(CGPoint::new(0.0, 25.0), CGSize::new(3840.0, 2059.0)), 387 | CGRect::new(CGPoint::new(3840.0, 1112.0), CGSize::new(1512.0, 950.0)), 388 | ], 389 | sc.update_screen_config().0 390 | ); 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/sys/window_server.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_int; 2 | 3 | use accessibility::AXUIElement; 4 | use accessibility_sys::{kAXErrorSuccess, pid_t, AXError, AXUIElementRef}; 5 | use core_foundation::{ 6 | array::CFArray, 7 | base::{CFType, CFTypeRef, ItemRef, TCFType}, 8 | boolean::CFBoolean, 9 | dictionary::CFDictionary, 10 | number::CFNumber, 11 | string::{CFString, CFStringRef}, 12 | }; 13 | use core_graphics::{ 14 | base::CGError, 15 | display::{ 16 | kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowID, CGWindowListCopyWindowInfo, 17 | }, 18 | window::{ 19 | kCGWindowBounds, kCGWindowLayer, kCGWindowListExcludeDesktopElements, kCGWindowNumber, 20 | kCGWindowOwnerPID, CGWindowListCreateDescriptionFromArray, 21 | }, 22 | }; 23 | use icrate::{ 24 | AppKit::NSWindow, 25 | Foundation::{CGPoint, CGRect, MainThreadMarker}, 26 | }; 27 | use serde::{Deserialize, Serialize}; 28 | 29 | use super::{ 30 | geometry::{CGRectDef, ToICrate}, 31 | screen::CoordinateConverter, 32 | }; 33 | 34 | /// The window ID used by the window server. 35 | /// 36 | /// Obtaining this from AXUIElement uses a private API and is *not* guaranteed. 37 | /// Any functionality depending on this should have a backup plan in case it 38 | /// breaks in the future. 39 | #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy, Serialize, Deserialize)] 40 | pub struct WindowServerId(pub CGWindowID); 41 | 42 | impl WindowServerId { 43 | pub fn new(id: CGWindowID) -> Self { 44 | WindowServerId(id) 45 | } 46 | 47 | pub fn as_u32(&self) -> u32 { 48 | self.0 49 | } 50 | } 51 | 52 | impl Into for WindowServerId { 53 | fn into(self) -> u32 { 54 | self.0 55 | } 56 | } 57 | 58 | impl TryFrom<&AXUIElement> for WindowServerId { 59 | type Error = accessibility::Error; 60 | fn try_from(element: &AXUIElement) -> Result { 61 | let mut id = 0; 62 | let res = unsafe { _AXUIElementGetWindow(element.as_concrete_TypeRef(), &mut id) }; 63 | if res != kAXErrorSuccess { 64 | return Err(accessibility::Error::Ax(res)); 65 | } 66 | Ok(WindowServerId(id)) 67 | } 68 | } 69 | 70 | #[derive(Debug, Clone, Serialize, Deserialize)] 71 | #[allow(unused)] 72 | pub struct WindowServerInfo { 73 | pub id: WindowServerId, 74 | pub pid: pid_t, 75 | pub layer: i32, 76 | #[serde(with = "CGRectDef")] 77 | pub frame: CGRect, 78 | } 79 | 80 | /// Returns a list of windows visible on the screen, in order starting with the 81 | /// frontmost. Excludes desktop elements. 82 | pub fn get_visible_windows_with_layer(layer: Option) -> Vec { 83 | get_visible_windows_raw() 84 | .iter() 85 | .filter_map(|win| make_info(win, layer)) 86 | .collect::>() 87 | } 88 | 89 | /// Returns a list of windows visible on the screen, in order starting with the 90 | /// frontmost. 91 | pub fn get_visible_windows_raw() -> CFArray> { 92 | // Note that the ordering is not documented. But 93 | // NSWindow::windowNumbersWithOptions *is* documented to return the windows 94 | // in order, so we could always combine their information if the behavior 95 | // changed. 96 | unsafe { 97 | CFArray::wrap_under_get_rule(CGWindowListCopyWindowInfo( 98 | kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, 99 | kCGNullWindowID, 100 | )) 101 | } 102 | } 103 | 104 | fn make_info( 105 | win: ItemRef>, 106 | layer_filter: Option, 107 | ) -> Option { 108 | let layer = get_num(&win, unsafe { kCGWindowLayer })?.try_into().ok()?; 109 | if !(layer_filter.is_none() || layer_filter == Some(layer)) { 110 | return None; 111 | } 112 | let id = get_num(&win, unsafe { kCGWindowNumber })?; 113 | let pid = get_num(&win, unsafe { kCGWindowOwnerPID })?; 114 | let frame: CFDictionary = win.find(unsafe { kCGWindowBounds })?.downcast()?; 115 | let frame = core_graphics_types::geometry::CGRect::from_dict_representation(&frame)?; 116 | Some(WindowServerInfo { 117 | id: WindowServerId(id.try_into().ok()?), 118 | pid: pid.try_into().ok()?, 119 | layer, 120 | frame: frame.to_icrate(), 121 | }) 122 | } 123 | 124 | pub fn get_windows(ids: &[WindowServerId]) -> Vec { 125 | if ids.is_empty() { 126 | return Vec::new(); 127 | } 128 | get_windows_inner(ids).iter().flat_map(|w| make_info(w, None)).collect() 129 | } 130 | 131 | pub fn get_window(id: WindowServerId) -> Option { 132 | get_windows_inner(&[id]).iter().next().and_then(|w| make_info(w, None)) 133 | } 134 | 135 | fn get_windows_inner(ids: &[WindowServerId]) -> CFArray> { 136 | let array = CFArray::from_copyable(ids); 137 | unsafe { 138 | CFArray::wrap_under_create_rule(CGWindowListCreateDescriptionFromArray( 139 | array.as_concrete_TypeRef(), 140 | )) 141 | } 142 | } 143 | 144 | fn get_num(dict: &CFDictionary, key: CFStringRef) -> Option { 145 | let item: CFNumber = dict.find(key)?.downcast()?; 146 | Some(item.to_i64()?) 147 | } 148 | 149 | pub fn get_window_at_point( 150 | point: CGPoint, 151 | converter: CoordinateConverter, 152 | mtm: MainThreadMarker, 153 | ) -> Option { 154 | let ns_loc = converter.convert_point(point)?; 155 | let win = unsafe { NSWindow::windowNumberAtPoint_belowWindowWithWindowNumber(ns_loc, 0, mtm) }; 156 | Some(WindowServerId(win as u32)) 157 | } 158 | 159 | unsafe extern "C" { 160 | fn _AXUIElementGetWindow(elem: AXUIElementRef, wid: *mut CGWindowID) -> AXError; 161 | 162 | fn GetProcessForPID(pid: pid_t, psn: *mut ProcessSerialNumber) -> CGError; 163 | } 164 | 165 | /// Sets the given window as the key window of the window server. 166 | pub fn make_key_window(pid: pid_t, wsid: WindowServerId) -> Result<(), ()> { 167 | // See https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468. 168 | #[allow(non_upper_case_globals)] 169 | const kCPSUserGenerated: u32 = 0x200; 170 | 171 | let mut event1 = [0; 0x100]; 172 | event1[0x04] = 0xf8; 173 | event1[0x08] = 0x01; 174 | event1[0x3a] = 0x10; 175 | event1[0x3c..0x3c + 4].copy_from_slice(&wsid.0.to_le_bytes()); 176 | event1[0x20..(0x20 + 0x10)].fill(0xff); 177 | 178 | let mut event2 = event1.clone(); 179 | event2[0x08] = 0x02; 180 | 181 | let mut psn = ProcessSerialNumber::default(); 182 | let check = |err| if err == 0 { Ok(()) } else { Err(()) }; 183 | unsafe { 184 | check(GetProcessForPID(pid, &mut psn))?; 185 | check(_SLPSSetFrontProcessWithOptions(&psn, wsid.0, kCPSUserGenerated))?; 186 | check(SLPSPostEventRecordTo(&psn, event1.as_ptr()))?; 187 | check(SLPSPostEventRecordTo(&psn, event2.as_ptr()))?; 188 | } 189 | Ok(()) 190 | } 191 | 192 | #[repr(C)] 193 | #[derive(Default)] 194 | struct ProcessSerialNumber { 195 | high: u32, 196 | low: u32, 197 | } 198 | 199 | #[link(name = "SkyLight", kind = "framework")] 200 | unsafe extern "C" { 201 | fn _SLPSSetFrontProcessWithOptions( 202 | psn: *const ProcessSerialNumber, 203 | wid: u32, 204 | mode: u32, 205 | ) -> CGError; 206 | fn SLPSPostEventRecordTo(psn: *const ProcessSerialNumber, bytes: *const u8) -> CGError; 207 | } 208 | 209 | /// This must be called to allow hiding the mouse from a background application. 210 | /// 211 | /// It relies on a private API, so not guaranteed to continue working, but it is 212 | /// discussed by Apple engineers on developer forums. 213 | pub fn allow_hide_mouse() -> Result<(), CGError> { 214 | let cid = unsafe { CGSMainConnectionID() }; 215 | let property = CFString::from_static_string("SetsCursorInBackground"); 216 | let err = unsafe { 217 | CGSSetConnectionProperty( 218 | cid, 219 | cid, 220 | property.as_concrete_TypeRef(), 221 | CFBoolean::true_value().as_CFTypeRef(), 222 | ) 223 | }; 224 | if err == 0 { 225 | Ok(()) 226 | } else { 227 | Err(err) 228 | } 229 | } 230 | 231 | type CGSConnectionID = c_int; 232 | 233 | #[link(name = "ApplicationServices", kind = "framework")] 234 | unsafe extern "C" { 235 | fn CGSMainConnectionID() -> CGSConnectionID; 236 | fn CGSSetConnectionProperty( 237 | cid: CGSConnectionID, 238 | target_cid: CGSConnectionID, 239 | key: CFStringRef, 240 | value: CFTypeRef, 241 | ) -> CGError; 242 | } 243 | --------------------------------------------------------------------------------