Repository: glzr-io/glazewm Branch: main Commit: 6e5242eaecc4 Files: 214 Total size: 873.3 KB Directory structure: gitextract_qulwdvhw/ ├── .cargo/ │ └── config.toml ├── .editorconfig ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── build.yaml │ ├── lint-check.yaml │ ├── package.yaml │ ├── pr-title-check.yaml │ ├── release.yaml │ └── winget-release.yaml ├── .gitignore ├── .vscode/ │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── CLAUDE.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE.md ├── README.md ├── README_zh.md ├── clippy.toml ├── packages/ │ ├── wm/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ ├── commands/ │ │ │ ├── container/ │ │ │ │ ├── attach_container.rs │ │ │ │ ├── detach_container.rs │ │ │ │ ├── flatten_child_split_containers.rs │ │ │ │ ├── flatten_split_container.rs │ │ │ │ ├── focus_container_by_id.rs │ │ │ │ ├── focus_in_direction.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── move_container_within_tree.rs │ │ │ │ ├── replace_container.rs │ │ │ │ ├── resize_tiling_container.rs │ │ │ │ ├── set_focused_descendant.rs │ │ │ │ ├── toggle_tiling_direction.rs │ │ │ │ └── wrap_in_split_container.rs │ │ │ ├── general/ │ │ │ │ ├── cycle_focus.rs │ │ │ │ ├── disable_binding_mode.rs │ │ │ │ ├── enable_binding_mode.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── platform_sync.rs │ │ │ │ ├── reload_config.rs │ │ │ │ ├── shell_exec.rs │ │ │ │ └── toggle_pause.rs │ │ │ ├── mod.rs │ │ │ ├── monitor/ │ │ │ │ ├── add_monitor.rs │ │ │ │ ├── focus_monitor.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── remove_monitor.rs │ │ │ │ ├── sort_monitors.rs │ │ │ │ └── update_monitor.rs │ │ │ ├── window/ │ │ │ │ ├── ignore_window.rs │ │ │ │ ├── manage_window.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── move_window_in_direction.rs │ │ │ │ ├── move_window_to_workspace.rs │ │ │ │ ├── resize_window.rs │ │ │ │ ├── run_window_rules.rs │ │ │ │ ├── set_window_position.rs │ │ │ │ ├── set_window_size.rs │ │ │ │ ├── unmanage_window.rs │ │ │ │ └── update_window_state.rs │ │ │ └── workspace/ │ │ │ ├── activate_workspace.rs │ │ │ ├── deactivate_workspace.rs │ │ │ ├── focus_workspace.rs │ │ │ ├── mod.rs │ │ │ ├── move_workspace_in_direction.rs │ │ │ ├── sort_workspaces.rs │ │ │ └── update_workspace_config.rs │ │ ├── events/ │ │ │ ├── handle_display_settings_changed.rs │ │ │ ├── handle_mouse_move.rs │ │ │ ├── handle_window_destroyed.rs │ │ │ ├── handle_window_focused.rs │ │ │ ├── handle_window_hidden.rs │ │ │ ├── handle_window_minimize_ended.rs │ │ │ ├── handle_window_minimized.rs │ │ │ ├── handle_window_moved_or_resized.rs │ │ │ ├── handle_window_moved_or_resized_end.rs │ │ │ ├── handle_window_shown.rs │ │ │ ├── handle_window_title_changed.rs │ │ │ └── mod.rs │ │ ├── ipc_server.rs │ │ ├── main.rs │ │ ├── models/ │ │ │ ├── container.rs │ │ │ ├── insertion_target.rs │ │ │ ├── mod.rs │ │ │ ├── monitor.rs │ │ │ ├── native_monitor_properties.rs │ │ │ ├── native_window_properties.rs │ │ │ ├── non_tiling_window.rs │ │ │ ├── root_container.rs │ │ │ ├── split_container.rs │ │ │ ├── tiling_window.rs │ │ │ ├── workspace.rs │ │ │ └── workspace_target.rs │ │ ├── pending_sync.rs │ │ ├── sys_tray.rs │ │ ├── traits/ │ │ │ ├── common_getters.rs │ │ │ ├── mod.rs │ │ │ ├── position_getters.rs │ │ │ ├── tiling_direction_getters.rs │ │ │ ├── tiling_size_getters.rs │ │ │ └── window_getters.rs │ │ ├── user_config.rs │ │ ├── wm.rs │ │ └── wm_state.rs │ ├── wm-cli/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ ├── lib.rs │ │ └── main.rs │ ├── wm-common/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── active_drag.rs │ │ ├── app_command.rs │ │ ├── display_state.rs │ │ ├── dtos/ │ │ │ ├── container_dto.rs │ │ │ ├── mod.rs │ │ │ ├── monitor_dto.rs │ │ │ ├── root_container_dto.rs │ │ │ ├── split_container_dto.rs │ │ │ ├── window_dto.rs │ │ │ └── workspace_dto.rs │ │ ├── hide_corner.rs │ │ ├── ipc.rs │ │ ├── lib.rs │ │ ├── parsed_config.rs │ │ ├── tiling_direction.rs │ │ ├── utils/ │ │ │ ├── iterator_ext.rs │ │ │ ├── mod.rs │ │ │ ├── try_warn.rs │ │ │ └── vec_deque_ext.rs │ │ ├── window_state.rs │ │ └── wm_event.rs │ ├── wm-ipc-client/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ ├── wm-macros/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── common/ │ │ │ ├── attributes.rs │ │ │ ├── branch.rs │ │ │ ├── error_handling.rs │ │ │ ├── mod.rs │ │ │ ├── named_parameter.rs │ │ │ ├── parenthesized.rs │ │ │ ├── peekable.rs │ │ │ └── spanned_string.rs │ │ ├── enum_from_inner/ │ │ │ └── mod.rs │ │ ├── lib.rs │ │ └── subenum/ │ │ ├── enum_attrs.rs │ │ ├── mod.rs │ │ └── variant_attr.rs │ ├── wm-platform/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ ├── dispatcher.rs │ │ ├── display.rs │ │ ├── display_listener.rs │ │ ├── error.rs │ │ ├── event_loop.rs │ │ ├── keybinding_listener.rs │ │ ├── lib.rs │ │ ├── models/ │ │ │ ├── color.rs │ │ │ ├── corner_style.rs │ │ │ ├── delta.rs │ │ │ ├── direction.rs │ │ │ ├── key.rs │ │ │ ├── key_code.rs │ │ │ ├── length_value.rs │ │ │ ├── mod.rs │ │ │ ├── opacity_value.rs │ │ │ ├── point.rs │ │ │ ├── rect.rs │ │ │ └── rect_delta.rs │ │ ├── mouse_listener.rs │ │ ├── native_window.rs │ │ ├── platform_event.rs │ │ ├── platform_impl/ │ │ │ ├── macos/ │ │ │ │ ├── application.rs │ │ │ │ ├── application_observer.rs │ │ │ │ ├── ax_ui_element.rs │ │ │ │ ├── ax_value.rs │ │ │ │ ├── display.rs │ │ │ │ ├── display_listener.rs │ │ │ │ ├── event_loop.rs │ │ │ │ ├── ffi.rs │ │ │ │ ├── keyboard_hook.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── mouse_listener.rs │ │ │ │ ├── native_window.rs │ │ │ │ ├── notification_center.rs │ │ │ │ ├── single_instance.rs │ │ │ │ └── window_listener.rs │ │ │ ├── mod.rs │ │ │ └── windows/ │ │ │ ├── com.rs │ │ │ ├── display.rs │ │ │ ├── display_listener.rs │ │ │ ├── event_loop.rs │ │ │ ├── keyboard_hook.rs │ │ │ ├── mod.rs │ │ │ ├── mouse_listener.rs │ │ │ ├── native_window.rs │ │ │ ├── single_instance.rs │ │ │ └── window_listener.rs │ │ ├── single_instance.rs │ │ ├── test.rs │ │ ├── thread_bound.rs │ │ └── window_listener.rs │ └── wm-watcher/ │ ├── Cargo.toml │ ├── build.rs │ └── src/ │ └── main.rs ├── resources/ │ ├── Info.plist │ ├── assets/ │ │ └── sample-config.yaml │ ├── scripts/ │ │ └── package.ps1 │ └── wix/ │ ├── bundle-theme.wxl │ ├── bundle-theme.xml │ ├── bundle.wxs │ ├── standalone-ui.wxs │ └── standalone.wxs ├── rust-toolchain.toml └── rustfmt.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [env] # Version number shown in CLI and system tray. Override this by setting the # version number on build/run (i.e. `VERSION_NUMBER="1.0.0" cargo run`). VERSION_NUMBER = "0.0.0" ================================================ FILE: .editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.md] max_line_length = off ================================================ FILE: .gitattributes ================================================ # Set CRLF as the line ending to use for all files that Git interprets as text files. * text=auto eol=crlf ================================================ FILE: .github/workflows/build.yaml ================================================ name: Build on: workflow_call: inputs: enable_ui_access: type: boolean default: false description: (Windows only) Enable UIAccess in the application manifest. version_number: type: string default: 0.0.0 description: Version number to use for the build. jobs: build: strategy: fail-fast: false matrix: include: # MacOS (Intel) - platform: macos-latest target: x86_64-apple-darwin # MacOS (Apple Silicon) - platform: macos-latest target: aarch64-apple-darwin # 64-bit Windows (Intel & AMD) - platform: windows-latest target: x86_64-pc-windows-msvc # 64-bit Windows (ARM) - platform: windows-latest target: aarch64-pc-windows-msvc runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@21dc36fb71dd22e3317045c0c31a3f4249868b17 with: targets: ${{ matrix.target }} toolchain: nightly - uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 with: shared-key: ${{ matrix.target }}-${{ hashFiles('Cargo.lock') }} - name: Build for ${{ matrix.target }} env: VERSION_NUMBER: ${{ inputs.version_number }} # Conditionally enable UIAccess feature for Windows. FEATURES: ${{ matrix.platform == 'windows-latest' && inputs.enable_ui_access && '--features ui_access' || '' }} # Include `wm-watcher` when building for Windows. SCOPE: ${{ matrix.platform == 'windows-latest' && '--workspace' || '' }} shell: bash run: cargo build --locked --release --target ${{ matrix.target }} $SCOPE $FEATURES - name: Create upload directory shell: bash run: | SOURCE_DIR="target/${{ matrix.target }}/release" UPLOAD_DIR="temp/$SOURCE_DIR" mkdir -p "$UPLOAD_DIR" # Move build artifacts. Suppress errors for missing platform-specific binaries. mv "$SOURCE_DIR"/glazewm{,-cli} \ "$SOURCE_DIR"/glazewm{,-cli,-watcher}.exe \ "$UPLOAD_DIR/" 2>/dev/null || true - uses: actions/upload-artifact@v6 with: if-no-files-found: error name: build-${{ matrix.target }} path: temp ================================================ FILE: .github/workflows/lint-check.yaml ================================================ name: Lint check on: push: pull_request: types: [opened, synchronize, reopened] jobs: lint-check: strategy: fail-fast: false matrix: include: # MacOS (Apple Silicon) - platform: macos-latest target: aarch64-apple-darwin # Windows (x64) - platform: windows-latest target: x86_64-pc-windows-msvc runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@21dc36fb71dd22e3317045c0c31a3f4249868b17 with: components: clippy, rustfmt targets: ${{ matrix.target }} toolchain: nightly - uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 with: shared-key: ${{ matrix.target }}-${{ hashFiles('Cargo.lock') }} - name: Check formatting (rustfmt) run: cargo fmt --check # Clippy itself runs `cargo check`, so this will also check for compilation errors. - name: Check linting (clippy) run: cargo clippy --all-targets --all-features -- -D warnings ================================================ FILE: .github/workflows/package.yaml ================================================ name: Package on: workflow_call: inputs: version_number: type: string default: 0.0.0 description: Version number to use for the build (e.g. `1.0.0`). workflow_dispatch: inputs: version_number: type: string default: 0.0.0 description: Version number to use for the build (e.g. `1.0.0`). jobs: build: uses: ./.github/workflows/build.yaml with: enable_ui_access: true version_number: ${{ inputs.version_number }} package-windows: needs: build runs-on: windows-latest env: VERSION: ${{ inputs.version_number }} steps: - uses: actions/checkout@v4 - name: Download build artifacts uses: actions/download-artifact@v8 with: pattern: build-* merge-multiple: true - name: Install WiX and its extensions run: | dotnet tool install --global wix --version 5.0.0 wix extension add WixToolset.UI.wixext/5 WixToolset.Util.wixext/5 WixToolset.BootstrapperApplications.wixext/5 - name: Install AzureSignTool run: | dotnet tool install --global AzureSignTool --version 5.0.0 - name: Run packaging script env: AZ_VAULT_URL: ${{ secrets.AZ_VAULT_URL }} AZ_CERT_NAME: ${{ secrets.AZ_CERT_NAME }} AZ_CLIENT_ID: ${{ secrets.AZ_CLIENT_ID }} AZ_CLIENT_SECRET: ${{ secrets.AZ_CLIENT_SECRET }} AZ_TENANT_ID: ${{ secrets.AZ_TENANT_ID }} RFC3161_TIMESTAMP_URL: ${{ vars.RFC3161_TIMESTAMP_URL }} run: | ./resources/scripts/package.ps1 -VersionNumber $env:VERSION - uses: actions/upload-artifact@v6 with: name: package-windows if-no-files-found: error path: out/installer-* package-macos: needs: build runs-on: macos-latest env: VERSION: ${{ inputs.version_number }} steps: - uses: actions/checkout@v4 - name: Download build artifacts uses: actions/download-artifact@v8 with: pattern: build-* merge-multiple: true - name: Create universal binaries run: | mkdir temp for binary in glazewm glazewm-cli; do lipo -create \ "target/x86_64-apple-darwin/release/$binary" \ "target/aarch64-apple-darwin/release/$binary" \ -output "temp/$binary" chmod +x "temp/$binary" done - name: Import signing certificate env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} run: | # Create a temporary keychain. KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db KEYCHAIN_PASSWORD=$(openssl rand -base64 32) security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" # Import the certificate. echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 security import $RUNNER_TEMP/certificate.p12 \ -P "$APPLE_CERTIFICATE_PASSWORD" \ -A \ -t cert \ -f pkcs12 \ -k "$KEYCHAIN_PATH" security list-keychain -d user -s "$KEYCHAIN_PATH" # Store the keychain path for later steps. echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV # Make sure to sign the binaries before they're copied into the app bundle. - name: Sign universal binaries env: APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} run: | for binary in glazewm glazewm-cli; do codesign --force --options runtime --timestamp \ --sign "$APPLE_SIGNING_IDENTITY" \ "temp/$binary" codesign --verify --verbose=2 "temp/$binary" done - name: Convert icon to ICNS run: | ICONSET_DIR="temp/icon.iconset" mkdir -p $ICONSET_DIR sips -z 16 16 resources/assets/icon.png --out $ICONSET_DIR/icon_16x16.png sips -z 32 32 resources/assets/icon.png --out $ICONSET_DIR/icon_16x16@2x.png sips -z 32 32 resources/assets/icon.png --out $ICONSET_DIR/icon_32x32.png sips -z 64 64 resources/assets/icon.png --out $ICONSET_DIR/icon_32x32@2x.png sips -z 128 128 resources/assets/icon.png --out $ICONSET_DIR/icon_128x128.png sips -z 256 256 resources/assets/icon.png --out $ICONSET_DIR/icon_128x128@2x.png sips -z 256 256 resources/assets/icon.png --out $ICONSET_DIR/icon_256x256.png sips -z 512 512 resources/assets/icon.png --out $ICONSET_DIR/icon_256x256@2x.png sips -z 512 512 resources/assets/icon.png --out $ICONSET_DIR/icon_512x512.png sips -z 1024 1024 resources/assets/icon.png --out $ICONSET_DIR/icon_512x512@2x.png iconutil -c icns $ICONSET_DIR -o temp/icon.icns - name: Create app bundle run: | CONTENTS_DIR="temp/GlazeWM.app/Contents" mkdir -p "$CONTENTS_DIR/MacOS" "$CONTENTS_DIR/Resources" cp temp/glazewm "$CONTENTS_DIR/MacOS/" cp temp/glazewm-cli "$CONTENTS_DIR/MacOS/" cp temp/icon.icns "$CONTENTS_DIR/Resources/icon.icns" chmod +x "$CONTENTS_DIR/MacOS"/* # Substitute version placeholder in Info.plist. sed "s/\${VERSION}/$VERSION/g" resources/Info.plist > "$CONTENTS_DIR/Info.plist" echo -n "APPL????" > "$CONTENTS_DIR/PkgInfo" - name: Sign app bundle env: APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} run: | codesign --force --options runtime --timestamp \ --sign "$APPLE_SIGNING_IDENTITY" \ temp/GlazeWM.app codesign --verify --verbose=2 --deep --strict temp/GlazeWM.app - name: Create DMG run: | DMG_DIR="temp/dmg-contents" mkdir -p $DMG_DIR cp -R temp/GlazeWM.app $DMG_DIR/ # Create a symbolic link to `/Applications` folder. ln -s /Applications $DMG_DIR/Applications # Create the DMG. hdiutil create -volname GlazeWM \ -srcfolder $DMG_DIR \ -ov -format UDZO \ installer-universal.dmg - name: Sign DMG env: APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} run: | codesign --force --timestamp \ --sign "$APPLE_SIGNING_IDENTITY" \ installer-universal.dmg - name: Notarize DMG env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | # Submit for notarization. xcrun notarytool submit installer-universal.dmg \ --apple-id "$APPLE_ID" \ --password "$APPLE_ID_PASSWORD" \ --team-id "$APPLE_TEAM_ID" \ --wait # Staple the notarization ticket to the DMG. xcrun stapler staple installer-universal.dmg - name: Upload DMG uses: actions/upload-artifact@v6 with: name: package-macos if-no-files-found: error path: installer-universal.dmg ================================================ FILE: .github/workflows/pr-title-check.yaml ================================================ name: PR title check on: pull_request: types: [opened, edited, synchronize, reopened] jobs: pr-title-check: runs-on: ubuntu-latest steps: - uses: glzr-io/actions/semantic-prs@main with: gh-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: workflow_dispatch: inputs: version_number: type: string description: Version number to use for the build (e.g. `1.0.0`). permissions: contents: write concurrency: group: release jobs: package: uses: ./.github/workflows/package.yaml secrets: inherit with: version_number: ${{ inputs.version_number }} release: needs: package runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Download installers uses: actions/download-artifact@v8 with: pattern: package-* merge-multiple: true - name: Rename installers env: # Add a `v` prefix to the version number. VERSION: v${{ inputs.version_number }} run: | mv installer-universal.exe "glazewm-$VERSION.exe" mv installer-arm64.msi "standalone-glazewm-$VERSION-arm64.msi" mv installer-x64.msi "standalone-glazewm-$VERSION-x64.msi" mv installer-universal.dmg "glazewm-$VERSION.dmg" - name: Create draft release env: # Add a `v` prefix to the version number. VERSION: v${{ inputs.version_number }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create "$VERSION" \ --title "$VERSION" \ --generate-notes \ --draft \ "./glazewm-$VERSION.exe#$VERSION for Windows (standard)" \ "./standalone-glazewm-$VERSION-arm64.msi#$VERSION for Windows (standalone, arm64)" \ "./standalone-glazewm-$VERSION-x64.msi#$VERSION for Windows (standalone, x64)" \ "./glazewm-$VERSION.dmg#$VERSION for macOS" ================================================ FILE: .github/workflows/winget-release.yaml ================================================ name: Winget release on: workflow_dispatch: release: types: [published] jobs: publish: runs-on: ubuntu-latest steps: - uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f with: identifier: glzr-io.glazewm installers-regex: 'glazewm-v[0-9.]+\.exe$' token: ${{ secrets.WINGET_TOKEN }} ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Build outputs target/ out/ # Misc .DS_Store *.pem *.env .wix/ # IDE files .vs/ .idea/ ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Launch", "type": "cppvsdbg", "request": "launch", "program": "${workspaceRoot}/target/debug/glazewm.exe", "preLaunchTask": "rust: cargo build", "args": ["start"], "stopAtEntry": false, "cwd": "${workspaceRoot}", "environment": [], "console": "integratedTerminal" // "externalConsole": true } ] } ================================================ FILE: .vscode/settings.json ================================================ { "rust-analyzer.check.command": "clippy", "editor.insertSpaces": true, "editor.tabSize": 2, "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, } ================================================ FILE: .vscode/tasks.json ================================================ { "version": "2.0.0", "tasks": [ { "type": "cargo", "command": "build", "problemMatcher": ["$rustc"], "group": "build", "label": "rust: cargo build" } ] } ================================================ FILE: CLAUDE.md ================================================ GlazeWM is a window manager for macOS and Windows, written in Rust. Crate structure: - **wm** (bin): Main application, which implements the core window management logic. Install path on Windows: `C:\Program Files\glzr.io\glazewm.exe` - **wm-cli** (bin, lib): CLI for interacting with the main application. Added to `$PATH` by default. Install path on Windows: `C:\Program Files\glzr.io\cli\glazewm.exe` - **wm-common** (lib): Shared types, utilities, and constants used across other crates. - **wm-ipc-client** (lib): WebSocket client library for IPC with the main application. - **wm-platform** (lib): Wrappers over platform-specific APIs; other crates do not call Windows/macOS APIs directly. - **wm-watcher** (Windows-only) (bin): Watchdog process ensuring proper cleanup when the main application exits. Install path on Windows: `C:\Program Files\glzr.io\glazewm-watcher.exe` - Be extremely concise. Sacrifice grammar for the sake of conciseness. - Do not leave partial or simplified implementations. - The required quality standard is high. Low quality code will be rejected. - Do not proceed with solutions that are hacky. Solutions must be robust, maintainable, and extendable. Ask guiding questions if uncertain about a solution. - Avoid `.unwrap()` wherever possible. - For error handling: - Use `crate::Error` and `crate::Result` within the `wm-platform` crate. - Use `anyhow` in all other crates. - For logging, use `tracing` macros (e.g. `tracing::info!("...")`). - Functions should always be documented. - Use punctuation mark at the end of all comments. - If using unsafe features, include a "SAFETY: ..." comment. - Wrap type names in backticks (e.g. `NativeMonitor`). Comment structure: ```rs /// /// /// (optional) /// /// (optional) /// /// (optional) # Example usage /// /// /// /// (optional) # Platform-specific /// /// pub fn my_function() { ... } ``` - Use `#[cfg(test)]` for test modules. - Write unit tests for core functionality. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to GlazeWM Thanks for your interest in improving GlazeWM 💛 There are fundamentally three ways to contribute: 1. **Opening issues**: If you believe you've found a bug or have a feature request, open an issue to discuss it. 2. **Helping triage issues**: Add supporting details and suggestions to existing issues. 3. **Submitting PRs**: Submit a PR that fixes a bug or implements a feature. The [#glazewm-dev channel ⚡](https://discord.com/invite/ud6z3qjRvM) is also available for any concerns not covered in this guide, please join us! ## Pull requests & dev workflow For PRs, a good place to start are the issues marked as [`good first issue`](https://github.com/glzr-io/glazewm/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) or [`help wanted`](https://github.com/glzr-io/glazewm/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22). PR's don't have a requirement to have a corresponding issue, but if there is one already, please drop a comment in the issue and we can assign it to you. ### Setup First fork, then clone the repo: ```shell git clone git@github.com:your-username/glazewm.git ``` If not already installed, [install Rust](https://rustup.rs/), then run: ```shell # `cargo build` will build all binaries and libraries. # `cargo run` will run the default binary, which is configured to be the wm. cargo build && cargo run ``` After making your changes, push to your fork and [submit a pull request](https://github.com/glzr-io/zebar/pulls) against the `main` branch. Please try to address only a single feature or fix in the PR so that it's easy to review. ### Tips If using VSCode, it's recommended to use the [Rust Analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) extension. Get automatic linting by adding this to your VSCode's `settings.json`: ```json { "rust-analyzer.check.command": "clippy" } ``` ## Codebase overview Knowledge of the entire codebase should never be required to make changes. The following should hopefully help with understanding a particular part of the codebase. ### Crates GlazeWM is organized into several Rust crates: - `wm` (bin): Main application, which implements the core window management logic. - Gets installed to `C:\Program Files\glzr.io\glazewm.exe`. - `wm-cli` (bin/lib): CLI for interacting with the main application. - Gets installed to `C:\Program Files\glzr.io\cli\glazewm.exe`. This is added to `$PATH` by default. - `wm-common` (lib): Shared types, utilities, and constants used across other crates. - `wm-ipc-client` (lib): WebSocket client library for IPC with the main application. - `wm-platform` (lib): Wrappers over platform-specific API's - other crates don't interact directly with the Windows and macOS API's. - `wm-watcher` (bin): Watchdog process that ensures proper cleanup when the main application exits. - Gets installed to `C:\Program Files\glzr.io\glazewm-watcher.exe`. ### Commands & events GlazeWM uses a command-event architecture. The state of the WM (stored in [`WmState`](https://github.com/glzr-io/glazewm/blob/main/packages/wm/src/wm_state.rs)) is modified via [commands](https://github.com/glzr-io/glazewm/tree/main/packages/wm/src/commands) and [events](https://github.com/glzr-io/glazewm/tree/main/packages/wm/src/events). - Commands are run as a result of keybindings, IPC calls, the CLI (which calls IPC internally), or by being called from another command. Most commands are just for internal use and might not have a public-facing API. - Events arise from the Windows platform (e.g. a window being created, destroyed, focused, etc.). Each of these events have a handler that then modifies the WM state. Commands and events are processed in a loop in [`start_wm`](https://github.com/glzr-io/glazewm/blob/main/packages/wm/src/main.rs#L68). ## Container tree Windows in GlazeWM are organized within a tree hierarchy with the following "container" types: - Root - Monitors (physical displays) - Workspaces (virtual groups of windows) - Split containers (for tiling layouts) - Windows (application windows) Here's an example container tree: ``` Root | +----------------+----------------+ | | Monitor 1 Monitor 2 | | +--------+--------+ | | | | Workspace 1 Workspace 2 Workspace 3 [horizontal] [vertical] [horizontal] | | | | | | +----+----+ Tiling Window +-----+-----+ | | (Spotify) | | Tiling Window | Tiling Window Floating Window (Terminal) | (Discord) (Slack) | | Split Container [vertical] | +----+----+ | | Tiling Window Tiling Window (Chrome) (VS Code) ``` Windows can be either tiling (nested within split containers) or non-tiling (floating, minimized, maximized, or fullscreen). Non-tiling windows are always direct children of a workspace. Split containers can only have windows as children, and must have at least one child window. ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "2" members = ["packages/*"] default-members = ["packages/wm", "packages/wm-cli"] [workspace.dependencies] anyhow = { version = "1", features = ["backtrace"] } clap = { version = "4", features = ["derive"] } futures-util = "0.3" home = "0.5" serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["raw_value"] } tauri-winres = "0.1" thiserror = "2" regex = "1" tokio = { version = "1", features = ["full"] } tokio-tungstenite = "0.26" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1", features = ["v4", "serde"] } wm-macros = { path = "packages/wm-macros" } [workspace.lints] clippy.all = { level = "warn", priority = -1 } clippy.pedantic = { level = "warn", priority = -1 } clippy.missing_errors_doc = "allow" ================================================ FILE: LICENSE.md ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================
> V3 is finally out - check out the changelog [here](https://github.com/glzr-io/GlazeWM/releases) 🔥
GlazeWM logo
# GlazeWM **A tiling window manager for Windows inspired by i3wm.** [![Discord invite][discord-badge]][discord-link] [![Downloads][downloads-badge]][downloads-link] [![Good first issues][issues-badge]][issues-link] GlazeWM lets you easily organize windows and adjust their layout on the fly by using keyboard-driven commands. [Installation](#installation) • [Default keybindings](#default-keybindings) • [Config documentation](#config-documentation) • [FAQ](#faq) • [Contributing ↗](https://github.com/glzr-io/glazewm/blob/main/CONTRIBUTING.md) ![Demo video][demo-video]
### 🌟 Key features - Simple YAML configuration - Multi-monitor support - Customizable rules for specific windows - Easy one-click installation - Integration with [Zebar](https://github.com/glzr-io/zebar) as a status bar ## Installation **The latest version of GlazeWM is downloadable via [releases](https://github.com/glzr-io/GlazeWM/releases).** Zebar can optionally be installed as well via a checkbox during installation. GlazeWM is also available through several package managers: **Winget** ```sh winget install GlazeWM ``` **Chocolatey** ```sh choco install glazewm ``` **Scoop** ```sh scoop bucket add extras scoop install extras/glazewm ``` ## Contributing Help fix something that annoys you, or add a feature you've been wanting for a long time! Contributions are very welcome. Local development and guidelines are available in the [contributing guide](https://github.com/glzr-io/glazewm/blob/main/CONTRIBUTING.md). ## Default keybindings On the first launch of GlazeWM, a default configuration can optionally be generated. Below is a cheat sheet of all available commands and their default keybindings. ![Infographic](/resources/assets/cheatsheet.png) ## Config documentation The [default config](https://github.com/glzr-io/glazewm/blob/main/resources/assets/sample-config.yaml) file is generated at `%userprofile%\.glzr\glazewm\config.yaml`. To use a different config file location, you can launch the GlazeWM executable with the CLI argument `--config="..."`, like so: ```sh ./glazewm.exe start --config="C:\\config.yaml" ``` Or pass a value for the `GLAZEWM_CONFIG_PATH` environment variable: ```sh setx GLAZEWM_CONFIG_PATH "C:\\config.yaml" ``` With the benefit of using a custom path being that you can choose a different name for the config file, such as `glazewm.yaml`. ### Config: General ```yaml general: # Commands to run when the WM has started (e.g. to run a script or launch # another application). startup_commands: [] # Commands to run just before the WM is shutdown. shutdown_commands: [] # Commands to run after the WM config has reloaded. config_reload_commands: [] # Whether to automatically focus windows underneath the cursor. focus_follows_cursor: false # Whether to switch back and forth between the previously focused # workspace when focusing the current workspace. toggle_workspace_on_refocus: false cursor_jump: # Whether to automatically move the cursor on the specified trigger. enabled: true # Trigger for cursor jump: # - 'monitor_focus': Jump when focus changes between monitors. # - 'window_focus': Jump when focus changes between windows. trigger: "monitor_focus" ``` ### Config: Keybindings The available keyboard shortcuts can be customized via the `keybindings` option. A keybinding consists of one or more key combinations and one or more commands to run when pressed. It's recommended to use the alt key for keybindings. The Windows key is unfortunately a pain to remap, since the OS reserves certain keybindings (e.g. `lwin+l`). ```yaml keybindings: # Command(s) to run. - commands: ["focus --workspace 1"] # Key combination(s) to trigger the keybinding. bindings: ["alt+1"] # Multiple commands can be run in a sequence (e.g. to move a window to a # workspace + focus workspace). - commands: ["move --workspace 1", "focus --workspace 1"] bindings: ["alt+shift+1"] ``` **Full list of keys that can be used for keybindings:**
Keys list | Key | Description | | --------------------- | ------------------------------------------------------------------------- | | `a` - `z` | Alphabetical letter keys | | `0` - `9` | Number keys | | `numpad0` - `numpad9` | Numerical keypad keys | | `f1` - `f24` | Function keys | | `shift` | Either left or right SHIFT key | | `lshift` | The left SHIFT key | | `rshift` | The right SHIFT key | | `control` | Either left or right CTRL key | | `lctrl` | The left CTRL key | | `rctrl` | The right CTRL key | | `alt` | Either left or right ALT key | | `lalt` | The left ALT key | | `ralt` | The right ALT key | | `lwin` | The left ⊞ Windows logo key | | `rwin` | The right ⊞ Windows logo key | | `space` | The spacebar key | | `escape` | The ESCAPE key | | `back` | The BACKSPACE key | | `tab` | The TAB key | | `enter` | The ENTER key | | `left` | The ← arrow key | | `right` | The → arrow key | | `up` | The ↑ arrow key | | `down` | The ↓ arrow key | | `num_lock` | The NUM LOCK key | | `scroll_lock` | The SCROLL LOCK key | | `caps_lock` | The CAPS LOCK key | | `page_up` | The PAGE UP key | | `page_down` | The PAGE DOWN key | | `insert` | The INSERT key | | `delete` | The DELETE key | | `end` | The END key | | `home` | The HOME key | | `print_screen` | The PRINT SCREEN key | | `multiply` | The `*` key (only on numpad) | | `add` | The `+` key (only on numpad) | | `subtract` | The `-` key (only on numpad) | | `decimal` | The DEL key (only on numpad) | | `divide` | The `/` key (only on numpad) | | `volume_up` | The volume up key | | `volume_down` | The volume down key | | `volume_mute` | The volume mute key | | `media_next_track` | The media next track key | | `media_prev_track` | The media prev track key | | `media_stop` | The media stop key | | `media_play_pause` | The media play/pause key | | `oem_semicolon` | The `;`/`:` key on a US standard keyboard (varies by keyboard) | | `oem_question` | The `/`/`?` key on a US standard keyboard (varies by keyboard) | | `oem_tilde` | The `` ` ``/`~` key on a US standard keyboard (varies by keyboard) | | `oem_open_brackets` | The `[`/`{` key on a US standard keyboard (varies by keyboard) | | `oem_pipe` | The `\`/`\|` key on a US standard keyboard (varies by keyboard) | | `oem_close_brackets` | The `]`/`}` key on a US standard keyboard (varies by keyboard) | | `oem_quotes` | The `'`/`"` key on a US standard keyboard (varies by keyboard) | | `oem_8` | The `` ` ``/`¬` key on a UK keyboard (varies by keyboard) | | `oem_102` | The `\`/`\|` key next to left Shift on ISO keyboards (varies by keyboard) | | `oem_plus` | The `=`/`+` key on a US standard keyboard (varies by keyboard) | | `oem_comma` | The `,`/`<` key on a US standard keyboard (varies by keyboard) | | `oem_minus` | The `-`/`_` key on a US standard keyboard (varies by keyboard) | | `oem_period` | The `.`/`>` key on a US standard keyboard (varies by keyboard) | | `muhenkan` | The 無変換 (non-convert) key for Japanese keyboard layouts | | `henkan` | The 変換 (convert) key for Japanese keyboard layouts |
If a key is not in the list above, it is likely still supported if you use its character in a keybinding (e.g. `alt+å` for the Norwegian Å character). > German and US international keyboards treat the right-side alt key differently. For these keyboard layouts, use `ralt+ctrl` instead of `ralt` to bind the right-side alt key. ### Config: Gaps The gaps between windows can be changed via the `gaps` property in the config file. Inner and outer gaps are set separately. ```yaml gaps: # Gap between adjacent windows. inner_gap: "20px" # Gap between windows and the screen edge. outer_gap: top: "20px" right: "20px" bottom: "20px" left: "20px" ``` ### Config: Workspaces Workspaces need to be predefined via the `workspaces` property in the config file. A workspace is automatically assigned to each monitor on startup. ```yaml workspaces: # This is the unique ID for the workspace. It's used in keybinding # commands, and is also the label shown in 3rd-party apps (e.g. Zebar) if # `display_name` is not provided. - name: "1" # Optional override for the workspace label used in 3rd-party apps. # Does not need to be unique. display_name: "Work" # Optionally force the workspace on a specific monitor if it exists. # 0 is your leftmost screen, 1 is the next one to the right, and so on. bind_to_monitor: 0 # Optionally prevent workspace from being deactivated when empty. keep_alive: false ``` ### Config: Window rules Commands can be run when a window is first launched. This is useful for adding window-specific behaviors like always starting a window as fullscreen or assigning to a specific workspace. Windows can be targeted by their process, class, and title. Multiple matching criteria can be used together to target a window more precisely. ```yaml window_rules: - commands: ["move --workspace 1"] match: # Move browsers to workspace 1. - window_process: { regex: "msedge|brave|chrome" } - commands: ["ignore"] match: # Ignores any Zebar windows. - window_process: { equals: "zebar" } # Ignores picture-in-picture windows for browsers. # Note that *both* the title and class must match for the rule to run. - window_title: { regex: "[Pp]icture.in.[Pp]icture" } window_class: { regex: "Chrome_WidgetWin_1|MozillaDialogClass" } ``` ### Config: Window effects Visual effects can be applied to windows via the `window_effects` option. Currently, colored borders are the only effect available with more to come in the future. > Note: Window effects are exclusive to Windows 11. ```yaml window_effects: # Visual effects to apply to the focused window. focused_window: # Highlight the window with a colored border. border: enabled: true color: "#0000ff" # Visual effects to apply to non-focused windows. other_windows: border: enabled: false color: "#d3d3d3" ``` ### Config: Window behavior The `window_behavior` config option exists to customize the states that a window can be in (`tiling`, `floating`, `minimized`, and `fullscreen`). ```yaml window_behavior: # New windows are created in this state whenever possible. # Allowed values: 'tiling', 'floating'. initial_state: "tiling" # Sets the default options for when a new window is created. This also # changes the defaults for when the state change commands, like # `set-floating`, are used without any flags. state_defaults: floating: # Whether to center floating windows by default. centered: true # Whether to show floating windows as always on top. shown_on_top: false fullscreen: # Maximize the window if possible. If the window doesn't have a # maximize button, then it'll be made fullscreen normally instead. maximized: false ``` ### Config: Binding modes Binding modes are used to modify keybindings while GlazeWM is running. A binding mode can be enabled with `wm-enable-binding-mode --name ` and disabled with `wm-disable-binding-mode --name `. ```yaml binding_modes: # When enabled, the focused window can be resized via arrow keys or HJKL. - name: "resize" keybindings: - commands: ["resize --width -2%"] bindings: ["h", "left"] - commands: ["resize --width +2%"] bindings: ["l", "right"] - commands: ["resize --height +2%"] bindings: ["k", "up"] - commands: ["resize --height -2%"] bindings: ["j", "down"] # Press enter/escape to return to default keybindings. - commands: ["wm-disable-binding-mode --name resize"] bindings: ["escape", "enter"] ``` ## FAQ **Q: How do I run GlazeWM on startup?** Right-click the GlazeWM icon in the system tray and select "Run on system startup". **Q: How can I create ``?** You can create custom layouts by changing the tiling direction with `alt+v`. This changes where the next window is placed _in relation to the current window_. If the current window's direction is horizontal, the new window will be placed to the right of it. If it is vertical, it will be placed below it. This also applies when moving windows; the tiling direction of the stationary window will affect where the moved window will be placed. Community-made scripts like [Dutch-Raptor/GAT-GWM](https://github.com/Dutch-Raptor/GAT-GWM) and [burgr033/GlazeWM-autotiling-python](https://github.com/burgr033/GlazeWM-autotiling-python) can be used to automatically change the tiling direction. Native support for automatic layouts isn't _currently_ supported. **Q: How do I create a rule for ``?** To match a specific application, you need a command to execute and either the window's process name, title, or class name. For example, if you use Flow-Launcher and want to make the settings window float, you can do the following: ```yaml window_rules: - commands: ["set-floating"] match: - window_process: { equals: "Flow.Launcher" } window_title: { equals: "Settings" } ``` Programs like Winlister or AutoHotkey's Window Spy can be useful for getting info about a window. **Q: How can I ignore GlazeWM's keybindings when `` is focused?** This isn't currently supported, however, the keybinding `alt+shift+p` in the default config is used to disable all other keybindings until `alt+shift+p` is pressed again. [discord-badge]: https://img.shields.io/discord/1041662798196908052.svg?logo=discord&colorB=7289DA [discord-link]: https://discord.gg/ud6z3qjRvM [downloads-badge]: https://img.shields.io/github/downloads/glzr-io/glazewm/total?logo=github&logoColor=white [downloads-link]: https://github.com/glzr-io/glazewm/releases [issues-badge]: https://img.shields.io/badge/good_first_issues-7057ff [issues-link]: https://github.com/orgs/glzr-io/projects/4/views/1?sliceBy%5Bvalue%5D=good+first+issue [demo-video]: resources/assets/demo.webp ================================================ FILE: README_zh.md ================================================
> V3 终于发布了 - 查看更新日志 [这里](https://github.com/glzr-io/GlazeWM/releases) 🔥
GlazeWM logo
# GlazeWM **一个受 i3wm 启发的 Windows 平铺窗口管理器。** [![Discord invite][discord-badge]][discord-link] [![Downloads][downloads-badge]][downloads-link] [![Good first issues][issues-badge]][issues-link] GlazeWM 让您可以通过键盘驱动的命令轻松组织窗口并即时调整其布局。 [安装](#安装) • [默认快捷键](#默认快捷键) • [配置文档](#配置文档) • [常见问题](#常见问题) • [贡献 ↗](https://github.com/glzr-io/glazewm/blob/main/CONTRIBUTING.md) ![Demo video][demo-video]
### 🌟 主要特性 - 简单的 YAML 配置 - 多显示器支持 - 针对特定窗口的可自定义规则 - 简单的一键安装 - 与 [Zebar](https://github.com/glzr-io/zebar) 状态栏集成 ## 安装 **GlazeWM 的最新版本可通过 [releases](https://github.com/glzr-io/GlazeWM/releases) 下载。** 在安装过程中可以通过复选框选择性安装 Zebar。 GlazeWM 也可以通过多个包管理器获得: **Winget** ```sh winget install GlazeWM ``` **Chocolatey** ```sh choco install glazewm ``` **Scoop** ```sh scoop bucket add extras scoop install extras/glazewm ``` ## 贡献 帮助修复困扰您的问题,或添加您一直想要的功能!我们非常欢迎贡献。 本地开发和指南可在 [贡献指南](https://github.com/glzr-io/glazewm/blob/main/CONTRIBUTING.md) 中找到。 ## 默认快捷键 在 GlazeWM 首次启动时,可以选择性地生成默认配置。 以下是所有可用命令及其默认快捷键的速查表。 ![Infographic](/resources/assets/cheatsheet_cn_ZH.png) ## 配置文档 [默认配置](https://github.com/glzr-io/glazewm/blob/main/resources/assets/sample-config.yaml) 文件生成在 `%userprofile%\.glzr\glazewm\config.yaml`。 要使用不同的配置文件位置,您可以使用 CLI 参数 `--config="..."` 启动 GlazeWM 可执行文件,如下所示: ```sh ./glazewm.exe start --config="C:\<配置文件路径>\config.yaml" ``` 或者为 `GLAZEWM_CONFIG_PATH` 环境变量传递一个值: ```sh setx GLAZEWM_CONFIG_PATH "C:\<配置文件路径>\config.yaml" ``` 使用自定义路径的好处是您可以为配置文件选择不同的名称,例如 `glazewm.yaml`。 ### 配置:常规 ```yaml general: # WM 启动时运行的命令(例如运行脚本或启动另一个应用程序)。 startup_commands: [] # WM 关闭前运行的命令。 shutdown_commands: [] # WM 配置重新加载后运行的命令。 config_reload_commands: [] # 是否自动聚焦光标下方的窗口。 focus_follows_cursor: false # 当聚焦当前工作区时,是否在先前聚焦的工作区之间来回切换。 toggle_workspace_on_refocus: false cursor_jump: # 是否在指定触发器上自动移动光标。 enabled: true # 光标跳转的触发器: # - 'monitor_focus': 当焦点在显示器之间切换时跳转。 # - 'window_focus': 当焦点在窗口之间切换时跳转。 trigger: "monitor_focus" ``` ### 配置:快捷键 可用的键盘快捷键可以通过 `keybindings` 选项自定义。快捷键由一个或多个按键组合和按下时运行的一个或多个命令组成。 建议使用 alt 键作为快捷键。不幸的是,Windows 键很难重新映射,因为操作系统保留了某些快捷键(例如 `lwin+l`)。 ```yaml keybindings: # 要运行的命令。 - commands: ["focus --workspace 1"] # 触发快捷键的按键组合。 bindings: ["alt+1"] # 可以按顺序运行多个命令(例如将窗口移动到工作区 + 聚焦工作区)。 - commands: ["move --workspace 1", "focus --workspace 1"] bindings: ["alt+shift+1"] ``` **可用于快捷键的完整按键列表:**
按键列表 | 按键 | 描述 | | --------------------- | -------------------------------------------------------------- | | `a` - `z` | 字母键 | | `0` - `9` | 数字键 | | `numpad0` - `numpad9` | 数字小键盘键 | | `f1` - `f24` | 功能键 | | `shift` | 左或右 SHIFT 键 | | `lshift` | 左 SHIFT 键 | | `rshift` | 右 SHIFT 键 | | `control` | 左或右 CTRL 键 | | `lctrl` | 左 CTRL 键 | | `rctrl` | 右 CTRL 键 | | `alt` | 左或右 ALT 键 | | `lalt` | 左 ALT 键 | | `ralt` | 右 ALT 键 | | `lwin` | 左 ⊞ Windows 徽标键 | | `rwin` | 右 ⊞ Windows 徽标键 | | `space` | 空格键 | | `escape` | ESCAPE 键 | | `back` | BACKSPACE 键 | | `tab` | TAB 键 | | `enter` | ENTER 键 | | `left` | ← 方向键 | | `right` | → 方向键 | | `up` | ↑ 方向键 | | `down` | ↓ 方向键 | | `num_lock` | NUM LOCK 键 | | `scroll_lock` | SCROLL LOCK 键 | | `caps_lock` | CAPS LOCK 键 | | `page_up` | PAGE UP 键 | | `page_down` | PAGE DOWN 键 | | `insert` | INSERT 键 | | `delete` | DELETE 键 | | `end` | END 键 | | `home` | HOME 键 | | `print_screen` | PRINT SCREEN 键 | | `multiply` | `*` 键(仅限数字小键盘) | | `add` | `+` 键(仅限数字小键盘) | | `subtract` | `-` 键(仅限数字小键盘) | | `decimal` | DEL 键(仅限数字小键盘) | | `divide` | `/` 键(仅限数字小键盘) | | `volume_up` | 音量增加键 | | `volume_down` | 音量减少键 | | `volume_mute` | 静音键 | | `media_next_track` | 媒体下一曲键 | | `media_prev_track` | 媒体上一曲键 | | `media_stop` | 媒体停止键 | | `media_play_pause` | 媒体播放/暂停键 | | `oem_semicolon` | 美式标准键盘上的 `;`/`:` 键(因键盘而异) | | `oem_question` | 美式标准键盘上的 `/`/`?` 键(因键盘而异) | | `oem_tilde` | 美式标准键盘上的 `` ` ``/`~` 键(因键盘而异) | | `oem_open_brackets` | 美式标准键盘上的 `[`/`{` 键(因键盘而异) | | `oem_pipe` | 美式标准键盘上的 `\`/`\|` 键(因键盘而异) | | `oem_close_brackets` | 美式标准键盘上的 `]`/`}` 键(因键盘而异) | | `oem_quotes` | 美式标准键盘上的 `'`/`"` 键(因键盘而异) | | `oem_plus` | 美式标准键盘上的 `=`/`+` 键(因键盘而异) | | `oem_comma` | 美式标准键盘上的 `,`/`<` 键(因键盘而异) | | `oem_minus` | 美式标准键盘上的 `-`/`_` 键(因键盘而异) | | `oem_period` | 美式标准键盘上的 `.`/`>` 键(因键盘而异) |
如果某个按键不在上述列表中,如果您在快捷键中使用其字符,它很可能仍然受支持(例如挪威语 Å 字符的 `alt+å`)。 > 德语和美式国际键盘对右侧 alt 键的处理不同。对于这些键盘布局,请使用 `ralt+ctrl` 而不是 `ralt` 来绑定右侧 alt 键。 ### 配置:间隙 窗口之间的间隙可以通过配置文件中的 `gaps` 属性更改。内部和外部间隙分别设置。 ```yaml gaps: # 相邻窗口之间的间隙。 inner_gap: "20px" # 窗口与屏幕边缘之间的间隙。 outer_gap: top: "20px" right: "20px" bottom: "20px" left: "20px" ``` ### 配置:工作区 工作区需要通过配置文件中的 `workspaces` 属性预定义。启动时,每个显示器会自动分配一个工作区。 ```yaml workspaces: # 这是工作区的唯一 ID。它用于快捷键命令,如果未提供 `display_name`, # 它也是第三方应用程序(例如 Zebar)中显示的标签。 - name: "1" # 第三方应用程序中使用的工作区标签的可选覆盖。 # 不需要是唯一的。 display_name: "工作" # 如果存在,可选择强制工作区在特定显示器上。 # 0 是您最左边的屏幕,1 是右边的下一个,依此类推。 bind_to_monitor: 0 # 可选择防止工作区在空时被停用。 keep_alive: false ``` ### 配置:窗口规则 可以在窗口首次启动时运行命令。这对于添加特定于窗口的行为很有用,比如始终以全屏模式启动窗口或分配到特定工作区。 窗口可以通过其进程、类和标题进行定位。可以一起使用多个匹配条件来更精确地定位窗口。 ```yaml window_rules: - commands: ["move --workspace 1"] match: # 将浏览器移动到工作区 1。 - window_process: { regex: "msedge|brave|chrome" } - commands: ["ignore"] match: # 忽略任何 Zebar 窗口。 - window_process: { equals: "zebar" } # 忽略浏览器的画中画窗口。 # 注意标题和类都必须匹配才能运行规则。 - window_title: { regex: "[Pp]icture.in.[Pp]icture" } window_class: { regex: "Chrome_WidgetWin_1|MozillaDialogClass" } ``` ### 配置:窗口效果 可以通过 `window_effects` 选项对窗口应用视觉效果。目前,彩色边框是唯一可用的效果,未来会有更多效果。 > 注意:窗口效果仅适用于 Windows 11。 ```yaml window_effects: # 应用于聚焦窗口的视觉效果。 focused_window: # 用彩色边框突出显示窗口。 border: enabled: true color: "#0000ff" # 应用于非聚焦窗口的视觉效果。 other_windows: border: enabled: false color: "#d3d3d3" ``` ### 配置:窗口行为 `window_behavior` 配置选项用于自定义窗口可以处于的状态(`tiling`、`floating`、`minimized` 和 `fullscreen`)。 ```yaml window_behavior: # 新窗口在可能的情况下以此状态创建。 # 允许的值:'tiling'、'floating'。 initial_state: "tiling" # 设置创建新窗口时的默认选项。这也会更改状态更改命令 # (如 `set-floating`)在不使用任何标志时的默认值。 state_defaults: floating: # 是否默认居中浮动窗口。 centered: true # 是否将浮动窗口显示为始终在顶部。 shown_on_top: false fullscreen: # 如果可能,最大化窗口。如果窗口没有最大化按钮, # 则会正常全屏显示。 maximized: false ``` ### 配置:绑定模式 绑定模式用于在 GlazeWM 运行时修改快捷键。 可以使用 `wm-enable-binding-mode --name <名称>` 启用绑定模式,使用 `wm-disable-binding-mode --name <名称>` 禁用。 ```yaml binding_modes: # 启用时,可以通过方向键或 HJKL 调整聚焦窗口的大小。 - name: "resize" keybindings: - commands: ["resize --width -2%"] bindings: ["h", "left"] - commands: ["resize --width +2%"] bindings: ["l", "right"] - commands: ["resize --height +2%"] bindings: ["k", "up"] - commands: ["resize --height -2%"] bindings: ["j", "down"] # 按 enter/escape 返回默认快捷键。 - commands: ["wm-disable-binding-mode --name resize"] bindings: ["escape", "enter"] ``` ## 常见问题 **问:如何在启动时运行 GlazeWM?** 通过右键单击 GlazeWM 可执行文件 -> `创建快捷方式` 为可执行文件创建快捷方式。将快捷方式放在启动文件夹中,您可以通过在文件资源管理器的顶部栏中输入 `shell:startup` 来访问该文件夹。 **问:如何创建 `<插入布局>`?** 您可以通过使用 `alt+v` 更改平铺方向来创建自定义布局。这会改变下一个窗口相对于当前窗口的放置位置。如果当前窗口的方向是水平的,新窗口将放置在其右侧。如果是垂直的,将放置在其下方。这也适用于移动窗口;固定窗口的平铺方向将影响移动窗口的放置位置。 社区制作的脚本如 [Dutch-Raptor/GAT-GWM](https://github.com/Dutch-Raptor/GAT-GWM) 和 [burgr033/GlazeWM-autotiling-python](https://github.com/burgr033/GlazeWM-autotiling-python) 可用于自动更改平铺方向。目前不支持自动布局的原生支持。 **问:如何为 `<插入应用程序>` 创建规则?** 要匹配特定应用程序,您需要一个要执行的命令以及窗口的进程名称、标题或类名称。例如,如果您使用 Flow-Launcher 并希望设置窗口浮动,您可以执行以下操作: ```yaml window_rules: - commands: ["set-floating"] match: - window_process: { equals: "Flow.Launcher" } window_title: { equals: "Settings" } ``` 像 Winlister 或 AutoHotkey 的 Window Spy 这样的程序对于获取窗口信息很有用。 **问:当 `<插入应用程序>` 聚焦时,如何忽略 GlazeWM 的快捷键?** 目前不支持此功能,但是,默认配置中的快捷键 `alt+shift+p` 用于禁用所有其他快捷键,直到再次按下 `alt+shift+p`。 [discord-badge]: https://img.shields.io/discord/1041662798196908052.svg?logo=discord&colorB=7289DA [discord-link]: https://discord.gg/ud6z3qjRvM [downloads-badge]: https://img.shields.io/github/downloads/glzr-io/glazewm/total?logo=github&logoColor=white [downloads-link]: https://github.com/glzr-io/glazewm/releases [issues-badge]: https://img.shields.io/badge/good_first_issues-7057ff [issues-link]: https://github.com/orgs/glzr-io/projects/4/views/1?sliceBy%5Bvalue%5D=good+first+issue [demo-video]: resources/assets/demo.webp ================================================ FILE: clippy.toml ================================================ # TODO: Would ideally move this to `Cargo.toml`, but it's not supported yet. # Ref: https://github.com/rust-lang/cargo/issues/12917#issuecomment-1795069197 doc-valid-idents = ["AppKit", "DisplayPort", ".."] ================================================ FILE: packages/wm/Cargo.toml ================================================ [package] name = "wm" version = "0.0.0" description = "GlazeWM is a tiling window manager for Windows inspired by i3 and Polybar." repository = "https://github.com/glzr-io/glazewm" license = "GPL-3" edition = "2021" default-run = "glazewm" [[bin]] name = "glazewm" path = "src/main.rs" [features] ui_access = [] [build-dependencies] tauri-winres = { workspace = true } [dependencies] anyhow = { workspace = true } auto-launch = "0.5" ambassador = "0.4" clap = { workspace = true } enum-as-inner = "0.6" futures-util = { workspace = true } home = { workspace = true } image = "0.25" serde = { workspace = true } serde_json = { workspace = true } serde_yaml = "0.9" shell-util = "0.0" tokio = { workspace = true } tokio-tungstenite = { workspace = true } tracing = { workspace = true } tracing-appender = "0.2" tracing-subscriber = { workspace = true } tray-icon = "0.21" uuid = { workspace = true } wm-cli = { path = "../wm-cli" } wm-common = { path = "../wm-common" } wm-ipc-client = { path = "../wm-ipc-client" } wm-macros = { workspace = true } wm-platform = { path = "../wm-platform" } ================================================ FILE: packages/wm/build.rs ================================================ use tauri_winres::VersionInfo; fn main() { println!("cargo:rerun-if-env-changed=VERSION_NUMBER"); let mut res = tauri_winres::WindowsResource::new(); // When the `ui_access` feature is enabled, the `uiAccess` attribute is // set to `true`. UIAccess is disabled by default because it requires the // application to be signed and installed in a secure location. let ui_access = { #[cfg(feature = "ui_access")] { "true" } #[cfg(not(feature = "ui_access"))] { "false" } }; // Conditionally enable UIAccess, which grants privilege to set the // foreground window and to set the position of elevated windows. // // Ref: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/user-account-control-only-elevate-uiaccess-applications-that-are-installed-in-secure-locations // // Additionally, declare support for per-monitor DPI awareness. let manifest_str = format!( r#" true PerMonitorV2 "# ); res.set_manifest(&manifest_str); res.set_icon("../../resources/assets/icon.ico"); // Set language to English (US). res.set_language(0x0409); res.set("OriginalFilename", "glazewm.exe"); res.set("ProductName", "GlazeWM"); res.set("FileDescription", "GlazeWM"); let version_parts = env!("VERSION_NUMBER") .split('.') .take(3) .map(|part| part.parse().unwrap_or(0)) .collect::>(); let [major, minor, patch] = <[u16; 3]>::try_from(version_parts).unwrap_or([0, 0, 0]); let version_str = format!("{major}.{minor}.{patch}.0"); res.set("FileVersion", &version_str); res.set("ProductVersion", &version_str); let version_u64 = (u64::from(major) << 48) | (u64::from(minor) << 32) | (u64::from(patch) << 16); res.set_version_info(VersionInfo::FILEVERSION, version_u64); res.set_version_info(VersionInfo::PRODUCTVERSION, version_u64); res.compile().unwrap(); } ================================================ FILE: packages/wm/src/commands/container/attach_container.rs ================================================ use anyhow::bail; use super::resize_tiling_container; use crate::{ models::Container, traits::{CommonGetters, TilingSizeGetters}, }; /// Inserts a child container at the specified index. /// /// The inserted child will be resized to fit the available space. pub fn attach_container( child: &Container, target_parent: &Container, target_index: Option, ) -> anyhow::Result<()> { if !child.is_detached() { bail!("Cannot attach an already attached container."); } if let Some(target_index) = target_index { // Ensure target index is within the bounds of the parent's children. let target_index = target_index.clamp(0, target_parent.child_count()); // Insert the child at the specified index. target_parent .borrow_children_mut() .insert(target_index, child.clone()); } else { target_parent.borrow_children_mut().push_back(child.clone()); } target_parent .borrow_child_focus_order_mut() .push_back(child.id()); *child.borrow_parent_mut() = Some(target_parent.clone()); // Resize the child and its siblings if it is a tiling container. if let Ok(child) = child.as_tiling_container() { let tiling_siblings = child.tiling_siblings().collect::>(); if tiling_siblings.is_empty() { child.set_tiling_size(1.0); return Ok(()); } // Set initial tiling size to 0, and then size up the container // to the target size. #[allow(clippy::cast_precision_loss)] let target_size = 1.0 / (tiling_siblings.len() + 1) as f32; child.set_tiling_size(0.0); resize_tiling_container(&child, target_size); } Ok(()) } ================================================ FILE: packages/wm/src/commands/container/detach_container.rs ================================================ use anyhow::Context; use super::flatten_split_container; use crate::{ models::Container, traits::{CommonGetters, TilingSizeGetters, MIN_TILING_SIZE}, }; /// Removes a container from the tree. /// /// If the container is a tiling container, the siblings will be resized to /// fill the freed up space. Will flatten empty parent split containers. #[allow(clippy::needless_pass_by_value)] pub fn detach_container(child_to_remove: Container) -> anyhow::Result<()> { // Flatten the parent split container if it'll be empty after removing // the child. if let Some(split_parent) = child_to_remove .parent() .and_then(|parent| parent.as_split().cloned()) { if split_parent.child_count() == 1 { flatten_split_container(split_parent)?; } } let parent = child_to_remove.parent().context("No parent.")?; parent .borrow_children_mut() .retain(|c| c.id() != child_to_remove.id()); parent .borrow_child_focus_order_mut() .retain(|id| *id != child_to_remove.id()); *child_to_remove.borrow_parent_mut() = None; // Resize the siblings if it is a tiling container. if let Ok(child_to_remove) = child_to_remove.as_tiling_container() { let tiling_siblings = parent.tiling_children().collect::>(); // TODO: Share logic with `resize_tiling_container`. let available_size = tiling_siblings.iter().fold(0.0, |sum, container| { sum + container.tiling_size() - MIN_TILING_SIZE }); // Adjust size of the siblings based on the freed up space. for sibling in &tiling_siblings { let resize_factor = (sibling.tiling_size() - MIN_TILING_SIZE) / available_size; let size_delta = resize_factor * child_to_remove.tiling_size(); sibling.set_tiling_size(sibling.tiling_size() + size_delta); } } Ok(()) } ================================================ FILE: packages/wm/src/commands/container/flatten_child_split_containers.rs ================================================ use super::flatten_split_container; use crate::{ models::Container, traits::{CommonGetters, TilingDirectionGetters}, }; /// Flattens any redundant split containers at the top-level of the given /// parent container. /// /// For example: /// ```ignore,compile_fail /// H[1 H[V[2, 3]]] -> H[1, 2, 3] /// H[1 H[2, 3]] -> H[1, 2, 3] /// H[V[1]] -> V[1] /// ``` pub fn flatten_child_split_containers( parent: &Container, ) -> anyhow::Result<()> { if let Ok(parent) = parent.as_direction_container() { // Get children that are either tiling windows or split containers. let tiling_children = parent .children() .into_iter() .filter(|child| child.is_tiling_window() || child.is_split()) .collect::>(); if tiling_children.len() == 1 { // Handle case where the parent is a split container and has a // single split container child. if let Some(split_child) = tiling_children[0].as_split() { flatten_split_container(split_child.clone())?; parent.set_tiling_direction(parent.tiling_direction().inverse()); } } else { let split_children = tiling_children .into_iter() .filter_map(|child| child.as_split().cloned()) .collect::>(); for split_child in split_children.iter().filter(|split_child| { split_child.tiling_direction() == parent.tiling_direction() }) { // Additionally flatten redundant top-level split containers in // the child. if split_child.child_count() == 1 { if let Some(split_grandchild) = split_child.children()[0].as_split() { flatten_split_container(split_grandchild.clone())?; } } flatten_split_container(split_child.clone())?; } } } Ok(()) } ================================================ FILE: packages/wm/src/commands/container/flatten_split_container.rs ================================================ use std::collections::VecDeque; use anyhow::Context; use crate::{ models::SplitContainer, traits::{CommonGetters, TilingSizeGetters}, }; /// Removes a split container from the tree and moves its children /// into the parent container. /// /// The children will be resized to fit the size of the split container. #[allow(clippy::needless_pass_by_value)] pub fn flatten_split_container( split_container: SplitContainer, ) -> anyhow::Result<()> { let parent = split_container.parent().context("No parent.")?; let updated_children = split_container.children().into_iter().inspect(|child| { *child.borrow_parent_mut() = Some(parent.clone()); // Resize tiling children to fit the size of the split container. if let Ok(tiling_child) = child.as_tiling_container() { tiling_child.set_tiling_size( split_container.tiling_size() * tiling_child.tiling_size(), ); } }); let index = split_container.index(); let focus_index = split_container.focus_index(); // Insert child at its original index in the parent. for (child_index, child) in updated_children.enumerate() { parent .borrow_children_mut() .insert(index + child_index, child); } // Insert child at its original focus index in the parent. for (child_focus_index, child_id) in split_container .borrow_child_focus_order() .iter() .enumerate() { parent .borrow_child_focus_order_mut() .insert(focus_index + child_focus_index, *child_id); } // Remove the split container from the tree. parent .borrow_children_mut() .retain(|c| c.id() != split_container.id()); parent .borrow_child_focus_order_mut() .retain(|id| *id != split_container.id()); *split_container.borrow_parent_mut() = None; *split_container.borrow_children_mut() = VecDeque::new(); Ok(()) } ================================================ FILE: packages/wm/src/commands/container/focus_container_by_id.rs ================================================ use anyhow::Context; use uuid::Uuid; use super::set_focused_descendant; use crate::wm_state::WmState; pub fn focus_container_by_id( container_id: &Uuid, state: &mut WmState, ) -> anyhow::Result<()> { let focus_target = state .container_by_id(*container_id) .context("No container with given id")?; // Set focus to the target container. set_focused_descendant(&focus_target, None); state.pending_sync.queue_focus_change().queue_cursor_jump(); Ok(()) } ================================================ FILE: packages/wm/src/commands/container/focus_in_direction.rs ================================================ use anyhow::Context; use wm_common::{TilingDirection, WindowState}; use wm_platform::Direction; use super::set_focused_descendant; use crate::{ models::{Container, TilingContainer}, traits::{CommonGetters, TilingDirectionGetters, WindowGetters}, wm_state::WmState, }; pub fn focus_in_direction( origin_container: &Container, direction: &Direction, state: &mut WmState, ) -> anyhow::Result<()> { let focus_target = match origin_container { Container::TilingWindow(_) => { // If a suitable focus target isn't found in the current workspace, // attempt to find a workspace in the given direction. tiling_focus_target(origin_container, direction)?.map_or_else( || workspace_focus_target(origin_container, direction, state), |container| Ok(Some(container)), )? } Container::NonTilingWindow(ref non_tiling_window) => { match non_tiling_window.state() { WindowState::Floating(_) => { floating_focus_target(origin_container, direction) } WindowState::Fullscreen(_) => { workspace_focus_target(origin_container, direction, state)? } _ => None, } } Container::Workspace(_) => { workspace_focus_target(origin_container, direction, state)? } _ => None, }; // Set focus to the target container. if let Some(focus_target) = focus_target { set_focused_descendant(&focus_target, None); state.pending_sync.queue_focus_change().queue_cursor_jump(); } Ok(()) } fn floating_focus_target( origin_container: &Container, direction: &Direction, ) -> Option { let is_floating = |sibling: &Container| { sibling.as_non_tiling_window().is_some_and(|window| { matches!(window.state(), WindowState::Floating(_)) }) }; let mut floating_siblings = origin_container.siblings().filter(is_floating); // Wrap if next/previous floating window is not found. match direction { Direction::Left => origin_container .next_siblings() .find(is_floating) .or_else(|| floating_siblings.last()), Direction::Right => origin_container .prev_siblings() .find(is_floating) .or_else(|| floating_siblings.next()), // Cannot focus vertically from a floating window. _ => None, } } /// Gets a focus target within the current workspace. Traverse upwards from /// the origin container to find an adjacent container that can be focused. fn tiling_focus_target( origin_container: &Container, direction: &Direction, ) -> anyhow::Result> { let tiling_direction = TilingDirection::from_direction(direction); let mut origin_or_ancestor = origin_container.clone(); // Traverse upwards from the focused container. Stop searching when a // workspace is encountered. while !origin_or_ancestor.is_workspace() { let parent = origin_or_ancestor .parent() .and_then(|parent| parent.as_direction_container().ok()) .context("No direction container.")?; // Skip if the tiling direction doesn't match. if parent.tiling_direction() != tiling_direction { origin_or_ancestor = parent.into(); continue; } // Get the next/prev tiling sibling depending on the tiling direction. let focus_target = match direction { Direction::Up | Direction::Left => origin_or_ancestor .prev_siblings() .find_map(|c| c.as_tiling_container().ok()), _ => origin_or_ancestor .next_siblings() .find_map(|c| c.as_tiling_container().ok()), }; match focus_target { Some(target) => { // Return once a suitable focus target is found. return Ok(match target { TilingContainer::TilingWindow(_) => Some(target.into()), TilingContainer::Split(split) => split .descendant_in_direction(&direction.inverse()) .map(Into::into), }); } None => origin_or_ancestor = parent.into(), } } Ok(None) } /// Gets a focus target outside of the current workspace in the given /// direction. /// /// This will descend into the workspace in the given direction, and will /// always return a tiling container. This makes it different from the /// `focus_workspace` command with `FocusWorkspaceTarget::Direction`. fn workspace_focus_target( origin_container: &Container, direction: &Direction, state: &WmState, ) -> anyhow::Result> { let monitor = origin_container.monitor().context("No monitor.")?; let target_workspace = state .monitor_in_direction(&monitor, direction)? .and_then(|monitor| monitor.displayed_workspace()); let focused_fullscreen = target_workspace .as_ref() .and_then(|workspace| workspace.descendant_focus_order().next()) .filter(|focused| match focused { Container::NonTilingWindow(window) => { matches!(window.state(), WindowState::Fullscreen(_)) } _ => false, }); let focus_target = focused_fullscreen .or_else(|| { target_workspace.as_ref().and_then(|workspace| { workspace .descendant_in_direction(&direction.inverse()) .map(Into::into) }) }) .or(target_workspace.map(Into::into)); Ok(focus_target) } ================================================ FILE: packages/wm/src/commands/container/mod.rs ================================================ mod attach_container; mod detach_container; mod flatten_child_split_containers; mod flatten_split_container; mod focus_container_by_id; mod focus_in_direction; mod move_container_within_tree; mod replace_container; mod resize_tiling_container; mod set_focused_descendant; mod toggle_tiling_direction; mod wrap_in_split_container; pub use attach_container::*; pub use detach_container::*; pub use flatten_child_split_containers::*; pub use flatten_split_container::*; pub use focus_container_by_id::*; pub use focus_in_direction::*; pub use move_container_within_tree::*; pub use replace_container::*; pub use resize_tiling_container::*; pub use set_focused_descendant::*; pub use toggle_tiling_direction::*; pub use wrap_in_split_container::*; ================================================ FILE: packages/wm/src/commands/container/move_container_within_tree.rs ================================================ use anyhow::Context; use wm_common::{VecDequeExt, WmEvent}; use super::{ attach_container, detach_container, flatten_child_split_containers, set_focused_descendant, }; use crate::{models::Container, traits::CommonGetters, wm_state::WmState}; /// Move a container to a new location in the tree. This detaches the /// container from its current parent and attaches it to the new parent at /// the specified index. /// /// If this container is a tiling container, its siblings are resized on /// detach, and the container is sized to the default tiling size with its /// new siblings. No changes to the container's tiling size are made if /// its parent stays the same. /// /// This will flatten any redundant split containers after moving the /// container, which can cause the target parent to become detached. For /// example, in the layout V[1 H[2]] where container 1 is moved down, the /// parent gets removed resulting in V[1 2]. pub fn move_container_within_tree( container_to_move: &Container, target_parent: &Container, target_index: usize, state: &WmState, ) -> anyhow::Result<()> { // Create iterator of parent, grandparent, and great-grandparent. let ancestors = container_to_move.ancestors().take(3).collect::>(); // Get lowest common ancestor (LCA) between `container_to_move` and // `target_parent`. This could be the `target_parent` itself. let lowest_common_ancestor = lowest_common_ancestor(container_to_move, target_parent) .context("No common ancestor between containers.")?; // If the container is already a child of the target parent, then shift // it to the target index. if container_to_move.parent().context("No parent.")? == *target_parent { target_parent .borrow_children_mut() .shift_to_index(target_index, container_to_move.clone()); if container_to_move.has_focus(None) { state.emit_event(WmEvent::FocusedContainerMoved { focused_container: container_to_move.to_dto()?, }); } return Ok(()); } // Handle case where target parent is the LCA. For example, when swapping // sibling containers or moving a container to a direct ancestor. if *target_parent == lowest_common_ancestor { return move_to_lowest_common_ancestor( container_to_move, &lowest_common_ancestor, target_index, state, ); } // Get ancestor of `container_to_move` that is a direct child of the LCA. // This could be the `container_to_move` itself. let container_to_move_ancestor = container_to_move .self_and_ancestors() .find(|ancestor| { ancestor.parent() == Some(lowest_common_ancestor.clone()) }) .context("Failed to get ancestor of container to move.")?; // Likewise, get ancestor of `target_parent` that is a direct child of // the LCA. let target_parent_ancestor = target_parent .self_and_ancestors() .find(|ancestor| { ancestor.parent() == Some(lowest_common_ancestor.clone()) }) .context("Failed to get ancestor of target parent.")?; // Get whether the container is the focused descendant in its original // subtree from the LCA. let is_focused_descendant = *container_to_move == container_to_move_ancestor || container_to_move .has_focus(Some(container_to_move_ancestor.clone())); // Get whether the ancestor of `container_to_move` appears before // `target_parent`'s ancestor in the child focus order of the LCA. let original_focus_index = container_to_move_ancestor.focus_index(); let is_subtree_focused = original_focus_index < target_parent_ancestor.focus_index(); detach_container(container_to_move.clone())?; attach_container( &container_to_move.clone(), &target_parent.clone(), Some(target_index), )?; // Set `container_to_move` as focused descendant within target subtree if // its original subtree had focus more recently (even if the container is // not the last focused within that subtree). if is_subtree_focused { set_focused_descendant( container_to_move, Some(&target_parent_ancestor), ); } // If the focused descendant is moved to the targets subtree, then the // target's ancestor should be placed before the original ancestor in // LCA's child focus order. if is_focused_descendant && is_subtree_focused { lowest_common_ancestor .borrow_child_focus_order_mut() .shift_to_index(original_focus_index, target_parent_ancestor.id()); } // After moving the container, flatten any redundant split containers. // For example, in the layout V[1 H[2]] where container 1 is moved down // to become V[H[1 2]], this will then need to be flattened to V[1 2]. for ancestor in ancestors.iter().rev() { flatten_child_split_containers(ancestor)?; } if container_to_move.has_focus(None) { state.emit_event(WmEvent::FocusedContainerMoved { focused_container: container_to_move.to_dto()?, }); } Ok(()) } fn move_to_lowest_common_ancestor( container_to_move: &Container, lowest_common_ancestor: &Container, target_index: usize, state: &WmState, ) -> anyhow::Result<()> { // Keep reference to focus index of container's ancestor in LCA's child // focus order. let original_focus_index = container_to_move .self_and_ancestors() .find(|ancestor| { ancestor.parent() == Some(lowest_common_ancestor.clone()) }) .map(|ancestor| ancestor.focus_index()) .context("Failed to get focus index of container's ancestor.")?; detach_container(container_to_move.clone())?; attach_container( &container_to_move.clone(), &lowest_common_ancestor.clone(), Some(target_index), )?; lowest_common_ancestor .borrow_child_focus_order_mut() .shift_to_index(original_focus_index, container_to_move.id()); if container_to_move.has_focus(None) { state.emit_event(WmEvent::FocusedContainerMoved { focused_container: container_to_move.to_dto()?, }); } Ok(()) } /// Gets the lowest container in the tree that has both `container_a` and /// `container_b` as descendants. pub fn lowest_common_ancestor( container_a: &Container, container_b: &Container, ) -> Option { let mut ancestor_a = Some(container_a.clone()); // Traverse upwards from container A. while let Some(current_ancestor_a) = ancestor_a { let mut ancestor_b = Some(container_b.clone()); // Traverse upwards from container B. while let Some(current_ancestor_b) = ancestor_b { if current_ancestor_a == current_ancestor_b { return Some(current_ancestor_a); } ancestor_b = current_ancestor_b.parent(); } ancestor_a = current_ancestor_a.parent(); } None } ================================================ FILE: packages/wm/src/commands/container/replace_container.rs ================================================ use anyhow::{bail, Context}; use wm_common::VecDequeExt; use super::{attach_container, detach_container, resize_tiling_container}; use crate::{ models::Container, traits::{CommonGetters, TilingSizeGetters}, }; /// Replaces a container at the specified index. /// /// The replaced container will be detached from the tree. pub fn replace_container( replacement_container: &Container, target_parent: &Container, target_index: usize, ) -> anyhow::Result<()> { if !replacement_container.is_detached() { bail!( "Cannot use an already attached container as replacement container." ); } let container_to_replace = target_parent .children() .get(target_index) .cloned() .with_context(|| format!("No container at index {target_index}."))?; let focus_index = container_to_replace.focus_index(); let tiling_size = container_to_replace .as_tiling_container() .map(|c| c.tiling_size()); // TODO: This will cause issues if the detach causes a wrapping split // container to flatten. Currently, that scenario shouldn't be possible. // We also can't attach first before detaching, because detaching // removes child based on ID and both containers might have the same ID. detach_container(container_to_replace)?; attach_container( replacement_container, target_parent, Some(target_index), )?; // Shift to the correct focus index. target_parent .borrow_child_focus_order_mut() .shift_to_index(focus_index, replacement_container.id()); // Match the tiling size of the replaced container if the replacement // is also a tiling container. if let Ok(tiling_size) = tiling_size { if let Ok(replacement_container) = replacement_container.as_tiling_container() { resize_tiling_container(&replacement_container, tiling_size); } } Ok(()) } ================================================ FILE: packages/wm/src/commands/container/resize_tiling_container.rs ================================================ use crate::{ models::TilingContainer, traits::{CommonGetters, TilingSizeGetters, MIN_TILING_SIZE}, }; pub fn resize_tiling_container( container_to_resize: &TilingContainer, target_size: f32, ) { let tiling_siblings = container_to_resize.tiling_siblings().collect::>(); // Ignore cases where the container is the only child. if tiling_siblings.is_empty() { container_to_resize.set_tiling_size(1.); return; } // Prevent the container from being smaller than the minimum size, and // larger than the space available from sibling containers. #[allow(clippy::cast_precision_loss)] let clamped_target_size = target_size.clamp( MIN_TILING_SIZE, 1. - (tiling_siblings.len() as f32 * MIN_TILING_SIZE), ); let size_delta = clamped_target_size - container_to_resize.tiling_size(); container_to_resize.set_tiling_size(clamped_target_size); // Get available tiling size amongst siblings. let available_size = tiling_siblings.iter().fold(0.0, |sum, container| { sum + container.tiling_size() - MIN_TILING_SIZE }); // Distribute the available tiling size amongst its siblings. for sibling in &tiling_siblings { // Get percentage of resize that affects this container. Siblings are // resized in proportion to their current size (i.e. larger containers // are shrunk more). let resize_factor = (sibling.tiling_size() - MIN_TILING_SIZE) / available_size; let size_delta = resize_factor * size_delta; sibling.set_tiling_size(sibling.tiling_size() - size_delta); } } ================================================ FILE: packages/wm/src/commands/container/set_focused_descendant.rs ================================================ use wm_common::VecDequeExt; use crate::{models::Container, traits::CommonGetters}; /// Set a given container as the focused container up to and including the /// end ancestor. pub fn set_focused_descendant( focused_descendant: &Container, end_ancestor: Option<&Container>, ) { let mut target = focused_descendant.clone(); // Traverse upwards, shifting the container's ancestors to the front in // their focus order. while let Some(parent) = target.parent() { parent .borrow_child_focus_order_mut() .shift_to_index(0, target.id()); // Exit if we've reached the end ancestor. if end_ancestor .as_ref() .is_some_and(|end_ancestor| target.id() == end_ancestor.id()) { break; } target = parent; } } ================================================ FILE: packages/wm/src/commands/container/toggle_tiling_direction.rs ================================================ use anyhow::Context; use wm_common::{TilingDirection, WmEvent}; use super::{flatten_split_container, wrap_in_split_container}; use crate::{ models::{Container, DirectionContainer, SplitContainer, TilingWindow}, traits::{CommonGetters, TilingDirectionGetters}, user_config::UserConfig, wm_state::WmState, }; pub fn toggle_tiling_direction( container: Container, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let direction_container = match container { Container::TilingWindow(tiling_window) => { toggle_window_direction(tiling_window, config) } Container::Workspace(workspace) => { workspace .set_tiling_direction(workspace.tiling_direction().inverse()); Ok(workspace.into()) } // Can only toggle tiling direction from a tiling window or workspace. _ => return Ok(()), }?; state.emit_event(WmEvent::TilingDirectionChanged { direction_container: direction_container.to_dto()?, new_tiling_direction: direction_container.tiling_direction(), }); Ok(()) } fn toggle_window_direction( tiling_window: TilingWindow, config: &UserConfig, ) -> anyhow::Result { let parent = tiling_window .direction_container() .context("No direction container.")?; // If the window is an only child, then either change the tiling // direction of its parent workspace or flatten its parent split // container. if tiling_window.tiling_siblings().count() == 0 { return match parent { DirectionContainer::Workspace(workspace) => { workspace .set_tiling_direction(workspace.tiling_direction().inverse()); Ok(workspace.into()) } DirectionContainer::Split(split_container) => { flatten_split_container(split_container.clone())?; tiling_window .direction_container() .context("No direction container.") } }; } // Create a new split container to wrap the window. let split_container = SplitContainer::new( parent.tiling_direction().inverse(), config.value.gaps.clone(), ); wrap_in_split_container( &split_container, &parent.into(), &[tiling_window.into()], )?; Ok(split_container.into()) } pub fn set_tiling_direction( container: Container, state: &mut WmState, config: &UserConfig, tiling_direction: &TilingDirection, ) -> anyhow::Result<()> { let direction_container = container .direction_container() .context("No direction container.")?; if direction_container.tiling_direction() == *tiling_direction { Ok(()) } else { toggle_tiling_direction(container, state, config) } } ================================================ FILE: packages/wm/src/commands/container/wrap_in_split_container.rs ================================================ use std::collections::VecDeque; use anyhow::Context; use crate::{ models::{Container, SplitContainer, TilingContainer}, traits::{CommonGetters, TilingSizeGetters}, }; pub fn wrap_in_split_container( split_container: &SplitContainer, target_parent: &Container, target_children: &[TilingContainer], ) -> anyhow::Result<()> { let starting_index = target_children .iter() .map(CommonGetters::index) .min() .context("Failed to get starting index.")?; target_parent .borrow_children_mut() .insert(starting_index, split_container.clone().into()); let starting_focus_index = target_children .iter() .map(CommonGetters::focus_index) .min() .context("Failed to get starting focus index.")?; target_parent .borrow_child_focus_order_mut() .insert(starting_focus_index, split_container.id()); // Get the total tiling size amongst all children. let total_tiling_size = target_children .iter() .map(TilingSizeGetters::tiling_size) .sum::(); let target_children_ids = target_children .iter() .map(CommonGetters::id) .collect::>(); let sorted_focus_ids = target_parent .borrow_child_focus_order() .iter() .filter(|id| target_children_ids.contains(id)) .copied() .collect::>(); // Set the split container's parent and tiling size. *split_container.borrow_parent_mut() = Some(target_parent.clone()); split_container.set_tiling_size(total_tiling_size); // Move the children from their original parent to the split container. for target_child in target_children { *target_child.borrow_parent_mut() = Some(split_container.clone().into()); split_container .borrow_children_mut() .push_back(target_child.clone().into()); target_parent .borrow_children_mut() .retain(|child| child != &target_child.clone().into()); target_parent .borrow_child_focus_order_mut() .retain(|id| id != &target_child.id()); // Scale the tiling size to the new split container. target_child .set_tiling_size(target_child.tiling_size() / total_tiling_size); } // Add original focus order to split container. *split_container.borrow_child_focus_order_mut() = sorted_focus_ids; Ok(()) } ================================================ FILE: packages/wm/src/commands/general/cycle_focus.rs ================================================ use anyhow::Context; use wm_common::WindowState; use crate::{ commands::container::set_focused_descendant, traits::{CommonGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; /// Cycles focus through windows of different states. In order, this will /// change from tiling -> floating -> fullscreen -> minimized, then back to /// tiling. /// /// Does nothing if a workspace is focused. #[allow(clippy::fn_params_excessive_bools)] pub fn cycle_focus( omit_floating: bool, omit_fullscreen: bool, omit_minimized: bool, omit_tiling: bool, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let focused_container = state.focused_container().context("No focused container.")?; if let Ok(window) = focused_container.as_window_container() { let workspace = window.workspace().context("No workspace.")?; let current = window.state(); let mut next = next_state(¤t, config); loop { // Break if we have cycled back to the current state. if current.is_same_state(&next) { break; } // Skip the next state if it is to be omitted. if (omit_floating && matches!(next, WindowState::Floating(_))) || omit_fullscreen && matches!(next, WindowState::Fullscreen(_)) || omit_minimized && matches!(next, WindowState::Minimized) || omit_tiling && matches!(next, WindowState::Tiling) { next = next_state(&next, config); continue; } // Get window that matches the next state. let window_of_type = workspace .descendant_focus_order() .filter_map(|descendant| descendant.as_window_container().ok()) .find(|descendant| { matches!( (descendant.state(), &next), (WindowState::Floating(_), WindowState::Floating(_)) | (WindowState::Fullscreen(_), WindowState::Fullscreen(_)) | (WindowState::Minimized, WindowState::Minimized) | (WindowState::Tiling, WindowState::Tiling) ) }); if let Some(window) = window_of_type { set_focused_descendant(&window.into(), None); state.pending_sync.queue_focus_change().queue_cursor_jump(); break; } next = next_state(&next, config); } } Ok(()) } fn next_state( current_state: &WindowState, config: &UserConfig, ) -> WindowState { match current_state { WindowState::Floating(_) => WindowState::Fullscreen( config .value .window_behavior .state_defaults .fullscreen .clone(), ), WindowState::Fullscreen(_) => WindowState::Minimized, WindowState::Minimized => WindowState::Tiling, WindowState::Tiling => WindowState::Floating( config.value.window_behavior.state_defaults.floating.clone(), ), } } ================================================ FILE: packages/wm/src/commands/general/disable_binding_mode.rs ================================================ use wm_common::WmEvent; use crate::wm_state::WmState; pub fn disable_binding_mode(name: &str, state: &mut WmState) { state.binding_modes = state .binding_modes .iter() .filter(|config| config.name != name) .cloned() .collect::>(); state.emit_event(WmEvent::BindingModesChanged { new_binding_modes: state.binding_modes.clone(), }); } ================================================ FILE: packages/wm/src/commands/general/enable_binding_mode.rs ================================================ use anyhow::Context; use wm_common::WmEvent; use crate::{user_config::UserConfig, wm_state::WmState}; pub fn enable_binding_mode( name: &str, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let binding_mode = config .value .binding_modes .iter() .find(|config| name == config.name) .with_context(|| { format!("No binding mode found with the name '{name}'.") })?; state.binding_modes = vec![binding_mode.clone()]; state.emit_event(WmEvent::BindingModesChanged { new_binding_modes: state.binding_modes.clone(), }); Ok(()) } ================================================ FILE: packages/wm/src/commands/general/mod.rs ================================================ mod cycle_focus; mod disable_binding_mode; mod enable_binding_mode; mod platform_sync; mod reload_config; mod shell_exec; mod toggle_pause; pub use cycle_focus::*; pub use disable_binding_mode::*; pub use enable_binding_mode::*; pub use platform_sync::*; pub use reload_config::*; pub use shell_exec::*; pub use toggle_pause::*; ================================================ FILE: packages/wm/src/commands/general/platform_sync.rs ================================================ use anyhow::Context; #[cfg(target_os = "windows")] use wm_common::WindowEffectConfig; use wm_common::{ CursorJumpTrigger, DisplayState, HideCorner, HideMethod, UniqueExt, WindowState, WmEvent, }; #[cfg(target_os = "windows")] use wm_platform::NativeWindowWindowsExt; #[cfg(target_os = "windows")] use wm_platform::{CornerStyle, OpacityValue}; use wm_platform::{Rect, WindowZOrder}; use crate::{ models::{Container, WindowContainer}, traits::{CommonGetters, PositionGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; pub fn platform_sync( state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let focused_container = state.focused_container().context("No focused container.")?; if state.pending_sync.needs_focus_update() { sync_focus(&focused_container, state)?; } if !state.pending_sync.containers_to_redraw().is_empty() || !state.pending_sync.workspaces_to_reorder().is_empty() { redraw_containers(&focused_container, state, config)?; } if state.pending_sync.needs_cursor_jump() && config.value.general.cursor_jump.enabled { jump_cursor(focused_container.clone(), state, config)?; } if state.pending_sync.needs_focused_effect_update() || state.pending_sync.needs_all_effects_update() { // Keep reference to the previous window that had focus effects // applied. let prev_effects_window = state.prev_effects_window.clone(); if let Ok(window) = focused_container.as_window_container() { apply_window_effects(&window, true, config); state.prev_effects_window = Some(window.clone()); } else { state.prev_effects_window = None; } // Get windows that should have the unfocused border applied to them. // For the sake of performance, we only update the border of the // previously focused window. If the `reset_window_effects` flag is // passed, the unfocused border is applied to all unfocused windows. let unfocused_windows = if state.pending_sync.needs_all_effects_update() { state.windows() } else { prev_effects_window.into_iter().collect() } .into_iter() .filter(|window| window.id() != focused_container.id()); for window in unfocused_windows { apply_window_effects(&window, false, config); } } state.pending_sync.clear(); Ok(()) } fn sync_focus( focused_container: &Container, state: &mut WmState, ) -> anyhow::Result<()> { let native_window = focused_container.as_window_container().ok(); // Sets focus to the appropriate target: // - If the container is a window, focuses that window. // - If the container is a workspace, "resets" focus by focusing the // desktop window. // // In either case, a `PlatformEvent::WindowFocused` event is subsequently // triggered. let result = if let Some(window) = native_window { tracing::info!("Setting focus to window: {window}"); window.native().focus() } else { tracing::info!("Setting focus to the desktop window."); state.dispatcher.reset_focus() }; if let Err(err) = result { tracing::warn!("Failed to set focus: {}", err); } state.emit_event(WmEvent::FocusChanged { focused_container: focused_container.to_dto()?, }); Ok(()) } /// Finds windows that should be brought to the top of their workspace's /// z-order. /// /// Windows are brought to front if they match the focused window's state /// (floating/tiling) and any of these conditions are met: /// * Focus has changed to a different window. /// * Focused window's state has changed (e.g. tiling -> floating). /// * Focused window has moved to a different workspace. fn windows_to_bring_to_front( focused_container: &Container, state: &WmState, ) -> anyhow::Result> { let focused_workspace = focused_container.workspace().context("No workspace.")?; // Add focused workspace if there's been a focus change. let workspaces_to_reorder = state .pending_sync .workspaces_to_reorder() .iter() .chain( state .pending_sync .needs_focus_update() .then_some(&focused_workspace), ) .unique_by(|workspace| workspace.id()); // Bring forward windows that match the focused state. Only do this for // tiling/floating windows. let windows_to_bring_to_front = workspaces_to_reorder .flat_map(|workspace| { let focused_descendant = workspace .descendant_focus_order() .next() .and_then(|container| container.as_window_container().ok()); match focused_descendant { Some(focused_descendant) => workspace .descendants() .filter_map(|descendant| descendant.as_window_container().ok()) .filter(|window| { let is_floating_or_tiling = matches!( window.state(), WindowState::Floating(_) | WindowState::Tiling ); is_floating_or_tiling && window.state().is_same_state(&focused_descendant.state()) }) .collect(), None => vec![], } }) .collect::>(); Ok(windows_to_bring_to_front) } #[allow(clippy::too_many_lines)] fn redraw_containers( focused_container: &Container, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let windows_to_redraw = state.windows_to_redraw(); let windows_to_bring_to_front = windows_to_bring_to_front(focused_container, state)?; let windows_to_update = { let mut windows = windows_to_redraw .iter() .chain(&windows_to_bring_to_front) .unique_by(|window| window.id()) .collect::>(); let descendant_focus_order = state .root_container .descendant_focus_order() .collect::>(); // Sort the windows to update by their focus order. The most recently // focused window will be updated first. // TODO: To reduce flicker, redraw windows that will be shown first, // then redraw the ones to be hidden last. windows.sort_by_key(|window| { descendant_focus_order .iter() .position(|order| order.id() == window.id()) }); windows }; // Get monitors by their optimal hide corner. let monitors_by_hide_corner = state.monitors_by_hide_corner(); for window in windows_to_update.iter().rev() { let should_bring_to_front = windows_to_bring_to_front.contains(window); let workspace = window.workspace().context("Window has no workspace.")?; let monitor = window.monitor().context("No monitor.")?; let hide_corner = monitors_by_hide_corner .iter() .find(|(m, _)| m.id() == monitor.id()) .map(|(_, hide_corner)| hide_corner) .context("Monitor not found in hide corner map.")?; // Whether the window should be shown above all other windows. let z_order = match window.state() { WindowState::Floating(config) if config.shown_on_top => { WindowZOrder::TopMost } WindowState::Fullscreen(config) if config.shown_on_top => { WindowZOrder::TopMost } _ if should_bring_to_front => { let focused_descendant = workspace .descendant_focus_order() .next() .and_then(|container| container.as_window_container().ok()); if let Some(focused_descendant) = focused_descendant { if window.id() == focused_descendant.id() { WindowZOrder::Normal } else { WindowZOrder::AfterWindow(focused_descendant.native().id()) } } else { WindowZOrder::Normal } } _ => WindowZOrder::Normal, }; // Set the z-order of the window. // // NOTE: macOS doesn't have a robust public API for setting the z-order // of a window. See `NativeWindow::raise` for more details. #[cfg(target_os = "windows")] if should_bring_to_front && !windows_to_redraw.contains(window) { tracing::info!("Updating window z-order: {window}"); if let Err(err) = window.native().set_z_order(&z_order) { tracing::warn!("Failed to set window z-order: {}", err); } } // Skip updating the window's position if it only required a z-order // change. if !windows_to_redraw.contains(window) { continue; } // Transition display state depending on whether window will be // shown or hidden. window.set_display_state( match (window.display_state(), workspace.is_displayed()) { (DisplayState::Hidden | DisplayState::Hiding, true) => { DisplayState::Showing } (DisplayState::Shown | DisplayState::Showing, false) => { DisplayState::Hiding } _ => window.display_state(), }, ); let is_visible = matches!( window.display_state(), DisplayState::Showing | DisplayState::Shown ); if let Err(err) = reposition_window(window, *hide_corner, &z_order, is_visible, config) { tracing::warn!("Failed to set window position: {}", err); } // Whether the window is either transitioning to or from fullscreen. // TODO: This check can be improved since `prev_state` can be // fullscreen without it needing to be marked as not fullscreen. #[cfg(target_os = "windows")] { let is_transitioning_fullscreen = match (window.prev_state(), window.state()) { (Some(_), WindowState::Fullscreen(s)) if !s.maximized => true, (Some(WindowState::Fullscreen(_)), _) => true, _ => false, }; if is_transitioning_fullscreen { if let Err(err) = window.native().mark_fullscreen(matches!( window.state(), WindowState::Fullscreen(_) )) { tracing::warn!("Failed to mark window as fullscreen: {}", err); } } } // Skip setting taskbar visibility if the window is hidden (has no // effect). Since cloaked windows are normally always visible in the // taskbar, we only need to set visibility if `show_all_in_taskbar` is // `false`. #[cfg(target_os = "windows")] if config.value.general.hide_method == HideMethod::Cloak && !config.value.general.show_all_in_taskbar && matches!( window.display_state(), DisplayState::Showing | DisplayState::Hiding ) { if let Err(err) = window.native().set_taskbar_visibility(is_visible) { tracing::warn!("Failed to set taskbar visibility: {}", err); } } } Ok(()) } fn reposition_window( window: &WindowContainer, hide_corner: HideCorner, // LINT: `z_order` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] z_order: &WindowZOrder, is_visible: bool, config: &UserConfig, ) -> anyhow::Result<()> { let rect = window .to_rect()? .apply_delta(&window.total_border_delta()?, None); // For `HideMethod::PlaceInCorner`, we need to reposition hidden windows // to the corner of the monitor. if config.value.general.hide_method == HideMethod::PlaceInCorner && !is_visible { const VISIBLE_SLIVER: i32 = 1; let monitor_rect = window .monitor() .context("No monitor.")? .native_properties() .working_area; let frame = window.native_properties().frame; let position_y = monitor_rect.bottom - VISIBLE_SLIVER; let position_x = match hide_corner { HideCorner::BottomLeft => { monitor_rect.left + VISIBLE_SLIVER - frame.width() } HideCorner::BottomRight => monitor_rect.right - VISIBLE_SLIVER, }; // Even though the window size is unchanged, `NativeWindow::set_frame` // is used instead of `NativeWindow::reposition` because the latter // resulted in occasional incorrect positionings on macOS. window.native().set_frame(&Rect::from_xy( position_x, position_y, frame.width(), frame.height(), ))?; return Ok(()); } if window.active_drag().is_some() { window.native().resize(rect.width(), rect.height())?; } else { #[cfg(target_os = "macos")] window.native().set_frame(&rect)?; #[cfg(target_os = "windows")] { use wm_platform::{ SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOSENDCHANGING, WS_MAXIMIZEBOX, }; // Restore window if it's minimized/maximized and shouldn't be. This // is needed to be able to move and resize it. let should_restore = match &window.state() { // Need to restore window if transitioning from maximized // fullscreen to non-maximized fullscreen. WindowState::Fullscreen(fullscreen) => { !fullscreen.maximized && window.native().is_maximized()? } // No need to restore window if it'll be minimized. Transitioning // from maximized to minimized works without having to // restore. WindowState::Minimized => false, _ => { window.native().is_minimized()? || window.native().is_maximized()? } }; if should_restore { // Restoring to position has the same effect as `ShowWindow` with // `SW_RESTORE`, but doesn't cause a flicker. window.native().restore(Some(&rect))?; } let mut swp_flags = SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOSENDCHANGING | SWP_ASYNCWINDOWPOS; match &window.state() { WindowState::Minimized => { if !window.native().is_minimized()? { window.native().minimize()?; } } WindowState::Fullscreen(fullscreen) if fullscreen.maximized && window.native().has_window_style(WS_MAXIMIZEBOX) => { if !window.native().is_maximized()? { window.native().maximize()?; } window.native().set_window_pos(z_order, &rect, swp_flags)?; } _ => { swp_flags |= SWP_FRAMECHANGED; window.native().set_window_pos(z_order, &rect, swp_flags)?; // When there's a mismatch between the DPI of the monitor and the // window, the window might be sized incorrectly after the first // move. If we set the position twice, inconsistencies after the // first move are resolved. if window.has_pending_dpi_adjustment() { window.native().set_window_pos(z_order, &rect, swp_flags)?; } } } // Set visibility based on the hide method. if config.value.general.hide_method == HideMethod::Cloak { window.native().set_cloaked(!is_visible)?; } else if is_visible { window.native().show()?; } else { window.native().hide()?; } } } Ok(()) } fn jump_cursor( focused_container: Container, state: &WmState, config: &UserConfig, ) -> anyhow::Result<()> { let cursor_jump = &config.value.general.cursor_jump; let jump_target = match cursor_jump.trigger { CursorJumpTrigger::WindowFocus => Some(focused_container), CursorJumpTrigger::MonitorFocus => { let target_monitor = focused_container.monitor().context("No monitor.")?; let cursor_monitor = state .dispatcher .cursor_position() .ok() .and_then(|pos| state.monitor_at_point(&pos)); // Jump to the target monitor if the cursor is not already on it. cursor_monitor .filter(|monitor| monitor.id() != target_monitor.id()) .map(|_| target_monitor.into()) } }; if let Some(jump_target) = jump_target { let center = jump_target.to_rect()?.center_point(); if let Err(err) = state.dispatcher.set_cursor_position(¢er) { tracing::warn!("Failed to set cursor position: {}", err); } } Ok(()) } fn apply_window_effects( // LINT: `window` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] window: &WindowContainer, is_focused: bool, config: &UserConfig, ) { let window_effects = &config.value.window_effects; // LINT: `effect_config` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] let effect_config = if is_focused { &window_effects.focused_window } else { &window_effects.other_windows }; // Skip if both focused + non-focused window effects are disabled. #[cfg(target_os = "windows")] if window_effects.focused_window.border.enabled || window_effects.other_windows.border.enabled { apply_border_effect(window, effect_config); } #[cfg(target_os = "windows")] if window_effects.focused_window.hide_title_bar.enabled || window_effects.other_windows.hide_title_bar.enabled { apply_hide_title_bar_effect(window, effect_config); } #[cfg(target_os = "windows")] if window_effects.focused_window.corner_style.enabled || window_effects.other_windows.corner_style.enabled { apply_corner_effect(window, effect_config); } #[cfg(target_os = "windows")] if window_effects.focused_window.transparency.enabled || window_effects.other_windows.transparency.enabled { apply_transparency_effect(window, effect_config); } } #[cfg(target_os = "windows")] fn apply_border_effect( window: &WindowContainer, effect_config: &WindowEffectConfig, ) { let border_color = if effect_config.border.enabled { Some(&effect_config.border.color) } else { None }; _ = window.native().set_border_color(border_color); let native = window.native().clone(); let border_color = border_color.cloned(); // Re-apply border color after a short delay to better handle // windows that change it themselves. tokio::task::spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(50)).await; _ = native.set_border_color(border_color.as_ref()); }); } #[cfg(target_os = "windows")] fn apply_hide_title_bar_effect( window: &WindowContainer, effect_config: &WindowEffectConfig, ) { _ = window .native() .set_title_bar_visibility(!effect_config.hide_title_bar.enabled); } #[cfg(target_os = "windows")] fn apply_corner_effect( window: &WindowContainer, effect_config: &WindowEffectConfig, ) { let corner_style = if effect_config.corner_style.enabled { &effect_config.corner_style.style } else { &CornerStyle::Default }; _ = window.native().set_corner_style(corner_style); } #[cfg(target_os = "windows")] fn apply_transparency_effect( window: &WindowContainer, effect_config: &WindowEffectConfig, ) { let transparency = if effect_config.transparency.enabled { &effect_config.transparency.opacity } else { // Reset the transparency to default. &OpacityValue::from_alpha(u8::MAX) }; _ = window.native().set_transparency(transparency); } ================================================ FILE: packages/wm/src/commands/general/reload_config.rs ================================================ use anyhow::Context; use tracing::{info, warn}; #[cfg(target_os = "windows")] use wm_common::{HideMethod, ParsedConfig}; use wm_common::{WindowRuleEvent, WmEvent}; #[cfg(target_os = "windows")] use wm_platform::NativeWindowWindowsExt; use crate::{ commands::{window::run_window_rules, workspace::sort_workspaces}, traits::{CommonGetters, TilingSizeGetters, WindowGetters}, user_config::UserConfig, wm::WindowManager, wm_state::WmState, }; pub fn reload_config( state: &mut WmState, config: &mut UserConfig, ) -> anyhow::Result<()> { info!("Config reloaded."); // Keep reference to old config for comparison. #[cfg(target_os = "windows")] let old_config = config.value.clone(); // Re-evaluate user config file and set its values in state. config.reload()?; // Re-run window rules on all active windows. for window in state.windows() { window.set_done_window_rules(Vec::new()); run_window_rules(window, &WindowRuleEvent::Manage, state, config)?; } update_workspace_configs(state, config)?; update_container_gaps(state, config); #[cfg(target_os = "windows")] update_window_effects(&old_config, state, config)?; // Ensure all windows are shown when hide method is changed. #[cfg(target_os = "windows")] if old_config.general.hide_method != config.value.general.hide_method && config.value.general.hide_method == HideMethod::Cloak { for window in state.windows() { let _ = window.native().show(); } } // Ensure all windows are shown in taskbar when `show_all_in_taskbar` is // changed. #[cfg(target_os = "windows")] if old_config.general.show_all_in_taskbar != config.value.general.show_all_in_taskbar && config.value.general.show_all_in_taskbar { for window in state.windows() { let _ = window.native().set_taskbar_visibility(true); } } // Clear active binding modes. state.binding_modes = Vec::new(); // Redraw full container tree. state .pending_sync .queue_container_to_redraw(state.root_container.clone()); // Emit the updated config. state.emit_event(WmEvent::UserConfigChanged { config_path: config .path .to_str() .context("Invalid config path.")? .to_string(), config_string: config.value_str.clone(), parsed_config: config.value.clone(), }); // Run config reload commands. WindowManager::run_commands( &config.value.general.config_reload_commands.clone(), state.focused_container().context("No focused container.")?, state, config, )?; Ok(()) } /// Update configs of active workspaces. fn update_workspace_configs( state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let workspaces = state.workspaces(); for workspace in &workspaces { let monitor = workspace.monitor().context("No monitor.")?; let workspace_config = config .value .workspaces .iter() .find(|config| config.name == workspace.config().name) .or_else(|| { // When the workspace config is not found, the current name of the // workspace has been removed. So, we reassign the first suitable // workspace config to the workspace. config .workspace_config_for_monitor(&monitor, &workspaces) .or_else(|| config.next_inactive_workspace_config(&workspaces)) }); match workspace_config { None => { warn!( "Unable to update workspace config. No available workspace configs." ); } Some(workspace_config) => { if *workspace_config != workspace.config() { workspace.set_config(workspace_config.clone()); sort_workspaces(&monitor, config)?; state.emit_event(WmEvent::WorkspaceUpdated { updated_workspace: workspace.to_dto()?, }); } } } } Ok(()) } /// Updates outer gap of workspaces and inner gaps of tiling containers. fn update_container_gaps(state: &mut WmState, config: &UserConfig) { let tiling_containers = state .root_container .self_and_descendants() .filter_map(|container| container.as_tiling_container().ok()); for container in tiling_containers { container.set_gaps_config(config.value.gaps.clone()); } for workspace in state.workspaces() { workspace.set_gaps_config(config.value.gaps.clone()); } } #[cfg(target_os = "windows")] fn update_window_effects( old_config: &ParsedConfig, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let focused_container = state.focused_container().context("No focused container.")?; let window_effects = &config.value.window_effects; let old_window_effects = &old_config.window_effects; // Window border effects are left at system defaults if disabled in the // config. However, when transitioning from colored borders to having // them disabled, it's best to reset to the system defaults. if !window_effects.focused_window.border.enabled && old_window_effects.focused_window.border.enabled { if let Ok(window) = focused_container.as_window_container() { _ = window.native().set_border_color(None); } } if !window_effects.other_windows.border.enabled && old_window_effects.other_windows.border.enabled { let unfocused_windows = state .windows() .into_iter() .filter(|window| window.id() != focused_container.id()); for window in unfocused_windows { _ = window.native().set_border_color(None); } } state.pending_sync.queue_all_effects_update(); Ok(()) } ================================================ FILE: packages/wm/src/commands/general/shell_exec.rs ================================================ use std::path::Path; #[cfg(target_os = "windows")] use anyhow::Context; #[cfg(target_os = "macos")] use shell_util::{CommandOptions, Shell}; #[cfg(target_os = "windows")] use wm_platform::DispatcherExtWindows; use crate::wm_state::WmState; pub fn shell_exec( command: &str, // LINT: `hide_window` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] hide_window: bool, state: &WmState, ) -> anyhow::Result<()> { let (program, args) = parse_command(command, state)?; tracing::info!( "Parsed command program: '{}', args: '{}'.", program, args ); // NOTE: The standard library's `Command::new` is not used because it // launches the program as a subprocess. This prevents cleanup of handles // held by our process (e.g. the IPC server port) until the subprocess // exits. let result = { #[cfg(target_os = "macos")] { Shell::spawn( &program, args.split_whitespace(), &CommandOptions::default(), ) } #[cfg(target_os = "windows")] { let home_dir = home::home_dir().context("Unable to get home directory.")?; // TODO: Use `Shell::spawn` instead. `ShellExecuteExW` is still used // to be able to launch programs from the App Paths registry // (`HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths`), like // `chrome` without it being in $PATH. state.dispatcher.shell_execute_ex( &program, &args, &home_dir, hide_window, ) } }; result.map_err(|err| { anyhow::anyhow!( "Shell exec failed for '{command}'. Make sure the program exists and is \ accessible from your shell. Error: {err}", ) })?; Ok(()) } /// Parses a command string into a program name/path and arguments. This /// also expands any environment variables found in the command string if /// they are wrapped in `%` characters. If the command string is a path, /// a file extension is required. /// /// This is similar to the `SHEvaluateSystemCommandTemplate` Win32 /// function. It also parses program name/path and arguments, but can't /// handle `/` as file path delimiters and it errors for certain programs /// (e.g. `code`). /// /// Returns a tuple containing the program name/path and arguments. /// /// # Examples /// /// ```no_run /// let (prog, args) = parse_command("code .")?; /// assert_eq!(prog, "code"); /// assert_eq!(args, "."); /// /// let (prog, args) = parse_command( /// r#"C:\Program Files\Git\git-bash --cd=C:\Users\larsb\.glaze-wm"#, /// )?; /// assert_eq!(prog, r#"C:\Program Files\Git\git-bash"#); /// assert_eq!(args, r#"--cd=C:\Users\larsb\.glaze-wm"#); /// ``` fn parse_command( command: &str, // LINT: `state` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] state: &WmState, ) -> anyhow::Result<(String, String)> { // Expand environment variables in the command string. let expanded_command = { #[cfg(target_os = "windows")] { state.dispatcher.expand_env_strings(command)? } #[cfg(target_os = "macos")] { // TODO: Expand env variables on macOS. command.to_string() } }; let command_parts = expanded_command.split_whitespace().collect::>(); // If the command starts with double quotes, then the program name/path // is wrapped in double quotes (e.g. `"C:\path\to\app.exe" --flag`). if expanded_command.starts_with('"') { // Find the closing double quote. let (closing_index, _) = expanded_command.match_indices('"').nth(2).ok_or_else(|| { anyhow::anyhow!( "Shell exec failed for '{command}': command doesn't have an ending `\"`." ) })?; return Ok(( expanded_command[1..closing_index].to_string(), expanded_command[closing_index + 1..].trim().to_string(), )); } // The first part is the program name if it doesn't contain a slash or // backslash. if let Some(first_part) = command_parts.first() { if !first_part.contains(&['/', '\\'][..]) { let args = command_parts[1..].join(" "); return Ok(((*first_part).to_string(), args)); } } let mut cumulative_path = Vec::new(); // Lastly, iterate over the command until a valid file path is found. for (part_index, &part) in command_parts.iter().enumerate() { cumulative_path.push(part); if Path::new(&cumulative_path.join(" ")).is_file() { return Ok(( cumulative_path.join(" "), command_parts[part_index + 1..].join(" "), )); } } anyhow::bail!( "Shell exec failed for '{command}': program path is not valid." ) } ================================================ FILE: packages/wm/src/commands/general/toggle_pause.rs ================================================ use wm_common::WmEvent; use crate::wm_state::WmState; /// Pauses or unpauses the WM. pub fn toggle_pause(state: &mut WmState) { let is_paused = !state.is_paused; state.is_paused = is_paused; // Redraw full container tree on unpause. if !is_paused { state .pending_sync .queue_container_to_redraw(state.root_container.clone()); } state.emit_event(WmEvent::PauseChanged { is_paused }); } ================================================ FILE: packages/wm/src/commands/mod.rs ================================================ pub mod container; pub mod general; pub mod monitor; pub mod window; pub mod workspace; ================================================ FILE: packages/wm/src/commands/monitor/add_monitor.rs ================================================ use anyhow::Context; use tracing::info; use wm_common::WmEvent; use wm_platform::Display; use crate::{ commands::{ container::{attach_container, move_container_within_tree}, workspace::{activate_workspace, sort_workspaces}, }, models::{Monitor, NativeMonitorProperties, Workspace}, traits::{CommonGetters, PositionGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; pub fn add_monitor( native_display: Display, native_properties: NativeMonitorProperties, state: &mut WmState, ) -> anyhow::Result { // Create `Monitor` instance. This uses the working area of the monitor // instead of the bounds of the display. The working area excludes // taskbars and other reserved display space. let monitor = Monitor::new(native_display, native_properties); attach_container( &monitor.clone().into(), &state.root_container.clone().into(), None, )?; info!("Monitor added: {monitor}"); state.emit_event(WmEvent::MonitorAdded { added_monitor: monitor.to_dto()?, }); Ok(monitor) } pub fn move_bounded_workspaces_to_new_monitor( monitor: &Monitor, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let bound_workspace_configs = config .value .workspaces .iter() .filter(|config| { config.bind_to_monitor.is_some_and(|monitor_index| { monitor.index() == monitor_index as usize }) }) .collect::>(); for workspace_config in bound_workspace_configs { let existing_workspace = state.workspace_by_name(&workspace_config.name); if let Some(existing_workspace) = existing_workspace { // Move workspaces that should be bound to the newly added monitor. move_workspace_to_monitor( &existing_workspace, monitor, state, config, )?; } else if workspace_config.keep_alive { // Activate all `keep_alive` workspaces for this monitor. activate_workspace( Some(&workspace_config.name), Some(monitor.clone()), state, config, )?; } } // Make sure the monitor has at least one workspace. This will // automatically prioritize bound workspace configs and fall back to the // first available one if needed. if monitor.child_count() == 0 { activate_workspace(None, Some(monitor.clone()), state, config)?; } Ok(()) } // TODO: Move to its own file once `swap-workspace` PR is merged. // Ref: https://github.com/glzr-io/glazewm/pull/980. pub fn move_workspace_to_monitor( workspace: &Workspace, target_monitor: &Monitor, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let origin_monitor = workspace.monitor().context("No monitor.")?; move_container_within_tree( &workspace.clone().into(), &target_monitor.clone().into(), target_monitor.child_count(), state, )?; let windows = workspace .descendants() .filter_map(|descendant| descendant.as_window_container().ok()); for window in windows { window.set_has_pending_dpi_adjustment(true); window.set_floating_placement( window .floating_placement() .translate_to_center(&workspace.to_rect()?), ); } // Get currently displayed workspace on the target monitor. let displayed_workspace = target_monitor .displayed_workspace() .context("No displayed workspace.")?; state .pending_sync .queue_container_to_redraw(workspace.clone()) .queue_container_to_redraw(displayed_workspace); match origin_monitor.child_count() { 0 => { // Prevent origin monitor from having no workspaces. activate_workspace(None, Some(origin_monitor), state, config)?; } _ => { // Redraw the workspace on the origin monitor. state.pending_sync.queue_container_to_redraw( origin_monitor .displayed_workspace() .context("No displayed workspace.")?, ); } } sort_workspaces(target_monitor, config)?; state.emit_event(WmEvent::WorkspaceUpdated { updated_workspace: workspace.to_dto()?, }); Ok(()) } ================================================ FILE: packages/wm/src/commands/monitor/focus_monitor.rs ================================================ use anyhow::Context; use crate::{ commands::workspace::focus_workspace, models::WorkspaceTarget, user_config::UserConfig, wm_state::WmState, }; /// Focuses a monitor by a given monitor index. pub fn focus_monitor( monitor_index: usize, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let monitors = state.monitors(); let target_monitor = monitors.get(monitor_index).with_context(|| { format!("Monitor at index {monitor_index} was not found.") })?; let workspace_name = target_monitor .displayed_workspace() .map(|workspace| workspace.config().name) .context("Failed to get target workspace name.")?; focus_workspace(WorkspaceTarget::Name(workspace_name), state, config) } ================================================ FILE: packages/wm/src/commands/monitor/mod.rs ================================================ mod add_monitor; mod focus_monitor; mod remove_monitor; mod sort_monitors; mod update_monitor; pub use add_monitor::*; pub use focus_monitor::*; pub use remove_monitor::*; pub use sort_monitors::*; pub use update_monitor::*; ================================================ FILE: packages/wm/src/commands/monitor/remove_monitor.rs ================================================ use anyhow::Context; use tracing::info; use wm_common::WmEvent; use crate::{ commands::{ container::{detach_container, move_container_within_tree}, workspace::sort_workspaces, }, models::Monitor, traits::CommonGetters, user_config::UserConfig, wm_state::WmState, }; #[allow(clippy::needless_pass_by_value)] pub fn remove_monitor( monitor: Monitor, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { info!("Removing monitor: {monitor}"); let target_monitor = state .monitors() .into_iter() .find(|m| m.id() != monitor.id()) .context("No target monitor to move workspaces.")?; // Avoid moving empty workspaces. let workspaces_to_move = monitor.workspaces().into_iter().filter(|workspace| { workspace.has_children() || workspace.config().keep_alive }); for workspace in workspaces_to_move { // Move workspace to target monitor. move_container_within_tree( &workspace.clone().into(), &target_monitor.clone().into(), target_monitor.child_count(), state, )?; sort_workspaces(&target_monitor, config)?; state.emit_event(WmEvent::WorkspaceUpdated { updated_workspace: workspace.to_dto()?, }); } detach_container(monitor.clone().into())?; state.emit_event(WmEvent::MonitorRemoved { removed_id: monitor.id(), removed_device_name: monitor.native_properties().device_name, }); Ok(()) } ================================================ FILE: packages/wm/src/commands/monitor/sort_monitors.rs ================================================ use crate::{ models::RootContainer, traits::{CommonGetters, PositionGetters}, }; /// Sorts the root container's monitors from left-to-right and /// top-to-bottom. pub fn sort_monitors(root: &RootContainer) -> anyhow::Result<()> { let monitors = root.monitors(); // Create a tuple of monitors and their rects. let mut monitors_with_rect = monitors .into_iter() .map(|monitor| { let rect = monitor.to_rect()?.clone(); anyhow::Ok((monitor, rect)) }) .try_collect::>()?; // Sort monitors from left-to-right, top-to-bottom. monitors_with_rect.sort_by(|(_, rect_a), (_, rect_b)| { if rect_a.x() == rect_b.x() { rect_a.y().cmp(&rect_b.y()) } else { rect_a.x().cmp(&rect_b.x()) } }); *root.borrow_children_mut() = monitors_with_rect .into_iter() .map(|(monitor, _)| monitor.into()) .collect(); Ok(()) } ================================================ FILE: packages/wm/src/commands/monitor/update_monitor.rs ================================================ use tracing::info; use wm_common::WmEvent; use wm_platform::Display; use crate::{ models::{Monitor, NativeMonitorProperties}, wm_state::WmState, }; pub fn update_monitor( monitor: &Monitor, native_display: &Display, native_properties: NativeMonitorProperties, state: &mut WmState, ) -> anyhow::Result<()> { monitor.set_native(native_display.clone()); monitor.set_native_properties(native_properties); info!("Monitor updated: {monitor}"); // TODO: Check that a property on the monitor actually changed. state.emit_event(WmEvent::MonitorUpdated { updated_monitor: monitor.to_dto()?, }); Ok(()) } ================================================ FILE: packages/wm/src/commands/window/ignore_window.rs ================================================ use anyhow::Context; use wm_common::WindowState; use crate::{ commands::container::{ detach_container, flatten_child_split_containers, }, models::WindowContainer, traits::{CommonGetters, WindowGetters}, wm_state::WmState, }; #[allow(clippy::needless_pass_by_value)] pub fn ignore_window( window: WindowContainer, state: &mut WmState, ) -> anyhow::Result<()> { // Create iterator of parent, grandparent, and great-grandparent. let ancestors = window.ancestors().take(3).collect::>(); state.ignored_windows.push(window.native().clone()); detach_container(window.clone().into())?; // After detaching the container, flatten any redundant split containers. // For example, in the layout V[1 H[2]] where container 1 is detached to // become V[H[2]], this will then need to be flattened to V[2]. for ancestor in ancestors.iter().rev() { flatten_child_split_containers(ancestor)?; } // Sibling containers need to be redrawn if the window was tiling. if window.state() == WindowState::Tiling { let ancestor_to_redraw = ancestors .into_iter() .find(|ancestor| !ancestor.is_detached()) .context("No ancestor to redraw.")?; state .pending_sync .queue_containers_to_redraw(ancestor_to_redraw.tiling_children()); } Ok(()) } ================================================ FILE: packages/wm/src/commands/window/manage_window.rs ================================================ use anyhow::Context; use tracing::info; use wm_common::{try_warn, WindowRuleEvent, WindowState, WmEvent}; use wm_platform::{NativeWindow, RectDelta}; use crate::{ commands::{ container::{attach_container, set_focused_descendant}, window::run_window_rules, }, models::{ Container, Monitor, NativeWindowProperties, NonTilingWindow, TilingWindow, WindowContainer, }, traits::{CommonGetters, PositionGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; pub fn manage_window( native_window: NativeWindow, target_parent: Option, state: &mut WmState, config: &mut UserConfig, ) -> anyhow::Result<()> { let Some(native_properties) = check_is_manageable(&native_window).unwrap_or(None) else { return Ok(()); }; // Create the window instance. This may fail if the window handle has // already been destroyed. let window = try_warn!(create_window( native_window, native_properties, target_parent, state, config )); // Set the newly added window as focus descendant. This means the window // rules will be run as if the window is focused. set_focused_descendant(&window.clone().into(), None); // Window might be detached if `ignore` command has been invoked. let updated_window = run_window_rules( window.clone(), &WindowRuleEvent::Manage, state, config, )?; if let Some(window) = updated_window { info!("New window managed: {window}"); state.emit_event(WmEvent::WindowManaged { managed_window: window.to_dto()?, }); // OS focus should be set to the newly added window in case it's not // already focused. state.pending_sync.queue_focus_change(); // Normally, a `PlatformEvent::WindowFocused` event is what triggers // focus effects and workspace reordering to be applied. However, when // a window is first launched, this event can come before the // window is managed, and so we need to force an update here. state.pending_sync.queue_focused_effect_update(); state.pending_sync.queue_workspace_to_reorder( window.workspace().context("No workspace.")?, ); // Sibling containers need to be redrawn if the window is tiling. state.pending_sync.queue_container_to_redraw( if window.state() == WindowState::Tiling { window.parent().context("No parent.")? } else { window.into() }, ); } Ok(()) } /// Checks if a window is manageable and retrieves its native properties. /// /// Returns `Ok(Some(properties))` if the window is manageable and its /// properties were retrieved successfully. fn check_is_manageable( native_window: &NativeWindow, ) -> anyhow::Result> { if !native_window.is_visible()? { return Ok(None); } #[cfg(target_os = "macos")] { use wm_platform::NativeWindowExtMacOs; let is_standard_window = native_window.role()? == "AXWindow" && native_window.subrole()? == "AXStandardWindow"; if !is_standard_window { return Ok(None); } } // Ensure window has a valid process name, title, etc. let native_properties = NativeWindowProperties::try_from(native_window)?; #[cfg(target_os = "windows")] { use wm_platform::{ NativeWindowWindowsExt, WS_CAPTION, WS_CHILD, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, }; // TODO: Temporary fix for managing Flow Launcher until a force manage // command is added. let is_flow_launcher = native_properties.process_name == "Flow.Launcher" && native_properties.title == "Flow.Launcher"; if !is_flow_launcher { // Ensure window is top-level (i.e. not a child window). Ignore // windows that cannot be focused or if they're unavailable in // task switcher (alt+tab menu). if native_window.has_window_style(WS_CHILD) || native_window .has_window_style_ex(WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW) { return Ok(None); } // Some applications spawn top-level windows for menus that // should be ignored. This includes the autocomplete popup in // Notepad++ and title bar menu in Keepass. Although not // foolproof, these can typically be identified by having an // owner window and no title bar. if native_window.has_owner_window() && !native_window.has_window_style(WS_CAPTION) { return Ok(None); } } } Ok(Some(native_properties)) } fn create_window( native_window: NativeWindow, native_properties: NativeWindowProperties, target_parent: Option, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result { let nearest_monitor = state .nearest_monitor(&native_window) .context("No nearest monitor.")?; let nearest_workspace = nearest_monitor .displayed_workspace() .context("No nearest workspace.")?; let gaps_config = config.value.gaps.clone(); let window_state = window_state_to_create(&native_properties, &nearest_monitor, config)?; // Attach the new window as the first child of the target parent (if // provided), otherwise, add as a sibling of the focused container. let (target_parent, target_index) = match target_parent { Some(parent) => (parent, 0), None => insertion_target(&window_state, state)?, }; let target_workspace = target_parent.workspace().context("No target workspace.")?; let prefers_centered = config .value .window_behavior .state_defaults .floating .centered; // Calculate where window should be placed when floating is enabled. Use // the original width/height of the window and optionally position it in // the center of the workspace. let is_same_workspace = nearest_workspace.id() == target_workspace.id(); let floating_placement = { let placement = if !is_same_workspace || prefers_centered { native_properties .frame .translate_to_center(&target_workspace.to_rect()?) } else { native_properties.frame.clone() }; // Clamp the window size to be within the workspace's outer gaps. 10px // is arbitrary - helps differentiate from tiling windows. let max_workspace_rect = target_workspace.max_workspace_rect()?; placement.clamp_size( max_workspace_rect.width() - 10, max_workspace_rect.height() - 10, ) }; // Window has no border delta unless it's later changed via the // `adjust_borders` command. let border_delta = RectDelta::zero(); let window_container: WindowContainer = match window_state { WindowState::Tiling => TilingWindow::new( None, native_window, native_properties, None, border_delta, floating_placement, false, gaps_config, Vec::new(), None, ) .into(), _ => NonTilingWindow::new( None, native_window, native_properties, window_state, None, border_delta, None, floating_placement, !prefers_centered, Vec::new(), None, ) .into(), }; attach_container( &window_container.clone().into(), &target_parent, Some(target_index), )?; // The OS might spawn the window on a different monitor to the target // parent, so adjustments might need to be made because of DPI. if nearest_monitor .has_dpi_difference(&window_container.clone().into())? { window_container.set_has_pending_dpi_adjustment(true); } Ok(window_container) } /// Gets the initial state for a window based on its native state. /// /// Note that maximized windows are initialized as tiling. fn window_state_to_create( native_properties: &NativeWindowProperties, nearest_monitor: &Monitor, config: &UserConfig, ) -> anyhow::Result { if native_properties.is_minimized { return Ok(WindowState::Minimized); } let nearest_workspace = nearest_monitor .displayed_workspace() .context("No workspace.")?; // Only initialize as fullscreen if the window *exceeds* the workspace // bounds (due to the 1px inset). // // For example, with 0px outer gaps and a window that covers the entire // workspace, it would still not be initialized as fullscreen. The window // needs to be within the workspace's outer gaps by at least 1px on each // side. if !native_properties.is_maximized && native_properties .frame .inset(1) .contains_rect(&nearest_workspace.max_workspace_rect()?) { return Ok(WindowState::Fullscreen( config .value .window_behavior .state_defaults .fullscreen .clone(), )); } // Initialize windows that can't be resized as floating. if !native_properties.is_resizable { return Ok(WindowState::Floating( config.value.window_behavior.state_defaults.floating.clone(), )); } Ok(WindowState::default_from_config(&config.value)) } /// Gets where to insert a new window in the container tree. /// /// Rules: /// - For non-tiling windows: Always append to the workspace. /// - For tiling windows: /// 1. Try to insert after the focused tiling window if one exists. /// 2. If a non-tiling window is focused, try to insert after the first /// tiling window found. /// 3. If no tiling windows exist, append to the workspace. /// /// Returns tuple of (parent container, insertion index). fn insertion_target( window_state: &WindowState, state: &WmState, ) -> anyhow::Result<(Container, usize)> { let focused_container = state.focused_container().context("No focused container.")?; let focused_workspace = focused_container.workspace().context("No workspace.")?; // For tiling windows, try to find a suitable tiling window to insert // next to. if *window_state == WindowState::Tiling { let sibling = match focused_container { Container::TilingWindow(_) => Some(focused_container), _ => focused_workspace .descendant_focus_order() .find(Container::is_tiling_window), }; if let Some(sibling) = sibling { return Ok(( sibling.parent().context("No parent.")?, sibling.index() + 1, )); } } // Default to appending to workspace. Ok(( focused_workspace.clone().into(), focused_workspace.child_count(), )) } ================================================ FILE: packages/wm/src/commands/window/mod.rs ================================================ mod ignore_window; mod manage_window; mod move_window_in_direction; mod move_window_to_workspace; mod resize_window; mod run_window_rules; mod set_window_position; mod set_window_size; mod unmanage_window; mod update_window_state; pub use ignore_window::*; pub use manage_window::*; pub use move_window_in_direction::*; pub use move_window_to_workspace::*; pub use resize_window::*; pub use run_window_rules::*; pub use set_window_position::*; pub use set_window_size::*; pub use unmanage_window::*; pub use update_window_state::*; ================================================ FILE: packages/wm/src/commands/window/move_window_in_direction.rs ================================================ use anyhow::Context; use wm_common::{TilingDirection, WindowState}; use wm_platform::{Direction, Rect}; use crate::{ commands::container::{ flatten_child_split_containers, flatten_split_container, move_container_within_tree, resize_tiling_container, set_focused_descendant, wrap_in_split_container, }, models::{ DirectionContainer, Monitor, NonTilingWindow, SplitContainer, TilingContainer, TilingWindow, WindowContainer, }, traits::{ CommonGetters, PositionGetters, TilingDirectionGetters, WindowGetters, }, user_config::UserConfig, wm_state::WmState, }; /// The distance in pixels to snap the window to the monitor's edge. const SNAP_DISTANCE: i32 = 15; pub fn move_window_in_direction( window: WindowContainer, direction: &Direction, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { match window { WindowContainer::TilingWindow(window) => { move_tiling_window(window, direction, state, config) } WindowContainer::NonTilingWindow(non_tiling_window) => { match non_tiling_window.state() { WindowState::Floating(_) => { move_floating_window(non_tiling_window, direction, state) } WindowState::Fullscreen(_) => move_to_workspace_in_direction( &non_tiling_window.into(), direction, state, ), _ => Ok(()), } } } } fn move_tiling_window( window_to_move: TilingWindow, direction: &Direction, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { // Flatten the parent split container if it only contains the window. if let Some(split_parent) = window_to_move .parent() .and_then(|parent| parent.as_split().cloned()) { if split_parent.child_count() == 1 { flatten_split_container(split_parent)?; } } let parent = window_to_move .direction_container() .context("No direction container.")?; let has_matching_tiling_direction = parent.tiling_direction() == TilingDirection::from_direction(direction); // Attempt to swap or move the window into a sibling container. if has_matching_tiling_direction { if let Some(sibling) = tiling_sibling_in_direction(&window_to_move, direction) { return move_to_sibling_container( window_to_move, sibling, direction, state, ); } } // Attempt to move the window to workspace in given direction. if (has_matching_tiling_direction || window_to_move.tiling_siblings().count() == 0) && parent.is_workspace() { return move_to_workspace_in_direction( &window_to_move.into(), direction, state, ); } // The window cannot be moved within the parent container, so traverse // upwards to find an ancestor that has the correct tiling direction. let target_ancestor = parent.ancestors().find_map(|ancestor| { ancestor.as_direction_container().ok().filter(|ancestor| { ancestor.tiling_direction() == TilingDirection::from_direction(direction) }) }); match target_ancestor { // If there is no suitable ancestor, then change the tiling direction // of the workspace. None => invert_workspace_tiling_direction( window_to_move, direction, state, config, ), // Otherwise, move the container into the given ancestor. This could // simply be the container's direct parent. Some(target_ancestor) => insert_into_ancestor( &window_to_move, &target_ancestor, direction, state, ), } } /// Gets the next sibling `TilingWindow` or `SplitContainer` in the given /// direction. fn tiling_sibling_in_direction( window: &TilingWindow, direction: &Direction, ) -> Option { match direction { Direction::Up | Direction::Left => window .prev_siblings() .find_map(|sibling| sibling.as_tiling_container().ok()), _ => window .next_siblings() .find_map(|sibling| sibling.as_tiling_container().ok()), } } fn move_to_sibling_container( window_to_move: TilingWindow, target_sibling: TilingContainer, direction: &Direction, state: &mut WmState, ) -> anyhow::Result<()> { let parent = window_to_move.parent().context("No parent.")?; match target_sibling { TilingContainer::TilingWindow(sibling_window) => { // Swap the window with sibling in given direction. move_container_within_tree( &window_to_move.clone().into(), &parent, sibling_window.index(), state, )?; state .pending_sync .queue_container_to_redraw(sibling_window) .queue_container_to_redraw(window_to_move); } TilingContainer::Split(sibling_split) => { let sibling_descendant = sibling_split.descendant_in_direction(&direction.inverse()); // Move the window into the sibling split container. if let Some(sibling_descendant) = sibling_descendant { let target_parent = sibling_descendant .direction_container() .context("No direction container.")?; let has_matching_tiling_direction = TilingDirection::from_direction(direction) == target_parent.tiling_direction(); let target_index = match direction { Direction::Down | Direction::Right if has_matching_tiling_direction => { sibling_descendant.index() } _ => sibling_descendant.index() + 1, }; move_container_within_tree( &window_to_move.into(), &target_parent.clone().into(), target_index, state, )?; state .pending_sync .queue_container_to_redraw(target_parent) .queue_containers_to_redraw(parent.tiling_children()); } } } Ok(()) } fn move_to_workspace_in_direction( window_to_move: &WindowContainer, direction: &Direction, state: &mut WmState, ) -> anyhow::Result<()> { let parent = window_to_move.parent().context("No parent.")?; let workspace = window_to_move.workspace().context("No workspace.")?; let monitor = parent.monitor().context("No monitor.")?; let target_workspace = state .monitor_in_direction(&monitor, direction)? .and_then(|monitor| monitor.displayed_workspace()); if let Some(target_workspace) = target_workspace { // Since the window is crossing monitors, adjustments might need to be // made because of DPI. if monitor.has_dpi_difference(&target_workspace.clone().into())? { window_to_move.set_has_pending_dpi_adjustment(true); } // Update floating placement since the window has to cross monitors. window_to_move.set_floating_placement( window_to_move .floating_placement() .translate_to_center(&target_workspace.to_rect()?), ); if let WindowContainer::NonTilingWindow(window_to_move) = &window_to_move { window_to_move.set_insertion_target(None); } let target_index = match direction { Direction::Down | Direction::Right => 0, _ => target_workspace.child_count(), }; // Focus should be reassigned within the original workspace after the // window is moved out. For example, if the focus order is 1. tiling // window and 2. fullscreen window, then we'd want to retain focus on a // tiling window on move. let focus_target = state.focus_target_after_removal(window_to_move); move_container_within_tree( &window_to_move.clone().into(), &target_workspace.clone().into(), target_index, state, )?; if let Some(focus_target) = focus_target { set_focused_descendant( &focus_target, Some(&workspace.clone().into()), ); } state .pending_sync .queue_container_to_redraw(window_to_move.clone()) .queue_containers_to_redraw(target_workspace.tiling_children()) .queue_containers_to_redraw(parent.tiling_children()) .queue_cursor_jump() .queue_workspace_to_reorder(target_workspace); } Ok(()) } fn invert_workspace_tiling_direction( window_to_move: TilingWindow, direction: &Direction, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let workspace = window_to_move.workspace().context("No workspace.")?; // Get top-level tiling children of the workspace. let workspace_children = workspace .tiling_children() .filter(|container| container.id() != window_to_move.id()) .collect::>(); // Create a new split container to wrap the window's siblings. For // example, in the layout H[1 V[2 3]] where container 3 is moved down, // we create a split container around 1 and 2. This results in // H[H[1 V[2 3]]], and V[H[1 V[2]] 3] after the tiling direction change. if workspace_children.len() > 1 { let split_container = SplitContainer::new( workspace.tiling_direction(), config.value.gaps.clone(), ); wrap_in_split_container( &split_container, &workspace.clone().into(), &workspace_children, )?; } // Invert the tiling direction of the workspace. workspace.set_tiling_direction(workspace.tiling_direction().inverse()); let target_index = match direction { Direction::Left | Direction::Up => 0, _ => workspace.child_count(), }; // Depending on the direction, place the window either before or after // the split container. move_container_within_tree( &window_to_move.clone().into(), &workspace.clone().into(), target_index, state, )?; // Workspace might have redundant split containers after the tiling // direction change. For example, V[H[1 2] 3] where container 3 is moved // up results in H[3 H[1 2]], and needs to be flattened to H[3 1 2]. flatten_child_split_containers(&workspace.clone().into())?; // Resize the window such that the split container and window are each // 0.5. resize_tiling_container(&window_to_move.into(), 0.5); state .pending_sync .queue_containers_to_redraw(workspace.tiling_children()); Ok(()) } fn insert_into_ancestor( window_to_move: &TilingWindow, target_ancestor: &DirectionContainer, direction: &Direction, state: &mut WmState, ) -> anyhow::Result<()> { // Traverse upwards to find container whose parent is the target // ancestor. Then, depending on the direction, insert before or after // that container. let window_ancestor = window_to_move .ancestors() .find(|container| { container .parent() .is_some_and(|parent| parent == target_ancestor.clone().into()) }) .context("Window ancestor not found.")?; let target_index = match direction { Direction::Up | Direction::Left => window_ancestor.index(), _ => window_ancestor.index() + 1, }; // Move the window into the container above. move_container_within_tree( &window_to_move.clone().into(), &target_ancestor.clone().into(), target_index, state, )?; state .pending_sync .queue_containers_to_redraw(target_ancestor.tiling_children()); Ok(()) } fn move_floating_window( window_to_move: NonTilingWindow, direction: &Direction, state: &mut WmState, ) -> anyhow::Result<()> { let new_position = new_floating_position(&window_to_move, direction, state)?; if let Some((position_rect, target_monitor)) = new_position { let monitor = window_to_move.monitor().context("No monitor.")?; // Mark window as needing DPI adjustment if it crosses monitors. The // handler for `PlatformEvent::LocationChanged` will update the // window's workspace if it goes out of bounds of its current // workspace. if monitor.id() != target_monitor.id() && monitor.has_dpi_difference(&target_monitor.into())? { window_to_move.set_has_pending_dpi_adjustment(true); } window_to_move.set_floating_placement(position_rect); state.pending_sync.queue_container_to_redraw(window_to_move); } Ok(()) } /// Returns a tuple of the new floating position and the target monitor. fn new_floating_position( window_to_move: &NonTilingWindow, direction: &Direction, state: &mut WmState, ) -> anyhow::Result> { let monitor = window_to_move.monitor().context("No monitor.")?; let monitor_rect = monitor.native_properties().working_area; let window_pos = window_to_move.native_properties().frame; let is_on_monitor_edge = match direction { Direction::Up => window_pos.top == monitor_rect.top, Direction::Down => window_pos.bottom == monitor_rect.bottom, Direction::Left => window_pos.left == monitor_rect.left, Direction::Right => window_pos.right == monitor_rect.right, }; // Window is on the edge of the monitor and should be moved to a // different monitor in the given direction. if is_on_monitor_edge { let next_monitor = state.monitor_in_direction(&monitor, direction)?; if let Some(next_monitor) = next_monitor { let monitor_rect = next_monitor.native().working_area()?.clone(); let position = snap_to_monitor_edge( &window_pos, &monitor_rect, &direction.inverse(), ) .clamp(&monitor_rect); return Ok(Some((position, next_monitor))); } return Ok(None); } let (monitor_length, window_length) = match direction { Direction::Up | Direction::Down => { (monitor_rect.height(), window_pos.height()) } _ => (monitor_rect.width(), window_pos.width()), }; let length_delta = monitor_length - window_length; // Calculate the distance the window should move based on the ratio of // the window's length to the monitor's length. #[allow(clippy::cast_precision_loss)] let move_distance = match window_length as f32 / monitor_length as f32 { x if (0.0..0.2).contains(&x) => length_delta / 5, x if (0.2..0.4).contains(&x) => length_delta / 4, x if (0.4..0.6).contains(&x) => length_delta / 3, _ => length_delta / 2, }; // Snap the window to the current monitor's edge if it's within 15px of // it after the move. let should_snap_to_edge = match direction { Direction::Up => { window_pos.top - move_distance - SNAP_DISTANCE < monitor_rect.top } Direction::Down => { window_pos.bottom + move_distance + SNAP_DISTANCE > monitor_rect.bottom } Direction::Left => { window_pos.left - move_distance - SNAP_DISTANCE < monitor_rect.left } Direction::Right => { window_pos.right + move_distance + SNAP_DISTANCE > monitor_rect.right } }; if should_snap_to_edge { let position = snap_to_monitor_edge(&window_pos, &monitor_rect, direction); return Ok(Some((position, monitor))); } // Snap the window to the current monitor's inverse edge if it's in // between two monitors or outside the bounds of the current monitor. let should_snap_to_inverse_edge = match direction { Direction::Up => window_pos.bottom > monitor_rect.bottom, Direction::Down => window_pos.top < monitor_rect.top, Direction::Left => window_pos.right > monitor_rect.right, Direction::Right => window_pos.left < monitor_rect.left, }; let position = if should_snap_to_inverse_edge { snap_to_monitor_edge(&window_pos, &monitor_rect, &direction.inverse()) } else { window_pos.translate_in_direction(direction, move_distance) }; Ok(Some((position, monitor))) } fn snap_to_monitor_edge( window_pos: &Rect, monitor_rect: &Rect, edge: &Direction, ) -> Rect { let (x, y) = match edge { Direction::Up => (window_pos.x(), monitor_rect.top), Direction::Down => { (window_pos.x(), monitor_rect.bottom - window_pos.height()) } Direction::Left => (monitor_rect.left, window_pos.y()), Direction::Right => { (monitor_rect.right - window_pos.width(), window_pos.y()) } }; window_pos.translate_to_coordinates(x, y) } ================================================ FILE: packages/wm/src/commands/window/move_window_to_workspace.rs ================================================ use anyhow::Context; use tracing::info; use wm_common::WindowState; use crate::{ commands::{ container::{move_container_within_tree, set_focused_descendant}, workspace::activate_workspace, }, models::{WindowContainer, WorkspaceTarget}, traits::{CommonGetters, PositionGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; pub fn move_window_to_workspace( window: WindowContainer, target: WorkspaceTarget, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let current_workspace = window.workspace().context("No workspace.")?; let current_monitor = current_workspace.monitor().context("No monitor.")?; let (target_workspace_name, target_workspace) = state.workspace_by_target(¤t_workspace, target, config)?; // Retrieve or activate the target workspace by its name. let target_workspace = match target_workspace { Some(_) => anyhow::Ok(target_workspace), _ => match target_workspace_name { Some(name) => { activate_workspace(Some(&name), None, state, config)?; Ok(state.workspace_by_name(&name)) } _ => Ok(None), }, }?; if let Some(target_workspace) = target_workspace { if target_workspace.id() == current_workspace.id() { return Ok(()); } info!( "Moving window to workspace: '{}'.", target_workspace.config().name ); let target_monitor = target_workspace.monitor().context("No monitor.")?; // Since target workspace could be on a different monitor, adjustments // might need to be made because of DPI. if current_monitor .has_dpi_difference(&target_monitor.clone().into())? { window.set_has_pending_dpi_adjustment(true); } // Update floating placement if the window has to cross monitors. if target_monitor.id() != current_monitor.id() { window.set_floating_placement( window .floating_placement() .translate_to_center(&target_workspace.to_rect()?), ); } if let WindowContainer::NonTilingWindow(window) = &window { window.set_insertion_target(None); } // Focus target is `None` if the window is not focused. let focus_target = state.focus_target_after_removal(&window); let focus_reset_target = if target_workspace.is_displayed() { None } else { target_monitor.descendant_focus_order().next() }; let insertion_sibling = target_workspace .descendant_focus_order() .filter_map(|descendant| descendant.as_window_container().ok()) .find(|descendant| descendant.state() == WindowState::Tiling); // Insert the window into the target workspace. match (window.is_tiling_window(), insertion_sibling.is_some()) { (true, true) => { if let Some(insertion_sibling) = insertion_sibling { move_container_within_tree( &window.clone().into(), &insertion_sibling.clone().parent().context("No parent.")?, insertion_sibling.index() + 1, state, )?; } } _ => { move_container_within_tree( &window.clone().into(), &target_workspace.clone().into(), target_workspace.child_count(), state, )?; } } // When moving a focused window within the tree to another workspace, // the target workspace will get displayed. If moving the window e.g. // from monitor 1 -> 2, and the target workspace is hidden on that // monitor, we want to reset focus to the workspace that was displayed // on that monitor. if let Some(focus_reset_target) = focus_reset_target { set_focused_descendant( &focus_reset_target, Some(&target_monitor.into()), ); } // Retain focus within the workspace from where the window was moved. if let Some(focus_target) = focus_target { set_focused_descendant(&focus_target, None); state.pending_sync.queue_focus_change(); } match window { WindowContainer::NonTilingWindow(_) => { state.pending_sync.queue_container_to_redraw(window); } WindowContainer::TilingWindow(_) => { state .pending_sync .queue_containers_to_redraw(current_workspace.tiling_children()) .queue_containers_to_redraw(target_workspace.tiling_children()); } } state .pending_sync .queue_workspace_to_reorder(target_workspace); } Ok(()) } ================================================ FILE: packages/wm/src/commands/window/resize_window.rs ================================================ use wm_platform::LengthValue; use super::set_window_size; use crate::{ models::WindowContainer, traits::{CommonGetters, PositionGetters, TilingSizeGetters}, wm_state::WmState, }; pub fn resize_window( window: &WindowContainer, width_delta: Option, height_delta: Option, state: &mut WmState, ) -> anyhow::Result<()> { let window_rect = window.to_rect()?; let target_width = match width_delta { Some(delta) => { let parent_width = match window.as_tiling_container() { Ok(tiling_window) => tiling_window .container_to_resize(true)? .and_then(|container| container.parent()) .and_then(|parent| { parent.to_rect().ok().map(|rect| rect.width()) }) .and_then(|parent_width| { let (horizontal_gap, _) = tiling_window.inner_gaps().ok()?; #[allow( clippy::cast_possible_wrap, clippy::cast_possible_truncation )] Some( parent_width - horizontal_gap * tiling_window.tiling_siblings().count() as i32, ) }), _ => window.parent().and_then(|parent| { parent.to_rect().ok().map(|rect| rect.width()) }), }; parent_width.map(|parent_width| { window_rect.width() + delta.to_px(parent_width, None) }) } _ => None, }; let target_height = match height_delta { Some(delta) => { let parent_height = match window.as_tiling_container() { Ok(tiling_window) => tiling_window .container_to_resize(false)? .and_then(|container| container.parent()) .and_then(|parent| { parent.to_rect().ok().map(|rect| rect.height()) }) .and_then(|parent_height| { let (_, vertical_gap) = tiling_window.inner_gaps().ok()?; #[allow( clippy::cast_possible_wrap, clippy::cast_possible_truncation )] Some( parent_height - vertical_gap * tiling_window.tiling_siblings().count() as i32, ) }), _ => window.parent().and_then(|parent| { parent.to_rect().ok().map(|rect| rect.height()) }), }; parent_height.map(|parent_height| { window_rect.height() + delta.to_px(parent_height, None) }) } _ => None, }; set_window_size( window.clone(), target_width.map(LengthValue::from_px), target_height.map(LengthValue::from_px), state, )?; Ok(()) } ================================================ FILE: packages/wm/src/commands/window/run_window_rules.rs ================================================ use tracing::info; use wm_common::WindowRuleEvent; use crate::{ models::WindowContainer, traits::{CommonGetters, WindowGetters}, user_config::UserConfig, wm::WindowManager, wm_state::WmState, }; /// Returns the window (if it's still attached) after running the window /// rules. pub fn run_window_rules( window: WindowContainer, event_type: &WindowRuleEvent, state: &mut WmState, config: &mut UserConfig, ) -> anyhow::Result> { let pending_window_rules = config.pending_window_rules(&window, event_type); let mut subject_window = window; for rule in pending_window_rules { info!("Running window rule with commands: {:?}.", rule.commands); for command in &rule.commands { WindowManager::run_command( command, subject_window.clone().into(), state, config, )?; // Update the subject container in case the container type changes. // For example, when going from a tiling to a floating window. subject_window = if subject_window.is_detached() { match state.window_from_native(&subject_window.native()) { Some(window) => window, None => return Ok(None), } } else { subject_window } } // Add the window rule as done. if rule.run_once { let window_rules = subject_window .done_window_rules() .into_iter() .chain(std::iter::once(rule)); subject_window.set_done_window_rules(window_rules.collect()); } } Ok(Some(subject_window)) } ================================================ FILE: packages/wm/src/commands/window/set_window_position.rs ================================================ use anyhow::Context; use wm_common::WindowState; use wm_platform::Rect; use crate::{ models::WindowContainer, traits::{CommonGetters, PositionGetters, WindowGetters}, wm_state::WmState, }; pub enum WindowPositionTarget { Centered, Coordinates(Option, Option), } pub fn set_window_position( window: WindowContainer, target: &WindowPositionTarget, state: &mut WmState, ) -> anyhow::Result<()> { if matches!(window.state(), WindowState::Floating(_)) { let placement = window.floating_placement(); let new_placement = match target { WindowPositionTarget::Centered => placement.translate_to_center( &window.workspace().context("No workspace.")?.to_rect()?, ), WindowPositionTarget::Coordinates(target_x, target_y) => { Rect::from_xy( target_x.unwrap_or(placement.x()), target_y.unwrap_or(placement.y()), placement.width(), placement.height(), ) } }; window.set_floating_placement(new_placement); // TODO: `has_custom_floating_placement` should be marked `true` if // manually positioned to be centered (e.g. via `position --centered`). let is_centered = matches!(target, WindowPositionTarget::Centered); window.set_has_custom_floating_placement(!is_centered); state.pending_sync.queue_container_to_redraw(window); } Ok(()) } ================================================ FILE: packages/wm/src/commands/window/set_window_size.rs ================================================ use anyhow::Context; use wm_common::WindowState; use wm_platform::{LengthValue, Rect}; use crate::{ commands::container::resize_tiling_container, models::{NonTilingWindow, TilingWindow, WindowContainer}, traits::{ CommonGetters, PositionGetters, TilingSizeGetters, WindowGetters, }, wm_state::WmState, }; /// Arbitrary defaults for minimum floating window dimensions. const MIN_FLOATING_WIDTH: i32 = 250; const MIN_FLOATING_HEIGHT: i32 = 140; pub fn set_window_size( window: WindowContainer, target_width: Option, target_height: Option, state: &mut WmState, ) -> anyhow::Result<()> { match window { WindowContainer::TilingWindow(window) => { set_tiling_window_size(&window, target_width, target_height, state)?; } WindowContainer::NonTilingWindow(window) => { if matches!(window.state(), WindowState::Floating(_)) { set_floating_window_size( &window, target_width, target_height, state, )?; } } } Ok(()) } fn set_tiling_window_size( window: &TilingWindow, target_width: Option, target_height: Option, state: &mut WmState, ) -> anyhow::Result<()> { if let Some(target_width) = target_width { set_tiling_window_length(window, &target_width, true, state)?; } if let Some(target_height) = target_height { set_tiling_window_length(window, &target_height, false, state)?; } Ok(()) } /// Updates either the width or height of a tiling window. fn set_tiling_window_length( window: &TilingWindow, target_length: &LengthValue, is_width_resize: bool, state: &mut WmState, ) -> anyhow::Result<()> { // When resizing a tiling window, the container to resize can actually be // an ancestor split container. let container_to_resize = window.container_to_resize(is_width_resize)?; if let Some(container_to_resize) = container_to_resize { let parent = container_to_resize.parent().context("No parent.")?; let (horizontal_gap, vertical_gap) = container_to_resize.inner_gaps()?; #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] let parent_length = if is_width_resize { parent.to_rect()?.width() - horizontal_gap * window.tiling_siblings().count() as i32 } else { parent.to_rect()?.height() - vertical_gap * window.tiling_siblings().count() as i32 }; // Convert the target length to a tiling size. let tiling_size = target_length.to_percentage(parent_length); // Skip the resize if the window is already at the target size. if container_to_resize.tiling_size() - tiling_size != 0. { resize_tiling_container(&container_to_resize, tiling_size); state .pending_sync .queue_containers_to_redraw(parent.tiling_children()); } } Ok(()) } fn set_floating_window_size( window: &NonTilingWindow, target_width: Option, target_height: Option, state: &mut WmState, ) -> anyhow::Result<()> { let monitor = window.monitor().context("No monitor")?; let monitor_rect = monitor.to_rect()?; let window_rect = window.to_rect()?; // Prevent resize from making the window smaller than minimum dimensions. // Always allow the size to be increased, even if the window would still // be within minimum dimension values. let length_with_clamp = |target_length: Option, current_length, min_length| { target_length.map_or(current_length, |target_length| { if target_length >= current_length { target_length } else { target_length.max(min_length) } }) }; let target_width_px = target_width .map(|target_width| target_width.to_px(monitor_rect.width(), None)); let new_width = length_with_clamp( target_width_px, window_rect.width(), MIN_FLOATING_WIDTH, ); let target_height_px = target_height .map(|target_height| target_height.to_px(monitor_rect.height(), None)); let new_height = length_with_clamp( target_height_px, window_rect.height(), MIN_FLOATING_HEIGHT, ); window.set_floating_placement(Rect::from_xy( window.floating_placement().x(), window.floating_placement().y(), new_width, new_height, )); state.pending_sync.queue_container_to_redraw(window.clone()); Ok(()) } ================================================ FILE: packages/wm/src/commands/window/unmanage_window.rs ================================================ use anyhow::Context; use wm_common::{WindowState, WmEvent}; use crate::{ commands::container::{ detach_container, flatten_child_split_containers, set_focused_descendant, }, models::WindowContainer, traits::{CommonGetters, WindowGetters}, wm_state::WmState, }; #[allow(clippy::needless_pass_by_value)] pub fn unmanage_window( window: WindowContainer, state: &mut WmState, ) -> anyhow::Result<()> { // Create iterator of parent, grandparent, and great-grandparent. let ancestors = window.ancestors().take(3).collect::>(); // Get container to switch focus to after the window has been removed. let focus_target = state.focus_target_after_removal(&window.clone()); detach_container(window.clone().into())?; // After detaching the container, flatten any redundant split containers. // For example, in the layout V[1 H[2]] where container 1 is detached to // become V[H[2]], this will then need to be flattened to V[2]. for ancestor in ancestors.iter().rev() { flatten_child_split_containers(ancestor)?; } state.emit_event(WmEvent::WindowUnmanaged { unmanaged_id: window.id(), #[allow(clippy::cast_possible_wrap, clippy::unnecessary_cast)] unmanaged_handle: window.native().id().0 as isize, }); // Reassign focus to suitable target. if let Some(focus_target) = focus_target { set_focused_descendant(&focus_target, None); state.pending_sync.queue_focus_change(); state.unmanaged_or_minimized_timestamp = Some(std::time::Instant::now()); } // Sibling containers need to be redrawn if the window was tiling. if window.state() == WindowState::Tiling { let ancestor_to_redraw = ancestors .into_iter() .find(|ancestor| !ancestor.is_detached()) .context("No ancestor to redraw.")?; state .pending_sync .queue_containers_to_redraw(ancestor_to_redraw.tiling_children()); } Ok(()) } ================================================ FILE: packages/wm/src/commands/window/update_window_state.rs ================================================ use anyhow::Context; use tracing::{info, warn}; use wm_common::WindowState; use crate::{ commands::container::{ move_container_within_tree, replace_container, resize_tiling_container, }, models::{Container, InsertionTarget, WindowContainer}, traits::{CommonGetters, TilingSizeGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; /// Updates the state of a window. /// /// Adds the window for redraw if there is a state change. /// /// Returns the window after the state change. pub fn update_window_state( window: WindowContainer, target_state: WindowState, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result { if window.state() == target_state { return Ok(window); } info!("Updating window state: {:?}.", target_state); match target_state { WindowState::Tiling => set_tiling(&window, state, config), _ => set_non_tiling(window, target_state, state), } } /// Updates the state of a window to be `WindowState::Tiling`. fn set_tiling( window: &WindowContainer, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result { let window = window .as_non_tiling_window() .context("Invalid window state.")? .clone(); let workspace = window.workspace().context("Window has no workspace.")?; // Check whether insertion target is still valid. let insertion_target = window.insertion_target().filter(|insertion_target| { insertion_target .target_parent .workspace() .is_some_and(|workspace| workspace.is_displayed()) }); // Get the position in the tree to insert the new tiling window. This // will be the window's previous tiling position if it has one, or // instead beside the last focused tiling window in the workspace. let (target_parent, target_index) = insertion_target .as_ref() .map(|insertion_target| { ( insertion_target.target_parent.clone(), insertion_target.target_index, ) }) // Fallback to the last focused tiling window within the workspace. .or_else(|| { let focused_window = workspace .descendant_focus_order() .find(Container::is_tiling_window)?; Some((focused_window.parent()?, focused_window.index() + 1)) }) // Default to inserting at the end of the workspace. .unwrap_or((workspace.clone().into(), workspace.child_count())); let tiling_window = window.to_tiling(config.value.gaps.clone()); // Replace the original window with the created tiling window. replace_container( &tiling_window.clone().into(), &window.parent().context("No parent.")?, window.index(), )?; move_container_within_tree( &tiling_window.clone().into(), &target_parent, target_index, state, )?; #[allow(clippy::cast_precision_loss)] if let Some(insertion_target) = &insertion_target { let size_scale = (insertion_target.prev_sibling_count + 1) as f32 / (tiling_window.tiling_siblings().count() + 1) as f32; // Scale the window's previous size based on the current number of // siblings. E.g. if the window was 0.5 with 1 sibling, and now has 2 // siblings, scale to 0.5 * (2/3) to maintain proportional sizing. let target_size = insertion_target.prev_tiling_size * size_scale; resize_tiling_container(&tiling_window.clone().into(), target_size); } state .pending_sync .queue_containers_to_redraw(target_parent.tiling_children()) .queue_workspace_to_reorder(workspace); Ok(tiling_window.into()) } /// Updates the state of a window to be either `WindowState::Floating`, /// `WindowState::Fullscreen`, or `WindowState::Minimized`. fn set_non_tiling( window: WindowContainer, target_state: WindowState, state: &mut WmState, ) -> anyhow::Result { // A window can only be updated to a minimized state if it is // natively minimized. // TODO: Consider doing the same for maximized and fullscreen states. if target_state == WindowState::Minimized && !window.native_properties().is_minimized { info!("No window state update. Minimizing window."); // TODO: Instead of doing the platform call directly here, instead add // a `queue_state_change` method to `PendingSync`. if let Err(err) = window.native().minimize() { warn!("Failed to minimize window: {}", err); } return Ok(window); } let workspace = window.workspace().context("No workspace.")?; match window { WindowContainer::NonTilingWindow(window) => { let current_state = window.state(); // Update the window's previous state if the discriminant changes. // TODO: Move out handling of active drag. Can then simplify calls to // `set_active_drag` in `handle_window_moved_or_resized_end`. if !current_state.is_same_state(&target_state) && window.active_drag().is_none() { window.set_prev_state(current_state); state.pending_sync.queue_workspace_to_reorder(workspace); } window.set_state(target_state); state.pending_sync.queue_container_to_redraw(window.clone()); Ok(window.into()) } WindowContainer::TilingWindow(window) => { let parent = window.parent().context("No parent")?; let non_tiling_window = window.to_non_tiling( target_state.clone(), Some(InsertionTarget { target_parent: parent.clone(), target_index: window.index(), prev_tiling_size: window.tiling_size(), prev_sibling_count: window.tiling_siblings().count(), }), ); // Non-tiling windows should always be direct children of the // workspace. if parent != workspace.clone().into() { move_container_within_tree( &window.clone().into(), &workspace.clone().into(), workspace.child_count(), state, )?; } replace_container( &non_tiling_window.clone().into(), &workspace.clone().into(), window.index(), )?; state .pending_sync .queue_container_to_redraw(non_tiling_window.clone()) .queue_containers_to_redraw(workspace.tiling_children()) .queue_workspace_to_reorder(workspace); Ok(non_tiling_window.into()) } } } ================================================ FILE: packages/wm/src/commands/workspace/activate_workspace.rs ================================================ use anyhow::Context; use tracing::info; use wm_common::{TilingDirection, WmEvent, WorkspaceConfig}; use super::sort_workspaces; use crate::{ commands::container::attach_container, models::{Monitor, Workspace}, traits::{CommonGetters, PositionGetters}, user_config::UserConfig, wm_state::WmState, }; /// Activates a workspace on the target monitor. /// /// If no workspace name is provided, the first suitable workspace defined /// in the user's config will be used. /// /// If no target monitor is provided, the workspace is activated on /// whichever monitor it is bound to, or the currently focused monitor. pub fn activate_workspace( workspace_name: Option<&str>, target_monitor: Option, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let workspace_config = workspace_config( workspace_name, target_monitor.clone(), state, config, )?; let target_monitor = target_monitor .or_else(|| { workspace_config .bind_to_monitor .and_then(|index| { state .monitors() .into_iter() .find(|monitor| monitor.index() == index as usize) }) .or_else(|| { state .focused_container() .and_then(|focused| focused.monitor()) }) }) .context("Failed to get a target monitor for the workspace.")?; let monitor_rect = target_monitor.to_rect()?; let tiling_direction = if monitor_rect.height() > monitor_rect.width() { TilingDirection::Vertical } else { TilingDirection::Horizontal }; let workspace = Workspace::new( workspace_config.clone(), config.value.gaps.clone(), tiling_direction, ); // Attach the created workspace to the specified monitor. attach_container( &workspace.clone().into(), &target_monitor.clone().into(), None, )?; sort_workspaces(&target_monitor, config)?; info!("Activating workspace: {workspace}"); state.emit_event(WmEvent::WorkspaceActivated { activated_workspace: workspace.to_dto()?, }); Ok(()) } /// Gets config for the workspace to activate. fn workspace_config( workspace_name: Option<&str>, target_monitor: Option, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result { let found_config = match workspace_name { Some(workspace_name) => config .inactive_workspace_configs(&state.workspaces()) .into_iter() .find(|config| config.name == workspace_name) .with_context(|| { format!( "Workspace with name '{workspace_name}' doesn't exist or is already active." ) }), None => target_monitor .and_then(|target_monitor| { config.workspace_config_for_monitor( &target_monitor, &state.workspaces(), ) }) .or_else(|| { config.next_inactive_workspace_config(&state.workspaces()) }) .context("No workspace config available to activate workspace."), }; found_config.cloned() } ================================================ FILE: packages/wm/src/commands/workspace/deactivate_workspace.rs ================================================ use tracing::info; use wm_common::WmEvent; use crate::{ commands::container::detach_container, models::Workspace, traits::CommonGetters, wm_state::WmState, }; /// Deactivates a given workspace. This removes the container from its /// parent monitor and emits a `WorkspaceDeactivated` event. #[allow(clippy::needless_pass_by_value)] pub fn deactivate_workspace( workspace: Workspace, state: &WmState, ) -> anyhow::Result<()> { info!("Deactivating workspace: {workspace}"); detach_container(workspace.clone().into())?; state.emit_event(WmEvent::WorkspaceDeactivated { deactivated_id: workspace.id(), deactivated_name: workspace.config().name, }); Ok(()) } ================================================ FILE: packages/wm/src/commands/workspace/focus_workspace.rs ================================================ use anyhow::Context; use tracing::info; use super::activate_workspace; use crate::{ commands::{ container::set_focused_descendant, workspace::deactivate_workspace, }, models::WorkspaceTarget, traits::CommonGetters, user_config::UserConfig, wm_state::WmState, }; /// Focuses a workspace by a given target. /// /// This target can be a workspace name, the most recently focused /// workspace, the next workspace, the previous workspace, or the workspace /// in a given direction from the currently focused workspace. /// /// The workspace will be activated if it isn't already active. pub fn focus_workspace( target: WorkspaceTarget, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let focused_workspace = state .focused_container() .and_then(|focused| focused.workspace()) .context("No workspace is currently focused.")?; let (target_workspace_name, target_workspace) = state.workspace_by_target(&focused_workspace, target, config)?; // Retrieve or activate the target workspace by its name. let target_workspace = match target_workspace { Some(_) => anyhow::Ok(target_workspace), _ => match target_workspace_name { Some(name) => { activate_workspace(Some(&name), None, state, config)?; Ok(state.workspace_by_name(&name)) } _ => Ok(None), }, }?; if let Some(target_workspace) = target_workspace { info!("Focusing workspace: {target_workspace}"); // Get the currently displayed workspace on the same monitor that the // workspace to focus is on. let displayed_workspace = target_workspace .monitor() .and_then(|monitor| monitor.displayed_workspace()) .context("No workspace is currently displayed.")?; // Set focus to whichever window last had focus in workspace. If the // workspace has no windows, then set focus to the workspace itself. let container_to_focus = target_workspace .descendant_focus_order() .next() .unwrap_or_else(|| target_workspace.clone().into()); set_focused_descendant(&container_to_focus, None); state.pending_sync.queue_focus_change(); // Display the workspace to switch focus to. state .pending_sync .queue_container_to_redraw(displayed_workspace) .queue_container_to_redraw(target_workspace); // Get empty workspace to destroy (if one is found). Cannot destroy // empty workspaces if they're the only workspace on the monitor. let workspace_to_destroy = state.workspaces().into_iter().find(|workspace| { !workspace.config().keep_alive && !workspace.has_children() && !workspace.is_displayed() }); if let Some(workspace) = workspace_to_destroy { deactivate_workspace(workspace, state)?; } // Save the currently focused workspace as recent. state.recent_workspace_name = Some(focused_workspace.config().name); state.pending_sync.queue_cursor_jump(); } Ok(()) } ================================================ FILE: packages/wm/src/commands/workspace/mod.rs ================================================ mod activate_workspace; mod deactivate_workspace; mod focus_workspace; mod move_workspace_in_direction; mod sort_workspaces; mod update_workspace_config; pub use activate_workspace::*; pub use deactivate_workspace::*; pub use focus_workspace::*; pub use move_workspace_in_direction::*; pub use sort_workspaces::*; pub use update_workspace_config::*; ================================================ FILE: packages/wm/src/commands/workspace/move_workspace_in_direction.rs ================================================ use anyhow::Context; use wm_common::WmEvent; use wm_platform::Direction; use super::{activate_workspace, deactivate_workspace, sort_workspaces}; use crate::{ commands::container::move_container_within_tree, models::Workspace, traits::{CommonGetters, PositionGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; pub fn move_workspace_in_direction( workspace: &Workspace, direction: &Direction, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let origin_monitor = workspace.monitor().context("No monitor.")?; let target_monitor = state.monitor_in_direction(&origin_monitor, direction)?; if let Some(target_monitor) = target_monitor { // Get currently displayed workspace on the target monitor. let displayed_workspace = target_monitor .displayed_workspace() .context("No displayed workspace.")?; move_container_within_tree( &workspace.clone().into(), &target_monitor.clone().into(), target_monitor.child_count(), state, )?; let windows = workspace .descendants() .filter_map(|descendant| descendant.as_window_container().ok()); for window in windows { window.set_has_pending_dpi_adjustment(true); window.set_floating_placement( window .floating_placement() .translate_to_center(&workspace.to_rect()?), ); } state .pending_sync .queue_cursor_jump() .queue_container_to_redraw(workspace.clone()) .queue_container_to_redraw(displayed_workspace); match origin_monitor.child_count() { 0 => { // Prevent origin monitor from having no workspaces. activate_workspace(None, Some(origin_monitor), state, config)?; } _ => { // Redraw the workspace on the origin monitor. state.pending_sync.queue_container_to_redraw( origin_monitor .displayed_workspace() .context("No displayed workspace.")?, ); } } // Get empty workspace to destroy (if one is found). Cannot destroy // empty workspaces if they're the only workspace on the monitor. let workspace_to_destroy = target_monitor.workspaces().into_iter().find(|workspace| { !workspace.config().keep_alive && !workspace.has_children() && !workspace.is_displayed() }); if let Some(workspace) = workspace_to_destroy { deactivate_workspace(workspace, state)?; } sort_workspaces(&target_monitor, config)?; state.emit_event(WmEvent::WorkspaceUpdated { updated_workspace: workspace.to_dto()?, }); } Ok(()) } ================================================ FILE: packages/wm/src/commands/workspace/sort_workspaces.rs ================================================ use anyhow::Context; use wm_common::VecDequeExt; use crate::{ models::Monitor, traits::CommonGetters, user_config::UserConfig, }; /// Sorts a monitor's workspaces by config order. pub fn sort_workspaces( monitor: &Monitor, config: &UserConfig, ) -> anyhow::Result<()> { let mut workspaces = monitor.workspaces(); config.sort_workspaces(&mut workspaces); for workspace in &workspaces { let target_index = &workspaces .iter() .position(|sorted_workspace| sorted_workspace.id() == workspace.id()) .context("Failed to get workspace target index.")?; monitor .borrow_children_mut() .shift_to_index(*target_index, workspace.clone().into()); } Ok(()) } ================================================ FILE: packages/wm/src/commands/workspace/update_workspace_config.rs ================================================ use anyhow::Context; use wm_common::{InvokeUpdateWorkspaceConfig, WmEvent, WorkspaceConfig}; use super::sort_workspaces; use crate::{ models::Workspace, traits::CommonGetters, user_config::UserConfig, wm_state::WmState, }; pub fn update_workspace_config( workspace: &Workspace, state: &WmState, config: &UserConfig, new_config: &InvokeUpdateWorkspaceConfig, ) -> anyhow::Result<()> { let current_config = workspace.config(); // Validate the workspace name change. if let Some(new_name) = &new_config.name { if new_name != ¤t_config.name { if let Some(_other_workspace) = state.workspace_by_name(new_name) { anyhow::bail!("The workspace \"{}\" already exists", new_name); } } } // Update the config with the incoming values. let updated_config = WorkspaceConfig { name: new_config .name .clone() .unwrap_or(current_config.name.clone()), display_name: new_config .display_name .clone() .or(current_config.display_name.clone()), bind_to_monitor: new_config .bind_to_monitor .or(current_config.bind_to_monitor), keep_alive: new_config.keep_alive.unwrap_or(current_config.keep_alive), }; workspace.set_config(updated_config); sort_workspaces( &workspace.monitor().context("No displayed workspace.")?, config, )?; // TODO: Re-assign bound workspaces to their respective monitors. state.emit_event(WmEvent::WorkspaceUpdated { updated_workspace: workspace.to_dto()?, }); Ok(()) } ================================================ FILE: packages/wm/src/events/handle_display_settings_changed.rs ================================================ use anyhow::Context; use wm_common::try_warn; use crate::{ commands::monitor::{ add_monitor, move_bounded_workspaces_to_new_monitor, remove_monitor, sort_monitors, update_monitor, }, models::{Monitor, NativeMonitorProperties}, traits::{CommonGetters, PositionGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; pub fn handle_display_settings_changed( state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { tracing::info!("Display settings changed."); // Ignore the event if retrieval of the displays or their properties // fails (can happen transiently during sleep/wake). let displays = try_warn!(state .dispatcher .sorted_displays() .map_err(anyhow::Error::from) .and_then(|displays| { displays .into_iter() .map(|display| { let properties = NativeMonitorProperties::try_from(&display)?; Ok((display, properties)) }) .try_collect::>() })); let mut pending_monitors = state.monitors(); let mut unmatched_displays = Vec::new(); // Match each display to an existing monitor and update it. for (display, properties) in displays { match find_matching_monitor(&pending_monitors, &properties) { Some((monitor, index)) => { update_monitor(monitor, &display, properties, state)?; pending_monitors.remove(index); } None => unmatched_displays.push((display, properties)), } } let mut new_monitors: Vec = Vec::new(); // Pair unmatched displays with unmatched monitors, or add new ones. for (display, properties) in unmatched_displays { if pending_monitors.is_empty() { let monitor = add_monitor(display, properties, state)?; new_monitors.push(monitor); } else { let monitor = pending_monitors.remove(0); update_monitor(&monitor, &display, properties, state)?; } } // Remove monitors that no longer have a corresponding display and move // their workspaces to other monitors. // // Prevent removal of the last monitor (i.e. for when all monitors are // disconnected). This will cause the WM's monitors to temporarily // mismatch the OS monitor state, however, it'll be updated correctly // when a new monitor is connected again. for monitor in pending_monitors { if state.monitors().len() > 1 { remove_monitor(monitor, state, config)?; } } // Sort monitors by position. sort_monitors(&state.root_container)?; for new_monitor in new_monitors { move_bounded_workspaces_to_new_monitor(&new_monitor, state, config)?; } for window in state.windows() { // Display setting changes can spread windows out sporadically, so mark // all windows as needing a DPI adjustment (just in case). window.set_has_pending_dpi_adjustment(true); // Need to update floating position of moved windows when a monitor is // disconnected or if the primary display is changed. The primary // display dictates the position of 0,0. let workspace = window.workspace().context("No workspace.")?; let should_recenter = if window.has_custom_floating_placement() { let workspace_rect = workspace.to_rect()?; // Keep the placement if it still intersects the workspace, since // `PlatformEvent::DisplaySettingsChanged` can be triggered by // non-monitor changes (e.g. unplugging a USB device). window .floating_placement() .intersection_area(&workspace_rect) == 0 } else { true }; if should_recenter { window.set_floating_placement( window .floating_placement() .translate_to_center(&workspace.to_rect()?), ); } } // Redraw full container tree. state .pending_sync .queue_container_to_redraw(state.root_container.clone()); Ok(()) } /// Finds the monitor matching the given display properties. /// /// Returns the monitor and its index within the list of monitors. fn find_matching_monitor<'a>( monitors: &'a [Monitor], properties: &NativeMonitorProperties, ) -> Option<(&'a Monitor, usize)> { monitors.iter().enumerate().find_map(|(index, monitor)| { let existing = monitor.native_properties(); let is_match = { #[cfg(target_os = "macos")] { existing.device_uuid == properties.device_uuid } // On Windows, match the monitor by: // 1. Its handle // 2. Its device path // 3. Its hardware ID (if unique) // // Monitor handles and device paths are unique, but can change over // time. The hardware ID is not guaranteed to be unique, so we // match against that last. #[cfg(target_os = "windows")] { existing.handle == properties.handle || existing.device_path.as_deref().is_some_and(|device_path| { properties.device_path.as_deref() == Some(device_path) }) || existing.hardware_id.as_deref().is_some_and(|hardware_id| { let is_unique = monitors .iter() .filter(|other_monitor| { other_monitor.native_properties().hardware_id.as_deref() == Some(hardware_id) }) .count() == 1; is_unique && properties.hardware_id.as_deref() == Some(hardware_id) }) } }; is_match.then_some((monitor, index)) }) } ================================================ FILE: packages/wm/src/events/handle_mouse_move.rs ================================================ use anyhow::Context; #[cfg(target_os = "macos")] use wm_common::try_warn; use wm_platform::{MouseButton, MouseEvent}; use crate::{ commands::container::set_focused_descendant, traits::CommonGetters, user_config::UserConfig, wm_state::WmState, }; #[cfg(target_os = "macos")] use crate::{ events::handle_window_moved_or_resized_end, traits::WindowGetters, }; pub fn handle_mouse_move( event: &MouseEvent, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { // Ignore mouse move events if the WM is paused. The mouse listener // should anyways be disabled when the WM is paused, but this is just in // case any events slipped through while disabling. if state.is_paused { return Ok(()); } // On macOS, detect when a window drag operation has ended by listening // to the release of left click. // // This cannot be used for Windows, since it leads to race conditions // where the mouse event comes in before the `MovedOrResized` event with // `is_interactive_end`. For example, if the user drags to maximize a // window, the WS_MAXIMIZED state is sometimes set after the mouse event. #[cfg(target_os = "macos")] if let MouseEvent::ButtonUp { button, .. } = event { if *button == MouseButton::Left { let active_drag_windows = state .windows() .into_iter() .filter(|window| window.active_drag().is_some()); // Only one window should ever be actively dragged at a time, but // just in case, iterate over all active drag windows. for window in active_drag_windows { let new_rect = try_warn!(window.native().frame()); window.update_native_properties(|properties| { properties.frame = new_rect; }); handle_window_moved_or_resized_end(&window, state, config)?; } } return Ok(()); } if let MouseEvent::Move { pressed_buttons, // LINT: `window_below_cursor` is only used on macOS. #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] window_below_cursor, position, .. } = event { // Ignore event if left/right-click is down. Otherwise, this causes // focus to jitter when a window is being resized by its drag // handles. Also ignore if the OS focused window isn't the same as // the WM's focused window. if pressed_buttons.contains(&MouseButton::Left) || pressed_buttons.contains(&MouseButton::Right) || !state.is_focus_synced || !config.value.general.focus_follows_cursor { return Ok(()); } let window_under_cursor = { #[cfg(target_os = "macos")] { window_below_cursor.and_then(|window_id| { use crate::traits::WindowGetters; state .windows() .into_iter() .find(|w| w.native().id() == window_id) }) } #[cfg(target_os = "windows")] { state .dispatcher .window_from_point(position)? .and_then(|native| state.window_from_native(&native)) } }; // Set focus to whichever window is currently under the cursor. if let Some(window) = window_under_cursor { let focused_container = state.focused_container().context("No focused container.")?; if focused_container.id() != window.id() { set_focused_descendant(&window.as_container(), None); state.pending_sync.queue_focus_change(); } } else { // Focus the monitor if no window is under the cursor. let cursor_monitor = state .monitor_at_point(position) .context("No monitor under cursor.")?; let focused_monitor = state .focused_container() .context("No focused container.")? .monitor() .context("Focused container has no monitor.")?; // Avoid setting focus to the same monitor. if cursor_monitor.id() != focused_monitor.id() { set_focused_descendant(&cursor_monitor.as_container(), None); state.pending_sync.queue_focus_change(); } } } Ok(()) } ================================================ FILE: packages/wm/src/events/handle_window_destroyed.rs ================================================ use anyhow::Context; use tracing::info; use wm_platform::WindowId; use crate::{ commands::{window::unmanage_window, workspace::deactivate_workspace}, traits::{CommonGetters, WindowGetters}, wm_state::WmState, }; pub fn handle_window_destroyed( native_window_id: WindowId, state: &mut WmState, ) -> anyhow::Result<()> { let found_window = state .windows() .into_iter() .find(|window| window.native().id() == native_window_id); // Unmanage the window if it's currently managed. if let Some(window) = found_window { let workspace = window.workspace().context("No workspace.")?; info!("Window closed: {window}"); unmanage_window(window, state)?; // Destroy parent workspace if window was killed while its workspace // was not displayed (e.g. via task manager). if !workspace.config().keep_alive && !workspace.has_children() && !workspace.is_displayed() { deactivate_workspace(workspace, state)?; } } Ok(()) } ================================================ FILE: packages/wm/src/events/handle_window_focused.rs ================================================ use anyhow::Context; use tracing::info; use wm_common::{DisplayState, WindowRuleEvent, WmEvent}; use wm_platform::NativeWindow; use crate::{ commands::{ container::set_focused_descendant, window::run_window_rules, workspace::focus_workspace, }, models::WorkspaceTarget, traits::{CommonGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; pub fn handle_window_focused( native_window: &NativeWindow, state: &mut WmState, config: &mut UserConfig, ) -> anyhow::Result<()> { let found_window = state.window_from_native(native_window); let focused_container = state.focused_container().context("No focused container.")?; // Update the focus sync state. If the OS focused window is not same as // the WM's focused container, then the focus is not synced. state.is_focus_synced = match focused_container.as_window_container() { Ok(window) => *window.native() == *native_window, _ => native_window.is_desktop_window().unwrap_or(false), }; // Handle overriding focus on close/minimize. After a window is closed // or minimized, the OS or the closed application might automatically // switch focus to a different window. To force focus to go to the WM's // target focus container, we reassign any focus events 100ms after // close/minimize. This will cause focus to briefly flicker to the OS // focus target and then to the WM's focus target. if should_override_focus(state) { state.pending_sync.queue_focus_change(); return Ok(()); } // Ignore the focus event if window is being hidden by the WM. if let Some(window) = &found_window { if window.display_state() == DisplayState::Hiding { return Ok(()); } } // Focus effect should be updated for any change in focus that shouldn't // be overwritten. The incoming focus event at this point is either: // 1. WM's focus container (window or workspace). This is the desktop // window in the case of a workspace. // 2. An ignored window. // 3. A window that received manual focus. state.pending_sync.queue_focused_effect_update(); if let Some(window) = found_window { let workspace = window.workspace().context("No workspace")?; // Native focus has been synced to the WM's focused container. if focused_container == window.clone().into() { state.is_focus_synced = true; state.pending_sync.queue_workspace_to_reorder(workspace); return Ok(()); } info!("Window manually focused: {window}"); // Handle focus events from windows on hidden workspaces. For example, // if Discord is forcefully shown by the OS when it's on a hidden // workspace, switch focus to Discord's workspace. if window.display_state() == DisplayState::Hidden { info!("Focusing off-screen window: {window}"); focus_workspace( WorkspaceTarget::Name(workspace.config().name), state, config, )?; } // Update the WM's focus state. set_focused_descendant(&window.clone().into(), None); // Run window rules for focus events. run_window_rules( window.clone(), &WindowRuleEvent::Focus, state, config, )?; state.is_focus_synced = true; state.pending_sync.queue_workspace_to_reorder(workspace); // Broadcast the focus change event. state.emit_event(WmEvent::FocusChanged { focused_container: window.to_dto()?, }); } Ok(()) } /// Returns true if focus should be reassigned to the WM's focus container. fn should_override_focus(state: &WmState) -> bool { let has_recent_unmanage = state .unmanaged_or_minimized_timestamp .is_some_and(|time| time.elapsed().as_millis() < 100); has_recent_unmanage && !state.is_focus_synced } ================================================ FILE: packages/wm/src/events/handle_window_hidden.rs ================================================ use tracing::info; use wm_common::{DisplayState, HideMethod}; use wm_platform::NativeWindow; use crate::{ commands::window::unmanage_window, traits::WindowGetters, user_config::UserConfig, wm_state::WmState, }; pub fn handle_window_hidden( native_window: &NativeWindow, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let found_window = state.window_from_native(native_window); if let Some(window) = found_window { info!("Window hidden: {window}"); // Update the display state. if config.value.general.hide_method != HideMethod::PlaceInCorner && window.display_state() == DisplayState::Hiding { window.set_display_state(DisplayState::Hidden); return Ok(()); } // Unmanage the window if it's not in a display state transition. Also, // since window events are not 100% guaranteed to be in correct order, // we need to ignore events where the window is not actually hidden. if window.display_state() == DisplayState::Shown && !window.native().is_visible().unwrap_or(false) { unmanage_window(window, state)?; } } Ok(()) } ================================================ FILE: packages/wm/src/events/handle_window_minimize_ended.rs ================================================ use tracing::info; use wm_common::{try_warn, WindowState}; use wm_platform::NativeWindow; use crate::{ commands::window::update_window_state, traits::WindowGetters, user_config::UserConfig, wm_state::WmState, }; pub fn handle_window_minimize_ended( native_window: &NativeWindow, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let found_window = state.window_from_native(native_window); // Update the window's state to not be minimized. if let Some(window) = found_window { let is_minimized = try_warn!(window.native().is_minimized()); window.update_native_properties(|properties| { properties.is_minimized = is_minimized; }); if !is_minimized && window.state() == WindowState::Minimized { info!("Window minimize ended: {window}"); let target_state = window .prev_state() .unwrap_or(WindowState::default_from_config(&config.value)); update_window_state(window.clone(), target_state, state, config)?; } } Ok(()) } ================================================ FILE: packages/wm/src/events/handle_window_minimized.rs ================================================ use tracing::info; use wm_common::{try_warn, WindowState}; use wm_platform::NativeWindow; use crate::{ commands::{ container::set_focused_descendant, window::update_window_state, }, traits::WindowGetters, user_config::UserConfig, wm_state::WmState, }; pub fn handle_window_minimized( native_window: &NativeWindow, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let found_window = state.window_from_native(native_window); // Update the window's state to be minimized. if let Some(window) = found_window { let is_minimized = try_warn!(window.native().is_minimized()); window.update_native_properties(|properties| { properties.is_minimized = is_minimized; }); if is_minimized && window.state() != WindowState::Minimized { info!("Window minimized: {window}"); let window = update_window_state( window.clone(), WindowState::Minimized, state, config, )?; // Clear the drag state, as a window can be minimized while // being dragged (e.g. via `toggle-minimized`). // TODO: Investigate other code paths where the drag state should be // cleared (e.g. most commands that call `update_window_state`). window.set_active_drag(None); // Focus should be reassigned after a window has been minimized. if let Some(focus_target) = state.focus_target_after_removal(&window) { set_focused_descendant(&focus_target, None); state.pending_sync.queue_focus_change().queue_cursor_jump(); state.unmanaged_or_minimized_timestamp = Some(std::time::Instant::now()); } } } Ok(()) } ================================================ FILE: packages/wm/src/events/handle_window_moved_or_resized.rs ================================================ use anyhow::Context; use wm_common::{ try_warn, ActiveDrag, ActiveDragOperation, DisplayState, FloatingStateConfig, FullscreenStateConfig, HideMethod, WindowState, }; #[cfg(target_os = "windows")] use wm_platform::NativeWindowWindowsExt; #[cfg(target_os = "macos")] use wm_platform::{LengthValue, MouseButton, RectDelta}; use wm_platform::{NativeWindow, Rect}; use crate::{ commands::{ container::{flatten_split_container, move_container_within_tree}, window::update_window_state, }, events::handle_window_moved_or_resized_end, models::{Monitor, NonTilingWindow, WindowContainer}, traits::{CommonGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; #[allow(clippy::too_many_lines)] pub fn handle_window_moved_or_resized( native_window: &NativeWindow, // LINT: `is_interactive_start` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] is_interactive_start: bool, // LINT: `is_interactive_end` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] is_interactive_end: bool, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let found_window = state.window_from_native(native_window); if let Some(window) = found_window { let old_frame_position = window.native_properties().frame; let frame_position = try_warn!(window.native().frame()); window.update_native_properties(|properties| { properties.frame = frame_position.clone(); }); // Handle windows that are actively being dragged. if !state.is_paused && window.active_drag().is_some() { let is_drag_end = { // On Windows, the drag operation has ended when // `is_interactive_end` is `true`. This corresponds to a // `EVENT_SYSTEM_MOVESIZEEND` event, which is unavailable on macOS. #[cfg(target_os = "windows")] { is_interactive_end } // On macOS, the drag operation has ended when the mouse button is // no longer down. This is a fallback mechanism since for macOS, // `is_interactive_end` is always `false`. The `MouseEvent` handler // also catches `MouseButtonUp` events, but this provides // additional safety. // TODO: Can probably remove this check and rely 100% on the mouse // event handler. #[cfg(target_os = "macos")] { !state.dispatcher.is_mouse_down(&MouseButton::Left) } }; if is_drag_end { return handle_window_moved_or_resized_end(&window, state, config); } return update_drag_state(&window, &frame_position, state, config); } let old_is_maximized = window.native_properties().is_maximized; let is_maximized = try_warn!(window.native().is_maximized()); // Ignore duplicate move/resize events. Window position changes can // trigger multiple events. For example, restoring from maximized can // trigger as many as 4 identical events on Windows. if old_frame_position == frame_position && old_is_maximized == is_maximized && !is_interactive_start { return Ok(()); } window.update_native_properties(|properties| { properties.is_maximized = is_maximized; }); // If the window is not maximized, update its cached shadow borders. // Maximized windows temporarily have 0 shadow borders, in which case // we should use its previous value for redraws. #[cfg(target_os = "windows")] { let shadow_borders = try_warn!(window.native().shadow_borders()); if !is_maximized { window.update_native_properties(|properties| { properties.shadow_borders = shadow_borders; }); } } let is_minimized = try_warn!(window.native().is_minimized()); // Ignore events for minimized windows. Let them be handled by the // `PlatformEvent::WindowMinimized` event handler instead. if is_minimized { return Ok(()); } // Detect whether the window is starting to be interactively moved or // resized by the user (e.g. via the window's drag handles). let is_drag_start = !state.is_paused && { #[cfg(target_os = "windows")] { // Drag events can be valid for all window states apart from // minimized. is_interactive_start && !matches!(window.state(), WindowState::Minimized) } #[cfg(target_os = "macos")] { // Drag events are never valid for minimized or maximized windows. let is_valid_state = !matches!( window.state(), WindowState::Fullscreen(FullscreenStateConfig { maximized: true, .. }) | WindowState::Minimized ); let is_dragging_other_window = state.windows().iter().any(|w| w.active_drag().is_some()); let is_left_click = state.dispatcher.is_mouse_down(&MouseButton::Left); // Only consider the window to be dragging if: // 1. The window is not minimized or maximized. // 2. No other window is being dragged. // 3. Left-click is down. // 4. The cursor is within 40px margin around the window's frame. if is_valid_state && !is_dragging_other_window && is_left_click { // The window frame can lag behind the cursor when moving or // resizing quickly, so allow for a bit of leeway. let frame_to_check = frame_position.apply_delta( &RectDelta::new( LengthValue::from_px(40), LengthValue::from_px(40), LengthValue::from_px(40), LengthValue::from_px(40), ), None, ); // TODO: Might be more robust to also check if the window under // the cursor (i.e. via `dispatcher.window_from_point`) is not a // different window. let cursor_position = state.dispatcher.cursor_position()?; frame_to_check.contains_point(&cursor_position) } else { false } } }; if is_drag_start { tracing::info!("Window started dragging: {window}"); window.set_active_drag(Some(ActiveDrag { operation: None, is_from_floating: matches!( window.state(), WindowState::Floating(_) ), #[cfg(target_os = "windows")] initial_position: old_frame_position.clone(), // The updated frame position is used here instead of the initial // frame position due to a quirk on macOS. When we resize an // AXUIElement to a value outside the allowed min/max width & // height, macOS doesn't actually apply that size. However, it // still reports the value we attempted to set until a subsequent // `WindowEvent::MovedOrResized` event. #[cfg(target_os = "macos")] initial_position: frame_position.clone(), })); #[cfg(target_os = "windows")] update_drag_state(&window, &frame_position, state, config)?; return Ok(()); } let nearest_monitor = state .nearest_monitor(&window.native()) .context("No nearest monitor.")?; // For `HideMethod::PlaceInCorner`, hiding/showing is implemented by // repositioning the window. Since the OS won't emit real // shown/hidden events in this mode, update `DisplayState` based on // whether the window has been moved to the monitor's bottom corner. if config.value.general.hide_method == HideMethod::PlaceInCorner { let is_in_corner = is_in_corner( &frame_position, &nearest_monitor.native_properties().working_area, ); // TODO: Consider redrawing if hidden and should be shown, or if // shown and should be hidden. // TODO: It can be valid for a floating window to be in the corner, // in which case, it currently doesn't get updated to // `DisplayState::Shown`. let display_state = match (window.display_state(), is_in_corner) { (DisplayState::Hiding, true) => DisplayState::Hidden, (DisplayState::Showing, false) => DisplayState::Shown, _ => window.display_state(), }; if display_state != window.display_state() { window.set_display_state(display_state); return Ok(()); } } let should_fullscreen = { let workspace = nearest_monitor .displayed_workspace() .context("No workspace.")?; let should_fullscreen = window.should_fullscreen(&workspace)?; match window.state() { // Override the fullscreen check for when an app self-exits // fullscreen (e.g. Chrome via F11) and restores its window to // a position that exactly covers the workspace rect. WindowState::Fullscreen(fullscreen) if !fullscreen.maximized && should_fullscreen => { let workspace_rect = workspace.max_workspace_rect()?; let old_frame = old_frame_position .apply_delta(&window.border_delta().inverse(), None); let new_frame = frame_position .apply_delta(&window.border_delta().inverse(), None); let old_exceeded = old_frame.inset(1).contains_rect(&workspace_rect); let new_exceeds = new_frame.inset(1).contains_rect(&workspace_rect); // The window should no longer be fullscreen if the old frame // exceeded the workspace bounds (app was in OS fullscreen), but // the new frame no longer does. Configs with 0px outer gaps // always use the `should_fullscreen` check, since the old frame // will never exceed the workspace bounds. if old_exceeded && !new_exceeds { false } else { should_fullscreen } } _ => should_fullscreen, } }; // Handle a window being maximized or entering fullscreen. if is_maximized || should_fullscreen { let is_same_state = is_maximized && matches!( window.state(), WindowState::Fullscreen(FullscreenStateConfig { maximized: true, .. }) ) || should_fullscreen && matches!( window.state(), WindowState::Fullscreen(FullscreenStateConfig { maximized: false, .. }) ); // Ignore if there's no state change. if is_same_state { return Ok(()); } let fullscreen_state = if let WindowState::Fullscreen( fullscreen_state, ) = window.state() { fullscreen_state } else { config .value .window_behavior .state_defaults .fullscreen .clone() }; let window = update_window_state( window.clone(), WindowState::Fullscreen(FullscreenStateConfig { maximized: is_maximized, ..fullscreen_state }), state, config, )?; if is_maximized { // Dequeue the window from redraw if it's maximized, since the // window is already in the correct state. state .pending_sync .dequeue_container_from_redraw(window.clone()); } // TODO: Handle a fullscreen window being moved from one monitor to // another. return Ok(()); } match window.state() { WindowState::Fullscreen(_) => { // Window is no longer maximized/fullscreen and should be restored. tracing::info!("Restoring window from fullscreen: {window}"); update_window_state( window.clone(), window.toggled_state(window.state(), config), state, config, )?; } WindowState::Floating(_) => { if let WindowContainer::NonTilingWindow(window) = window { update_floating_window_position( &window, frame_position, &nearest_monitor, state, )?; } } _ => {} } } Ok(()) } // TODO: Move to shared location. `handle_window_moved_or_resized_end.rs` // also uses this. pub fn update_floating_window_position( window: &NonTilingWindow, frame_position: Rect, nearest_monitor: &Monitor, state: &mut WmState, ) -> anyhow::Result<()> { tracing::info!( "Updating floating window position: {}", window.as_window_container()? ); // Update state with the new location of the floating window. window.set_floating_placement(frame_position); window.set_has_custom_floating_placement(true); let monitor = window.monitor().context("No monitor.")?; // Update the window's workspace if it goes out of bounds of its // current workspace. if monitor.id() != nearest_monitor.id() { let updated_workspace = nearest_monitor .displayed_workspace() .context("Failed to get workspace of nearest monitor.")?; tracing::info!( "Floating window moved to new workspace: {updated_workspace}", ); window.set_insertion_target(None); move_container_within_tree( &window.clone().into(), &updated_workspace.clone().into(), updated_workspace.child_count(), state, )?; } Ok(()) } /// Updates the window operation based on changes in frame position. /// /// This function determines whether a window is being moved or resized and /// updates its operation state accordingly. If the window is being moved, /// it's set to floating mode. fn update_drag_state( window: &WindowContainer, frame_position: &Rect, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let Some(active_drag) = window.active_drag() else { return Ok(()); }; // Ignore if the window position has not changed yet. if *frame_position == active_drag.initial_position { return Ok(()); } // Determine the drag operation if not already set. let is_move = if let Some(operation) = active_drag.operation { matches!(operation, ActiveDragOperation::Move) } else { let is_move = *frame_position != active_drag.initial_position && frame_position.height() == active_drag.initial_position.height() && frame_position.width() == active_drag.initial_position.width(); let operation = if is_move { ActiveDragOperation::Move } else { ActiveDragOperation::Resize }; window.set_active_drag(Some(ActiveDrag { operation: Some(operation), ..active_drag.clone() })); is_move }; // Transition window to be floating while it's being dragged, but only // after it has been moved at least 10px from its initial position. The // 10px threshold is to account for small movements that may be // accidental. if is_move && !matches!(window.state(), WindowState::Floating(_)) { let move_distance = frame_position .center_point() .distance_between(&active_drag.initial_position.center_point()); // Dragging operations on a maximized window can only occur on Windows. // The OS immediately restores it while it's being dragged, so we need // to update state accordingly without a redraw. let is_maximized = matches!( window.state(), WindowState::Fullscreen(FullscreenStateConfig { maximized: true, .. }) ); if move_distance >= 10.0 || is_maximized { let parent = window.parent().context("No parent")?; let is_fullscreen = matches!(window.state(), WindowState::Fullscreen(_)) && !is_maximized; let window = update_window_state( window.clone(), WindowState::Floating(FloatingStateConfig { centered: false, ..config.value.window_behavior.state_defaults.floating }), state, config, )?; // `update_window_state` automatically adds the window for redraw, // which we don't want in this case. However, for fullscreen windows, // we do actually want it to be resized initially so that it's // easier to move around while dragging. if !is_fullscreen { state .pending_sync .dequeue_container_from_redraw(window.clone()); } // Flatten the parent split container if it only contains the window. // TODO: Consider doing this to `move_container_within_tree`, so that // the behavior is consistent. if let Some(split_parent) = parent.as_split() { if split_parent.child_count() == 1 { flatten_split_container(split_parent.clone())?; // Hacky fix to redraw siblings after flattening. The parent is // queued for redraw from the state change, which gets detached // on flatten. // TODO: Change `queue_containers_to_redraw` to iterate over its // descendant windows and store those instead. state .pending_sync .queue_containers_to_redraw(window.tiling_siblings()); } } } } Ok(()) } /// Gets whether the window is in the corner of the monitor. fn is_in_corner(window_frame: &Rect, monitor_rect: &Rect) -> bool { // Visible portion of the window used when positioning windows in the // monitor's corner. See `platform_sync` for how hidden windows are // positioned. const VISIBLE_SLIVER_PX: i32 = 1; // Allow 1px of leeway. let is_left_corner = (window_frame.right - VISIBLE_SLIVER_PX - monitor_rect.left).abs() <= 1; // Allow 1px of leeway. let is_right_corner = (window_frame.x() + VISIBLE_SLIVER_PX - monitor_rect.right).abs() <= 1; // On macOS, the window's title bar is prevented from being positioned // outside of monitor's working area, so we need to allow ~55px of // vertical leeway. Title bar height varies, but can be up to 52px. // TODO: See if possible to make this dynamic based on the window's title // bar height. let is_bottom_of_monitor = (window_frame.y() - monitor_rect.bottom).abs() <= 55; (is_left_corner || is_right_corner) && is_bottom_of_monitor } #[cfg(test)] mod tests { use wm_platform::Rect; use super::is_in_corner; #[test] fn matches_corner_positions() { let monitor = Rect::from_xy(0, 0, 1920, 1080); let frame_in_right_corner = Rect::from_xy(1919, 1050, 600, 600); assert!(is_in_corner(&frame_in_right_corner, &monitor)); let frame_in_left_corner = Rect::from_xy(1, 1050, 600, 600); assert!(is_in_corner(&frame_in_left_corner, &monitor)); } #[test] fn does_not_match_non_corner_positions() { let monitor = Rect::from_xy(0, 0, 1920, 1080); let frame = Rect::from_xy(100, 100, 800, 600); assert!(!is_in_corner(&frame, &monitor)); } } ================================================ FILE: packages/wm/src/events/handle_window_moved_or_resized_end.rs ================================================ use anyhow::Context; use wm_common::{ try_warn, FullscreenStateConfig, TilingDirection, WindowState, }; use wm_platform::{LengthValue, Point, Rect}; use crate::{ commands::{ container::{move_container_within_tree, wrap_in_split_container}, window::{set_window_size, update_window_state}, }, events::update_floating_window_position, models::{ DirectionContainer, NonTilingWindow, SplitContainer, TilingContainer, WindowContainer, }, traits::{ CommonGetters, PositionGetters, TilingDirectionGetters, WindowGetters, }, user_config::UserConfig, wm_state::WmState, }; /// Handles the event for when a window is finished being moved or resized /// by the user (e.g. via the window's drag handles). /// /// This resizes the window if it's a tiling window and attach a dragged /// floating window. /// /// TODO: Move this to a better location - maybe a new `active_drag_ext` /// mod. pub fn handle_window_moved_or_resized_end( window: &WindowContainer, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result<()> { let Some(active_drag) = window.active_drag() else { return Ok(()); }; match &window { WindowContainer::NonTilingWindow(window) => { let is_maximized = try_warn!(window.native().is_maximized()); window.update_native_properties(|properties| { properties.is_maximized = is_maximized; }); let nearest_monitor = state .nearest_monitor(&window.native()) .context("Failed to get workspace of nearest monitor.")?; let should_fullscreen = window.should_fullscreen( &nearest_monitor .displayed_workspace() .context("No workspace.")?, )?; if is_maximized || should_fullscreen { let fullscreen_state = if let WindowState::Fullscreen( fullscreen_state, ) = window.state() { fullscreen_state } else { config .value .window_behavior .state_defaults .fullscreen .clone() }; let window = update_window_state( window.clone().into(), WindowState::Fullscreen(FullscreenStateConfig { maximized: is_maximized, ..fullscreen_state }), state, config, )?; window.set_active_drag(None); if is_maximized { // Dequeue the window from redraw if it's maximized, since the // window is already in the correct state. state .pending_sync .dequeue_container_from_redraw(window.clone()); } else { // Force a redraw to snap the window to the monitor edges. // TODO: Skip redraw if it's already matches fullscreen frame. state.pending_sync.queue_container_to_redraw(window.clone()); } return Ok(()); } if active_drag.is_from_floating { update_floating_window_position( window, window.native_properties().frame, &nearest_monitor, state, )?; window.set_active_drag(None); } else { // Window is a temporary floating window that should be // reverted back to tiling. let window = drop_as_tiling_window(window, state, config)?; window.set_active_drag(None); } } WindowContainer::TilingWindow(window) => { tracing::info!( "Tiling window move/resize ended: {}", window.as_window_container()? ); let frame = window.native_properties().frame; // Update the window's size based on the new frame position. This // means we use the actual window dimensions as the source of truth. set_window_size( window.clone().into(), Some(LengthValue::from_px(frame.width())), Some(LengthValue::from_px(frame.height())), state, )?; window.set_active_drag(None); // Force a redraw of the window to snap it back to its original // position. This is necessary when: // - The window is the only tiling window in the workspace. // - The window is not past the movement threshold for transitioning // to floating while being dragged. // - Resizing in a direction that doesn't change the window's tiling // size. state.pending_sync.queue_container_to_redraw(window.clone()); } } Ok(()) } /// Handles transition from temporary floating window to tiling window on /// drag end. #[allow(clippy::too_many_lines)] fn drop_as_tiling_window( moved_window: &NonTilingWindow, state: &mut WmState, config: &UserConfig, ) -> anyhow::Result { tracing::info!( "Tiling window drag ended: {}", moved_window.as_window_container()? ); let mouse_pos = state.dispatcher.cursor_position()?; let mouse_workspace = state .monitor_at_point(&mouse_pos) .and_then(|monitor| monitor.displayed_workspace()) .or_else(|| moved_window.workspace()) .context("Couldn't find workspace for window drop.")?; // Get the workspace, split containers, and other windows under the // dragged window. let containers_at_pos = state .containers_at_point(&mouse_workspace.clone().into(), &mouse_pos) .into_iter() .filter(|container| container.id() != moved_window.id()); // Get the deepest direction container under the dragged window. let target_parent: DirectionContainer = containers_at_pos .filter_map(|container| container.as_direction_container().ok()) .fold(mouse_workspace.into(), |acc, container| { if container.ancestors().count() > acc.ancestors().count() { container } else { acc } }); // If the target parent has no children (i.e. an empty workspace), then // add the window directly. if target_parent.tiling_children().count() == 0 { move_container_within_tree( &moved_window.clone().into(), &target_parent.clone().into(), 0, state, )?; moved_window.set_insertion_target(None); return update_window_state( moved_window.as_window_container()?, WindowState::Tiling, state, config, ); } let nearest_container = target_parent .children() .into_iter() .filter_map(|container| container.as_tiling_container().ok()) .try_fold(None, |acc: Option, container| match acc { Some(acc) => { let is_nearer = acc.to_rect()?.distance_to_point(&mouse_pos) < container.to_rect()?.distance_to_point(&mouse_pos); anyhow::Ok(Some(if is_nearer { acc } else { container })) } None => Ok(Some(container)), })? .context("No nearest container.")?; let tiling_direction = target_parent.tiling_direction(); let drop_position = drop_position(&mouse_pos, &nearest_container.to_rect()?); let moved_window = update_window_state( moved_window.clone().into(), WindowState::Tiling, state, config, )?; let should_split = nearest_container.is_tiling_window() && match tiling_direction { TilingDirection::Horizontal => { drop_position == DropPosition::Top || drop_position == DropPosition::Bottom } TilingDirection::Vertical => { drop_position == DropPosition::Left || drop_position == DropPosition::Right } }; if should_split { let split_container = SplitContainer::new( tiling_direction.inverse(), config.value.gaps.clone(), ); wrap_in_split_container( &split_container, &target_parent.clone().into(), &[nearest_container], )?; let target_index = match drop_position { DropPosition::Top | DropPosition::Left => 0, _ => 1, }; move_container_within_tree( &moved_window.clone().into(), &split_container.into(), target_index, state, )?; } else { let target_index = match drop_position { DropPosition::Top | DropPosition::Left => nearest_container.index(), _ => nearest_container.index() + 1, }; move_container_within_tree( &moved_window.clone().into(), &target_parent.clone().into(), target_index, state, )?; } state.pending_sync.queue_container_to_redraw(target_parent); Ok(moved_window) } /// Represents where the window was dropped over another. #[derive(Debug, Clone, PartialEq)] enum DropPosition { Top, Bottom, Left, Right, } /// Gets the drop position for a window based on the mouse position. /// /// This approach divides the window rect into an "X", creating four /// triangular quadrants, to determine which side the cursor is closest to. fn drop_position(mouse_pos: &Point, rect: &Rect) -> DropPosition { let delta_x = mouse_pos.x - rect.center_point().x; let delta_y = mouse_pos.y - rect.center_point().y; if delta_x.abs() > delta_y.abs() { // Window is in the left or right triangle. if delta_x > 0 { DropPosition::Right } else { DropPosition::Left } } else { // Window is in the top or bottom triangle. if delta_y > 0 { DropPosition::Bottom } else { DropPosition::Top } } } ================================================ FILE: packages/wm/src/events/handle_window_shown.rs ================================================ use tracing::info; use wm_common::{DisplayState, HideMethod}; use wm_platform::NativeWindow; use crate::{ commands::window::manage_window, traits::WindowGetters, user_config::UserConfig, wm_state::WmState, }; pub fn handle_window_shown( native_window: NativeWindow, state: &mut WmState, config: &mut UserConfig, ) -> anyhow::Result<()> { let found_window = state.window_from_native(&native_window); if let Some(window) = found_window { info!("Window shown: {window}"); // Update display state if window is already managed. if config.value.general.hide_method != HideMethod::PlaceInCorner && window.display_state() == DisplayState::Showing { window.set_display_state(DisplayState::Shown); } else { state.pending_sync.queue_container_to_redraw(window); } } else { // If the window is not managed, manage it. manage_window(native_window, None, state, config)?; } Ok(()) } ================================================ FILE: packages/wm/src/events/handle_window_title_changed.rs ================================================ use tracing::info; use wm_common::{try_warn, WindowRuleEvent}; use wm_platform::NativeWindow; use crate::{ commands::window::run_window_rules, traits::WindowGetters, user_config::UserConfig, wm_state::WmState, }; pub fn handle_window_title_changed( native_window: &NativeWindow, state: &mut WmState, config: &mut UserConfig, ) -> anyhow::Result<()> { let found_window = state.window_from_native(native_window); if let Some(window) = found_window { info!("Window title changed: {window}"); let title = try_warn!(window.native().title()); window.update_native_properties(|properties| { properties.title = title; }); // Run window rules for title change events. run_window_rules( window, &WindowRuleEvent::TitleChange, state, config, )?; } Ok(()) } ================================================ FILE: packages/wm/src/events/mod.rs ================================================ mod handle_display_settings_changed; mod handle_mouse_move; mod handle_window_destroyed; mod handle_window_focused; mod handle_window_hidden; mod handle_window_minimize_ended; mod handle_window_minimized; mod handle_window_moved_or_resized; mod handle_window_moved_or_resized_end; mod handle_window_shown; mod handle_window_title_changed; pub use handle_display_settings_changed::*; pub use handle_mouse_move::*; pub use handle_window_destroyed::*; pub use handle_window_focused::*; pub use handle_window_hidden::*; pub use handle_window_minimize_ended::*; pub use handle_window_minimized::*; pub use handle_window_moved_or_resized::*; pub use handle_window_moved_or_resized_end::*; pub use handle_window_shown::*; pub use handle_window_title_changed::*; ================================================ FILE: packages/wm/src/ipc_server.rs ================================================ use std::{iter, net::SocketAddr}; use anyhow::{bail, Context}; use clap::Parser; use futures_util::{SinkExt, StreamExt}; use tokio::{ net::{TcpListener, TcpStream}, sync::{broadcast, mpsc}, task, }; use tokio_tungstenite::{accept_async, tungstenite::Message}; use tracing::{info, warn}; use uuid::Uuid; use wm_common::{ AppCommand, AppMetadataData, BindingModesData, ClientResponseData, ClientResponseMessage, CommandData, EventSubscribeData, EventSubscriptionMessage, FocusedData, MonitorsData, QueryCommand, ServerMessage, SubscribableEvent, TilingDirectionData, WindowsData, WmEvent, WorkspacesData, DEFAULT_IPC_PORT, }; use crate::{ traits::{CommonGetters, TilingDirectionGetters}, user_config::UserConfig, wm::WindowManager, }; pub struct IpcServer { abort_handle: task::AbortHandle, pub message_rx: mpsc::UnboundedReceiver<( String, mpsc::UnboundedSender, broadcast::Sender<()>, )>, _event_rx: broadcast::Receiver<(SubscribableEvent, WmEvent)>, event_tx: broadcast::Sender<(SubscribableEvent, WmEvent)>, _unsubscribe_rx: broadcast::Receiver, unsubscribe_tx: broadcast::Sender, } impl IpcServer { pub async fn start() -> anyhow::Result { let (message_tx, message_rx) = mpsc::unbounded_channel(); let (event_tx, _event_rx) = broadcast::channel(16); let (unsubscribe_tx, _unsubscribe_rx) = broadcast::channel(16); let server_addr = format!("127.0.0.1:{DEFAULT_IPC_PORT}"); let server = TcpListener::bind(server_addr.clone()).await?; info!("IPC server started on: '{}'.", server_addr); let task = task::spawn(async move { while let Ok((stream, addr)) = server.accept().await { let message_tx = message_tx.clone(); task::spawn(async move { if let Err(err) = Self::handle_connection(stream, addr, message_tx).await { warn!("Error handling connection: {}", err); } }); } }); Ok(Self { abort_handle: task.abort_handle(), #[allow(clippy::used_underscore_binding)] _event_rx, event_tx, message_rx, unsubscribe_tx, #[allow(clippy::used_underscore_binding)] _unsubscribe_rx, }) } async fn handle_connection( stream: TcpStream, addr: SocketAddr, message_tx: mpsc::UnboundedSender<( String, mpsc::UnboundedSender, broadcast::Sender<()>, )>, ) -> anyhow::Result<()> { info!("Incoming IPC connection from: {}.", addr); let ws_stream = accept_async(stream) .await .context("Error during websocket handshake.")?; let (mut outgoing, mut incoming) = ws_stream.split(); let (response_tx, mut response_rx) = mpsc::unbounded_channel(); let (disconnection_tx, _) = broadcast::channel(16); let res = async { loop { tokio::select! { Some(response) = response_rx.recv() => { outgoing.send(response).await?; } message = incoming.next() => { match message { Some(Ok(message)) => { if message.is_text() || message.is_binary() { message_tx.send(( message.to_text()?.to_string(), response_tx.clone(), disconnection_tx.clone(), ))?; } } Some(Err(err)) => bail!("WebSocket error: {}", err), None => { // WebSocket connection closed. break Ok(()); }, } } } } } .await; info!("IPC disconnection from: {}.", addr); if let Err(err) = disconnection_tx.send(()) { warn!("Failed to broadcast disconnection: {}", err); } res } pub fn process_message( &self, message: String, response_tx: &mpsc::UnboundedSender, disconnection_tx: &broadcast::Sender<()>, wm: &mut WindowManager, config: &mut UserConfig, ) -> anyhow::Result<()> { let app_command = AppCommand::try_parse_from( iter::once("").chain(message.split_whitespace()), ); let response_data = app_command .map_err(anyhow::Error::msg) .and_then(|app_command| { self.handle_app_command( app_command, response_tx, disconnection_tx, wm, config, ) }); // Respond to the client with the result of the command. response_tx .send(Self::to_client_response_msg(message, response_data)?) .map_err(|err| { anyhow::anyhow!("Failed to send response: {}", err) })?; Ok(()) } #[allow(clippy::too_many_lines)] fn handle_app_command( &self, app_command: AppCommand, response_tx: &mpsc::UnboundedSender, disconnection_tx: &broadcast::Sender<()>, wm: &mut WindowManager, config: &mut UserConfig, ) -> anyhow::Result { let response_data = match app_command { AppCommand::Query { command } => match command { QueryCommand::Windows => { ClientResponseData::Windows(WindowsData { windows: wm .state .windows() .into_iter() .map(|window| window.to_dto()) .try_collect()?, }) } QueryCommand::Workspaces => { ClientResponseData::Workspaces(WorkspacesData { workspaces: wm .state .workspaces() .into_iter() .map(|workspace| workspace.to_dto()) .try_collect()?, }) } QueryCommand::Monitors => { ClientResponseData::Monitors(MonitorsData { monitors: wm .state .monitors() .into_iter() .map(|monitor| monitor.to_dto()) .try_collect()?, }) } QueryCommand::BindingModes => { ClientResponseData::BindingModes(BindingModesData { binding_modes: wm.state.binding_modes.clone(), }) } QueryCommand::Focused => { let focused_container = wm .state .focused_container() .context("No focused container.")?; ClientResponseData::Focused(FocusedData { focused: focused_container.to_dto()?, }) } QueryCommand::AppMetadata => { ClientResponseData::AppMetadata(AppMetadataData { version: env!("VERSION_NUMBER").to_string(), }) } QueryCommand::TilingDirection => { let direction_container = wm .state .focused_container() .and_then(|focused| focused.direction_container()) .context("No direction container.")?; ClientResponseData::TilingDirection(TilingDirectionData { direction_container: direction_container.to_dto()?, tiling_direction: direction_container.tiling_direction(), }) } QueryCommand::Paused => { ClientResponseData::Paused(wm.state.is_paused) } }, AppCommand::Command { subject_container_id, command, } => { let subject_container_id = wm.process_commands( &vec![command], subject_container_id, config, )?; ClientResponseData::Command(CommandData { subject_container_id, }) } AppCommand::Sub { events } => { let subscription_id = Uuid::new_v4(); info!("New event subscription {}: {:?}", subscription_id, events); let response_tx = response_tx.clone(); let mut event_rx = self.event_tx.subscribe(); let mut unsubscribe_rx = self.unsubscribe_tx.subscribe(); let mut disconnection_rx = disconnection_tx.subscribe(); task::spawn(async move { loop { tokio::select! { Ok(()) = disconnection_rx.recv() => { break; } Ok(id) = unsubscribe_rx.recv() => { if id == subscription_id { break; } } Ok((event_type, event)) = event_rx.recv() => { // Check whether the event is one of the subscribed events. if events.contains(&event_type) || events.contains(&SubscribableEvent::All) { let send_result = Self::to_event_subscription_msg( subscription_id, event, ) .and_then(|event_msg| { response_tx .send(event_msg) .map_err(anyhow::Error::from) }); if let Err(err) = send_result { warn!("Error emitting WM event: {}", err); break; } } } } } }); ClientResponseData::EventSubscribe(EventSubscribeData { subscription_id, }) } AppCommand::Unsub { subscription_id } => { self .unsubscribe_tx .send(subscription_id) .context("Failed to unsubscribe from event.")?; ClientResponseData::EventUnsubscribe } AppCommand::Start { .. } => bail!("Unsupported IPC command."), }; Ok(response_data) } fn to_client_response_msg( client_message: String, response_data: anyhow::Result, ) -> anyhow::Result { let error = response_data.as_ref().err().map(ToString::to_string); let success = response_data.as_ref().is_ok(); let message = ServerMessage::ClientResponse(ClientResponseMessage { client_message, data: response_data.ok(), error, success, }); let message_json = serde_json::to_string(&message)?; Ok(Message::Text(message_json.into())) } fn to_event_subscription_msg( subscription_id: Uuid, event: WmEvent, ) -> anyhow::Result { let message = ServerMessage::EventSubscription(EventSubscriptionMessage { data: Some(event), error: None, subscription_id, success: true, }); let message_json = serde_json::to_string(&message)?; Ok(Message::Text(message_json.into())) } pub fn process_event(&mut self, event: WmEvent) -> anyhow::Result<()> { let event_type = match event { WmEvent::ApplicationExiting => SubscribableEvent::ApplicationExiting, WmEvent::BindingModesChanged { .. } => { SubscribableEvent::BindingModesChanged } WmEvent::FocusChanged { .. } => SubscribableEvent::FocusChanged, WmEvent::FocusedContainerMoved { .. } => { SubscribableEvent::FocusedContainerMoved } WmEvent::MonitorAdded { .. } => SubscribableEvent::MonitorAdded, WmEvent::MonitorUpdated { .. } => SubscribableEvent::MonitorUpdated, WmEvent::MonitorRemoved { .. } => SubscribableEvent::MonitorRemoved, WmEvent::TilingDirectionChanged { .. } => { SubscribableEvent::TilingDirectionChanged } WmEvent::UserConfigChanged { .. } => { SubscribableEvent::UserConfigChanged } WmEvent::WindowManaged { .. } => SubscribableEvent::WindowManaged, WmEvent::WindowUnmanaged { .. } => { SubscribableEvent::WindowUnmanaged } WmEvent::WorkspaceActivated { .. } => { SubscribableEvent::WorkspaceActivated } WmEvent::WorkspaceDeactivated { .. } => { SubscribableEvent::WorkspaceDeactivated } WmEvent::WorkspaceUpdated { .. } => { SubscribableEvent::WorkspaceUpdated } WmEvent::PauseChanged { .. } => SubscribableEvent::PauseChanged, }; self .event_tx .send((event_type, event)) .map_err(|err| anyhow::anyhow!("Failed to send event: {}", err))?; Ok(()) } pub fn stop(&self) { info!("Shutting down IPC server."); self.abort_handle.abort(); } } impl Drop for IpcServer { fn drop(&mut self) { self.stop(); } } ================================================ FILE: packages/wm/src/main.rs ================================================ // The `windows` or `console` subsystem (default is `console`) determines // whether a console window is spawned on launch, if not already ran // through a console. The following prevents this additional console window // in release mode. #![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] #![warn(clippy::all, clippy::pedantic)] #![feature(iterator_try_collect)] #[cfg(target_os = "macos")] use std::io::IsTerminal; use std::{env, path::PathBuf, process, time::Duration}; use anyhow::{Context, Error}; use tokio::{process::Command, signal}; use tracing::Level; use tracing_subscriber::{ fmt::{self, writer::MakeWriterExt}, layer::SubscriberExt, }; use wm_common::{AppCommand, InvokeCommand, Verbosity, WmEvent}; #[cfg(target_os = "macos")] use wm_platform::DispatcherExtMacOs; use wm_platform::{ Dispatcher, DisplayListener, EventLoop, KeybindingListener, MouseEventKind, MouseListener, PlatformEvent, SingleInstance, WindowListener, }; use crate::{ ipc_server::IpcServer, sys_tray::SystemTray, user_config::UserConfig, wm::WindowManager, }; mod commands; mod events; mod ipc_server; mod models; mod pending_sync; mod sys_tray; mod traits; mod user_config; mod wm; mod wm_state; /// Main entry point for the application. /// /// Conditionally starts the WM or runs a CLI command based on the given /// subcommand. fn main() -> anyhow::Result<()> { let args = std::env::args().collect::>(); let app_command = AppCommand::parse_with_default(&args); if let AppCommand::Start { config_path, verbosity, } = app_command { let rt = tokio::runtime::Runtime::new()?; let (event_loop, dispatcher) = EventLoop::new()?; let task_handle = std::thread::spawn(move || { rt.block_on(async { let start_res = start_wm(config_path, verbosity, &dispatcher).await; if let Err(err) = &start_res { // If unable to start the WM, the error is fatal and a message // dialog is shown. tracing::error!("{:?}", err); dispatcher.show_error_dialog("Fatal error", &err.to_string()); } if let Err(err) = dispatcher.stop_event_loop() { // Forcefully exit the process to ensure the event loop is // stopped. tracing::error!("Failed to stop event loop gracefully: {}", err); process::exit(1); } start_res }) }); // Run event loop (blocks until shutdown). This must be on the main // thread for macOS compatibility. event_loop.run()?; // Wait for clean exit of the WM. task_handle.join().unwrap() } else { let rt = tokio::runtime::Runtime::new()?; rt.block_on(wm_cli::start(args)) } } #[allow(clippy::too_many_lines)] async fn start_wm( config_path: Option, verbosity: Verbosity, dispatcher: &Dispatcher, ) -> anyhow::Result<()> { setup_logging(&verbosity)?; // Ensure that only one instance of the WM is running. let _single_instance = SingleInstance::new()?; #[cfg(target_os = "macos")] { if !dispatcher.has_ax_permission(true) { anyhow::bail!( "Accessibility permissions are not granted. In System Preferences, \ go to Privacy & Security > Accessibility and enable GlazeWM." ); } } // Parse and validate user config. let mut config = UserConfig::new(config_path)?; // Add application icon to system tray. let mut tray = SystemTray::new(&config.path, dispatcher.clone())?; let mut wm = WindowManager::new(&mut config, dispatcher.clone())?; let mut ipc_server = IpcServer::start().await?; // On Windows, start watcher process for restoring hidden windows on // crash. macOS' hidden windows are always accessible. #[cfg(target_os = "windows")] if let Err(err) = start_watcher_process() { tracing::warn!( "Failed to start watcher process: {err}{}", cfg!(debug_assertions) .then_some(".\n Run `cargo build -p wm-watcher` to build it.") .unwrap_or_default() ); } // On macOS, update the current process' PATH variable so that // `shell-exec` can resolve programs defined in the shell's PATH. Skip if // running via a terminal. #[cfg(target_os = "macos")] if !std::io::stdin().is_terminal() { update_path_env(); } // Start listening for platform events after populating initial state. let mut window_listener = WindowListener::new(dispatcher)?; let mut display_listener = DisplayListener::new(dispatcher)?; let mut mouse_listener = MouseListener::new( if config.value.general.focus_follows_cursor { &[MouseEventKind::Move, MouseEventKind::LeftButtonUp] } else { &[MouseEventKind::LeftButtonUp] }, dispatcher, )?; let mut keybinding_listener = KeybindingListener::new( &config .active_keybinding_configs(&[], false) .flat_map(|kb| kb.bindings) .collect::>(), dispatcher, )?; // Run user's startup commands. if let Err(err) = wm.process_commands( &config.value.general.startup_commands.clone(), None, &mut config, ) { tracing::error!("{:?}", err); dispatcher.show_error_dialog("Non-fatal error", &err.to_string()); } // Create an interval for periodically cleaning up invalid windows. let mut cleanup_interval = tokio::time::interval(Duration::from_secs(5)); loop { let res = tokio::select! { _ = signal::ctrl_c() => { tracing::info!("Received SIGINT signal."); break; }, Some(()) = wm.exit_rx.recv() => { tracing::info!("Exiting through WM command."); break; }, Some(()) = tray.exit_rx.recv() => { tracing::info!("Exiting through system tray."); break; }, Some(event) = mouse_listener.next_event() => { tracing::debug!("Received mouse event: {:?}", event); wm.process_event(PlatformEvent::Mouse(event), &mut config) }, Some(event) = window_listener.next_event() => { tracing::debug!("Received window event: {:?}", event); wm.process_event(PlatformEvent::Window(event), &mut config) }, Some(()) = display_listener.next_event() => { tracing::debug!("Received display settings changed event."); wm.process_event(PlatformEvent::DisplaySettingsChanged, &mut config) }, Some(event) = keybinding_listener.next_event() => { tracing::debug!("Received keyboard event: {:?}", event); wm.process_event(PlatformEvent::Keybinding(event), &mut config) } _ = cleanup_interval.tick() => { if wm.state.is_paused { Ok(()) } else { wm.state.cleanup_invalid_windows() } }, Some(( message, response_tx, disconnection_tx )) = ipc_server.message_rx.recv() => { tracing::info!("Received IPC message: {:?}", message); if let Err(err) = ipc_server.process_message( message, &response_tx, &disconnection_tx, &mut wm, &mut config, ) { tracing::error!("{:?}", err); } Ok(()) }, Some(wm_event) = wm.event_rx.recv() => { tracing::debug!("Received WM event: {:?}", wm_event); // Disable mouse listener when the WM is paused. if let WmEvent::PauseChanged { is_paused } = wm_event { let _ = mouse_listener.enable(!is_paused); } // Update keybinding and mouse listeners on config changes. if matches!( wm_event, WmEvent::UserConfigChanged { .. } | WmEvent::BindingModesChanged { .. } | WmEvent::PauseChanged { .. } ) { keybinding_listener.update( &config .active_keybinding_configs(&wm.state.binding_modes, false) .flat_map(|kb| kb.bindings) .collect::>(), ); mouse_listener.set_enabled_events( if config.value.general.focus_follows_cursor { &[MouseEventKind::Move, MouseEventKind::LeftButtonUp] } else { &[MouseEventKind::LeftButtonUp] }, )?; } if let Err(err) = ipc_server.process_event(wm_event) { tracing::error!("{:?}", err); } Ok(()) }, Some(()) = tray.config_reload_rx.recv() => { wm.process_commands( &vec![InvokeCommand::WmReloadConfig], None, &mut config, ).map(|_| ()) }, }; if let Err(err) = res { tracing::error!("{:?}", err); dispatcher.show_error_dialog("Non-fatal error", &err.to_string()); } } tracing::info!("Window manager shutting down."); wm.cleanup(&mut config, &mut ipc_server); Ok(()) } /// Initialize logging with the specified verbosity level. /// /// Error logs are saved to `~/.glzr/glazewm/errors.log`. fn setup_logging(verbosity: &Verbosity) -> anyhow::Result<()> { let error_log_dir = home::home_dir() .context("Unable to get home directory.")? .join(".glzr/glazewm/"); let error_writer = tracing_appender::rolling::never(error_log_dir, "errors.log"); let subscriber = tracing_subscriber::registry() .with( // Output to stdout with specified verbosity level. fmt::Layer::new() .with_writer(std::io::stdout.with_max_level(verbosity.level())), ) .with( // Output to error log file. fmt::Layer::new() .with_writer(error_writer.with_max_level(Level::ERROR)), ); tracing::subscriber::set_global_default(subscriber)?; tracing::info!( "Starting WM with log level {:?}.", verbosity.level().to_string() ); Ok(()) } /// Launches watcher binary (Windows-only). This is a separate process that /// is responsible for restoring hidden windows in case the main WM process /// crashes. /// /// This assumes the watcher binary exists in the same directory as the /// WM binary. #[allow(unused)] fn start_watcher_process() -> anyhow::Result { let watcher_path = env::current_exe()? .parent() .context("Failed to resolve path to the watcher process.")? .join("glazewm-watcher"); Command::new(&watcher_path) .spawn() .context("Failed to start watcher process.") } /// Updates the current process' PATH by querying the login shell. /// /// Apps launched outside a terminal (Spotlight, Finder, login items) /// inherit a PATH that only contains `/usr/bin:/bin:/usr/sbin:/sbin`. This /// causes `shell-exec` to fail for binaries that aren't in the system /// PATH. #[cfg(target_os = "macos")] fn update_path_env() { let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string()); // Use `-l` and `-i` (login + interactive) so that both profile and rc // files are sourced. let path_var = match std::process::Command::new(&shell) .args(["-lic", "printf '%s' \"$PATH\""]) .output() { Ok(output) if output.status.success() => { String::from_utf8(output.stdout) .ok() .filter(|path| !path.is_empty()) } _ => None, }; if let Some(path) = path_var { std::env::set_var("PATH", path); } else { tracing::warn!( "Failed to query login shell for PATH. Keeping existing PATH." ); } } ================================================ FILE: packages/wm/src/models/container.rs ================================================ use std::{ cell::{Ref, RefMut}, collections::VecDeque, }; use ambassador::Delegate; use enum_as_inner::EnumAsInner; use uuid::Uuid; use wm_common::{ ActiveDrag, ContainerDto, DisplayState, GapsConfig, TilingDirection, WindowRuleConfig, WindowState, }; use wm_platform::{Direction, NativeWindow, Rect, RectDelta}; #[allow(clippy::wildcard_imports)] use crate::{ models::{ Monitor, NativeWindowProperties, NonTilingWindow, RootContainer, SplitContainer, TilingWindow, Workspace, }, traits::*, user_config::UserConfig, }; /// A container of any type. /// /// Uses: /// /// * [`wm_macros::SubEnum`] to define subtypes of containers. /// * [`wm_macros::EnumFromInner`] to define conversions between the enum /// and wrapped types. /// * [`ambassador::Delegate`] to delegate common getters to the contained /// types. E.g. implements [`CommonGetters`] for [Container] by /// forwarding the call to the item contained in the enum variant. /// /// # Example /// Conversion between the different container types: /// ``` /// fn example(split: SplitContainer) { /// // Convert a `SplitContainer` into a `Container` /// let container: Container = split.into(); // Will be a `Container::Split` /// /// // Could also have gone straight to a [TilingContainer] from SplitContainer /// // let tiling: TilingContainer = split.into(); // Will be a `TilingContainer::Split` /// /// // Try to convert a [Container] into a sub container type ([TilingContainer] in this case). /// let tiling: TilingContainer = container.try_into().unwrap(); // Will be a `TilingContainer::Split` /// tiling.tiling_size(); // Can use methods from the `TilingSizeGetters` trait. /// /// // Try to convert a one sub container type into another. ([TilingContainer] to [DirectionContainer] in this case). /// let direction: DirectionContainer = tiling.try_into().unwrap(); // Will be a `DirectionContainer::Split` /// direction.tiling_direction(); // Can use methods from the `TilingDirectionGetters` trait. /// /// // Convert a sub container back into a [Container] /// let container: Container = direction.into(); // Will be a `Container::Split` /// } /// ``` #[derive( Clone, Debug, EnumAsInner, wm_macros::EnumFromInner, Delegate, wm_macros::SubEnum, )] #[delegate(CommonGetters)] #[delegate(PositionGetters)] #[subenum(defaults, { /// Subenum of [Container] #[derive(Clone, Debug, EnumAsInner, Delegate, wm_macros::EnumFromInner)] #[delegate(CommonGetters)] #[delegate(PositionGetters)] })] #[subenum(TilingContainer, { /// Subset of containers that implement the following traits: /// * `CommonGetters` /// * `PositionGetters` /// * `TilingSizeGetters` #[delegate(TilingSizeGetters)] })] #[subenum(WindowContainer, { /// Subset of containers that implement the following traits: /// * `CommonGetters` /// * `PositionGetters` /// * `WindowGetters` #[delegate(WindowGetters)] })] #[subenum(DirectionContainer, { /// Subset of containers that implement the following traits: /// * `CommonGetters` /// * `PositionGetters` /// * `DirectionGetters` #[delegate(TilingDirectionGetters)] })] pub enum Container { Root(RootContainer), Monitor(Monitor), #[subenum(DirectionContainer)] Workspace(Workspace), #[subenum(TilingContainer, DirectionContainer)] Split(SplitContainer), #[subenum(TilingContainer, WindowContainer)] TilingWindow(TilingWindow), #[subenum(WindowContainer)] NonTilingWindow(NonTilingWindow), } impl PartialEq for Container { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } impl Eq for Container {} impl PartialEq for TilingContainer { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } impl Eq for TilingContainer {} impl PartialEq for WindowContainer { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } impl Eq for WindowContainer {} impl std::fmt::Display for WindowContainer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Truncate title if longer than 20 chars. Need to use `chars()` // instead of byte slices to handle invalid byte indices. let title = { let title = self.native_properties().title; if title.len() > 20 { format!("{}...", title.chars().take(17).collect::()) } else { title } }; let class = { #[cfg(target_os = "windows")] { self.native_properties().class_name } #[cfg(not(target_os = "windows"))] { String::new() } }; let process = self.native_properties().process_name; write!( f, "Window(id={:?}, process={}, class={}, title={})", self.native().id(), process, class, title, )?; Ok(()) } } impl PartialEq for DirectionContainer { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } impl Eq for DirectionContainer {} /// Implements the `Debug` trait for a given container struct. /// /// Expects that the struct has a `to_dto()` method. #[macro_export] macro_rules! impl_container_debug { ($type:ty) => { impl std::fmt::Debug for $type { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { std::fmt::Debug::fmt( &self.to_dto().map_err(|_| std::fmt::Error), f, ) } } }; } ================================================ FILE: packages/wm/src/models/insertion_target.rs ================================================ use crate::models::Container; #[derive(Debug, Clone)] pub struct InsertionTarget { pub target_parent: Container, pub target_index: usize, pub prev_tiling_size: f32, pub prev_sibling_count: usize, } ================================================ FILE: packages/wm/src/models/mod.rs ================================================ mod container; mod insertion_target; mod monitor; mod native_monitor_properties; mod native_window_properties; mod non_tiling_window; mod root_container; mod split_container; mod tiling_window; mod workspace; mod workspace_target; pub use container::*; pub use insertion_target::*; pub use monitor::*; pub use native_monitor_properties::*; pub use native_window_properties::*; pub use non_tiling_window::*; pub use root_container::*; pub use split_container::*; pub use tiling_window::*; pub use workspace::*; pub use workspace_target::*; ================================================ FILE: packages/wm/src/models/monitor.rs ================================================ use std::{ cell::{Ref, RefCell, RefMut}, collections::VecDeque, rc::Rc, }; use anyhow::Context; use uuid::Uuid; use wm_common::{ContainerDto, MonitorDto}; use wm_platform::{Display, Rect}; use crate::{ impl_common_getters, impl_container_debug, models::{ Container, DirectionContainer, NativeMonitorProperties, TilingContainer, WindowContainer, Workspace, }, traits::{CommonGetters, PositionGetters}, }; #[derive(Clone)] pub struct Monitor(Rc>); struct MonitorInner { id: Uuid, parent: Option, children: VecDeque, child_focus_order: VecDeque, native: Display, native_properties: NativeMonitorProperties, } impl Monitor { pub fn new( native_display: Display, native_properties: NativeMonitorProperties, ) -> Self { let monitor = MonitorInner { id: Uuid::new_v4(), parent: None, children: VecDeque::new(), child_focus_order: VecDeque::new(), native: native_display, native_properties, }; Self(Rc::new(RefCell::new(monitor))) } pub fn native(&self) -> Display { self.0.borrow().native.clone() } pub fn set_native(&self, native: Display) { self.0.borrow_mut().native = native; } pub fn native_properties(&self) -> NativeMonitorProperties { self.0.borrow().native_properties.clone() } pub fn set_native_properties( &self, native_properties: NativeMonitorProperties, ) { self.0.borrow_mut().native_properties = native_properties; } pub fn displayed_workspace(&self) -> Option { self .child_focus_order() .next() .and_then(|child| child.as_workspace().cloned()) } pub fn workspaces(&self) -> Vec { self .children() .into_iter() .filter_map(|container| container.as_workspace().cloned()) .collect() } /// Whether there is a difference in DPI between this monitor and the /// parent monitor of another container. pub fn has_dpi_difference( &self, other: &Container, ) -> anyhow::Result { let dpi = self.native_properties().dpi; let other_dpi = other .monitor() .map(|monitor| monitor.native_properties().dpi) .context("Failed to get DPI of other monitor.")?; Ok(dpi != other_dpi) } pub fn to_dto(&self) -> anyhow::Result { let rect = self.to_rect()?; let children = self .children() .iter() .map(CommonGetters::to_dto) .try_collect()?; Ok(ContainerDto::Monitor(MonitorDto { id: self.id(), parent_id: self.parent().map(|parent| parent.id()), children, child_focus_order: self.0.borrow().child_focus_order.clone().into(), has_focus: self.has_focus(None), width: rect.width(), height: rect.height(), x: rect.x(), y: rect.y(), dpi: self.native_properties().dpi, scale_factor: self.native_properties().scale_factor, #[cfg(target_os = "windows")] handle: Some(self.native_properties().handle), #[cfg(not(target_os = "windows"))] handle: None, device_name: self.native_properties().device_name, #[cfg(target_os = "windows")] device_path: self.native_properties().device_path, #[cfg(not(target_os = "windows"))] device_path: None, #[cfg(target_os = "windows")] hardware_id: self.native_properties().hardware_id, #[cfg(not(target_os = "windows"))] hardware_id: None, working_rect: self.native_properties().working_area, })) } } impl_container_debug!(Monitor); impl_common_getters!(Monitor); impl PositionGetters for Monitor { fn to_rect(&self) -> anyhow::Result { Ok(self.0.borrow().native_properties.bounds.clone()) } } impl std::fmt::Display for Monitor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Monitor(device_name={})", self.native_properties().device_name, ) } } ================================================ FILE: packages/wm/src/models/native_monitor_properties.rs ================================================ use wm_platform::{Display, Rect}; #[cfg(target_os = "windows")] use wm_platform::{DisplayDeviceExtWindows, DisplayExtWindows}; #[derive(Debug, Clone)] pub struct NativeMonitorProperties { #[cfg(target_os = "macos")] pub device_uuid: String, #[cfg(target_os = "windows")] pub handle: isize, #[cfg(target_os = "windows")] pub hardware_id: Option, #[cfg(target_os = "windows")] pub device_path: Option, pub device_name: String, pub working_area: Rect, pub bounds: Rect, pub dpi: u32, pub scale_factor: f32, } impl NativeMonitorProperties { pub fn try_from(native_display: &Display) -> anyhow::Result { let display_device = native_display.main_device()?; Ok(Self { #[cfg(target_os = "macos")] device_uuid: display_device.id().0, #[cfg(target_os = "windows")] handle: native_display.hmonitor().0, #[cfg(target_os = "windows")] hardware_id: display_device.hardware_id(), #[cfg(target_os = "windows")] device_path: display_device.device_path(), device_name: native_display.name()?, working_area: native_display.working_area()?, bounds: native_display.bounds()?, dpi: native_display.dpi()?, scale_factor: native_display.scale_factor()?, }) } } ================================================ FILE: packages/wm/src/models/native_window_properties.rs ================================================ use wm_platform::{NativeWindow, Rect}; #[cfg(target_os = "windows")] use wm_platform::{NativeWindowWindowsExt, RectDelta}; #[derive(Debug, Clone)] pub struct NativeWindowProperties { pub title: String, #[cfg(target_os = "windows")] pub class_name: String, pub process_name: String, pub frame: Rect, pub is_minimized: bool, pub is_maximized: bool, pub is_resizable: bool, #[cfg(target_os = "windows")] pub shadow_borders: RectDelta, } impl TryFrom<&NativeWindow> for NativeWindowProperties { type Error = anyhow::Error; fn try_from(native_window: &NativeWindow) -> Result { Ok(Self { title: native_window.title()?, #[cfg(target_os = "windows")] class_name: native_window.class_name()?, process_name: native_window.process_name()?, frame: native_window.frame()?, is_minimized: native_window.is_minimized()?, is_maximized: native_window.is_maximized()?, is_resizable: native_window.is_resizable()?, #[cfg(target_os = "windows")] shadow_borders: native_window.shadow_borders()?, }) } } ================================================ FILE: packages/wm/src/models/non_tiling_window.rs ================================================ use std::{ cell::{Ref, RefCell, RefMut}, collections::VecDeque, rc::Rc, }; use anyhow::Context; use uuid::Uuid; use wm_common::{ ActiveDrag, ContainerDto, DisplayState, GapsConfig, WindowDto, WindowRuleConfig, WindowState, }; use wm_platform::{NativeWindow, Rect, RectDelta}; use crate::{ impl_common_getters, impl_container_debug, impl_window_getters, models::{ Container, DirectionContainer, InsertionTarget, NativeWindowProperties, TilingContainer, TilingWindow, WindowContainer, }, traits::{CommonGetters, PositionGetters, WindowGetters}, }; #[derive(Clone)] pub struct NonTilingWindow(Rc>); struct NonTilingWindowInner { id: Uuid, parent: Option, children: VecDeque, child_focus_order: VecDeque, native: NativeWindow, native_properties: NativeWindowProperties, state: WindowState, prev_state: Option, insertion_target: Option, display_state: DisplayState, border_delta: RectDelta, has_pending_dpi_adjustment: bool, floating_placement: Rect, has_custom_floating_placement: bool, done_window_rules: Vec, active_drag: Option, } impl NonTilingWindow { #[allow(clippy::too_many_arguments)] pub fn new( id: Option, native: NativeWindow, properties: NativeWindowProperties, state: WindowState, prev_state: Option, border_delta: RectDelta, insertion_target: Option, floating_placement: Rect, has_custom_floating_placement: bool, done_window_rules: Vec, active_drag: Option, ) -> Self { let window = NonTilingWindowInner { id: id.unwrap_or_else(Uuid::new_v4), parent: None, children: VecDeque::new(), child_focus_order: VecDeque::new(), native, native_properties: properties, state, prev_state, insertion_target, display_state: DisplayState::Shown, border_delta, has_pending_dpi_adjustment: false, floating_placement, has_custom_floating_placement, done_window_rules, active_drag, }; Self(Rc::new(RefCell::new(window))) } pub fn insertion_target(&self) -> Option { self.0.borrow().insertion_target.clone() } pub fn set_insertion_target( &self, insertion_target: Option, ) { self.0.borrow_mut().insertion_target = insertion_target; } pub fn to_tiling(&self, gaps_config: GapsConfig) -> TilingWindow { let prev_state = if self.active_drag().is_some() { self.prev_state() } else { Some(self.state()) }; TilingWindow::new( Some(self.id()), self.native().clone(), self.native_properties().clone(), prev_state, self.border_delta(), self.floating_placement(), self.has_custom_floating_placement(), gaps_config, self.done_window_rules(), self.active_drag(), ) } pub fn to_dto(&self) -> anyhow::Result { let rect = self.to_rect()?; Ok(ContainerDto::Window(WindowDto { id: self.id(), parent_id: self.parent().map(|parent| parent.id()), has_focus: self.has_focus(None), tiling_size: None, width: rect.width(), height: rect.height(), x: rect.x(), y: rect.y(), state: self.state(), prev_state: self.prev_state(), display_state: self.display_state(), border_delta: self.border_delta(), floating_placement: self.floating_placement(), #[allow(clippy::cast_possible_wrap, clippy::unnecessary_cast)] handle: self.native().id().0 as isize, title: self.native_properties().title, #[cfg(target_os = "windows")] class_name: self.native_properties().class_name, process_name: self.native_properties().process_name, active_drag: self.active_drag(), })) } } impl_container_debug!(NonTilingWindow); impl_common_getters!(NonTilingWindow); impl_window_getters!(NonTilingWindow); impl PositionGetters for NonTilingWindow { fn to_rect(&self) -> anyhow::Result { match self.state() { WindowState::Fullscreen(_) => { let monitor = self.monitor().context("No monitor.")?; #[cfg(target_os = "windows")] { monitor.to_rect() } #[cfg(target_os = "macos")] { // On macOS, the public APIs only allow window placement within // the display's working area. Ok(monitor.native_properties().working_area) } } _ => Ok(self.floating_placement()), } } } ================================================ FILE: packages/wm/src/models/root_container.rs ================================================ use std::{ cell::{Ref, RefCell, RefMut}, collections::VecDeque, rc::Rc, }; use anyhow::bail; use uuid::Uuid; use wm_common::{ContainerDto, RootContainerDto}; use wm_platform::Rect; use crate::{ impl_common_getters, impl_container_debug, models::{ Container, DirectionContainer, Monitor, TilingContainer, WindowContainer, }, traits::{CommonGetters, PositionGetters}, }; /// Root node of the container tree. #[derive(Clone)] pub struct RootContainer(Rc>); struct RootContainerInner { id: Uuid, parent: Option, children: VecDeque, child_focus_order: VecDeque, } impl Default for RootContainer { fn default() -> Self { let root = RootContainerInner { id: Uuid::new_v4(), parent: None, children: VecDeque::new(), child_focus_order: VecDeque::new(), }; Self(Rc::new(RefCell::new(root))) } } impl RootContainer { pub fn new() -> Self { Self::default() } pub fn monitors(&self) -> Vec { self .children() .into_iter() .filter_map(|container| container.as_monitor().cloned()) .collect() } pub fn to_dto(&self) -> anyhow::Result { let children = self .children() .iter() .map(CommonGetters::to_dto) .try_collect()?; Ok(ContainerDto::Root(RootContainerDto { id: self.id(), parent_id: None, children, child_focus_order: self.0.borrow().child_focus_order.clone().into(), })) } } impl_container_debug!(RootContainer); impl_common_getters!(RootContainer); impl PositionGetters for RootContainer { fn to_rect(&self) -> anyhow::Result { bail!("Root container does not have a position.") } } ================================================ FILE: packages/wm/src/models/split_container.rs ================================================ use std::{ cell::{Ref, RefCell, RefMut}, collections::VecDeque, rc::Rc, }; use anyhow::Context; use uuid::Uuid; use wm_common::{ ContainerDto, GapsConfig, SplitContainerDto, TilingDirection, }; use wm_platform::Rect; use crate::{ impl_common_getters, impl_container_debug, impl_position_getters_as_resizable, impl_tiling_direction_getters, impl_tiling_size_getters, models::{ Container, DirectionContainer, TilingContainer, WindowContainer, }, traits::{ CommonGetters, PositionGetters, TilingDirectionGetters, TilingSizeGetters, }, }; #[derive(Clone)] pub struct SplitContainer(Rc>); struct SplitContainerInner { id: Uuid, parent: Option, children: VecDeque, child_focus_order: VecDeque, tiling_size: f32, tiling_direction: TilingDirection, gaps_config: GapsConfig, } impl SplitContainer { pub fn new( tiling_direction: TilingDirection, gaps_config: GapsConfig, ) -> Self { let split = SplitContainerInner { id: Uuid::new_v4(), parent: None, children: VecDeque::new(), child_focus_order: VecDeque::new(), tiling_size: 1.0, tiling_direction, gaps_config, }; Self(Rc::new(RefCell::new(split))) } pub fn to_dto(&self) -> anyhow::Result { let rect = self.to_rect()?; let children = self .children() .iter() .map(CommonGetters::to_dto) .try_collect()?; Ok(ContainerDto::Split(SplitContainerDto { id: self.id(), parent_id: self.parent().map(|parent| parent.id()), children, child_focus_order: self.0.borrow().child_focus_order.clone().into(), has_focus: self.has_focus(None), tiling_size: self.tiling_size(), tiling_direction: self.tiling_direction(), width: rect.width(), height: rect.height(), x: rect.x(), y: rect.y(), })) } } impl_container_debug!(SplitContainer); impl_common_getters!(SplitContainer); impl_tiling_size_getters!(SplitContainer); impl_tiling_direction_getters!(SplitContainer); impl_position_getters_as_resizable!(SplitContainer); ================================================ FILE: packages/wm/src/models/tiling_window.rs ================================================ use std::{ cell::{Ref, RefCell, RefMut}, collections::VecDeque, rc::Rc, }; use anyhow::Context; use uuid::Uuid; use wm_common::{ ActiveDrag, ContainerDto, DisplayState, GapsConfig, TilingDirection, WindowDto, WindowRuleConfig, WindowState, }; use wm_platform::{NativeWindow, Rect, RectDelta}; use crate::{ impl_common_getters, impl_container_debug, impl_position_getters_as_resizable, impl_tiling_size_getters, impl_window_getters, models::{ Container, DirectionContainer, InsertionTarget, NativeWindowProperties, NonTilingWindow, TilingContainer, WindowContainer, }, traits::{ CommonGetters, PositionGetters, TilingDirectionGetters, TilingSizeGetters, WindowGetters, }, }; #[derive(Clone)] pub struct TilingWindow(Rc>); struct TilingWindowInner { id: Uuid, parent: Option, children: VecDeque, child_focus_order: VecDeque, tiling_size: f32, native: NativeWindow, native_properties: NativeWindowProperties, state: WindowState, prev_state: Option, display_state: DisplayState, border_delta: RectDelta, has_pending_dpi_adjustment: bool, floating_placement: Rect, has_custom_floating_placement: bool, gaps_config: GapsConfig, done_window_rules: Vec, active_drag: Option, } impl TilingWindow { #[allow(clippy::too_many_arguments)] pub fn new( id: Option, native: NativeWindow, properties: NativeWindowProperties, prev_state: Option, border_delta: RectDelta, floating_placement: Rect, has_custom_floating_placement: bool, gaps_config: GapsConfig, done_window_rules: Vec, active_drag: Option, ) -> Self { let window = TilingWindowInner { id: id.unwrap_or_else(Uuid::new_v4), parent: None, children: VecDeque::new(), child_focus_order: VecDeque::new(), tiling_size: 1.0, native, native_properties: properties, state: WindowState::Tiling, prev_state, display_state: DisplayState::Shown, border_delta, has_pending_dpi_adjustment: false, floating_placement, has_custom_floating_placement, gaps_config, done_window_rules, active_drag, }; Self(Rc::new(RefCell::new(window))) } pub fn to_non_tiling( &self, state: WindowState, insertion_target: Option, ) -> NonTilingWindow { NonTilingWindow::new( Some(self.id()), self.native().clone(), self.native_properties().clone(), state, Some(WindowState::Tiling), self.border_delta(), insertion_target, self.floating_placement(), self.has_custom_floating_placement(), self.done_window_rules(), self.active_drag(), ) } pub fn to_dto(&self) -> anyhow::Result { let rect = self.to_rect()?; Ok(ContainerDto::Window(WindowDto { id: self.id(), parent_id: self.parent().map(|parent| parent.id()), has_focus: self.has_focus(None), tiling_size: Some(self.tiling_size()), width: rect.width(), height: rect.height(), x: rect.x(), y: rect.y(), state: self.state(), prev_state: self.prev_state(), display_state: self.display_state(), border_delta: self.border_delta(), floating_placement: self.floating_placement(), #[allow(clippy::cast_possible_wrap, clippy::unnecessary_cast)] handle: self.native().id().0 as isize, title: self.native_properties().title, #[cfg(target_os = "windows")] class_name: self.native_properties().class_name, process_name: self.native_properties().process_name, active_drag: self.active_drag(), })) } } impl_container_debug!(TilingWindow); impl_common_getters!(TilingWindow); impl_tiling_size_getters!(TilingWindow); impl_position_getters_as_resizable!(TilingWindow); impl_window_getters!(TilingWindow); ================================================ FILE: packages/wm/src/models/workspace.rs ================================================ use std::{ cell::{Ref, RefCell, RefMut}, collections::VecDeque, rc::Rc, }; use anyhow::Context; use uuid::Uuid; use wm_common::{ ContainerDto, GapsConfig, TilingDirection, WorkspaceConfig, WorkspaceDto, }; use wm_platform::{Rect, RectDelta}; use crate::{ impl_common_getters, impl_container_debug, impl_tiling_direction_getters, models::{ Container, DirectionContainer, TilingContainer, WindowContainer, }, traits::{CommonGetters, PositionGetters, TilingDirectionGetters}, }; #[derive(Clone)] pub struct Workspace(Rc>); #[derive(Debug)] struct WorkspaceInner { id: Uuid, parent: Option, children: VecDeque, child_focus_order: VecDeque, config: WorkspaceConfig, gaps_config: GapsConfig, tiling_direction: TilingDirection, } impl Workspace { pub fn new( config: WorkspaceConfig, gaps_config: GapsConfig, tiling_direction: TilingDirection, ) -> Self { let workspace = WorkspaceInner { id: Uuid::new_v4(), parent: None, children: VecDeque::new(), child_focus_order: VecDeque::new(), config, gaps_config, tiling_direction, }; Self(Rc::new(RefCell::new(workspace))) } /// Underlying config for the workspace. pub fn config(&self) -> WorkspaceConfig { self.0.borrow().config.clone() } /// Update the underlying config for the workspace. pub fn set_config(&self, config: WorkspaceConfig) { self.0.borrow_mut().config = config; } /// Whether the workspace is currently displayed by the parent monitor. pub fn is_displayed(&self) -> bool { self .monitor() .and_then(|monitor| monitor.displayed_workspace()) .is_some_and(|workspace| workspace.id() == self.id()) } pub fn set_gaps_config(&self, gaps_config: GapsConfig) { self.0.borrow_mut().gaps_config = gaps_config; } /// Effective outer gaps for this workspace. /// /// Uses `single_window_outer_gap` when the workspace has a single tiling /// window, otherwise falls back to `outer_gap`. pub fn outer_gaps(&self) -> RectDelta { let is_single_window = self.tiling_children().nth(1).is_none(); let gaps_config = &self.0.borrow().gaps_config; let gaps = if is_single_window { gaps_config .single_window_outer_gap .as_ref() .unwrap_or(&gaps_config.outer_gap) } else { &gaps_config.outer_gap }; // TODO: Should this be scaled by the monitor's DPI? gaps.clone() } /// Gets the bounds of a workspace with the given outer gap config. fn workspace_rect_with_gap_config( &self, outer_gaps: &RectDelta, ) -> anyhow::Result { let monitor = self.monitor().context("Workspace has no parent monitor.")?; let gaps_config = &self.0.borrow().gaps_config; let scale_factor = if gaps_config.scale_with_dpi { monitor.native_properties().scale_factor } else { 1. }; // Get the delta between the monitor's bounds and its working area. let monitor_bounds = monitor.native_properties().bounds; let working_area_delta = monitor .native_properties() .working_area .delta(&monitor_bounds); Ok( monitor_bounds // Scale the gaps if `scale_with_dpi` is enabled. Outer gap config // values can be a percentage (relative to the monitor bounds), so // the outer gap delta needs to be applied prior to the working // area delta. .apply_delta(&outer_gaps.inverse(), Some(scale_factor)) .apply_delta(&working_area_delta, None), ) } /// Gets the maximum bounds of a workspace considering both `outer_gap` /// and `single_window_outer_gap` config values. pub fn max_workspace_rect(&self) -> anyhow::Result { let gaps_config = &self.0.borrow().gaps_config; // Get the workspace rect using `outer_gap`. let multi_window_rect = self.workspace_rect_with_gap_config(&gaps_config.outer_gap)?; let Some(single_gap) = &gaps_config.single_window_outer_gap else { return Ok(multi_window_rect); }; // Get the workspace rect using `single_window_outer_gap`. let single_window_rect = self.workspace_rect_with_gap_config(single_gap)?; Ok(multi_window_rect.union(&single_window_rect)) } pub fn to_dto(&self) -> anyhow::Result { let rect = self.to_rect()?; let config = self.config(); let children = self .children() .iter() .map(CommonGetters::to_dto) .try_collect()?; Ok(ContainerDto::Workspace(WorkspaceDto { id: self.id(), name: config.name, display_name: config.display_name, parent_id: self.parent().map(|parent| parent.id()), children, child_focus_order: self.0.borrow().child_focus_order.clone().into(), has_focus: self.has_focus(None), is_displayed: self.is_displayed(), width: rect.width(), height: rect.height(), x: rect.x(), y: rect.y(), tiling_direction: self.tiling_direction(), })) } } impl_container_debug!(Workspace); impl_common_getters!(Workspace); impl_tiling_direction_getters!(Workspace); impl PositionGetters for Workspace { fn to_rect(&self) -> anyhow::Result { self.workspace_rect_with_gap_config(&self.outer_gaps()) } } impl std::fmt::Display for Workspace { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Workspace(name={}, tiling_direction={:?})", self.config().name, self.tiling_direction(), ) } } ================================================ FILE: packages/wm/src/models/workspace_target.rs ================================================ use wm_platform::Direction; pub enum WorkspaceTarget { Name(String), Recent, NextActive, PreviousActive, NextActiveInMonitor, PreviousActiveInMonitor, Next, Previous, #[allow(dead_code)] Direction(Direction), } ================================================ FILE: packages/wm/src/pending_sync.rs ================================================ use std::collections::HashMap; use uuid::Uuid; use crate::{ models::{Container, Workspace}, traits::CommonGetters, }; #[derive(Debug, Default)] #[allow(clippy::struct_excessive_bools)] pub struct PendingSync { /// Containers (and their descendants) that have a pending redraw. containers_to_redraw: HashMap, /// Workspaces where z-order should be updated. Windows that match the /// focused window's state should be brought to the front. workspaces_to_reorder: Vec, /// Whether native focus should be reassigned to the WM's focused /// container. needs_focus_update: bool, /// Whether window effect for the focused window should be updated. needs_focused_effect_update: bool, /// Whether window effects for all windows should be updated. needs_all_effects_update: bool, /// Whether to jump the cursor to the focused container (if enabled in /// user config). needs_cursor_jump: bool, } impl PendingSync { pub fn has_changes(&self) -> bool { !self.containers_to_redraw.is_empty() || !self.workspaces_to_reorder.is_empty() || self.needs_focus_update || self.needs_focused_effect_update || self.needs_all_effects_update || self.needs_cursor_jump } pub fn clear(&mut self) -> &mut Self { self.containers_to_redraw.clear(); self.workspaces_to_reorder.clear(); self.needs_focus_update = false; self.needs_focused_effect_update = false; self.needs_all_effects_update = false; self.needs_cursor_jump = false; self } pub fn queue_container_to_redraw(&mut self, container: T) -> &mut Self where T: Into, { let container: Container = container.into(); self.containers_to_redraw.insert(container.id(), container); self } pub fn queue_containers_to_redraw( &mut self, containers: I, ) -> &mut Self where I: IntoIterator, T: Into, { for container in containers { let container: Container = container.into(); self.containers_to_redraw.insert(container.id(), container); } self } pub fn dequeue_container_from_redraw( &mut self, container: T, ) -> &mut Self where T: Into, { self.containers_to_redraw.remove(&container.into().id()); self } pub fn queue_workspace_to_reorder( &mut self, workspace: Workspace, ) -> &mut Self { self.workspaces_to_reorder.push(workspace); self } pub fn queue_focus_change(&mut self) -> &mut Self { self.needs_focus_update = true; self } pub fn queue_focused_effect_update(&mut self) -> &mut Self { self.needs_focused_effect_update = true; self } pub fn queue_all_effects_update(&mut self) -> &mut Self { self.needs_all_effects_update = true; self } pub fn queue_cursor_jump(&mut self) -> &mut Self { self.needs_cursor_jump = true; self } pub fn needs_focus_update(&self) -> bool { self.needs_focus_update } pub fn needs_focused_effect_update(&self) -> bool { self.needs_focused_effect_update } pub fn needs_all_effects_update(&self) -> bool { self.needs_all_effects_update } pub fn needs_cursor_jump(&self) -> bool { self.needs_cursor_jump } pub fn containers_to_redraw(&self) -> &HashMap { &self.containers_to_redraw } pub fn workspaces_to_reorder(&self) -> &Vec { &self.workspaces_to_reorder } } ================================================ FILE: packages/wm/src/sys_tray.rs ================================================ use std::{ fmt::{self, Display}, path::Path, str::FromStr, sync::{Arc, Mutex}, }; use anyhow::Context; use auto_launch::AutoLaunch; use tokio::sync::mpsc; use tray_icon::{ menu::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem}, Icon, TrayIcon, TrayIconBuilder, }; #[cfg(target_os = "windows")] use wm_platform::DispatcherExtWindows; use wm_platform::{Dispatcher, ThreadBound}; #[derive(Debug, Clone, Eq, PartialEq)] enum TrayMenuId { ReloadConfig, ShowConfigFolder, #[cfg(target_os = "windows")] ToggleWindowAnimations, RunOnStartup, Exit, } impl Display for TrayMenuId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { TrayMenuId::ReloadConfig => write!(f, "reload_config"), TrayMenuId::ShowConfigFolder => write!(f, "show_config_folder"), #[cfg(target_os = "windows")] TrayMenuId::ToggleWindowAnimations => { write!(f, "toggle_window_animations") } TrayMenuId::RunOnStartup => write!(f, "run_on_startup"), TrayMenuId::Exit => write!(f, "exit"), } } } impl FromStr for TrayMenuId { type Err = anyhow::Error; fn from_str(event: &str) -> Result { match event { "show_config_folder" => Ok(Self::ShowConfigFolder), "reload_config" => Ok(Self::ReloadConfig), #[cfg(target_os = "windows")] "toggle_window_animations" => Ok(Self::ToggleWindowAnimations), "exit" => Ok(Self::Exit), _ => anyhow::bail!("Invalid tray menu event: {}", event), } } } pub struct SystemTray { pub config_reload_rx: mpsc::UnboundedReceiver<()>, pub exit_rx: mpsc::UnboundedReceiver<()>, _icon_thread: Option>, _tray_icon: ThreadBound, } impl SystemTray { /// Install the system tray on the main thread after the run loop starts. pub fn new( config_path: &Path, dispatcher: Dispatcher, ) -> anyhow::Result { let (exit_tx, exit_rx) = mpsc::unbounded_channel(); let (config_reload_tx, config_reload_rx) = mpsc::unbounded_channel(); let animations_enabled = Arc::new(Mutex::new({ #[cfg(target_os = "windows")] { dispatcher.window_animations_enabled().unwrap_or(false) } #[cfg(not(target_os = "windows"))] { false } })); let run_on_startup_enabled = Arc::new(Mutex::new( auto_launch_instance() .and_then(|auto_launch| { auto_launch.is_enabled().map_err(Into::into) }) .unwrap_or(false), )); let tray_icon = dispatcher.dispatch_sync(|| { let tray_icon = Self::create_tray_icon( *animations_enabled.lock().unwrap(), *run_on_startup_enabled.lock().unwrap(), ) .unwrap(); ThreadBound::new(tray_icon, dispatcher.clone()) })?; // Spawn thread to handle tray menu events. let config_path = config_path.to_owned(); let icon_thread = std::thread::spawn(move || { let menu_event_rx = MenuEvent::receiver(); while let Ok(event) = menu_event_rx.recv() { if let Ok(menu_event) = TrayMenuId::from_str(event.id.as_ref()) { if let Err(err) = Self::handle_menu_event( &menu_event, &dispatcher, &config_path, &config_reload_tx, &exit_tx, &animations_enabled, &run_on_startup_enabled, ) { tracing::warn!("Failed to handle tray menu event: {}", err); } } } }); Ok(Self { config_reload_rx, exit_rx, _icon_thread: Some(icon_thread), _tray_icon: tray_icon, }) } fn create_tray_icon( // LINT: `animations_enabled` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] animations_enabled: bool, run_on_startup_enabled: bool, ) -> anyhow::Result { let reload_config_item = MenuItem::with_id( TrayMenuId::ReloadConfig, "Reload config", true, None, ); let config_dir_item = MenuItem::with_id( TrayMenuId::ShowConfigFolder, "Show config folder", true, None, ); #[cfg(target_os = "windows")] let toggle_animations_item = CheckMenuItem::with_id( TrayMenuId::ToggleWindowAnimations, "Window animations", true, animations_enabled, None, ); let run_on_startup_item = CheckMenuItem::with_id( TrayMenuId::RunOnStartup, "Run on system startup", true, run_on_startup_enabled, None, ); let exit_item = MenuItem::with_id(TrayMenuId::Exit, "Exit", true, None); let tray_menu = Menu::new(); tray_menu.append_items(&[ &reload_config_item, &config_dir_item, #[cfg(target_os = "windows")] &toggle_animations_item, &run_on_startup_item, &PredefinedMenuItem::separator(), &exit_item, ])?; let icon = Self::load_icon(include_bytes!( "../../../resources/assets/icon.png" ))?; let tray_icon = TrayIconBuilder::new() .with_menu(Box::new(tray_menu)) .with_tooltip(format!("GlazeWM v{}", env!("VERSION_NUMBER"))) .with_icon(icon) .build()?; Ok(tray_icon) } fn load_icon(bytes: &[u8]) -> anyhow::Result { let (icon_rgba, icon_width, icon_height) = { let image = image::load_from_memory(bytes) .context("Failed to to create tray icon image from resource.")? .into_rgba8(); let (width, height) = image.dimensions(); let rgba = image.into_raw(); (rgba, width, height) }; Ok(tray_icon::Icon::from_rgba( icon_rgba, icon_width, icon_height, )?) } fn handle_menu_event( menu_id: &TrayMenuId, dispatcher: &Dispatcher, config_path: &Path, config_reload_tx: &mpsc::UnboundedSender<()>, exit_tx: &mpsc::UnboundedSender<()>, // LINT: `animations_enabled` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] animations_enabled: &Arc>, run_on_startup_enabled: &Arc>, ) -> anyhow::Result<()> { tracing::info!("Processing tray menu event: {:?}", menu_id); match menu_id { TrayMenuId::ShowConfigFolder => { dispatcher.open_file_explorer(config_path)?; Ok(()) } TrayMenuId::ReloadConfig => { config_reload_tx.send(())?; Ok(()) } #[cfg(target_os = "windows")] TrayMenuId::ToggleWindowAnimations => { let mut animations_enabled = animations_enabled.lock().unwrap(); dispatcher.set_window_animations_enabled(!*animations_enabled)?; *animations_enabled = !*animations_enabled; Ok(()) } TrayMenuId::RunOnStartup => { let mut run_on_startup_enabled = run_on_startup_enabled.lock().unwrap(); if *run_on_startup_enabled { auto_launch_instance()?.disable()?; } else { auto_launch_instance()?.enable()?; } *run_on_startup_enabled = !*run_on_startup_enabled; Ok(()) } TrayMenuId::Exit => { exit_tx.send(())?; Ok(()) } } } } /// Creates a new [`AutoLaunch`] instance for managing auto-launch at /// system startup. fn auto_launch_instance() -> anyhow::Result { // TODO: Is wrapping the exe path in quotes necessary? let formatted_exe_path = format!("\"{}\"", std::env::current_exe()?.to_string_lossy()); let args: [&str; 0] = []; #[cfg(target_os = "windows")] let instance = AutoLaunch::new("GlazeWM", &formatted_exe_path, &args); #[cfg(target_os = "macos")] let instance = AutoLaunch::new("GlazeWM", &formatted_exe_path, false, &args); Ok(instance) } ================================================ FILE: packages/wm/src/traits/common_getters.rs ================================================ use std::{ cell::{Ref, RefMut}, collections::VecDeque, }; use ambassador::delegatable_trait; use uuid::Uuid; use wm_common::ContainerDto; use crate::models::{ Container, DirectionContainer, Monitor, TilingContainer, WindowContainer, Workspace, }; #[delegatable_trait] pub trait CommonGetters { /// A unique identifier for the container. fn id(&self) -> Uuid; fn as_container(&self) -> Container; fn as_tiling_container(&self) -> anyhow::Result; fn as_window_container(&self) -> anyhow::Result; fn as_direction_container(&self) -> anyhow::Result; fn to_dto(&self) -> anyhow::Result; fn borrow_parent(&self) -> Ref<'_, Option>; fn borrow_parent_mut(&self) -> RefMut<'_, Option>; fn borrow_children(&self) -> Ref<'_, VecDeque>; fn borrow_children_mut(&self) -> RefMut<'_, VecDeque>; fn borrow_child_focus_order(&self) -> Ref<'_, VecDeque>; fn borrow_child_focus_order_mut(&self) -> RefMut<'_, VecDeque>; /// Gets the parent container, unless this container is the root. fn parent(&self) -> Option { self.borrow_parent().clone() } /// Direct children of this container. fn children(&self) -> VecDeque { self.borrow_children().clone() } /// Number of children that this container has. fn child_count(&self) -> usize { self.borrow_children().len() } /// Whether this container has any direct children. fn has_children(&self) -> bool { !self.borrow_children().is_empty() } /// Whether this container is detached from the tree (i.e. it does not /// have a parent). fn is_detached(&self) -> bool { self.borrow_parent().as_ref().is_none() } /// Index of this container amongst its siblings. /// /// Returns 0 if the container has no parent. fn index(&self) -> usize { self .borrow_parent() .as_ref() .and_then(|parent| { parent .borrow_children() .iter() .position(|child| child.id() == self.id()) }) .unwrap_or(0) } /// Gets child container with the given ID. fn child_by_id(&self, child_id: &Uuid) -> Option { self .borrow_children() .iter() .find(|child| &child.id() == child_id) .cloned() } fn tiling_children( &self, ) -> Box + '_> { Box::new( self .children() .into_iter() .filter_map(|container| container.try_into().ok()), ) } fn descendants(&self) -> Descendants { Descendants { stack: self.children(), } } fn self_and_descendants(&self) -> Descendants { let mut stack = self.children(); stack.push_front(self.as_container()); Descendants { stack } } /// Children in order of last focus. fn child_focus_order(&self) -> Box + '_> { let child_focus_order = self.borrow_child_focus_order(); Box::new(std::iter::from_fn(move || { for child_id in child_focus_order.iter() { if let Some(child) = self.child_by_id(child_id) { return Some(child); } } None })) } /// Leaf nodes (i.e. windows and workspaces) in order of last focus. fn descendant_focus_order( &self, ) -> Box + '_> { let mut stack = Vec::new(); stack.push(self.as_container()); Box::new(std::iter::from_fn(move || { while let Some(current) = stack.pop() { // Get containers that have no children. Descendant also cannot be // the container itself. if current.id() != self.id() && !current.has_children() { return Some(current); } // Reverse the child focus order so that the first element is // pushed last and popped first. for focus_child_id in current.borrow_child_focus_order().iter().rev() { if let Some(focus_child) = current.child_by_id(focus_child_id) { stack.push(focus_child); } } } None })) } fn siblings(&self) -> Box + '_> { Box::new( self .parent() .into_iter() .flat_map(|parent| parent.children()) .filter(move |sibling| sibling.id() != self.id()), ) } fn self_and_siblings(&self) -> Box + '_> { Box::new( self .parent() .into_iter() .flat_map(|parent| parent.children()), ) } fn prev_siblings(&self) -> Box + '_> { Box::new( self .self_and_siblings() .collect::>() .into_iter() .take(self.index()) .rev(), ) } fn next_siblings(&self) -> Box + '_> { Box::new( self .self_and_siblings() .collect::>() .into_iter() .skip(self.index() + 1), ) } fn tiling_siblings( &self, ) -> Box + '_> { Box::new( self .siblings() .filter_map(|container| container.try_into().ok()), ) } fn ancestors(&self) -> Ancestors { Ancestors { start: self.parent(), } } fn self_and_ancestors(&self) -> Ancestors { Ancestors { start: Some(self.as_container()), } } /// Workspace that this container belongs to. /// /// Note that this might return the container itself. fn workspace(&self) -> Option { self .self_and_ancestors() .find_map(|container| container.as_workspace().cloned()) } /// Monitor that this container belongs to. /// /// Note that this might return the container itself. fn monitor(&self) -> Option { self .self_and_ancestors() .find_map(|container| container.as_monitor().cloned()) } /// Nearest direction container (i.e. split container or workspace) that /// this container belongs to. /// /// Note that this might return the container itself. fn direction_container(&self) -> Option { self .self_and_ancestors() .find_map(|container| container.try_into().ok()) } /// Index of this container in parent's child focus order. /// /// Returns 0 if the container has no parent. fn focus_index(&self) -> usize { self .parent() .and_then(|parent| { parent .borrow_child_focus_order() .iter() .position(|id| id == &self.id()) }) .unwrap_or(0) } /// Whether this container or a descendant has focus. /// /// If `end_ancestor` is provided, then the check for focus will be up to /// and including the `end_ancestor`. fn has_focus(&self, end_ancestor: Option) -> bool { self .self_and_ancestors() .take_while(|ancestor| end_ancestor.as_ref() != Some(ancestor)) .chain(end_ancestor.clone()) .all(|ancestor| ancestor.focus_index() == 0) } } /// An iterator over ancestors of a given container. pub struct Ancestors { start: Option, } impl Iterator for Ancestors { type Item = Container; fn next(&mut self) -> Option { self.start.take().inspect(|container| { self.start = container.parent(); }) } } /// An iterator over descendants of a given container. pub struct Descendants { stack: VecDeque, } impl Iterator for Descendants { type Item = Container; fn next(&mut self) -> Option { if let Some(container) = self.stack.pop_front() { self.stack.extend(container.children()); return Some(container); } None } } /// Implements the `CommonGetters` trait for a given struct. /// /// Expects that the struct has a wrapping `RefCell` containing a struct /// with an `id`, `parent`, `children`, and `child_focus_order` field. #[macro_export] macro_rules! impl_common_getters { ($struct_name:ident) => { impl CommonGetters for $struct_name { fn id(&self) -> Uuid { self.0.borrow().id } fn as_container(&self) -> Container { self.clone().into() } fn as_tiling_container(&self) -> anyhow::Result { TryInto::::try_into(self.as_container()) .map_err(anyhow::Error::msg) } fn as_window_container(&self) -> anyhow::Result { TryInto::::try_into(self.as_container()) .map_err(anyhow::Error::msg) } fn as_direction_container( &self, ) -> anyhow::Result { TryInto::::try_into(self.as_container()) .map_err(anyhow::Error::msg) } fn to_dto(&self) -> anyhow::Result { self.to_dto() } fn borrow_parent(&self) -> Ref<'_, Option> { Ref::map(self.0.borrow(), |inner| &inner.parent) } fn borrow_parent_mut(&self) -> RefMut<'_, Option> { RefMut::map(self.0.borrow_mut(), |inner| &mut inner.parent) } fn borrow_children(&self) -> Ref<'_, VecDeque> { Ref::map(self.0.borrow(), |inner| &inner.children) } fn borrow_children_mut(&self) -> RefMut<'_, VecDeque> { RefMut::map(self.0.borrow_mut(), |inner| &mut inner.children) } fn borrow_child_focus_order(&self) -> Ref<'_, VecDeque> { Ref::map(self.0.borrow(), |inner| &inner.child_focus_order) } fn borrow_child_focus_order_mut( &self, ) -> RefMut<'_, VecDeque> { RefMut::map(self.0.borrow_mut(), |inner| { &mut inner.child_focus_order }) } } }; } ================================================ FILE: packages/wm/src/traits/mod.rs ================================================ mod common_getters; mod position_getters; mod tiling_direction_getters; mod tiling_size_getters; mod window_getters; pub use common_getters::*; pub use position_getters::*; pub use tiling_direction_getters::*; pub use tiling_size_getters::*; pub use window_getters::*; ================================================ FILE: packages/wm/src/traits/position_getters.rs ================================================ use ambassador::delegatable_trait; use wm_platform::Rect; #[delegatable_trait] pub trait PositionGetters { fn to_rect(&self) -> anyhow::Result; } /// Implements the `PositionGetters` trait for tiling containers that can /// be resized. This is used by `SplitContainer` and `TilingWindow`. /// /// Expects that the struct has a wrapping `RefCell` containing a struct /// with an `id` and a `parent` field. #[macro_export] macro_rules! impl_position_getters_as_resizable { ($struct_name:ident) => { impl PositionGetters for $struct_name { fn to_rect(&self) -> anyhow::Result { let parent = self .parent() .and_then(|parent| parent.as_direction_container().ok()) .context("Parent does not have a tiling direction.")?; let parent_rect = parent.to_rect()?; let (horizontal_gap, vertical_gap) = self.inner_gaps()?; let inner_gap = match parent.tiling_direction() { TilingDirection::Vertical => vertical_gap, TilingDirection::Horizontal => horizontal_gap, }; #[allow( clippy::cast_precision_loss, clippy::cast_possible_truncation, clippy::cast_possible_wrap )] let (width, height) = match parent.tiling_direction() { TilingDirection::Vertical => { let available_height = parent_rect.height() - inner_gap * self.tiling_siblings().count() as i32; let height = (self.tiling_size() * available_height as f32) as i32; (parent_rect.width(), height) } TilingDirection::Horizontal => { let available_width = parent_rect.width() - inner_gap * self.tiling_siblings().count() as i32; let width = (available_width as f32 * self.tiling_size()).round() as i32; (width, parent_rect.height()) } }; let (x, y) = { let mut prev_siblings = self .prev_siblings() .filter_map(|sibling| sibling.as_tiling_container().ok()); match prev_siblings.next() { None => (parent_rect.x(), parent_rect.y()), Some(sibling) => { let sibling_rect = sibling.to_rect()?; match parent.tiling_direction() { TilingDirection::Vertical => ( parent_rect.x(), sibling_rect.y() + sibling_rect.height() + inner_gap, ), TilingDirection::Horizontal => ( sibling_rect.x() + sibling_rect.width() + inner_gap, parent_rect.y(), ), } } } }; Ok(Rect::from_xy(x, y, width, height)) } } }; } ================================================ FILE: packages/wm/src/traits/tiling_direction_getters.rs ================================================ use ambassador::delegatable_trait; use wm_common::TilingDirection; use wm_platform::Direction; use super::CommonGetters; use crate::models::{TilingContainer, TilingWindow}; #[delegatable_trait] pub trait TilingDirectionGetters: CommonGetters { fn tiling_direction(&self) -> TilingDirection; fn set_tiling_direction(&self, tiling_direction: TilingDirection); /// Traverses down a container in search of a descendant in the given /// direction. For example, for `Direction::Right`, get the right-most /// container. /// /// Any non-tiling containers are ignored. fn descendant_in_direction( &self, direction: &Direction, ) -> Option { let child = self.child_in_direction(direction)?; // Traverse further down if the child is a split container. match child { TilingContainer::Split(split_child) => { split_child.descendant_in_direction(direction) } TilingContainer::TilingWindow(window) => Some(window), } } fn child_in_direction( &self, direction: &Direction, ) -> Option { // When the tiling direction is the inverse of the direction, return // the last focused tiling child. if self.tiling_direction() != TilingDirection::from_direction(direction) { return self .child_focus_order() .find_map(|c| c.as_tiling_container().ok()); } match direction { Direction::Up | Direction::Left => self.tiling_children().next(), _ => self.tiling_children().last(), } } } /// Implements the `TilingDirectionGetters` trait for a given struct. /// /// Expects that the struct has a wrapping `RefCell` containing a struct /// with a `tiling_direction` field. #[macro_export] macro_rules! impl_tiling_direction_getters { ($struct_name:ident) => { impl TilingDirectionGetters for $struct_name { fn tiling_direction(&self) -> TilingDirection { self.0.borrow().tiling_direction.clone() } fn set_tiling_direction(&self, tiling_direction: TilingDirection) { self.0.borrow_mut().tiling_direction = tiling_direction; } } }; } ================================================ FILE: packages/wm/src/traits/tiling_size_getters.rs ================================================ use std::cell::Ref; use ambassador::delegatable_trait; use anyhow::Context; use wm_common::{GapsConfig, TilingDirection}; use super::{CommonGetters, TilingDirectionGetters}; use crate::models::{Container, DirectionContainer, TilingContainer}; pub const MIN_TILING_SIZE: f32 = 0.01; #[delegatable_trait] pub trait TilingSizeGetters: CommonGetters { fn tiling_size(&self) -> f32; fn set_tiling_size(&self, tiling_size: f32); fn gaps_config(&self) -> Ref<'_, GapsConfig>; fn set_gaps_config(&self, gaps_config: GapsConfig); /// Gets the horizontal and vertical gaps between windows in pixels. fn inner_gaps(&self) -> anyhow::Result<(i32, i32)> { let monitor = self.monitor().context("No monitor.")?; let monitor_rect = monitor.native_properties().bounds; let gaps_config = self.gaps_config(); let scale_factor = if gaps_config.scale_with_dpi { monitor.native_properties().scale_factor } else { 1. }; Ok(( gaps_config .inner_gap .to_px(monitor_rect.height(), Some(scale_factor)), gaps_config .inner_gap .to_px(monitor_rect.width(), Some(scale_factor)), )) } /// Gets the container to resize when resizing a tiling window. fn container_to_resize( &self, is_width_resize: bool, ) -> anyhow::Result> { let parent = self.direction_container().context("No parent.")?; let tiling_direction = parent.tiling_direction(); // Whether the resize is in the inverse of its tiling direction. let is_inverse_resize = match tiling_direction { TilingDirection::Horizontal => !is_width_resize, TilingDirection::Vertical => is_width_resize, }; let container_to_resize = if is_inverse_resize { match parent { // Prevent workspaces from being resized. DirectionContainer::Split(parent) => Some(parent.into()), DirectionContainer::Workspace(_) => None, } } else { let grandparent = parent.parent().context("No grandparent.")?; if self.tiling_siblings().count() > 0 { // Window can only be resized if it has siblings. Some(self.as_tiling_container()?) } else { // Resize grandparent in layouts like H[1 V[2 H[3]]], where // container 3 is resized horizontally. match grandparent { Container::Split(grandparent) => Some(grandparent.into()), _ => None, } } }; Ok(container_to_resize) } } /// Implements the `TilingSizeGetters` trait for a given struct. /// /// Expects that the struct has a wrapping `RefCell` containing a struct /// with a `tiling_size` field. #[macro_export] macro_rules! impl_tiling_size_getters { ($struct_name:ident) => { impl TilingSizeGetters for $struct_name { fn tiling_size(&self) -> f32 { self.0.borrow().tiling_size } fn set_tiling_size(&self, tiling_size: f32) { self.0.borrow_mut().tiling_size = tiling_size; } fn gaps_config(&self) -> Ref<'_, GapsConfig> { Ref::map(self.0.borrow(), |inner| &inner.gaps_config) } fn set_gaps_config(&self, gaps_config: GapsConfig) { self.0.borrow_mut().gaps_config = gaps_config; } } }; } ================================================ FILE: packages/wm/src/traits/window_getters.rs ================================================ use std::cell::Ref; use ambassador::delegatable_trait; use wm_common::{ActiveDrag, DisplayState, WindowRuleConfig, WindowState}; use wm_platform::{LengthValue, NativeWindow, Rect, RectDelta}; use crate::{ models::{NativeWindowProperties, Workspace}, traits::CommonGetters, user_config::UserConfig, }; #[delegatable_trait] pub trait WindowGetters: CommonGetters { fn state(&self) -> WindowState; fn set_state(&self, state: WindowState); fn prev_state(&self) -> Option; fn set_prev_state(&self, state: WindowState); /// Gets the "toggled" window state based on the current state and a /// given target state. /// /// This will return the first valid state in the following order: /// 1. If the window is not currently in the target state, return the /// target state. /// 2. The previous state exists if one exists (excluding minimized). /// 3. The state from `window_behavior.initial_state` in the user config. /// 4. Default to either floating/tiling depending on the current state. fn toggled_state( &self, target_state: WindowState, config: &UserConfig, ) -> WindowState { let possible_states = [ Some(target_state), self .prev_state() .filter(|state| *state != WindowState::Minimized), Some(WindowState::default_from_config(&config.value)), ]; // Return the first possible state with a different discriminant. possible_states .into_iter() .find_map(|state| { state.filter(|state| !self.state().is_same_state(state)) }) // Default to tiling from a non-tiling state, and floating from a // tiling state. .unwrap_or_else(|| match self.state() { WindowState::Tiling => WindowState::Floating( config.value.window_behavior.state_defaults.floating.clone(), ), _ => WindowState::Tiling, }) } fn native(&self) -> Ref<'_, NativeWindow>; fn border_delta(&self) -> RectDelta; fn set_border_delta(&self, border_delta: RectDelta); fn total_border_delta(&self) -> anyhow::Result { let border_delta = self.border_delta(); #[cfg(target_os = "windows")] let shadow_border_delta = self.native_properties().shadow_borders; #[cfg(not(target_os = "windows"))] let shadow_border_delta = RectDelta::zero(); // TODO: Allow percentage length values. Ok(RectDelta { left: LengthValue::from_px( border_delta.left.to_px(0, None) + shadow_border_delta.left.to_px(0, None), ), right: LengthValue::from_px( border_delta.right.to_px(0, None) + shadow_border_delta.right.to_px(0, None), ), top: LengthValue::from_px( border_delta.top.to_px(0, None) + shadow_border_delta.top.to_px(0, None), ), bottom: LengthValue::from_px( border_delta.bottom.to_px(0, None) + shadow_border_delta.bottom.to_px(0, None), ), }) } /// Gets whether the window should be fullscreen for the given workspace. /// /// A window is considered fullscreen if its frame covers or exceeds the /// workspace bounds, meaning all sides extend into the outer gaps. /// /// NOTE: The OS can be off by up to 1px when positioning windows. fn should_fullscreen( &self, workspace: &Workspace, ) -> anyhow::Result { let workspace_rect = workspace.max_workspace_rect()?; let frame = self .native_properties() .frame .apply_delta(&self.border_delta().inverse(), None); let should_fullscreen = match self.state() { // Keep as fullscreen if the frame covers the workspace bounds. WindowState::Fullscreen(fullscreen) if !fullscreen.maximized => { frame.contains_rect(&workspace_rect.inset(1)) } // Change to fullscreen if the frame *exceeds* the workspace bounds. // NOTE: This is never possible with 0px outer gaps; the window has // to be made fullscreen via the `set-fullscreen` command. _ => frame.inset(1).contains_rect(&workspace_rect), }; Ok(should_fullscreen) } fn display_state(&self) -> DisplayState; fn set_display_state(&self, display_state: DisplayState); // LINT: `has_pending_dpi_adjustment` is only used on Windows. #[allow(unused)] fn has_pending_dpi_adjustment(&self) -> bool; fn set_has_pending_dpi_adjustment( &self, has_pending_dpi_adjustment: bool, ); fn floating_placement(&self) -> Rect; fn set_floating_placement(&self, floating_placement: Rect); fn has_custom_floating_placement(&self) -> bool; fn set_has_custom_floating_placement( &self, has_custom_floating_placement: bool, ); fn done_window_rules(&self) -> Vec; fn set_done_window_rules( &self, done_window_rules: Vec, ); fn active_drag(&self) -> Option; fn set_active_drag(&self, active_drag: Option); /// Gets the cached native window properties. fn native_properties(&self) -> NativeWindowProperties; /// Updates the cached native window properties using a closure. fn update_native_properties(&self, updater: F) where F: FnOnce(&mut NativeWindowProperties); } /// Implements the `WindowGetters` trait for a given struct. /// /// Expects that the struct has a wrapping `RefCell` containing a struct /// with a `state`, `prev_state`, `native`, `has_pending_dpi_adjustment`, /// `border_delta`, `display_state`, and a `done_window_rules` field. #[macro_export] macro_rules! impl_window_getters { ($struct_name:ident) => { impl WindowGetters for $struct_name { fn state(&self) -> WindowState { self.0.borrow().state.clone() } fn set_state(&self, state: WindowState) { self.0.borrow_mut().state = state; } fn prev_state(&self) -> Option { self.0.borrow().prev_state.clone() } fn set_prev_state(&self, state: WindowState) { self.0.borrow_mut().prev_state = Some(state); } fn native(&self) -> Ref<'_, NativeWindow> { Ref::map(self.0.borrow(), |inner| &inner.native) } fn border_delta(&self) -> RectDelta { self.0.borrow().border_delta.clone() } fn set_border_delta(&self, border_delta: RectDelta) { self.0.borrow_mut().border_delta = border_delta; } fn display_state(&self) -> DisplayState { self.0.borrow().display_state.clone() } fn set_display_state(&self, display_state: DisplayState) { self.0.borrow_mut().display_state = display_state; } // LINT: `has_pending_dpi_adjustment` is only used on Windows. #[allow(unused)] fn has_pending_dpi_adjustment(&self) -> bool { self.0.borrow().has_pending_dpi_adjustment } fn set_has_pending_dpi_adjustment( &self, has_pending_dpi_adjustment: bool, ) { self.0.borrow_mut().has_pending_dpi_adjustment = has_pending_dpi_adjustment; } fn floating_placement(&self) -> Rect { self.0.borrow().floating_placement.clone() } fn set_floating_placement(&self, floating_placement: Rect) { self.0.borrow_mut().floating_placement = floating_placement; } fn has_custom_floating_placement(&self) -> bool { self.0.borrow().has_custom_floating_placement.clone() } fn set_has_custom_floating_placement( &self, has_custom_floating_placement: bool, ) { self.0.borrow_mut().has_custom_floating_placement = has_custom_floating_placement; } fn done_window_rules(&self) -> Vec { self.0.borrow().done_window_rules.clone() } fn set_done_window_rules( &self, done_window_rules: Vec, ) { self.0.borrow_mut().done_window_rules = done_window_rules; } fn active_drag(&self) -> Option { self.0.borrow().active_drag.clone() } fn set_active_drag(&self, active_drag: Option) { self.0.borrow_mut().active_drag = active_drag; } fn native_properties(&self) -> NativeWindowProperties { self.0.borrow().native_properties.clone() } fn update_native_properties(&self, updater: F) where F: FnOnce(&mut NativeWindowProperties), { updater(&mut self.0.borrow_mut().native_properties); } } }; } ================================================ FILE: packages/wm/src/user_config.rs ================================================ use std::{collections::HashMap, env, fs, path::PathBuf}; use anyhow::{Context, Result}; use wm_common::{ InvokeCommand, KeybindingConfig, MatchType, ParsedConfig, WindowMatchConfig, WindowRuleConfig, WindowRuleEvent, WorkspaceConfig, }; use crate::{ models::{Monitor, WindowContainer, Workspace}, traits::{CommonGetters, WindowGetters}, }; /// Resource string for the sample config file. const SAMPLE_CONFIG: &str = include_str!("../../../resources/assets/sample-config.yaml"); #[derive(Debug)] pub struct UserConfig { /// Path to the user config file. pub path: PathBuf, /// Parsed user config value. pub value: ParsedConfig, /// Unparsed user config string. pub value_str: String, /// Hashmap of window rule event types (e.g. `WindowRuleEvent::Manage`) /// and the corresponding window rules of that type. window_rules_by_event: HashMap>, } impl UserConfig { /// Creates an instance of `UserConfig`. Reads and validates the user /// config from the given path. /// /// Creates a new config file from sample if it doesn't exist. pub fn new(config_path: Option) -> anyhow::Result { let default_config_path = home::home_dir() .context("Unable to get home directory.")? .join(".glzr/glazewm/config.yaml"); let config_path = config_path .or_else(|| env::var("GLAZEWM_CONFIG_PATH").ok().map(PathBuf::from)) .unwrap_or(default_config_path); let (config_value, config_str) = Self::read(&config_path)?; let window_rules_by_event = Self::window_rules_by_event(&config_value); Ok(Self { path: config_path, value: config_value, value_str: config_str, window_rules_by_event, }) } /// Reads and validates the user config from the given path. /// /// Creates a new config file from sample if it doesn't exist. fn read( config_path: &PathBuf, ) -> anyhow::Result<(ParsedConfig, String)> { if !config_path.exists() { Self::create_sample(config_path)?; } let config_str = fs::read_to_string(config_path) .context("Unable to read config file.")?; // TODO: Improve error formatting of serde_yaml errors. Something // similar to https://github.com/AlexanderThaller/format_serde_error let config_value = serde_yaml::from_str(&config_str)?; Ok((config_value, config_str)) } /// Initializes a new config file from the sample config resource. fn create_sample(config_path: &PathBuf) -> Result<()> { let parent_dir = config_path.parent().context("Invalid config path.")?; fs::create_dir_all(parent_dir).with_context(|| { format!("Unable to create directory {}.", &config_path.display()) })?; fs::write(config_path, SAMPLE_CONFIG).with_context(|| { format!("Unable to write to {}.", config_path.display()) })?; Ok(()) } pub fn reload(&mut self) -> anyhow::Result<()> { let (config_value, config_str) = Self::read(&self.path)?; self.window_rules_by_event = Self::window_rules_by_event(&config_value); self.value = config_value; self.value_str = config_str; Ok(()) } fn default_window_rules( config_value: &ParsedConfig, ) -> Vec { let mut window_rules = Vec::new(); let floating_defaults = &config_value.window_behavior.state_defaults.floating; // Default float rules. window_rules.push(WindowRuleConfig { commands: vec![InvokeCommand::SetFloating { centered: Some(floating_defaults.centered), shown_on_top: Some(floating_defaults.shown_on_top), x_pos: None, y_pos: None, width: None, height: None, }], match_window: vec![ WindowMatchConfig { window_class: Some(MatchType::Equals { equals: // W10/W11 system dialog shown when moving and deleting files. "OperationStatusWindow".to_string(), }), ..WindowMatchConfig::default() }, WindowMatchConfig { window_class: Some(MatchType::Equals { equals: // W10/W11 system dialogs (e.g. File Explorer save/open dialog). "#32770".to_string(), }), ..WindowMatchConfig::default() }, ], on: vec![WindowRuleEvent::Manage], run_once: true, }); // Default ignore rules. window_rules.push(WindowRuleConfig { commands: vec![InvokeCommand::Ignore], match_window: vec![ WindowMatchConfig { window_process: Some(MatchType::Equals { equals: "SearchApp".to_string(), }), ..WindowMatchConfig::default() }, WindowMatchConfig { window_process: Some(MatchType::Equals { equals: "SearchHost".to_string(), }), ..WindowMatchConfig::default() }, WindowMatchConfig { window_process: Some(MatchType::Equals { equals: "ShellExperienceHost".to_string(), }), ..WindowMatchConfig::default() }, WindowMatchConfig { window_process: Some(MatchType::Equals { // W10/11 start menu. equals: "StartMenuExperienceHost".to_string(), }), ..WindowMatchConfig::default() }, WindowMatchConfig { window_process: Some(MatchType::Equals { // W10/11 screen snipping tool. equals: "ScreenClippingHost".to_string(), }), ..WindowMatchConfig::default() }, WindowMatchConfig { window_process: Some(MatchType::Equals { // W11 lock screen. equals: "LockApp".to_string(), }), ..WindowMatchConfig::default() }, ], on: vec![WindowRuleEvent::Manage], run_once: true, }); window_rules } fn window_rules_by_event( config_value: &ParsedConfig, ) -> HashMap> { let mut window_rules_by_event = HashMap::new(); // Combine user-defined window rules with the default ones. let default_window_rules = Self::default_window_rules(config_value); let all_window_rules = config_value .window_rules .iter() .chain(default_window_rules.iter()); for window_rule in all_window_rules { for event_type in &window_rule.on { window_rules_by_event .entry(event_type.clone()) .or_insert_with(Vec::new) .push(window_rule.clone()); } } window_rules_by_event } /// Window rules that should be applied to the window when the given /// event occurs. pub fn pending_window_rules( &self, window: &WindowContainer, event: &WindowRuleEvent, ) -> Vec { let window_title = window.native_properties().title; #[cfg(target_os = "windows")] let window_class = window.native_properties().class_name; let window_process = window.native_properties().process_name; let pending_window_rules = self .window_rules_by_event .get(event) .unwrap_or(&Vec::new()) .iter() .filter(|rule| { // Skip if window has already ran the rule. if window.done_window_rules().contains(rule) { return false; } // Check if the window matches the rule. rule.match_window.iter().any(|match_config| { let is_process_match = match_config .window_process .as_ref() .is_none_or(|match_type| { // TODO: Temp fix for matching Zebar on both platforms with // the same process name. Consider using lowercase for every // `equals` match type. if window_process == "Zebar" { match_type.is_match("Zebar") || match_type.is_match("zebar") } else { match_type.is_match(&window_process) } }); let is_class_match = { #[cfg(target_os = "windows")] { match_config.window_class.as_ref().is_none_or(|match_type| { match_type.is_match(&window_class) }) } #[cfg(not(target_os = "windows"))] { match_config.window_class.is_none() } }; let is_title_match = match_config .window_title .as_ref() .is_none_or(|match_type| match_type.is_match(&window_title)); is_process_match && is_class_match && is_title_match }) }) .cloned() .collect::>(); pending_window_rules } pub fn inactive_workspace_configs( &self, active_workspaces: &[Workspace], ) -> Vec<&WorkspaceConfig> { self .value .workspaces .iter() .filter(|config| { !active_workspaces .iter() .any(|workspace| workspace.config().name == config.name) }) .collect() } pub fn workspace_config_for_monitor( &self, monitor: &Monitor, active_workspaces: &[Workspace], ) -> Option<&WorkspaceConfig> { let inactive_configs = self.inactive_workspace_configs(active_workspaces); inactive_configs.into_iter().find(|&config| { config .bind_to_monitor .as_ref() .is_some_and(|monitor_index| { monitor.index() == *monitor_index as usize }) }) } /// Gets the first inactive workspace config, prioritizing configs that /// don't have a monitor binding. pub fn next_inactive_workspace_config( &self, active_workspaces: &[Workspace], ) -> Option<&WorkspaceConfig> { let inactive_configs = self.inactive_workspace_configs(active_workspaces); inactive_configs .iter() .find(|config| config.bind_to_monitor.is_none()) .or(inactive_configs.first()) .copied() } pub fn workspace_config_index( &self, workspace_name: &str, ) -> Option { self .value .workspaces .iter() .position(|config| config.name == workspace_name) } pub fn sort_workspaces(&self, workspaces: &mut [Workspace]) { workspaces.sort_by_key(|workspace| { self.workspace_config_index(&workspace.config().name) }); } /// Keybinding configs that should be active for the current binding mode /// and pause state. /// /// When paused, only the configs with `InvokeCommand::WmTogglePause` are /// returned so that unpausing remains possible. pub fn active_keybinding_configs( &self, binding_modes: &[wm_common::BindingModeConfig], is_paused: bool, ) -> impl Iterator { let source_configs = if let Some(first_mode) = binding_modes.first() { &first_mode.keybindings } else { &self.value.keybindings } .clone(); source_configs.into_iter().filter(move |kb| { if is_paused { kb.commands .contains(&wm_common::InvokeCommand::WmTogglePause) } else { true } }) } } ================================================ FILE: packages/wm/src/wm.rs ================================================ use anyhow::{bail, Context}; use tokio::sync::mpsc::{self}; use tracing::warn; use uuid::Uuid; #[cfg(target_os = "windows")] use wm_common::TitleBarVisibility; use wm_common::{ FloatingStateConfig, FullscreenStateConfig, InvokeCommand, WindowState, WmEvent, }; #[cfg(target_os = "windows")] use wm_platform::NativeWindowWindowsExt; use wm_platform::{ Dispatcher, LengthValue, PlatformEvent, RectDelta, WindowEvent, }; use crate::{ commands::{ container::{ focus_container_by_id, focus_in_direction, set_tiling_direction, toggle_tiling_direction, }, general::{ cycle_focus, disable_binding_mode, enable_binding_mode, platform_sync, reload_config, shell_exec, toggle_pause, }, monitor::focus_monitor, window::{ ignore_window, move_window_in_direction, move_window_to_workspace, resize_window, set_window_position, set_window_size, update_window_state, WindowPositionTarget, }, workspace::{ focus_workspace, move_workspace_in_direction, update_workspace_config, }, }, events::{ handle_display_settings_changed, handle_mouse_move, handle_window_destroyed, handle_window_focused, handle_window_hidden, handle_window_minimize_ended, handle_window_minimized, handle_window_moved_or_resized, handle_window_shown, handle_window_title_changed, }, ipc_server::IpcServer, models::{Container, WorkspaceTarget}, traits::{CommonGetters, WindowGetters}, user_config::UserConfig, wm_state::WmState, }; pub struct WindowManager { pub event_rx: mpsc::UnboundedReceiver, pub exit_rx: mpsc::UnboundedReceiver<()>, pub state: WmState, } impl WindowManager { pub fn new( config: &mut UserConfig, dispatcher: Dispatcher, ) -> anyhow::Result { let (event_tx, event_rx) = mpsc::unbounded_channel(); let (exit_tx, exit_rx) = mpsc::unbounded_channel(); let mut state = WmState::new(dispatcher, event_tx, exit_tx); state.populate(config)?; Ok(Self { event_rx, exit_rx, state, }) } pub fn process_event( &mut self, event: PlatformEvent, config: &mut UserConfig, ) -> anyhow::Result<()> { let state = &mut self.state; match event { PlatformEvent::DisplaySettingsChanged => { handle_display_settings_changed(state, config) } PlatformEvent::Keybinding(keybinding_event) => { // Find the keybinding config that matches this keybinding. let commands = config .active_keybinding_configs( &self.state.binding_modes, self.state.is_paused, ) .find(|kb_config| { kb_config.bindings.contains(&keybinding_event.0) }) .map(|kb_config| kb_config.commands.clone()); if let Some(commands) = commands { self.process_commands(&commands, None, config)?; } // Return early since we don't want to redraw twice. return Ok(()); } PlatformEvent::Mouse(event) => { handle_mouse_move(&event, state, config) } PlatformEvent::Window(window_event) => match window_event { WindowEvent::Focused { window, .. } => { handle_window_focused(&window, state, config) } WindowEvent::Shown { window, .. } => { handle_window_shown(window, state, config) } WindowEvent::Hidden { window, .. } => { handle_window_hidden(&window, state, config) } WindowEvent::MovedOrResized { window, is_interactive_start, is_interactive_end, .. } => handle_window_moved_or_resized( &window, is_interactive_start, is_interactive_end, state, config, ), WindowEvent::Minimized { window, .. } => { handle_window_minimized(&window, state, config) } WindowEvent::MinimizeEnded { window, .. } => { handle_window_minimize_ended(&window, state, config) } WindowEvent::TitleChanged { window, .. } => { handle_window_title_changed(&window, state, config) } WindowEvent::Destroyed { window_id, .. } => { handle_window_destroyed(window_id, state) } }, }?; if !state.is_paused && state.pending_sync.has_changes() { platform_sync(state, config)?; } Ok(()) } pub fn process_commands( &mut self, commands: &Vec, subject_container_id: Option, config: &mut UserConfig, ) -> anyhow::Result { let state = &mut self.state; // Get the container to run WM commands with. let subject_container = match subject_container_id { Some(id) => state.container_by_id(id).with_context(|| { format!("No container found with the given ID '{id}'.") })?, None => state .focused_container() .context("No subject container for command.")?, }; let new_subject_container_id = WindowManager::run_commands( commands, subject_container, state, config, )?; if state.pending_sync.has_changes() { platform_sync(state, config)?; } Ok(new_subject_container_id) } pub fn run_commands( commands: &Vec, subject_container: Container, state: &mut WmState, config: &mut UserConfig, ) -> anyhow::Result { let mut current_subject_container = subject_container; for command in commands { WindowManager::run_command( command, current_subject_container.clone(), state, config, )?; // Update the subject container in case the container type changes. // For example, when going from a tiling to a floating window. current_subject_container = if current_subject_container.is_detached() { match state.container_by_id(current_subject_container.id()) { Some(container) => container, None => break, } } else { current_subject_container } } Ok(current_subject_container.id()) } #[allow(clippy::too_many_lines)] pub fn run_command( command: &InvokeCommand, subject_container: Container, state: &mut WmState, config: &mut UserConfig, ) -> anyhow::Result<()> { // No-op if WM is currently paused. if state.is_paused && *command != InvokeCommand::WmTogglePause { return Ok(()); } if subject_container.is_detached() { bail!("Cannot run command because subject container is detached."); } match &command { InvokeCommand::AdjustBorders(args) => { match subject_container.as_window_container() { Ok(window) => { let args = args.clone(); let border_delta = RectDelta::new( args.left.unwrap_or(LengthValue::from_px(0)), args.top.unwrap_or(LengthValue::from_px(0)), args.right.unwrap_or(LengthValue::from_px(0)), args.bottom.unwrap_or(LengthValue::from_px(0)), ); window.set_border_delta(border_delta); state.pending_sync.queue_container_to_redraw(window); Ok(()) } _ => Ok(()), } } InvokeCommand::Close => { match subject_container.as_window_container() { Ok(window) => { // Window handle might no longer be valid here. if let Err(err) = window.native().close() { warn!("Failed to close window: {:?}", err); } Ok(()) } _ => Ok(()), } } InvokeCommand::Focus(args) => { if let Some(direction) = &args.direction { focus_in_direction(&subject_container, direction, state)?; } if let Some(direction) = &args.workspace_in_direction { focus_workspace( WorkspaceTarget::Direction(direction.clone()), state, config, )?; } if let Some(container_id) = &args.container_id { focus_container_by_id(container_id, state)?; } if let Some(name) = &args.workspace { focus_workspace( WorkspaceTarget::Name(name.clone()), state, config, )?; } if let Some(monitor_index) = &args.monitor { focus_monitor(*monitor_index, state, config)?; } if args.next_active_workspace { focus_workspace(WorkspaceTarget::NextActive, state, config)?; } if args.prev_active_workspace { focus_workspace(WorkspaceTarget::PreviousActive, state, config)?; } if args.next_workspace { focus_workspace(WorkspaceTarget::Next, state, config)?; } if args.prev_workspace { focus_workspace(WorkspaceTarget::Previous, state, config)?; } if args.recent_workspace { focus_workspace(WorkspaceTarget::Recent, state, config)?; } if args.next_active_workspace_on_monitor { focus_workspace( WorkspaceTarget::NextActiveInMonitor, state, config, )?; } if args.prev_active_workspace_on_monitor { focus_workspace( WorkspaceTarget::PreviousActiveInMonitor, state, config, )?; } Ok(()) } InvokeCommand::Ignore => { match subject_container.as_window_container() { Ok(window) => ignore_window(window, state), _ => Ok(()), } } InvokeCommand::Move(args) => { match subject_container.as_window_container() { Ok(window) => { if let Some(direction) = &args.direction { move_window_in_direction( window.clone(), direction, state, config, )?; } if let Some(direction) = &args.workspace_in_direction { move_window_to_workspace( window.clone(), WorkspaceTarget::Direction(direction.clone()), state, config, )?; } if let Some(name) = &args.workspace { move_window_to_workspace( window.clone(), WorkspaceTarget::Name(name.clone()), state, config, )?; } if args.next_active_workspace { move_window_to_workspace( window.clone(), WorkspaceTarget::NextActive, state, config, )?; } if args.prev_active_workspace { move_window_to_workspace( window.clone(), WorkspaceTarget::PreviousActive, state, config, )?; } if args.next_workspace { move_window_to_workspace( window.clone(), WorkspaceTarget::Next, state, config, )?; } if args.prev_workspace { move_window_to_workspace( window.clone(), WorkspaceTarget::Previous, state, config, )?; } if args.recent_workspace { move_window_to_workspace( window.clone(), WorkspaceTarget::Recent, state, config, )?; } if args.next_active_workspace_on_monitor { move_window_to_workspace( window.clone(), WorkspaceTarget::NextActiveInMonitor, state, config, )?; } if args.prev_active_workspace_on_monitor { move_window_to_workspace( window, WorkspaceTarget::PreviousActiveInMonitor, state, config, )?; } Ok(()) } _ => Ok(()), } } InvokeCommand::MoveWorkspace { direction } => { let workspace = subject_container.workspace().context("No workspace.")?; move_workspace_in_direction(&workspace, direction, state, config) } InvokeCommand::Position(args) => { match subject_container.as_window_container() { Ok(window) => { if args.centered { set_window_position( window, &WindowPositionTarget::Centered, state, ) } else { set_window_position( window, &WindowPositionTarget::Coordinates(args.x_pos, args.y_pos), state, ) } } _ => Ok(()), } } InvokeCommand::UpdateWorkspaceConfig { workspace, new_config, } => { let workspace = if let Some(workspace_name) = workspace { state .workspace_by_name(workspace_name) .context("Workspace doesn't exist.")? } else { subject_container.workspace().context("No workspace.")? }; update_workspace_config(&workspace, state, config, new_config) } InvokeCommand::Resize(args) => { match subject_container.as_window_container() { Ok(window) => resize_window( &window, args.width.clone(), args.height.clone(), state, ), _ => Ok(()), } } InvokeCommand::SetFloating { centered, shown_on_top, x_pos, y_pos, width, height, } => match subject_container.as_window_container() { Ok(window) => { let floating_defaults = &config.value.window_behavior.state_defaults.floating; let centered = centered.unwrap_or(floating_defaults.centered); let window = update_window_state( window.clone(), WindowState::Floating(FloatingStateConfig { centered, shown_on_top: shown_on_top .unwrap_or(floating_defaults.shown_on_top), }), state, config, )?; // Allow size and position to be set if window has not previously // been manually placed. if !window.has_custom_floating_placement() { if width.is_some() || height.is_some() { set_window_size( window.clone(), width.clone(), height.clone(), state, )?; } if centered { set_window_position( window, &WindowPositionTarget::Centered, state, )?; } else if x_pos.is_some() || y_pos.is_some() { set_window_position( window, &WindowPositionTarget::Coordinates(*x_pos, *y_pos), state, )?; } } Ok(()) } _ => Ok(()), }, InvokeCommand::SetFullscreen { maximized, shown_on_top, } => match subject_container.as_window_container() { Ok(window) => { let fullscreen_defaults = &config.value.window_behavior.state_defaults.fullscreen; update_window_state( window.clone(), WindowState::Fullscreen(FullscreenStateConfig { maximized: maximized .unwrap_or(fullscreen_defaults.maximized), shown_on_top: shown_on_top .unwrap_or(fullscreen_defaults.shown_on_top), }), state, config, )?; Ok(()) } _ => Ok(()), }, InvokeCommand::SetMinimized => { match subject_container.as_window_container() { Ok(window) => { update_window_state( window.clone(), WindowState::Minimized, state, config, )?; Ok(()) } _ => Ok(()), } } InvokeCommand::SetTiling => { match subject_container.as_window_container() { Ok(window) => { update_window_state( window, WindowState::Tiling, state, config, )?; Ok(()) } _ => Ok(()), } } InvokeCommand::SetTitleBarVisibility { // LINT: `visibility` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] visibility, } => match subject_container.as_window_container() { #[cfg(target_os = "windows")] Ok(window) => { _ = window.native().set_title_bar_visibility( *visibility == TitleBarVisibility::Shown, ); Ok(()) } _ => Ok(()), }, // LINT: `args` is only used on Windows. #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] InvokeCommand::SetTransparency(args) => { match subject_container.as_window_container() { #[cfg(target_os = "windows")] Ok(window) => { if let Some(opacity) = &args.opacity { _ = window.native().set_transparency(opacity); } if let Some(opacity_delta) = &args.opacity_delta { _ = window.native().adjust_transparency(opacity_delta); } Ok(()) } _ => Ok(()), } } InvokeCommand::ShellExec { hide_window, command, } => shell_exec(&command.join(" "), *hide_window, state), InvokeCommand::Size(args) => { match subject_container.as_window_container() { Ok(window) => set_window_size( window, args.width.clone(), args.height.clone(), state, ), _ => Ok(()), } } InvokeCommand::ToggleFloating { centered, shown_on_top, } => match subject_container.as_window_container() { Ok(window) => { let floating_defaults = &config.value.window_behavior.state_defaults.floating; let centered = centered.unwrap_or(floating_defaults.centered); let target_state = WindowState::Floating(FloatingStateConfig { centered, shown_on_top: shown_on_top .unwrap_or(floating_defaults.shown_on_top), }); let window = update_window_state( window.clone(), window.toggled_state(target_state, config), state, config, )?; if !window.has_custom_floating_placement() && centered { set_window_position( window, &WindowPositionTarget::Centered, state, )?; } Ok(()) } _ => Ok(()), }, InvokeCommand::ToggleFullscreen { maximized, shown_on_top, } => match subject_container.as_window_container() { Ok(window) => { let fullscreen_defaults = &config.value.window_behavior.state_defaults.fullscreen; let target_state = WindowState::Fullscreen(FullscreenStateConfig { maximized: maximized .unwrap_or(fullscreen_defaults.maximized), shown_on_top: shown_on_top .unwrap_or(fullscreen_defaults.shown_on_top), }); update_window_state( window.clone(), window.toggled_state(target_state, config), state, config, )?; Ok(()) } _ => Ok(()), }, InvokeCommand::ToggleMinimized => { match subject_container.as_window_container() { Ok(window) => { update_window_state( window.clone(), window.toggled_state(WindowState::Minimized, config), state, config, )?; Ok(()) } _ => Ok(()), } } InvokeCommand::ToggleTiling => { match subject_container.as_window_container() { Ok(window) => { update_window_state( window.clone(), window.toggled_state(WindowState::Tiling, config), state, config, )?; Ok(()) } _ => Ok(()), } } InvokeCommand::ToggleTilingDirection => { toggle_tiling_direction(subject_container, state, config) } InvokeCommand::SetTilingDirection { tiling_direction } => { set_tiling_direction( subject_container, state, config, tiling_direction, ) } InvokeCommand::WmCycleFocus { omit_floating, omit_fullscreen, omit_minimized, omit_tiling, } => cycle_focus( *omit_floating, *omit_fullscreen, *omit_minimized, *omit_tiling, state, config, ), InvokeCommand::WmDisableBindingMode { name } => { disable_binding_mode(name, state); Ok(()) } InvokeCommand::WmEnableBindingMode { name } => { enable_binding_mode(name, state, config) } InvokeCommand::WmExit => state.emit_exit(), InvokeCommand::WmRedraw => { state .pending_sync .queue_container_to_redraw(state.root_container.clone()); Ok(()) } InvokeCommand::WmReloadConfig => reload_config(state, config), InvokeCommand::WmTogglePause => { toggle_pause(state); Ok(()) } } } /// Runs cleanup tasks when the WM is exiting. pub(crate) fn cleanup( &mut self, config: &mut UserConfig, ipc_server: &mut IpcServer, ) { self.state.emit_event(WmEvent::ApplicationExiting); // Ensure that the WM is unpaused, otherwise, shutdown commands won't // get executed. self.state.is_paused = false; // Run user's shutdown commands. if let Err(err) = self.process_commands( &config.value.general.shutdown_commands.clone(), None, config, ) { tracing::warn!("Failed to run shutdown commands: {:?}", err); } // Emit remaining WM events before exiting. while let Ok(wm_event) = self.event_rx.try_recv() { tracing::info!( "Emitting WM event before shutting down: {:?}", wm_event ); if let Err(err) = ipc_server.process_event(wm_event) { tracing::warn!("{:?}", err); } } } } ================================================ FILE: packages/wm/src/wm_state.rs ================================================ use std::time::Instant; use anyhow::Context; use tokio::sync::mpsc::{self}; use tracing::warn; use uuid::Uuid; use wm_common::{BindingModeConfig, HideCorner, WindowState, WmEvent}; use wm_platform::{ Direction, Dispatcher, Display, NativeWindow, Point, Rect, }; #[cfg(target_os = "windows")] use wm_platform::{NativeWindowWindowsExt, OpacityValue}; use crate::{ commands::{ container::set_focused_descendant, general::platform_sync, monitor::{add_monitor, move_bounded_workspaces_to_new_monitor}, window::{manage_window, unmanage_window}, }, models::{ Container, Monitor, NativeMonitorProperties, RootContainer, WindowContainer, Workspace, WorkspaceTarget, }, pending_sync::PendingSync, traits::{CommonGetters, PositionGetters, WindowGetters}, user_config::UserConfig, }; pub struct WmState { /// Root node of the container tree. Monitors are the children of the /// root node, followed by workspaces, then split containers/windows. pub root_container: RootContainer, pub dispatcher: Dispatcher, pub pending_sync: PendingSync, /// Name of the most recently focused workspace. /// /// Used for the `general.toggle_workspace_on_refocus` option on /// workspace focus. pub recent_workspace_name: Option, /// The previously focused window that had focus effects applied. /// /// Used to efficiently update window effects by only removing focus /// effects from the previous window rather than all windows when focus /// changes. pub prev_effects_window: Option, /// Time since a previously focused window was unmanaged or minimized. /// /// Used to decide whether to override incoming focus events. pub unmanaged_or_minimized_timestamp: Option, /// Configs of currently enabled binding modes. pub binding_modes: Vec, /// Windows that the WM should ignore. Windows can be added via the /// `ignore` command. pub ignored_windows: Vec, /// Whether the WM is paused. pub is_paused: bool, /// Whether the OS focused window is the same as the WM focused window. pub is_focus_synced: bool, /// Whether the initial state has been populated. has_initialized: bool, /// Sender for emitting WM-related events. event_tx: mpsc::UnboundedSender, /// Sender for gracefully shutting down the WM. exit_tx: mpsc::UnboundedSender<()>, } impl WmState { pub fn new( dispatcher: Dispatcher, event_tx: mpsc::UnboundedSender, exit_tx: mpsc::UnboundedSender<()>, ) -> Self { Self { root_container: RootContainer::new(), dispatcher, pending_sync: PendingSync::default(), prev_effects_window: None, recent_workspace_name: None, unmanaged_or_minimized_timestamp: None, binding_modes: Vec::new(), ignored_windows: Vec::new(), is_paused: false, is_focus_synced: false, has_initialized: false, event_tx, exit_tx, } } /// Populates the initial WM state by creating containers for all /// existing windows and monitors. pub fn populate( &mut self, config: &mut UserConfig, ) -> anyhow::Result<()> { // Get the originally focused window when the WM was started. let focused_window = self.dispatcher.focused_window().ok(); // Create a monitor, and consequently a workspace, for each detected // native monitor. for native_display in self.dispatcher.sorted_displays()? { if let Ok(native_properties) = NativeMonitorProperties::try_from(&native_display) { let monitor = add_monitor(native_display, native_properties, self)?; move_bounded_workspaces_to_new_monitor(&monitor, self, config)?; } } // Manage windows in reverse z-order (bottom to top). This helps to // preserve the original stacking order. for native_window in self.dispatcher.visible_windows()?.into_iter().rev() { let nearest_workspace = self .nearest_monitor(&native_window) .and_then(|m| m.displayed_workspace()); if let Some(workspace) = nearest_workspace { manage_window( native_window, Some(workspace.into()), self, config, )?; } } let container_to_focus = focused_window .and_then(|focused_window| { self.window_from_native(&focused_window).map(Into::into) }) .or_else(|| self.windows().pop().map(Into::into)) .or_else(|| self.workspaces().pop().map(Into::into)) .context("Failed to get container to focus.")?; set_focused_descendant(&container_to_focus, None); self.is_focus_synced = true; self .pending_sync .queue_focus_change() .queue_all_effects_update(); for workspace in self.workspaces() { self.pending_sync.queue_workspace_to_reorder(workspace); } platform_sync(self, config)?; self.has_initialized = true; Ok(()) } pub fn monitors(&self) -> Vec { self.root_container.monitors() } pub fn workspaces(&self) -> Vec { self .monitors() .iter() .flat_map(Monitor::workspaces) .collect() } /// Gets workspaces sorted by their position in the user config. pub fn sorted_workspaces(&self, config: &UserConfig) -> Vec { let mut workspaces = self.workspaces(); config.sort_workspaces(&mut workspaces); workspaces } pub fn windows(&self) -> Vec { self .root_container .descendants() .filter_map(|container| container.try_into().ok()) .collect() } /// Gets the monitor that encompasses the largest portion of a given /// window. /// /// Defaults to the first monitor if the nearest monitor is invalid. pub fn nearest_monitor( &self, native_window: &NativeWindow, ) -> Option { self .monitor_from_native( &self.dispatcher.nearest_display(native_window).ok()?, ) .or(self.monitors().first().cloned()) } /// Gets monitor that corresponds to the given `Display`. pub fn monitor_from_native( &self, native_display: &Display, ) -> Option { self .monitors() .into_iter() .find(|monitor| monitor.native() == *native_display) } /// Gets the closest monitor in a given direction. /// /// Uses i3wm's algorithm for finding best guess. pub fn monitor_in_direction( &self, origin_monitor: &Monitor, direction: &Direction, ) -> anyhow::Result> { let origin_rect = origin_monitor.native_properties().bounds; // Create a tuple of monitors and their rect. let monitors_with_rect = self .monitors() .into_iter() .map(|monitor| { let rect = monitor.native_properties().bounds; anyhow::Ok((monitor, rect)) }) .try_collect::>()?; let closest_monitor = monitors_with_rect .into_iter() .filter(|(_, rect)| match direction { Direction::Right => { rect.x() > origin_rect.x() && rect.y_overlap(&origin_rect) > 0 } Direction::Left => { rect.x() < origin_rect.x() && rect.y_overlap(&origin_rect) > 0 } Direction::Down => { rect.y() > origin_rect.y() && rect.x_overlap(&origin_rect) > 0 } Direction::Up => { rect.y() < origin_rect.y() && rect.x_overlap(&origin_rect) > 0 } }) .min_by(|(_, rect_a), (_, rect_b)| match direction { Direction::Right => rect_a.x().cmp(&rect_b.x()), Direction::Left => rect_b.x().cmp(&rect_a.x()), Direction::Down => rect_a.y().cmp(&rect_b.y()), Direction::Up => rect_b.y().cmp(&rect_a.y()), }) .map(|(monitor, _)| monitor); Ok(closest_monitor) } /// Determines the preferred hide corner for each monitor. Used for /// [`HideMethod::PlaceInCorner`]. /// /// The corner is chosen by simulating a 400x400 window frame in the /// bottom-left and bottom-right of the monitor's working area, then /// picking the side that overlaps the least with other monitors' /// working areas (ties favor bottom-right). pub fn monitors_by_hide_corner(&self) -> Vec<(Monitor, HideCorner)> { const TEST_FRAME_SIZE: i32 = 400; const VISIBLE_SLIVER: i32 = 1; let monitors = self.monitors(); let working_areas = monitors .iter() .map(|monitor| monitor.native_properties().working_area) .collect::>(); monitors .into_iter() .enumerate() .map(|(idx, monitor)| { let monitor_rect = &working_areas[idx]; let test_frame_y = monitor_rect.bottom - TEST_FRAME_SIZE; let left_test_frame = Rect::from_xy( monitor_rect.left - TEST_FRAME_SIZE + VISIBLE_SLIVER, test_frame_y, TEST_FRAME_SIZE, TEST_FRAME_SIZE, ); let right_test_frame = Rect::from_xy( monitor_rect.right - VISIBLE_SLIVER, test_frame_y, TEST_FRAME_SIZE, TEST_FRAME_SIZE, ); let overlap_area = |test_frame: &Rect| -> i32 { working_areas .iter() .enumerate() .filter(|(i, _)| *i != idx) .map(|(_, rect)| test_frame.intersection_area(rect)) .sum() }; let left_overlap = overlap_area(&left_test_frame); let right_overlap = overlap_area(&right_test_frame); let corner = if left_overlap < right_overlap { HideCorner::BottomLeft } else { HideCorner::BottomRight }; (monitor, corner) }) .collect() } /// Gets window that corresponds to the given `NativeWindow`. pub fn window_from_native( &self, native_window: &NativeWindow, ) -> Option { self .windows() .into_iter() .find(|window| &*window.native() == native_window) } pub fn workspace_by_name( &self, workspace_name: &str, ) -> Option { self .workspaces() .into_iter() .find(|workspace| workspace.config().name == workspace_name) } /// Gets a workspace and its name by the given target. /// /// Returns a tuple of the workspace name and the `Workspace` instance /// if active. #[allow(clippy::too_many_lines)] pub fn workspace_by_target( &self, origin_workspace: &Workspace, target: WorkspaceTarget, config: &UserConfig, ) -> anyhow::Result<(Option, Option)> { let (name, workspace) = match target { WorkspaceTarget::Name(name) => { #[allow(clippy::match_bool)] match origin_workspace.config().name == name { false => (Some(name.clone()), self.workspace_by_name(&name)), // Toggle the workspace if it's already focused. true if config.value.general.toggle_workspace_on_refocus => ( self.recent_workspace_name.clone(), self .recent_workspace_name .as_ref() .and_then(|name| self.workspace_by_name(name)), ), true => (None, None), } } WorkspaceTarget::Recent => ( self.recent_workspace_name.clone(), self .recent_workspace_name .as_ref() .and_then(|name| self.workspace_by_name(name)), ), WorkspaceTarget::NextActive => { let active_workspaces = self.sorted_workspaces(config); let origin_index = active_workspaces .iter() .position(|workspace| workspace.id() == origin_workspace.id()) .context("Failed to get index of given workspace.")?; let next_active_workspace = active_workspaces .get(origin_index + 1) .or_else(|| active_workspaces.first()); ( next_active_workspace.map(|workspace| workspace.config().name), next_active_workspace.cloned(), ) } WorkspaceTarget::PreviousActive => { let active_workspaces = self.sorted_workspaces(config); let origin_index = active_workspaces .iter() .position(|workspace| workspace.id() == origin_workspace.id()) .context("Failed to get index of given workspace.")?; let prev_active_workspace = active_workspaces.get( origin_index .checked_sub(1) .unwrap_or(active_workspaces.len() - 1), ); ( prev_active_workspace.map(|workspace| workspace.config().name), prev_active_workspace.cloned(), ) } WorkspaceTarget::NextActiveInMonitor => { let monitor = origin_workspace .monitor() .context("No monitor in workspace")?; let mut workspace_in_monitor = monitor.workspaces(); config.sort_workspaces(&mut workspace_in_monitor); let origin_index = workspace_in_monitor .iter() .position(|workspace| workspace.id() == origin_workspace.id()) .context("Failed to get index of give workspace")?; let next_active_workspace_in_monitor = workspace_in_monitor .get(origin_index + 1) .or_else(|| workspace_in_monitor.first()); ( next_active_workspace_in_monitor .map(|workspace| workspace.config().name), next_active_workspace_in_monitor.cloned(), ) } WorkspaceTarget::PreviousActiveInMonitor => { let monitor = origin_workspace .monitor() .context("No monitor in workspace")?; let mut workspace_in_monitor = monitor.workspaces(); config.sort_workspaces(&mut workspace_in_monitor); let origin_index = workspace_in_monitor .iter() .position(|workspace| workspace.id() == origin_workspace.id()) .context("Failed to get index of give workspace")?; let prev_active_workspace_in_monitor = workspace_in_monitor.get( origin_index .checked_sub(1) .unwrap_or(workspace_in_monitor.len() - 1), ); ( prev_active_workspace_in_monitor .map(|workspace| workspace.config().name), prev_active_workspace_in_monitor.cloned(), ) } WorkspaceTarget::Next => { let workspaces = &config.value.workspaces; let origin_name = origin_workspace.config().name.clone(); let origin_index = workspaces .iter() .position(|workspace| workspace.name == origin_name) .context("Failed to get index of given workspace.")?; let next_workspace_config = workspaces .get(origin_index + 1) .or_else(|| workspaces.first()); let next_workspace_name = next_workspace_config.map(|config| config.name.clone()); let next_workspace = next_workspace_name .as_ref() .and_then(|name| self.workspace_by_name(name)); (next_workspace_name, next_workspace) } WorkspaceTarget::Previous => { let workspaces = &config.value.workspaces; let origin_name = origin_workspace.config().name.clone(); let origin_index = workspaces .iter() .position(|workspace| workspace.name == origin_name) .context("Failed to get index of given workspace.")?; let previous_workspace_config = workspaces.get( origin_index.checked_sub(1).unwrap_or(workspaces.len() - 1), ); let previous_workspace_name = previous_workspace_config.map(|config| config.name.clone()); let previous_workspace = previous_workspace_name .as_ref() .and_then(|name| self.workspace_by_name(name)); (previous_workspace_name, previous_workspace) } WorkspaceTarget::Direction(direction) => { let origin_monitor = origin_workspace.monitor().context("No focused monitor.")?; let target_workspace = self .monitor_in_direction(&origin_monitor, &direction)? .and_then(|monitor| monitor.displayed_workspace()); ( target_workspace .as_ref() .map(|workspace| workspace.config().name), target_workspace, ) } }; Ok((name, workspace)) } /// Gets windows that should be redrawn. /// /// When redrawing after a command that changes a window's type (e.g. /// tiling -> floating), the original detached window might still be /// queued for a redraw and should be filtered out. pub fn windows_to_redraw(&self) -> Vec { self .pending_sync .containers_to_redraw() .values() .flat_map(CommonGetters::self_and_descendants) .filter(|container| !container.is_detached()) .filter_map(|container| container.try_into().ok()) .collect() } /// Gets the currently focused container. This can either be a window or /// a workspace without any descendant windows. pub fn focused_container(&self) -> Option { self.root_container.descendant_focus_order().next() } /// Emits a WM event through an MSPC channel. /// /// Does not emit events while the WM is paused or populating initial /// state. This is to prevent events (e.g. workspace activation events) /// from being emitted via IPC server before the initial state is /// prepared. pub fn emit_event(&self, event: WmEvent) { if self.has_initialized && (!self.is_paused || matches!(event, WmEvent::PauseChanged { .. })) { if let Err(err) = self.event_tx.send(event) { warn!("Failed to send event: {}", err); } } } /// Starts graceful shutdown via an MSPC channel. pub fn emit_exit(&self) -> anyhow::Result<()> { self.exit_tx.send(())?; Ok(()) } pub fn container_by_id(&self, id: Uuid) -> Option { self .root_container .self_and_descendants() .find(|container| container.id() == id) } /// Gets container to focus after the given window is unmanaged, /// minimized, or moved to another workspace. pub fn focus_target_after_removal( &self, removed_window: &WindowContainer, ) -> Option { // If the removed window is not focused, no need to change focus. if self.focused_container() != Some(removed_window.clone().into()) { return None; } // Get descendant focus order excluding the removed container. let workspace = removed_window.workspace()?; let descendant_focus_order = workspace .descendant_focus_order() .filter(|descendant| descendant.id() != removed_window.id()) .collect::>(); // Get focus target that matches the removed window type. This applies // for windows that aren't in a minimized state. let focus_target_of_type = descendant_focus_order .iter() .filter_map(|descendant| descendant.as_window_container().ok()) .find(|descendant| { matches!( (descendant.state(), removed_window.state()), (WindowState::Tiling, WindowState::Tiling) | (WindowState::Floating(_), WindowState::Floating(_)) | (WindowState::Fullscreen(_), WindowState::Fullscreen(_)) ) }) .map(Into::into); if focus_target_of_type.is_some() { return focus_target_of_type; } let non_minimized_focus_target = descendant_focus_order .iter() .filter_map(|descendant| descendant.as_window_container().ok()) .find(|descendant| descendant.state() != WindowState::Minimized) .map(Into::into); non_minimized_focus_target .or(descendant_focus_order.first().cloned()) .or(Some(workspace.into())) } /// Returns all containers that contain the given point. #[allow(clippy::unused_self)] pub fn containers_at_point( &self, origin_container: &Container, point: &Point, ) -> Vec { origin_container .descendants() .filter(|descendant| { descendant .to_rect() .is_ok_and(|rect| rect.contains_point(point)) }) .collect() } /// Returns the monitor that contains the given point. pub fn monitor_at_point(&self, point: &Point) -> Option { self .monitors() .iter() .find(|monitor| { monitor .to_rect() .is_ok_and(|rect| rect.contains_point(point)) }) .cloned() } /// Cleans up windows that are no longer alive. /// /// This addresses the "ghost window" issue where applications may /// terminate without sending window destroy events, leaving invalid /// windows in WM state. /// /// See: pub fn cleanup_invalid_windows(&mut self) -> anyhow::Result<()> { let invalid_windows = self .windows() .into_iter() .filter(|window| !window.native().is_valid()); for window in invalid_windows { tracing::info!("Removing invalid window: {}", window); unmanage_window(window, self)?; } Ok(()) } } impl Drop for WmState { fn drop(&mut self) { let managed_windows = self.windows(); for window in &managed_windows { // Redraw windows to their intended positions. On macOS, this will // unhide windows that are on other workspaces. if let Ok(rect) = window.to_rect() { if let Err(err) = window.native().set_frame(&rect) { warn!("Failed to redraw window on cleanup: {:?}", err); } } // Reset any effects on Windows. #[cfg(target_os = "windows")] { if let Err(err) = window.native().show() { warn!("Failed to show window: {:?}", err); } let _ = window.native().set_taskbar_visibility(true); let _ = window.native().set_border_color(None); let _ = window .native() .set_transparency(&OpacityValue::from_alpha(u8::MAX)); } } } } ================================================ FILE: packages/wm-cli/Cargo.toml ================================================ [package] name = "wm-cli" version = "0.0.0" edition = "2021" [lib] path = "src/lib.rs" [[bin]] name = "glazewm-cli" path = "src/main.rs" [build-dependencies] tauri-winres = { workspace = true } [dependencies] anyhow = { workspace = true } futures-util = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tokio-tungstenite = { workspace = true } uuid = { workspace = true } wm-common = { path = "../wm-common" } wm-ipc-client = { path = "../wm-ipc-client" } ================================================ FILE: packages/wm-cli/build.rs ================================================ use tauri_winres::VersionInfo; fn main() { println!("cargo:rerun-if-env-changed=VERSION_NUMBER"); let mut res = tauri_winres::WindowsResource::new(); res.set_icon("../../resources/assets/icon.ico"); // Set language to English (US). res.set_language(0x0409); res.set("OriginalFilename", "glazewm.exe"); res.set("ProductName", "GlazeWM CLI"); res.set("FileDescription", "GlazeWM CLI"); let version_parts = env!("VERSION_NUMBER") .split('.') .take(3) .map(|part| part.parse().unwrap_or(0)) .collect::>(); let [major, minor, patch] = <[u16; 3]>::try_from(version_parts).unwrap_or([0, 0, 0]); let version_str = format!("{major}.{minor}.{patch}.0"); res.set("FileVersion", &version_str); res.set("ProductVersion", &version_str); let version_u64 = (u64::from(major) << 48) | (u64::from(minor) << 32) | (u64::from(patch) << 16); res.set_version_info(VersionInfo::FILEVERSION, version_u64); res.set_version_info(VersionInfo::PRODUCTVERSION, version_u64); res.compile().unwrap(); } ================================================ FILE: packages/wm-cli/src/lib.rs ================================================ #![warn(clippy::all, clippy::pedantic)] #![allow(clippy::missing_errors_doc)] use anyhow::Context; use wm_common::ClientResponseData; use wm_ipc_client::IpcClient; pub async fn start(args: Vec) -> anyhow::Result<()> { let mut client = IpcClient::connect().await?; let message = args[1..].join(" "); client .send(&message) .await .context("Failed to send command to IPC server.")?; let client_response = client .client_response(&message) .await .context("Failed to receive response from IPC server.")?; match client_response.data { // For event subscriptions, omit the initial response message and // continuously output subsequent event messages. Some(ClientResponseData::EventSubscribe(data)) => loop { let event_subscription = client .event_subscription(&data.subscription_id) .await .context("Failed to receive response from IPC server.")?; println!("{}", serde_json::to_string(&event_subscription)?); }, // For all other messages, output and exit when the first response // message is received. _ => { println!("{}", serde_json::to_string(&client_response)?); } } Ok(()) } ================================================ FILE: packages/wm-cli/src/main.rs ================================================ use std::{env, process::Command}; use anyhow::Context; use wm_cli::start; use wm_common::AppCommand; #[tokio::main] async fn main() -> anyhow::Result<()> { let args = std::env::args().collect::>(); let app_command = AppCommand::parse_with_default(&args); match app_command { AppCommand::Start { .. } => { let exe_path = env::current_exe()?; let exe_dir = exe_path .parent() .context("Failed to resolve path to the current executable.")? .to_owned(); // Main executable is either in the current directory (when running // debug/release builds) or in the parent directory when packaged. let main_path = [exe_dir.join("glazewm.exe"), exe_dir.join("../glazewm.exe")] .into_iter() .find(|path| path.exists() && *path != exe_path) .and_then(|path| path.to_str().map(ToString::to_string)) .context("Failed to resolve path to the main executable.")?; // UIAccess applications can't be started directly, so we need to use // CMD to start it. The start command is used to avoid a long-running // CMD process in the background. Command::new("cmd") .args( ["/C", "start", "", &main_path] .into_iter() .chain(args.iter().skip(1).map(String::as_str)), ) .spawn() .context("Failed to start main executable.")?; Ok(()) } _ => start(args).await, } } ================================================ FILE: packages/wm-common/Cargo.toml ================================================ [package] name = "wm-common" version = "0.0.0" edition = "2021" [lib] path = "src/lib.rs" [dependencies] anyhow = { workspace = true } clap = { workspace = true } regex = { workspace = true } serde = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } wm-platform = { path = "../wm-platform" } ================================================ FILE: packages/wm-common/src/active_drag.rs ================================================ use serde::{Deserialize, Serialize}; use wm_platform::Rect; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ActiveDrag { /// Whether the drag is a move or resize. pub operation: Option, /// Whether the drag is from a floating window. /// /// If `true`, it means we shouldn't drop the window as a tiling window /// on drag end. pub is_from_floating: bool, /// Initial position when the drag started. /// /// Used to calculate movement distance. pub initial_position: Rect, } #[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize)] pub enum ActiveDragOperation { Move, Resize, } ================================================ FILE: packages/wm-common/src/app_command.rs ================================================ use std::{iter, path::PathBuf}; use clap::{error::KindFormatter, Args, Parser, ValueEnum}; use serde::{Deserialize, Deserializer, Serialize}; use tracing::Level; use uuid::Uuid; use wm_platform::{Delta, Direction, LengthValue, OpacityValue}; use crate::TilingDirection; const VERSION: &str = env!("VERSION_NUMBER"); #[derive(Clone, Debug, Parser)] #[clap(name = "glazewm", author, version = VERSION, about, long_about = None)] pub enum AppCommand { /// Starts the window manager. Start { /// Custom path to user config file. /// /// The default path is `%userprofile%/.glzr/glazewm/config.yaml` #[clap(short = 'c', long = "config", value_hint = clap::ValueHint::FilePath)] config_path: Option, #[clap(flatten)] verbosity: Verbosity, }, /// Retrieves and outputs a specific part of the window manager's state. /// /// Requires an already running instance of the window manager. #[clap(alias = "q")] Query { #[clap(subcommand)] command: QueryCommand, }, /// Invokes a window manager command. /// /// Requires an already running instance of the window manager. #[clap(alias = "c")] Command { #[clap(long = "id")] subject_container_id: Option, #[clap(subcommand)] command: InvokeCommand, }, /// Subscribes to one or more WM events (e.g. `window_close`), and /// continuously outputs the incoming events. /// /// Requires an already running instance of the window manager. Sub { /// WM event(s) to subscribe to. #[clap(short = 'e', long, value_enum, num_args = 1..)] events: Vec, }, /// Unsubscribes from a prior event subscription. /// /// Requires an already running instance of the window manager. Unsub { /// Subscription ID to unsubscribe from. #[clap(long = "id")] subscription_id: Uuid, }, } impl AppCommand { /// Parses `AppCommand` from command line arguments. /// /// Defaults to `AppCommand::Start` if no arguments are provided. #[must_use] pub fn parse_with_default(args: &Vec) -> Self { if args.len() == 1 { AppCommand::Start { config_path: None, verbosity: Verbosity { verbose: false, quiet: false, }, } } else { AppCommand::parse_from(args) } } } /// Verbosity flags to be used with `#[command(flatten)]`. #[derive(Args, Clone, Debug)] #[clap(about = None, long_about = None)] pub struct Verbosity { /// Enables verbose logging. #[clap(short = 'v', long, action)] verbose: bool, /// Disables logging. #[clap(short = 'q', long, action, conflicts_with = "verbose")] quiet: bool, } impl Verbosity { /// Gets the log level based on the verbosity flags. #[must_use] pub fn level(&self) -> Level { match (self.verbose, self.quiet) { (true, _) => Level::DEBUG, (_, true) => Level::ERROR, _ => Level::INFO, } } } #[derive(Clone, Debug, Parser)] pub enum QueryCommand { /// Outputs metadata about the application (e.g. version number). AppMetadata, /// Outputs the active binding modes. BindingModes, /// Outputs the focused container (either a window or an empty /// workspace). Focused, /// Outputs the tiling direction of the focused container. TilingDirection, /// Outputs all monitors. Monitors, /// Outputs all windows. Windows, /// Outputs all active workspaces. Workspaces, /// Outputs whether the window manager is paused. Paused, } #[derive(Clone, Debug, PartialEq, ValueEnum)] #[clap(rename_all = "snake_case")] pub enum SubscribableEvent { All, ApplicationExiting, BindingModesChanged, FocusChanged, FocusedContainerMoved, MonitorAdded, MonitorUpdated, MonitorRemoved, TilingDirectionChanged, UserConfigChanged, WindowManaged, WindowUnmanaged, WorkspaceActivated, WorkspaceDeactivated, WorkspaceUpdated, PauseChanged, } #[derive(Clone, Debug, Parser, PartialEq, Serialize)] pub enum InvokeCommand { AdjustBorders(InvokeAdjustBordersCommand), Close, Focus(InvokeFocusCommand), Ignore, Move(InvokeMoveCommand), MoveWorkspace { #[clap(long)] direction: Direction, }, Position(InvokePositionCommand), Resize(InvokeResizeCommand), UpdateWorkspaceConfig { #[clap(long, allow_hyphen_values = true)] workspace: Option, #[clap(flatten)] new_config: InvokeUpdateWorkspaceConfig, }, SetFloating { #[clap(long, default_missing_value = "true", require_equals = true, num_args = 0..=1)] shown_on_top: Option, #[clap(long, default_missing_value = "true", require_equals = true, num_args = 0..=1)] centered: Option, #[clap(long, allow_hyphen_values = true)] x_pos: Option, #[clap(long, allow_hyphen_values = true)] y_pos: Option, #[clap(long, allow_hyphen_values = true)] width: Option, #[clap(long, allow_hyphen_values = true)] height: Option, }, SetFullscreen { #[clap(long, default_missing_value = "true", require_equals = true, num_args = 0..=1)] shown_on_top: Option, #[clap(long, default_missing_value = "true", require_equals = true, num_args = 0..=1)] maximized: Option, }, SetMinimized, SetTiling, SetTitleBarVisibility { #[clap(required = true, value_enum)] visibility: TitleBarVisibility, }, SetTransparency(SetTransparencyCommand), ShellExec { #[clap(long, action)] hide_window: bool, #[clap(required = true, trailing_var_arg = true)] command: Vec, }, // Reuse `InvokeResizeCommand` struct. Size(InvokeResizeCommand), ToggleFloating { #[clap(long, default_missing_value = "true", require_equals = true, num_args = 0..=1)] shown_on_top: Option, #[clap(long, default_missing_value = "true", require_equals = true, num_args = 0..=1)] centered: Option, }, ToggleFullscreen { #[clap(long, default_missing_value = "true", require_equals = true, num_args = 0..=1)] shown_on_top: Option, #[clap(long, default_missing_value = "true", require_equals = true, num_args = 0..=1)] maximized: Option, }, ToggleMinimized, ToggleTiling, ToggleTilingDirection, SetTilingDirection { #[clap(required = true)] tiling_direction: TilingDirection, }, WmCycleFocus { #[clap(long, default_value_t = false)] omit_floating: bool, #[clap(long, default_value_t = false)] omit_fullscreen: bool, #[clap(long, default_value_t = true)] omit_minimized: bool, #[clap(long, default_value_t = false)] omit_tiling: bool, }, WmDisableBindingMode { #[clap(long)] name: String, }, WmEnableBindingMode { #[clap(long)] name: String, }, WmExit, WmRedraw, WmReloadConfig, WmTogglePause, } impl<'de> Deserialize<'de> for InvokeCommand { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { // Clap expects an array of string slices where the first argument is // the binary name/path. When deserializing commands from the user // config, we therefore have to prepend an additional empty argument. let unparsed = String::deserialize(deserializer)?; let unparsed_split = iter::once("").chain(unparsed.split_whitespace()); InvokeCommand::try_parse_from(unparsed_split).map_err(|err| { // Format the error message and remove the "error: " prefix. let err_msg = err.apply::().to_string(); serde::de::Error::custom(err_msg.trim_start_matches("error: ")) }) } } #[derive(Clone, Debug, PartialEq, Serialize, ValueEnum)] #[clap(rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum TitleBarVisibility { Shown, Hidden, } #[derive(Args, Clone, Debug, PartialEq, Serialize)] #[group(required = true, multiple = true)] pub struct InvokeAdjustBordersCommand { #[clap(long, allow_hyphen_values = true)] pub top: Option, #[clap(long, allow_hyphen_values = true)] pub right: Option, #[clap(long, allow_hyphen_values = true)] pub bottom: Option, #[clap(long, allow_hyphen_values = true)] pub left: Option, } #[derive(Args, Clone, Debug, PartialEq, Serialize)] #[group(required = true, multiple = false)] #[allow(clippy::struct_excessive_bools)] pub struct InvokeFocusCommand { #[clap(long)] pub direction: Option, #[clap(long)] pub container_id: Option, #[clap(long)] pub workspace_in_direction: Option, #[clap(long)] pub workspace: Option, #[clap(long)] pub monitor: Option, #[clap(long)] pub next_active_workspace: bool, #[clap(long)] pub prev_active_workspace: bool, #[clap(long)] pub next_workspace: bool, #[clap(long)] pub prev_workspace: bool, #[clap(long)] pub next_active_workspace_on_monitor: bool, #[clap(long)] pub prev_active_workspace_on_monitor: bool, #[clap(long)] pub recent_workspace: bool, } #[derive(Args, Clone, Debug, PartialEq, Serialize)] #[group(required = true, multiple = false)] #[allow(clippy::struct_excessive_bools)] pub struct InvokeMoveCommand { /// Direction to move the window. #[clap(long)] pub direction: Option, /// Move window to workspace in specified direction. #[clap(long)] pub workspace_in_direction: Option, /// Name of workspace to move the window. #[clap(long)] pub workspace: Option, #[clap(long)] pub next_active_workspace: bool, #[clap(long)] pub prev_active_workspace: bool, #[clap(long)] pub next_workspace: bool, #[clap(long)] pub prev_workspace: bool, #[clap(long)] pub next_active_workspace_on_monitor: bool, #[clap(long)] pub prev_active_workspace_on_monitor: bool, #[clap(long)] pub recent_workspace: bool, } #[derive(Args, Clone, Debug, PartialEq, Serialize)] #[group(required = true, multiple = true)] pub struct InvokeResizeCommand { #[clap(long, allow_hyphen_values = true)] pub width: Option, #[clap(long, allow_hyphen_values = true)] pub height: Option, } #[derive(Args, Clone, Debug, PartialEq, Serialize)] #[group(required = true, multiple = true)] pub struct SetTransparencyCommand { #[clap(long)] pub opacity: Option, #[clap(long, allow_hyphen_values = true)] pub opacity_delta: Option>, } #[derive(Args, Clone, Debug, PartialEq, Serialize)] #[group(required = true, multiple = true)] pub struct InvokePositionCommand { #[clap(long, action)] pub centered: bool, #[clap(long, allow_hyphen_values = true)] pub x_pos: Option, #[clap(long, allow_hyphen_values = true)] pub y_pos: Option, } #[derive(Args, Clone, Debug, PartialEq, Serialize)] #[group(required = true, multiple = true)] pub struct InvokeUpdateWorkspaceConfig { #[clap(long, allow_hyphen_values = true)] pub name: Option, #[clap(long, allow_hyphen_values = true)] pub display_name: Option, #[clap(long)] pub bind_to_monitor: Option, #[clap(long)] pub keep_alive: Option, } ================================================ FILE: packages/wm-common/src/display_state.rs ================================================ use serde::{Deserialize, Serialize}; /// Represents whether something is shown, hidden, or in an intermediary /// state. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum DisplayState { Shown, Showing, Hidden, Hiding, } ================================================ FILE: packages/wm-common/src/dtos/container_dto.rs ================================================ use serde::{Deserialize, Serialize}; use super::{ MonitorDto, RootContainerDto, SplitContainerDto, WindowDto, WorkspaceDto, }; /// User-friendly representation of a container. /// /// Used for IPC and debug logging. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ContainerDto { Root(RootContainerDto), Monitor(MonitorDto), Workspace(WorkspaceDto), Split(SplitContainerDto), Window(WindowDto), } ================================================ FILE: packages/wm-common/src/dtos/mod.rs ================================================ mod container_dto; mod monitor_dto; mod root_container_dto; mod split_container_dto; mod window_dto; mod workspace_dto; pub use container_dto::*; pub use monitor_dto::*; pub use root_container_dto::*; pub use split_container_dto::*; pub use window_dto::*; pub use workspace_dto::*; ================================================ FILE: packages/wm-common/src/dtos/monitor_dto.rs ================================================ use serde::{Deserialize, Serialize}; use uuid::Uuid; use wm_platform::Rect; use super::ContainerDto; /// User-friendly representation of a monitor. /// /// Used for IPC and debug logging. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct MonitorDto { pub id: Uuid, pub parent_id: Option, pub children: Vec, pub child_focus_order: Vec, pub has_focus: bool, pub width: i32, pub height: i32, pub x: i32, pub y: i32, pub dpi: u32, pub scale_factor: f32, pub handle: Option, pub device_name: String, pub device_path: Option, pub hardware_id: Option, pub working_rect: Rect, } ================================================ FILE: packages/wm-common/src/dtos/root_container_dto.rs ================================================ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::ContainerDto; /// User-friendly representation of a root container. /// /// Used for IPC and debug logging. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct RootContainerDto { pub id: Uuid, pub parent_id: Option, pub children: Vec, pub child_focus_order: Vec, } ================================================ FILE: packages/wm-common/src/dtos/split_container_dto.rs ================================================ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::ContainerDto; use crate::TilingDirection; /// User-friendly representation of a split container. /// /// Used for IPC and debug logging. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SplitContainerDto { pub id: Uuid, pub parent_id: Option, pub children: Vec, pub child_focus_order: Vec, pub has_focus: bool, pub tiling_size: f32, pub width: i32, pub height: i32, pub x: i32, pub y: i32, pub tiling_direction: TilingDirection, } ================================================ FILE: packages/wm-common/src/dtos/window_dto.rs ================================================ use serde::{Deserialize, Serialize}; use uuid::Uuid; use wm_platform::{Rect, RectDelta}; use crate::{ActiveDrag, DisplayState, WindowState}; /// User-friendly representation of a tiling or non-tiling window. /// /// Used for IPC and debug logging. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WindowDto { pub id: Uuid, pub parent_id: Option, pub has_focus: bool, pub tiling_size: Option, pub width: i32, pub height: i32, pub x: i32, pub y: i32, pub state: WindowState, pub prev_state: Option, pub display_state: DisplayState, pub border_delta: RectDelta, pub floating_placement: Rect, pub handle: isize, pub title: String, #[cfg(target_os = "windows")] pub class_name: String, pub process_name: String, pub active_drag: Option, } ================================================ FILE: packages/wm-common/src/dtos/workspace_dto.rs ================================================ use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::ContainerDto; use crate::TilingDirection; /// User-friendly representation of a workspace. /// /// Used for IPC and debug logging. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WorkspaceDto { pub id: Uuid, pub name: String, pub display_name: Option, pub parent_id: Option, pub children: Vec, pub child_focus_order: Vec, pub has_focus: bool, pub is_displayed: bool, pub width: i32, pub height: i32, pub x: i32, pub y: i32, pub tiling_direction: TilingDirection, } ================================================ FILE: packages/wm-common/src/hide_corner.rs ================================================ /// Represents the corner of a monitor that a window should be hidden in. #[derive(Debug, Clone, Copy, PartialEq)] pub enum HideCorner { BottomLeft, BottomRight, } ================================================ FILE: packages/wm-common/src/ipc.rs ================================================ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{BindingModeConfig, ContainerDto, TilingDirection, WmEvent}; pub const DEFAULT_IPC_PORT: u32 = 6123; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "messageType", rename_all = "snake_case")] pub enum ServerMessage { ClientResponse(ClientResponseMessage), EventSubscription(EventSubscriptionMessage), } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientResponseMessage { pub client_message: String, pub data: Option, pub error: Option, pub success: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum ClientResponseData { AppMetadata(AppMetadataData), BindingModes(BindingModesData), Command(CommandData), EventSubscribe(EventSubscribeData), EventUnsubscribe, Focused(FocusedData), Monitors(MonitorsData), TilingDirection(TilingDirectionData), Windows(WindowsData), Workspaces(WorkspacesData), Paused(bool), } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AppMetadataData { pub version: String, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct BindingModesData { pub binding_modes: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CommandData { pub subject_container_id: Uuid, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EventSubscribeData { pub subscription_id: Uuid, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct FocusedData { pub focused: ContainerDto, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct MonitorsData { pub monitors: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct TilingDirectionData { pub tiling_direction: TilingDirection, pub direction_container: ContainerDto, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WindowsData { pub windows: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WorkspacesData { pub workspaces: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EventSubscriptionMessage { pub data: Option, pub error: Option, pub subscription_id: Uuid, pub success: bool, } ================================================ FILE: packages/wm-common/src/lib.rs ================================================ #![warn(clippy::all, clippy::pedantic)] #![allow(clippy::missing_errors_doc)] mod active_drag; mod app_command; mod display_state; mod dtos; mod hide_corner; mod ipc; mod parsed_config; mod tiling_direction; mod utils; mod window_state; mod wm_event; pub use active_drag::*; pub use app_command::*; pub use display_state::*; pub use dtos::*; pub use hide_corner::*; pub use ipc::*; pub use parsed_config::*; pub use tiling_direction::*; pub use utils::*; pub use window_state::*; pub use wm_event::*; ================================================ FILE: packages/wm-common/src/parsed_config.rs ================================================ use serde::{Deserialize, Serialize}; use wm_platform::{ Color, CornerStyle, Key, Keybinding, LengthValue, OpacityValue, RectDelta, }; use crate::app_command::InvokeCommand; #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct ParsedConfig { pub binding_modes: Vec, pub gaps: GapsConfig, pub general: GeneralConfig, pub keybindings: Vec, pub window_behavior: WindowBehaviorConfig, pub window_effects: WindowEffectsConfig, pub window_rules: Vec, pub workspaces: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all(serialize = "camelCase"))] pub struct BindingModeConfig { /// Name of the binding mode. pub name: String, /// Display name of the binding mode. #[serde(default)] pub display_name: Option, /// Keybindings that will be active when the binding mode is active. #[serde(default)] pub keybindings: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct GapsConfig { /// Whether to scale the gaps with the DPI of the monitor. pub scale_with_dpi: bool, /// Gap between adjacent windows. pub inner_gap: LengthValue, /// Gap between windows and the screen edge. pub outer_gap: RectDelta, /// Gap between window and the screen edge if there is only one window /// in the workspace pub single_window_outer_gap: Option, } impl Default for GapsConfig { fn default() -> Self { GapsConfig { scale_with_dpi: true, inner_gap: LengthValue::from_px(0), outer_gap: RectDelta::new( LengthValue::from_px(0), LengthValue::from_px(0), LengthValue::from_px(0), LengthValue::from_px(0), ), single_window_outer_gap: None, } } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct GeneralConfig { /// Config for automatically moving the cursor. pub cursor_jump: CursorJumpConfig, /// Whether to automatically focus windows underneath the cursor. pub focus_follows_cursor: bool, /// Whether to switch back and forth between the previously focused /// workspace when focusing the current workspace. pub toggle_workspace_on_refocus: bool, /// Commands to run when the WM has started (e.g. to run a script or /// launch another application). pub startup_commands: Vec, /// Commands to run just before the WM is shutdown. pub shutdown_commands: Vec, /// Commands to run after the WM config has reloaded. pub config_reload_commands: Vec, /// How windows should be hidden when switching workspaces. #[serde(deserialize_with = "deserialize_hide_method")] pub hide_method: HideMethod, /// Affects which windows get shown in the native Windows taskbar. pub show_all_in_taskbar: bool, } impl Default for GeneralConfig { fn default() -> Self { GeneralConfig { cursor_jump: CursorJumpConfig::default(), focus_follows_cursor: false, toggle_workspace_on_refocus: true, startup_commands: vec![], shutdown_commands: vec![], config_reload_commands: vec![], hide_method: { #[cfg(target_os = "macos")] { HideMethod::PlaceInCorner } #[cfg(not(target_os = "macos"))] { HideMethod::Cloak } }, show_all_in_taskbar: false, } } } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct CursorJumpConfig { /// Whether to automatically move the cursor on the specified trigger. pub enabled: bool, /// Trigger for cursor jump. pub trigger: CursorJumpTrigger, } #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum CursorJumpTrigger { #[default] MonitorFocus, WindowFocus, } #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum HideMethod { Hide, #[default] Cloak, PlaceInCorner, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct KeybindingConfig { /// Keyboard shortcut to trigger the keybinding. #[serde( deserialize_with = "deserialize_bindings", serialize_with = "serialize_bindings" )] pub bindings: Vec, /// WM commands to run when the keybinding is triggered. pub commands: Vec, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct WindowBehaviorConfig { /// New windows are created in this state whenever possible. pub initial_state: InitialWindowState, /// Sets the default options for when a new window is created. This also /// changes the defaults for when the state change commands, like /// `set_floating`, are used without any flags. pub state_defaults: WindowStateDefaultsConfig, } #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum InitialWindowState { #[default] Tiling, Floating, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct WindowStateDefaultsConfig { pub floating: FloatingStateConfig, pub fullscreen: FullscreenStateConfig, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct FloatingStateConfig { /// Whether to center new floating windows. pub centered: bool, /// Whether to show floating windows as always on top. pub shown_on_top: bool, } impl Default for FloatingStateConfig { fn default() -> Self { FloatingStateConfig { centered: true, shown_on_top: false, } } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct FullscreenStateConfig { /// Whether to prefer fullscreen windows to be maximized. pub maximized: bool, /// Whether to show fullscreen windows as always on top. pub shown_on_top: bool, } impl Default for FullscreenStateConfig { fn default() -> Self { FullscreenStateConfig { maximized: true, shown_on_top: false, } } } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct WindowEffectsConfig { /// Visual effects to apply to the focused window. pub focused_window: WindowEffectConfig, /// Visual effects to apply to non-focused windows. pub other_windows: WindowEffectConfig, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct WindowEffectConfig { /// Config for optionally applying a colored border. pub border: BorderEffectConfig, /// Config for optionally hiding the title bar. pub hide_title_bar: HideTitleBarEffectConfig, /// Config for optionally changing the corner style. pub corner_style: CornerEffectConfig, /// Config for optionally applying transparency. pub transparency: TransparencyEffectConfig, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct BorderEffectConfig { /// Whether to enable the effect. pub enabled: bool, /// Color of the window border. pub color: Color, } impl Default for BorderEffectConfig { fn default() -> Self { BorderEffectConfig { enabled: false, color: Color { r: 140, g: 190, b: 255, a: 255, }, } } } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct HideTitleBarEffectConfig { /// Whether to enable the effect. pub enabled: bool, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct CornerEffectConfig { /// Whether to enable the effect. pub enabled: bool, /// Style of the window corners. pub style: CornerStyle, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct TransparencyEffectConfig { /// Whether to enable the effect. pub enabled: bool, /// The opacity to apply. pub opacity: OpacityValue, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all(serialize = "camelCase"))] pub struct WindowRuleConfig { pub commands: Vec, #[serde(rename = "match")] pub match_window: Vec, #[serde(default = "default_window_rule_on")] pub on: Vec, #[serde(default = "default_bool::")] pub run_once: bool, } #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(default, rename_all(serialize = "camelCase"))] pub struct WindowMatchConfig { pub window_process: Option, pub window_class: Option, pub window_title: Option, } /// Due to limitations in `serde_yaml`, we need to use an untagged enum /// instead of a regular enum for serialization. Using a regular enum /// causes issues with flow-style objects in YAML. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(untagged)] pub enum MatchType { Equals { equals: String }, Includes { includes: String }, Regex { regex: String }, NotEquals { not_equals: String }, NotRegex { not_regex: String }, } impl MatchType { /// Whether the given value is a match for the match type. #[must_use] pub fn is_match(&self, value: &str) -> bool { match self { MatchType::Equals { equals } => value == equals, MatchType::Includes { includes } => value.contains(includes), MatchType::Regex { regex } => { regex::Regex::new(regex).is_ok_and(|re| re.is_match(value)) } MatchType::NotEquals { not_equals } => value != not_equals, MatchType::NotRegex { not_regex } => { regex::Regex::new(not_regex).is_ok_and(|re| !re.is_match(value)) } } } } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum WindowRuleEvent { /// When a window receives native focus. Focus, /// When a window is initially managed. Manage, /// When the title of a window changes. TitleChange, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all(serialize = "camelCase"))] pub struct WorkspaceConfig { pub name: String, #[serde(default)] pub display_name: Option, #[serde(default)] pub bind_to_monitor: Option, #[serde(default = "default_bool::")] pub keep_alive: bool, } /// Helper function for setting a default value for a boolean field. const fn default_bool() -> bool { V } /// Helper function for setting a default value for window rule events. fn default_window_rule_on() -> Vec { vec![WindowRuleEvent::Manage, WindowRuleEvent::TitleChange] } /// Helper function for serializing a vector of keybindings. /// /// Returns a vector of strings (e.g. `["cmd+shift+a", "ctrl+shift+b"]`). fn serialize_bindings( bindings: &[Keybinding], serializer: S, ) -> Result where S: serde::Serializer, { let binding_strings: Vec = bindings .iter() .map(|binding| { binding .keys() .iter() .map(|key| key.to_string().to_lowercase()) .collect::>() .join("+") }) .collect(); binding_strings.serialize(serializer) } /// Helper function for deserializing a vector of strings into keybindings. /// /// Returns a vector of [`Keybinding`]. fn deserialize_bindings<'de, D>( deserializer: D, ) -> Result, D::Error> where D: serde::de::Deserializer<'de>, { let s: Vec<&str> = serde::de::Deserialize::deserialize(deserializer)?; s.iter() .map(|keybinding_str| { let keys: Vec = keybinding_str .split('+') .map(|key| { key.trim().parse().or_else(|_| Key::try_from_literal(key)) }) .collect::, _>>() .map_err(serde::de::Error::custom)?; Keybinding::new(keys).map_err(serde::de::Error::custom) }) .collect() } /// Helper function for deserializing [`HideMethod`]. /// /// On macOS, [`HideMethod::Hide`] and [`HideMethod::Cloak`] are not valid /// and are automatically converted to [`HideMethod::PlaceInCorner`]. fn deserialize_hide_method<'de, D>( deserializer: D, ) -> Result where D: serde::de::Deserializer<'de>, { // LINT: The deserialized value is ignored on macOS, but we still want // to produce an error for invalid values. #[allow(unused_variables)] let method = HideMethod::deserialize(deserializer)?; #[cfg(target_os = "macos")] { Ok(HideMethod::PlaceInCorner) } #[cfg(not(target_os = "macos"))] { Ok(method) } } ================================================ FILE: packages/wm-common/src/tiling_direction.rs ================================================ use std::str::FromStr; use anyhow::bail; use serde::{Deserialize, Serialize}; use wm_platform::Direction; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum TilingDirection { Horizontal, Vertical, } impl TilingDirection { /// Gets the inverse of a given tiling direction. /// /// Example: /// ``` /// # use wm_platform::TilingDirection; /// let dir = TilingDirection::Horizontal.inverse(); /// assert_eq!(dir, TilingDirection::Vertical); /// ``` #[must_use] pub fn inverse(&self) -> Self { match self { Self::Horizontal => Self::Vertical, Self::Vertical => Self::Horizontal, } } /// Gets the tiling direction that is needed when moving or shifting /// focus in a given direction. /// /// Example: /// ``` /// # use wm_platform::{Direction, TilingDirection}; /// let dir = TilingDirection::from_direction(&Direction::Left); /// assert_eq!(dir, TilingDirection::Horizontal); /// ``` #[must_use] pub fn from_direction(direction: &Direction) -> Self { match direction { Direction::Left | Direction::Right => Self::Horizontal, Direction::Up | Direction::Down => Self::Vertical, } } } impl FromStr for TilingDirection { type Err = anyhow::Error; /// Parses a string into a tiling direction. /// /// Example: /// ``` /// # use wm_platform::TilingDirection; /// # use std::str::FromStr; /// let dir = TilingDirection::from_str("horizontal"); /// assert_eq!(dir.unwrap(), TilingDirection::Horizontal); /// /// let dir = TilingDirection::from_str("vertical"); /// assert_eq!(dir.unwrap(), TilingDirection::Vertical); /// ``` fn from_str(unparsed: &str) -> anyhow::Result { match unparsed { "horizontal" => Ok(Self::Horizontal), "vertical" => Ok(Self::Vertical), _ => bail!("Not a valid tiling direction: {}", unparsed), } } } ================================================ FILE: packages/wm-common/src/utils/iterator_ext.rs ================================================ use std::{collections::HashSet, hash::Hash}; /// Extension trait for iterators. pub trait UniqueExt: Iterator { /// Returns an iterator that yields unique elements based on the key /// function. /// /// The key function must return a value that implements `Hash` and `Eq`. fn unique_by(self, key_fn: F) -> UniqueBy where Self: Sized, K: Hash + Eq, F: FnMut(&Self::Item) -> K; } pub struct UniqueBy { iter: I, key_fn: F, seen: HashSet, } // Implement the `Iterator` trait for the `UniqueBy` struct. impl Iterator for UniqueBy where I: Iterator, K: Hash + Eq, F: FnMut(&I::Item) -> K, { type Item = I::Item; fn next(&mut self) -> Option { for item in self.iter.by_ref() { let key = (self.key_fn)(&item); if self.seen.insert(key) { return Some(item); } } None } } // Implement the extension trait for all iterators. impl UniqueExt for I { fn unique_by(self, key_fn: F) -> UniqueBy where Self: Sized, K: Hash + Eq, F: FnMut(&Self::Item) -> K, { UniqueBy { iter: self, key_fn, seen: HashSet::new(), } } } ================================================ FILE: packages/wm-common/src/utils/mod.rs ================================================ mod iterator_ext; mod try_warn; mod vec_deque_ext; pub use iterator_ext::*; pub use vec_deque_ext::*; ================================================ FILE: packages/wm-common/src/utils/try_warn.rs ================================================ /// Utility macro that logs a warning and returns early if the given /// expression is an error. #[macro_export] macro_rules! try_warn { ($expr:expr) => { match $expr { Ok(val) => val, Err(err) => { tracing::warn!("Operation failed: {:?}", err); return Ok(()); } } }; } ================================================ FILE: packages/wm-common/src/utils/vec_deque_ext.rs ================================================ use std::collections::VecDeque; pub trait VecDequeExt where T: PartialEq, { /// Shifts a value to a specified index in a `VecDeque`. /// /// Inserts at index if value doesn't already exist in the `VecDeque`. fn shift_to_index(&mut self, target_index: usize, item: T); } impl VecDequeExt for VecDeque where T: PartialEq, { fn shift_to_index(&mut self, target_index: usize, value: T) { if let Some(index) = self.iter().position(|e| e == &value) { self.remove(index); // Adjust for when the target index becomes out of bounds because of // the removal above. self.insert(target_index.clamp(0, self.len()), value); } } } ================================================ FILE: packages/wm-common/src/window_state.rs ================================================ use serde::{Deserialize, Serialize}; use crate::{ parsed_config::{ FloatingStateConfig, FullscreenStateConfig, InitialWindowState, }, ParsedConfig, }; /// Represents the possible states a window can have. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum WindowState { Floating(FloatingStateConfig), Fullscreen(FullscreenStateConfig), Minimized, Tiling, } impl WindowState { #[must_use] pub fn default_from_config(config: &ParsedConfig) -> Self { match config.window_behavior.initial_state { InitialWindowState::Tiling => Self::Tiling, InitialWindowState::Floating => Self::Floating( config.window_behavior.state_defaults.floating.clone(), ), } } #[must_use] pub fn is_same_state(&self, other: &Self) -> bool { std::mem::discriminant(self) == std::mem::discriminant(other) } } ================================================ FILE: packages/wm-common/src/wm_event.rs ================================================ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ dtos::ContainerDto, parsed_config::{BindingModeConfig, ParsedConfig}, TilingDirection, }; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde( tag = "eventType", rename_all = "snake_case", rename_all_fields = "camelCase" )] pub enum WmEvent { ApplicationExiting, BindingModesChanged { new_binding_modes: Vec, }, FocusChanged { focused_container: ContainerDto, }, FocusedContainerMoved { focused_container: ContainerDto, }, MonitorAdded { added_monitor: ContainerDto, }, MonitorRemoved { removed_id: Uuid, removed_device_name: String, }, MonitorUpdated { updated_monitor: ContainerDto, }, TilingDirectionChanged { direction_container: ContainerDto, new_tiling_direction: TilingDirection, }, UserConfigChanged { config_path: String, config_string: String, parsed_config: ParsedConfig, }, WindowManaged { managed_window: ContainerDto, }, WindowUnmanaged { unmanaged_id: Uuid, unmanaged_handle: isize, }, WorkspaceActivated { activated_workspace: ContainerDto, }, WorkspaceDeactivated { deactivated_id: Uuid, deactivated_name: String, }, WorkspaceUpdated { updated_workspace: ContainerDto, }, PauseChanged { is_paused: bool, }, } ================================================ FILE: packages/wm-ipc-client/Cargo.toml ================================================ [package] name = "wm-ipc-client" version = "0.0.0" edition = "2021" [lib] path = "src/lib.rs" [dependencies] anyhow = { workspace = true } futures-util = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tokio-tungstenite = { workspace = true } uuid = { workspace = true } wm-common = { path = "../wm-common" } ================================================ FILE: packages/wm-ipc-client/src/lib.rs ================================================ #![allow(clippy::missing_errors_doc)] use anyhow::Context; use futures_util::{SinkExt, StreamExt}; use tokio::net::TcpStream; use tokio_tungstenite::{ connect_async, tungstenite::Message, MaybeTlsStream, WebSocketStream, }; use uuid::Uuid; use wm_common::{ ClientResponseMessage, EventSubscriptionMessage, ServerMessage, DEFAULT_IPC_PORT, }; pub struct IpcClient { stream: WebSocketStream>, } impl IpcClient { pub async fn connect() -> anyhow::Result { let server_addr = format!("ws://127.0.0.1:{DEFAULT_IPC_PORT}"); let (stream, _) = connect_async(server_addr) .await .context("Failed to connect to IPC server.")?; Ok(Self { stream }) } /// Sends a message to the IPC server. pub async fn send(&mut self, message: &str) -> anyhow::Result<()> { self .stream .send(Message::Text(message.into())) .await .context("Failed to send command.")?; Ok(()) } /// Waits and returns the next reply from the IPC server. pub async fn next_message(&mut self) -> anyhow::Result { let response = self .stream .next() .await .context("Failed to receive response.")? .context("Invalid response message.")?; let json_response = serde_json::from_str::(response.to_text()?)?; Ok(json_response) } pub async fn client_response( &mut self, client_message: &str, ) -> Option { while let Ok(response) = self.next_message().await { if let ServerMessage::ClientResponse(client_response) = response { if client_response.client_message == client_message { return Some(client_response); } } } None } pub async fn event_subscription( &mut self, subscription_id: &Uuid, ) -> Option { while let Ok(response) = self.next_message().await { if let ServerMessage::EventSubscription(event_sub) = response { if &event_sub.subscription_id == subscription_id { return Some(event_sub); } } } None } } ================================================ FILE: packages/wm-macros/Cargo.toml ================================================ [package] name = "wm-macros" version = "0.1.0" edition = "2024" [lib] proc-macro = true [dependencies] syn = "2.0.103" quote = "1.0" proc-macro2 = "1.0" ================================================ FILE: packages/wm-macros/src/common/attributes.rs ================================================ //! Utilities for working with [syn::Attribute] pub mod prelude { pub use super::FindAttributes; } /// Trait for filtering lists of [syn::Attribute] by name and type. pub trait FindAttributes { /// Find attributes by name. E.g. `#[name]`, `#[name(...)]` or `#[name = /// ]` fn find_attrs( &self, name: &str, ) -> impl Iterator; /// Find list attributes by name. E.g. `#[name(...)]` fn find_list_attrs( &self, name: &str, ) -> impl Iterator { self .find_attrs(name) .filter_map(|attr| attr.meta.require_list().ok()) } } impl FindAttributes for Vec { fn find_attrs( &self, name: &str, ) -> impl Iterator { self.iter().filter(move |attr| { attr.path().get_ident().is_some_and(|ident| ident == name) }) } } impl FindAttributes for &[syn::Attribute] { fn find_attrs( &self, name: &str, ) -> impl Iterator { self.iter().filter(move |attr| { attr.path().get_ident().is_some_and(|ident| ident == name) }) } } ================================================ FILE: packages/wm-macros/src/common/branch.rs ================================================ //! Types and traits for branching and combining parsing operations. //! Implements functionality similar to `nom`s alternatives and //! combinators, but for `syn::parse::ParseStream`. use crate::prelude::*; /// Trait for tuples where all items can be parsed from a /// parse stream. pub trait ParseableTuple where Self: Sized, { type FirstItem: syn::parse::Parse; /// Parses all items in the tuple `T` from the stream in the order they /// appear in the tuple, and parses `Sep` in between each item. Returns /// all parsed items in a tuple, or the first error to occur. /// /// Do not use this directly, use the [Ordered] type instead, fn parse_tuple( stream: syn::parse::ParseStream, ) -> syn::Result where Sep: syn::parse::Parse; } /// Trait for tuples where all items can be peeked and parsed from a parse /// stream. // Used with [Unordered] #[allow(dead_code)] pub trait PeekableTuple where Self: Sized, { /// Iterates until all items in the tuple `T` have been parsed, or an /// error occurs. Parsing is attempted in the order of the items in /// the tuple, although if an item is not found, it may be skipped and /// reattempted for the next item(s). /// /// Do not use this directly, use the [Unordered] type instead. fn peek_parse_tuple( stream: syn::parse::ParseStream, ) -> syn::Result where Sep: syn::parse::Parse + crate::common::peekable::Peekable; } macro_rules! replace_expr { ($idc:expr, $sub:expr) => { $sub }; } macro_rules! get_first_item { ($first:tt, $($types:tt),+) => { $first }; } macro_rules! impl_for_tuple { ($($types:tt),+ | $($numbers:tt),+) => { // Generic ensures that all types in the tuple implement `syn::parse::Parse`. impl<$($types),+> ParseableTuple for ($($types,)+) where $($types : syn::parse::Parse),+ { type FirstItem = get_first_item!($($types),+); fn parse_tuple(stream: syn::parse::ParseStream) -> syn::Result where Sep: syn::parse::Parse { $( // Also check if the stream is empty to allow for missing trailing Optional items if $numbers != 0 && !stream.is_empty() { stream.parse::()?; } #[allow(non_snake_case)] let $types = stream.parse::<$types>()?; )+ // Pack the parsed items into a tuple Ok(($($types),+)) } } // Generic ensures that all types in the tuple implement `syn::parse::Parse` and `Peekable`. impl<$($types),+> PeekableTuple for ($($types,)+) where $($types : syn::parse::Parse + crate::common::peekable::Peekable),+{ fn peek_parse_tuple(stream: syn::parse::ParseStream) -> syn::Result where Sep: syn::parse::Parse + crate::common::peekable::Peekable { use crate::common::peekable::prelude::*; // Creates a tuple with the same number of items as the tuple, but with each item being // `None`. let mut output: ($(Option<$types>,)+) = ($(replace_expr!($types, None),)+); // Iterate while any of the items in the tuple are `None`. while $(output.$numbers.is_none())||+ { let lookahead = stream.lookahead1(); $( if output.$numbers.is_none() && lookahead.tpeek::<$types>() { output.$numbers = Some(stream.parse::<$types>()?); } // Insert an else before the next item in the tuple, to create `else if` on subsequent // unpacking )else+ else { return Err(lookahead.error()); } // If we havn't yet parsed all items in the tuple, parse the separator. // Also check if the stream is empty, which allows missing Optional items to be skipped // TODO: Better handling of [Optional] items, so that we can handle them without needing // the stream to end. if !stream.is_empty() && $(output.$numbers.is_none())||+ { stream.parse::()?; } } Ok(($( // Saftey, the output is guaranteed to have all items otherwise the loop would have errored // out. output.$numbers.unwrap(), )+)) } } }; } // Implement the `ParseableTuple` and `PeekableTuple` traits for tuples of // different sizes. impl_for_tuple!(T, U | 0, 1); impl_for_tuple!(T, U, V | 0, 1, 2); impl_for_tuple!(T, U, V, W | 0, 1, 2, 3); impl_for_tuple!(T, U, V, W, X | 0, 1, 2, 3, 4); impl_for_tuple!(T, U, V, W, X, Y | 0, 1, 2, 3, 4, 5); /// Type wrapper to parse all items in tuple `T` in order, using /// `Sep` as the separator between items. /// /// # Example /// Parse [syn::Ident] and [syn::LitStr] from the stream, which are /// separated by a comma. E.g. `some_name, "some string"`. If the order is /// reversed, it will fail to parse. /// ``` /// fn example(stream: syn::parse::ParseStream) -> syn::Result<()> { /// type T = (syn::Ident, syn::LitStr); /// /// stream.parse::>()?; /// } /// /// fn main() { /// # use quote::quote; /// let tokens = quote! { some_name, "some string" }.into(); /// let error = quote! { "some string", some_name }.into(); /// /// assert!(example(tokens).is_ok()); /// assert!(example(error).is_err()); /// } /// ``` pub struct Ordered where T: ParseableTuple, Sep: syn::parse::Parse, { pub items: T, sep: std::marker::PhantomData, } impl syn::parse::Parse for Ordered where T: ParseableTuple, Sep: syn::parse::Parse, { fn parse(input: syn::parse::ParseStream) -> syn::Result { let output = T::parse_tuple::(input)? as T; Ok(Self { items: output, sep: std::marker::PhantomData, }) } } /// Implement [Peekable] for [Ordered] if the first item in the /// tuple is peekable, by forwarding the implementation to the first item. impl crate::common::peekable::Peekable for Ordered where T: ParseableTuple, Sep: syn::parse::Parse, FirstItem: crate::common::peekable::Peekable, { fn display() -> &'static str { FirstItem::display() } fn peek(stream: S) -> bool where S: crate::common::peekable::PeekableStream, { FirstItem::peek(stream) } } impl core::ops::Deref for Ordered where T: ParseableTuple, Sep: syn::parse::Parse, { type Target = T; fn deref(&self) -> &Self::Target { &self.items } } /// Type wrapper to parse all items in tuple `T` in any order, using `Sep` /// as the separator between items. /// /// # Example /// Parse [syn::Ident] and [syn::LitStr] from the stream in any order, /// which are separated by a comma. E.g. `some_name, "some string"` or /// `"some string", some_name`. /// ``` /// fn example(stream: proc_macro::TokenStream) -> syn::Result<(syn::Ident, syn::LitStr)> { /// type T = (syn::Ident, syn::LitStr); /// /// stream.parse2::>().map(|Unordered(t, _)| t) /// } /// /// fn main() { /// # use quote::quote; /// let normal = quote! { some_name, "some string" }.into(); /// let reversed = quote! { "some string", some_name }.into(); /// let error = quote! {some_name, other_name}.into(); /// /// assert!(example(normal).is_ok()); /// assert!(example(reversed).is_ok()); /// assert!(example(error).is_err()); /// } /// ``` // Util function that will be used in future. #[allow(dead_code)] pub struct Unordered where T: PeekableTuple, Sep: syn::parse::Parse + crate::common::peekable::Peekable, { pub items: T, sep: std::marker::PhantomData, } impl syn::parse::Parse for Unordered where T: PeekableTuple, Sep: syn::parse::Parse + crate::common::peekable::Peekable, { fn parse(input: syn::parse::ParseStream) -> syn::Result { let output = T::peek_parse_tuple::(input)? as T; Ok(Self { items: output, sep: std::marker::PhantomData, }) } } impl core::ops::Deref for Unordered where T: PeekableTuple, Sep: syn::parse::Parse + crate::common::peekable::Peekable, { type Target = T; fn deref(&self) -> &Self::Target { &self.items } } /// Type wrapper to parse `If` if it is present, otherwise parse `Else`. /// `If` must be peekable so it can be checked if it is present before /// parsing. /// /// # Example /// Parse [syn::Ident] if it is present, otherwise parse [syn::LitStr]. /// ``` /// type IfElseType = IfElse; /// /// fn example(stream: syn::parse::ParseStream) -> syn::Result { /// stream.parse::() /// } /// /// fn main() { /// # use quote::quote; /// let if_tokens = quote! { some_name }.into(); /// let else_tokens = quote! { "some string" }.into(); /// /// assert!(example(if_tokens).is_ok_and(|if_else| if_else.is_if())); /// assert!(example(else_tokens).is_ok_and(|if_else| if_else.is_else())); /// } pub enum IfElse where If: syn::parse::Parse + crate::common::peekable::Peekable, Else: syn::parse::Parse, { If(If), Else(Else), } // Methods are used in doc tests, but linter doesn't pick that up. #[allow(dead_code)] impl IfElse where If: syn::parse::Parse + crate::common::peekable::Peekable, Else: syn::parse::Parse, { pub fn is_if(&self) -> bool { matches!(self, Self::If(_)) } pub fn is_else(&self) -> bool { matches!(self, Self::Else(_)) } } impl syn::parse::Parse for IfElse where If: syn::parse::Parse + crate::common::peekable::Peekable, Else: syn::parse::Parse, { fn parse(input: syn::parse::ParseStream) -> syn::Result { if input.tpeek::() { Ok(Self::If(input.parse()?)) } else { Ok(Self::Else(input.parse()?)) } } } /// Type wrapper to parse `T` only if it is present, otherwise returns /// None. /// /// To be used in combination with [Ordered] then all optional items must /// be last in the tuple, and the stream must end to indicate that the /// optional items will not be present. /// /// To be used in combination with [Unordered] the stream must end to /// indicate that the [Optional]s will not be present. /// /// # Example /// Parse [syn::Ident] if it is present, otherwise return None. /// ``` /// type OptionalType = Optional; /// fn example(stream: syn::parse::ParseStream) -> syn::Result { /// stream.parse::() /// } /// /// fn main() { /// # use quote::quote; /// let present = quote! { some_name }.into(); /// let not_present = quote! {}.into(); /// let other_present = quote! { "some_string" }.into(); /// /// assert!(example(present).is_ok_and(|opt| opt.is_some())); /// assert!(example(not_present).is_ok_and(|opt| opt.is_none())); /// assert!(example(other_present).is_ok_and(|opt| opt.is_none())); /// } /// ``` /// Used in combination with [Ordered] to parse a [syn::Ident] and /// optionally a [syn::LitStr]: /// ``` /// type OrderedOptionalType = Ordered<(syn::Ident, Optional), /// syn::Token![,]>; /// /// fn example(stream: syn::parse::ParseStream) -> /// syn::Result { /// stream.parse::() /// } /// /// fn main() { /// let present = quote! { some_name, "some string" }.into(); /// let missing = quote! { some_name }.into(); /// // Will error since the stream did not end after the Ordered to indicate no optionals. /// let error = quote! { some_name, not_a_string }.into(); /// /// assert!(example(present).is_ok_and(|Ordered { items: (ident, string), ..}| string.is_some())); /// assert!(example(missing).is_ok_and(|Ordered { items: (ident, string), ..}| string.is_none())); /// assert!(example(error).is_err()); /// } /// ``` /// Used in combination with [Unordered] it can be used to parse a /// [syn::Ident] and optionally a [syn::LitStr] in any order: /// ``` /// type UnorderedOptionalType = Unordered<(syn::Ident, Optional), syn::Token![,]>; /// /// fn example(stream: syn::parse::ParseStream) -> syn::Result { /// stream.parse::() /// } /// /// fn main() { /// let present = quote! { some_name, "some string" }.into(); /// let not_present = quote! { some_name }.into(); /// let backwards = quote! { "some string", some_name }.into(); /// // Will error since the stream did not end after the Unordered to indicate no optionals. /// let error = quote! { some_name, not_a_string }.into(); /// /// assert!(example(present).is_ok_and(|Unordered { items: (ident, string), ..}| string.is_some())); /// assert!(example(not_present).is_ok_and(|Unordered { items: (ident, string), ..}| string.is_none())); /// assert!(example(backwards).is_ok_and(|Unordered { items: (ident, string), ..}| string.is_some())); /// assert!(example(error).is_err()); /// } /// ``` pub enum Optional where T: syn::parse::Parse + crate::common::peekable::Peekable, { Some(T), None, } impl syn::parse::Parse for Optional where T: syn::parse::Parse + crate::common::peekable::Peekable, { fn parse(input: syn::parse::ParseStream) -> syn::Result { if input.is_empty() { return Ok(Self::None); } if input.tpeek::() { Ok(Self::Some(input.parse()?)) } else { Ok(Self::None) } } } impl crate::common::peekable::Peekable for Optional where T: syn::parse::Parse + crate::common::peekable::Peekable, { fn display() -> &'static str { T::display() } fn peek(stream: S) -> bool where S: crate::common::peekable::PeekableStream, { if stream.is_empty() { return true; } T::peek(stream) } } // Methods are used in doc tests, but linter doesn't pick that up. #[allow(dead_code)] impl Optional where T: syn::parse::Parse + crate::common::peekable::Peekable, { pub fn is_some(&self) -> bool { matches!(self, Self::Some(_)) } pub fn is_none(&self) -> bool { matches!(self, Self::None) } #[allow(clippy::wrong_self_convention)] pub fn to_opt(self) -> Option { match self { Self::Some(value) => Some(value), Self::None => None, } } } ================================================ FILE: packages/wm-macros/src/common/error_handling.rs ================================================ //! Utilities for simplifying error handling and diagnostics. pub mod prelude { // ToSpanError is not used yet, but will be used in the future. // TODO: Remove unused_imports allow #[allow(unused_imports)] pub use super::{EmitError, ThenError, ToError, ToSpanError}; } /// Extends the `bool` type with a method that returns an error if the /// value is `true`. pub trait ThenError where Self: Sized, { /// Returns `Err(error)` if the value is `true`, otherwise returns /// `Ok(self)`. /// /// # Example /// ``` /// # fn example(string: &str, string_span: syn::Span) -> syn::Result<()> { /// string.is_empty().then_error(string_span.error("Expected a non-empty string"))?; /// # } /// ``` fn then_error(self, error: E) -> Result; } impl ThenError for bool { fn then_error(self, error: E) -> Result { if self { Err(error) } else { Ok(self) } } } /// Extension trait for any type that can be tokenized to create a /// [syn::Error] at its location. pub trait ToError { /// Create a [syn::Error] at the span of this object with the given /// message. /// /// # Example /// ``` /// # fn example(ident: syn::Ident) -> syn::Result<()> { /// return Err(ident.error("Didn't expect an identifier here")); /// # } /// ``` fn error(&self, message: D) -> syn::Error; } impl ToError for T where T: quote::ToTokens, { fn error(&self, message: D) -> syn::Error { syn::Error::new_spanned(self, message) } } // Very likely to be used in future. // TODO: Remove dead code allow #[allow(dead_code)] /// Extension trait for any [syn::spanned::Spanned] type that creates a /// [syn::Error] at its location. Use [ToError] where possible, as it /// creates more accurately spanned errors. pub trait ToSpanError { /// Creates a [syn::Error] at the location of this span with the given /// message. /// /// If the object can be tokenized, prefer using [ToError] instead, as it /// gives more accurately spanned errors. /// /// # Example /// ``` /// # fn example(stream: syn::parse::ParseStream) -> syn::Result<()> { /// return Err(stream.span().serror("Expected ...")); /// # } /// ``` fn serror(&self, message: D) -> syn::Error; } impl ToSpanError for T where T: syn::spanned::Spanned, { fn serror(&self, message: D) -> syn::Error { syn::Error::new(self.span(), message) } } // Very likely to be used in future. // TODO: Remove dead code allow #[allow(dead_code)] pub trait EmitError { /// Directly emits a warning message at the span of this object. fn emit_warning>(&self, message: D); /// Emits a help message at the span of this object. fn emit_help>(&self, message: D); /// Emits a note message at the span of this object. fn emit_note>(&self, message: D); } impl EmitError for T where T: syn::spanned::Spanned, { fn emit_warning>(&self, message: D) { self.span().unwrap().warning(message).emit(); } fn emit_help>(&self, message: D) { self.span().unwrap().help(message).emit(); } fn emit_note>(&self, message: D) { self.span().unwrap().note(message).emit(); } } ================================================ FILE: packages/wm-macros/src/common/mod.rs ================================================ /// Utilities for parsing attributes pub mod attributes; /// Utilities for parsing alternatives and combinators pub mod branch; /// Extension traits and utilities to make error handling more succinct pub mod error_handling; /// Utilities for parsing a named parameter (`name = value`) pub mod named_parameter; /// Type for a parsable type within parentheses pub mod parenthesized; /// Trait to get a `Peek` object from compatible syn types pub mod peekable; /// An owned version of a `syn::LitStr` pub mod spanned_string; pub(crate) use peekable::custom_keyword; ================================================ FILE: packages/wm-macros/src/common/named_parameter.rs ================================================ //! Types for parsing `name = value` patterns. /// Type to represent a `name = value` pair, where `name` can be peeked and /// both `name` and `value` can be parsed. /// /// # Example /// Parse a name-value pair of [syn::Ident] and [syn::LitStr]. /// ``` /// type NamedParam = NamedParameter; /// /// fn example(stream: syn::parse::ParseStream) -> syn::Result { /// stream.parse::() /// } /// /// fn main() { /// # use quote::quote; /// let tokens = quote! { some_name = "some_value" }.into(); /// /// assert!(example(tokens).is_ok()); /// } /// ``` // Will be used in future. #[allow(dead_code)] pub struct NamedParameter where Name: syn::parse::Parse + crate::common::peekable::Peekable, Param: syn::parse::Parse, { pub name: Name, pub param: Param, } impl syn::parse::Parse for NamedParameter where Name: syn::parse::Parse + crate::common::peekable::Peekable, Param: syn::parse::Parse, { fn parse(input: syn::parse::ParseStream) -> syn::Result { let name = input.parse()?; input.parse::()?; let param = input.parse()?; Ok(Self { name, param }) } } impl crate::common::peekable::Peekable for NamedParameter where Name: syn::parse::Parse + crate::common::peekable::Peekable, Param: syn::parse::Parse, { fn peek(stream: S) -> bool where S: crate::common::peekable::PeekableStream, { Name::peek(stream) } fn display() -> &'static str { Name::display() } } ================================================ FILE: packages/wm-macros/src/common/parenthesized.rs ================================================ //! Type wrappers for parsing content that is within delimiters, such as //! parenthesis or brackets. /// Type wrapper for parsing content within parenthesis. /// /// # Example /// Parse a [syn::Ident] within parenthesis: /// ``` /// type ParenthesizedIdent = wm_macros::Parenthesized; /// fn example(stream: syn::parse::ParseStream) -> syn::Result { /// stream.parse::() /// } /// /// fn main() { /// # use quote::quote; /// let tokens = quote! { (some_name) }.into(); /// /// assert!(example(tokens).is_ok()); /// } /// ``` // Util type that will be used in future #[allow(dead_code)] pub struct Parenthesized(pub T) where T: syn::parse::Parse; impl syn::parse::Parse for Parenthesized where T: syn::parse::Parse, { fn parse(input: syn::parse::ParseStream) -> syn::Result { let content; syn::parenthesized!(content in input); Ok(Parenthesized(content.parse()?)) } } impl core::ops::Deref for Parenthesized where T: syn::parse::Parse, { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } impl core::ops::DerefMut for Parenthesized where T: syn::parse::Parse, { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } ================================================ FILE: packages/wm-macros/src/common/peekable.rs ================================================ //! Custom peek implementation to support generics and custom peek //! functions. // Syn has a `Peek` trait, but it works weirdly. There is `syn::Ident` the // type which implements `Parse` and `syn::Ident` the function which // implements `Peek`. This file is for implementing a custom method of // peeking using the type itself rather than a function using the same name // as a type. This allows peeking to work nicer with generics. pub mod prelude { pub use super::{Peekable, TPeek}; } /// Trait for any stream that can peek at the next token. pub trait PeekableStream { fn is_empty(&self) -> bool; fn peek(&self, token: T) -> bool; } impl PeekableStream for &syn::parse::ParseStream<'_> { fn peek(&self, token: T) -> bool { (*self).peek(token) } fn is_empty(&self) -> bool { (*self).is_empty() } } impl PeekableStream for syn::parse::ParseStream<'_> { fn peek(&self, token: T) -> bool { (*self).peek(token) } fn is_empty(&self) -> bool { (*self).is_empty() } } impl PeekableStream for syn::parse::ParseBuffer<'_> { fn peek(&self, token: T) -> bool { self.peek(token) } fn is_empty(&self) -> bool { self.is_empty() } } impl PeekableStream for syn::parse::Lookahead1<'_> { fn peek(&self, token: T) -> bool { self.peek(token) } fn is_empty(&self) -> bool { self.peek(syn::parse::End) } } impl PeekableStream for &syn::parse::Lookahead1<'_> { fn peek(&self, token: T) -> bool { (*self).peek(token) } fn is_empty(&self) -> bool { (*self).peek(syn::parse::End) } } /// Custom trait for types that can be peeked. pub trait Peekable { /// Gets the type's `Peek` implementation, since in syn the type /// implements `Parse` but there is a function with the same path that /// implements `Peek`. So this trait is used to get the function (`Peek`) /// from the type (`Parse`). /// /// # Examble /// ``` /// fn peek_then_parse(stream: syn::parse::ParseStream) -> syn::Result { /// if stream.peek(T::peekable()) { /// let value = stream.parse::()?; /// } /// } /// ``` fn peek(stream: T) -> bool where T: PeekableStream; // Useful for debugging and custom error messages. #[allow(dead_code)] fn display() -> &'static str; } /// Trait for the types in syn that have a corresponding function that /// implements [syn::parse::Peek]. E.g. it implements a method for /// [syn::Ident] the type that returns [syn::Ident] the function. pub trait SynPeek { fn peekable() -> impl syn::parse::Peek; // To be forwarded to [Peekable] #[allow(dead_code)] fn display() -> &'static str; } /// Implement [Peekable] for [SynPeek] impl Peekable for T { fn peek(stream: S) -> bool where S: PeekableStream, { stream.peek(T::peekable()) } fn display() -> &'static str { T::display() } } /// Helper fucntion to get the display string for a peekable type. // Used in a macro call #[allow(dead_code)] pub fn get_peek_display(_peek: T) -> &'static str { use syn::token::Token; T::Token::display() } /// Extends the [PeekableStream] trait with a method to peek at a type /// rather than a value. /// /// # Example /// ``` /// # fn example(stream: syn::parse::ParseStream) -> syn::Result<()> { /// // Allows for /// stream.tpeek::()?; /// // Rather than /// stream.peek(syn::Ident)?; /// # } pub trait TPeek<'a> { fn tpeek(&'a self) -> bool where T: Peekable; } impl<'a, T> TPeek<'a> for T where &'a T: PeekableStream + 'a, { fn tpeek(&'a self) -> bool where U: Peekable, { U::peek(self) } } /// Custom keyword macro to define a syn custom keyword that also /// implements Peekable. macro_rules! custom_keyword { ($name:ident) => { syn::custom_keyword!($name); impl $crate::common::peekable::SynPeek for $name { fn peekable() -> impl syn::parse::Peek { $name } fn display() -> &'static str { crate::common::peekable::get_peek_display(Self::peekable()) } } }; } pub(crate) use custom_keyword; /// Macro for implementing [SynPeek] for a type that implements /// [syn::parse::Peek]. macro_rules! impl_syn_peek { ($($name:tt)+) => { impl SynPeek for $($name)+ { fn peekable() -> impl syn::parse::Peek { $($name)+ } fn display() -> &'static str { crate::common::peekable::get_peek_display(Self::peekable()) } } }; } impl_syn_peek!(syn::Ident); impl_syn_peek!(syn::LitStr); // TODO: Other syn types // Copied from syn::Token! impl_syn_peek!(syn::Token![abstract]); impl_syn_peek!(syn::Token![as]); impl_syn_peek!(syn::Token![async]); impl_syn_peek!(syn::Token![auto]); impl_syn_peek!(syn::Token![await]); impl_syn_peek!(syn::Token![become]); impl_syn_peek!(syn::Token![box]); impl_syn_peek!(syn::Token![break]); impl_syn_peek!(syn::Token![const]); impl_syn_peek!(syn::Token![continue]); impl_syn_peek!(syn::Token![crate]); impl_syn_peek!(syn::Token![default]); impl_syn_peek!(syn::Token![do]); impl_syn_peek!(syn::Token![dyn]); impl_syn_peek!(syn::Token![else]); impl_syn_peek!(syn::Token![enum]); impl_syn_peek!(syn::Token![extern]); impl_syn_peek!(syn::Token![final]); impl_syn_peek!(syn::Token![fn]); impl_syn_peek!(syn::Token![for]); impl_syn_peek!(syn::Token![if]); impl_syn_peek!(syn::Token![impl]); impl_syn_peek!(syn::Token![in]); impl_syn_peek!(syn::Token![let]); impl_syn_peek!(syn::Token![loop]); impl_syn_peek!(syn::Token![macro]); impl_syn_peek!(syn::Token![match]); impl_syn_peek!(syn::Token![mod]); impl_syn_peek!(syn::Token![move]); impl_syn_peek!(syn::Token![mut]); impl_syn_peek!(syn::Token![override]); impl_syn_peek!(syn::Token![priv]); impl_syn_peek!(syn::Token![pub]); impl_syn_peek!(syn::Token![ref]); impl_syn_peek!(syn::Token![return]); impl_syn_peek!(syn::Token![Self]); impl_syn_peek!(syn::Token![self]); impl_syn_peek!(syn::Token![static]); impl_syn_peek!(syn::Token![struct]); impl_syn_peek!(syn::Token![super]); impl_syn_peek!(syn::Token![trait]); impl_syn_peek!(syn::Token![try]); impl_syn_peek!(syn::Token![type]); impl_syn_peek!(syn::Token![typeof]); impl_syn_peek!(syn::Token![union]); impl_syn_peek!(syn::Token![unsafe]); impl_syn_peek!(syn::Token![unsized]); impl_syn_peek!(syn::Token![use]); impl_syn_peek!(syn::Token![virtual]); impl_syn_peek!(syn::Token![where]); impl_syn_peek!(syn::Token![while]); impl_syn_peek!(syn::Token![yield]); impl_syn_peek!(syn::Token![&]); impl_syn_peek!(syn::Token![&&]); impl_syn_peek!(syn::Token![&=]); impl_syn_peek!(syn::Token![@]); impl_syn_peek!(syn::Token![^]); impl_syn_peek!(syn::Token![^=]); impl_syn_peek!(syn::Token![:]); impl_syn_peek!(syn::Token![,]); impl_syn_peek!(syn::Token![$]); impl_syn_peek!(syn::Token![.]); impl_syn_peek!(syn::Token![..]); impl_syn_peek!(syn::Token![...]); impl_syn_peek!(syn::Token![..=]); impl_syn_peek!(syn::Token![=]); impl_syn_peek!(syn::Token![==]); impl_syn_peek!(syn::Token![=>]); impl_syn_peek!(syn::Token![>=]); impl_syn_peek!(syn::Token![>]); impl_syn_peek!(syn::Token![<-]); impl_syn_peek!(syn::Token![<=]); impl_syn_peek!(syn::Token![<]); impl_syn_peek!(syn::Token![-]); impl_syn_peek!(syn::Token![-=]); impl_syn_peek!(syn::Token![!=]); impl_syn_peek!(syn::Token![!]); impl_syn_peek!(syn::Token![|]); impl_syn_peek!(syn::Token![|=]); impl_syn_peek!(syn::Token![||]); impl_syn_peek!(syn::Token![::]); impl_syn_peek!(syn::Token![%]); impl_syn_peek!(syn::Token![%=]); impl_syn_peek!(syn::Token![+]); impl_syn_peek!(syn::Token![+=]); impl_syn_peek!(syn::Token![#]); impl_syn_peek!(syn::Token![?]); impl_syn_peek!(syn::Token![->]); impl_syn_peek!(syn::Token![;]); impl_syn_peek!(syn::Token![<<]); impl_syn_peek!(syn::Token![<<=]); impl_syn_peek!(syn::Token![>>]); impl_syn_peek!(syn::Token![>>=]); impl_syn_peek!(syn::Token![/]); impl_syn_peek!(syn::Token![/=]); impl_syn_peek!(syn::Token![*]); impl_syn_peek!(syn::Token![*=]); impl_syn_peek!(syn::Token![~]); impl_syn_peek!(syn::Token![_]); ================================================ FILE: packages/wm-macros/src/common/spanned_string.rs ================================================ //! Type for a String with an associated source code span. /// A String with an associated source code span. /// An owned version of a syn::LitStr. #[derive(Debug, Clone)] pub struct SpannedString { pub value: String, pub span: proc_macro2::Span, } impl SpannedString { pub fn new(value: String, span: proc_macro2::Span) -> Self { SpannedString { value, span } } pub fn from_lit_str(lit_str: syn::LitStr) -> Self { SpannedString { value: lit_str.value(), span: lit_str.span(), } } } impl From for String { fn from(spanned_string: SpannedString) -> Self { spanned_string.value } } impl From<&SpannedString> for syn::LitStr { fn from(spanned_string: &SpannedString) -> Self { syn::LitStr::new(&spanned_string.value, spanned_string.span) } } impl From for SpannedString { fn from(lit_str: syn::LitStr) -> Self { SpannedString::from_lit_str(lit_str) } } /// Parse a `SpannedString` from a `syn::LitStr`. impl syn::parse::Parse for SpannedString { fn parse(input: syn::parse::ParseStream) -> syn::Result { let lit_str: syn::LitStr = input.parse()?; Ok(lit_str.into()) } } /// Convert a `SpannedString` to a `syn::LitStr` for token generation. impl quote::ToTokens for SpannedString { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let lit_str: syn::LitStr = self.into(); lit_str.to_tokens(tokens); } } ================================================ FILE: packages/wm-macros/src/enum_from_inner/mod.rs ================================================ //! Macro to derive `From` and `TryFrom` implementations for enum variants use quote::ToTokens as _; use crate::prelude::*; /// Macro to derive `From` and `TryFrom` implementations for enum variants /// Derives `From` for each variant with format `Enum::Variant(T)`, and /// implements `TryFrom` for each variant, returning an error if the /// variant does not match. pub fn enum_from_inner( input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); let name = &input.ident; let enum_data = match input.data { syn::Data::Enum(data) => data, _ => { return name .error("This macro can only be used on enums") .to_compile_error() .into(); } }; let variants = enum_data.variants.iter().map(|variant| { let ident = &variant.ident; let inner_type = match &variant.fields { syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { &fields.unnamed[0].ty } // Don't error on Unit variants, just do nothing. syn::Fields::Unit => { return quote::quote! {}; } _ => { return variant .error("Enum variants must have exactly one unnamed field") .to_compile_error(); } }; let error = format!( "Cannot convert this variant of enum `{}` to {}", name, // syn::Type doesn't print well, so convert it to a token stream // which works well enough. inner_type.to_token_stream() ); quote::quote! { impl From<#inner_type> for #name { fn from(value: #inner_type) -> Self { #name::#ident(value) } } impl TryFrom<#name> for #inner_type { type Error = &'static str; fn try_from(value: #name) -> Result { match value { #name::#ident(inner) => Ok(inner), _ => Err(#error), } } } impl<'a> TryFrom<&'a #name> for &'a #inner_type { type Error = &'static str; fn try_from(value: &'a #name) -> Result { match value { #name::#ident(inner) => Ok(inner), _ => Err(#error), } } } } }); quote::quote! { #(#variants)* } .into() } ================================================ FILE: packages/wm-macros/src/lib.rs ================================================ // Enable proc macro diagnostics to allow emitting warnings and errors in // line #![feature(proc_macro_diagnostic)] mod common; mod enum_from_inner; mod subenum; use proc_macro::TokenStream; mod prelude { pub use crate::common::{ attributes::prelude::*, error_handling::prelude::*, peekable::prelude::*, }; } /// Creates subenums from a main enum, and defines /// * `impl From for MainEnum` /// * `impl TryFrom for SubEnum` /// * `impl TryFrom for SubEnumTwo` where `SubEnumOne` and /// `SubEnumTwo` share variant(s). /// /// Accepts a defaults block of attributes to be added to every subenum /// ``` /// #[subenum(defaults, { /// /// Subenum of [X] /// #[derive(Clone, Debug)] /// })] /// ``` /// /// And any number of subenum declarations, which are defined as /// ``` /// #[subenum(SubenumName, { /// /// Subset of [X] that can be checked for equality. /// #[derive(PartialEq)] // Will also derive [Clone] and [Debug] from the defaults block /// })] /// ``` /// /// # Example /// ``` /// /// Your main enum documentation /// // Note that the defaults block does not apply to the main enum itself. /// #[derive(Clone, Debug, wm_macros::SubEnum)] /// #[subenum(defaults, { /// /// Subenum of [MainEnum] /// #[derive(Clone, Debug)] /// })] /// #[subenum(Similar, { /// /// Subset of [MainEnum] that can be checked for equality. /// #[derive(PartialEq)] // Will also derive [Clone] and [Debug] from the defaults block /// })] /// #[subenum(Hashable, { /// /// Subset of [MainEnum] that can be hashed. /// #[derive(Hash)] // Will also derive [Clone] and [Debug] from the defaults block /// })] /// pub enum MainEnum { /// Path(PathBuf), /// #[subenum(Similar, Hashable)] /// Length(i32), /// #[subenum(Hashable)] /// Name(String) /// } /// /// let name = String::from("example"); /// let name_enum = MainEnum::from(name); /// /// // Try to convert MainEnum to Hashable. /// let hashable_name = Hashable::try_from(name_enum).unwrap(); // Will succeed, as `Name` is present in the `Hashable` subenum. /// hashable_name.hash(); /// /// let similar_name = Similar::try_from(hashable_name.clone()); /// assert!(similar_name.is_err()); // Will fail, as `Name` is not present in the `Similar` subenum. /// /// // And convert it back (infallible). /// let name_enum: MainEnum = hashable_name.into(); /// /// let length = 42; /// let length_enum: MainEnum = length.into(); /// /// let similar_length = Similar::try_from(length_enum).unwrap(); /// /// let other_length = Hashable::Length(42); /// let other_similar_length = Similar::try_from(other_length).unwrap(); // Convert between subenums that share variants. /// /// assert_eq!(similar_length, other_similar_length); /// ``` #[proc_macro_derive(SubEnum, attributes(subenum))] pub fn sub_enum(input: TokenStream) -> TokenStream { subenum::sub_enum(input) } /// Creates `impl From for Enum` and `impl TryFrom for Inner` /// /// # Example /// ``` /// struct One; /// struct Two; /// /// #[derive(wm_macros::EnumFromInner)] /// enum MyEnum { /// One(One), /// Two(Two), /// } /// /// let one = One; /// let my_enum: MyEnum = one.into(); // Converts One into MyEnum::One(One) /// /// let one = my_enum.try_into().unwrap(); // Attempts to convert MyEnum::One(One) into One /// /// let two = Two; /// let my_enum: MyEnum = two.into(); // Converts Two into MyEnum::Two(Two) /// /// let one = my_enum.try_into(); // Will fail, as MyEnum::Two(Two) cannot be converted to One /// assert!(one.is_err()); /// ``` #[proc_macro_derive(EnumFromInner)] pub fn enum_from_inner(input: TokenStream) -> TokenStream { enum_from_inner::enum_from_inner(input) } ================================================ FILE: packages/wm-macros/src/subenum/enum_attrs.rs ================================================ use crate::{common::branch::Ordered, prelude::*}; mod kw { crate::common::custom_keyword!(defaults); } /// Collects all `#[subenum(...)]` attributes from the given iterator of /// attributes and returns a list of `Subenum` instances. pub fn collect_sub_enums<'a>( attrs: impl Iterator, ) -> syn::Result> { attrs .map(|attr| attr.parse_args()) .collect::>>() } /// Each subenum can either be a declaration with a name and attribute /// block, or a defaults block to append to every subenum. pub enum Subenum { Defaults(proc_macro2::TokenStream), Declaration(SubenumDeclaration), } impl syn::parse::Parse for Subenum { fn parse(input: syn::parse::ParseStream) -> syn::Result { type Defaults = Ordered<(kw::defaults, AttrBlock), syn::Token![,]>; if input.tpeek::() { let Ordered { items: (_, AttrBlock { attrs }), .. } = input.parse::()?; return Ok(Self::Defaults(attrs)); } let declaration: SubenumDeclaration = input.parse()?; Ok(Self::Declaration(declaration)) } } /// Parser for `, {...}` where name is the name of the subenum, and /// the block contents are passed through as is. pub struct SubenumDeclaration { pub name: syn::Ident, pub attrs: proc_macro2::TokenStream, } impl syn::parse::Parse for SubenumDeclaration { fn parse(input: syn::parse::ParseStream) -> syn::Result { type Declaration = Ordered<(syn::Ident, AttrBlock), syn::Token![,]>; let Ordered { items: (name, AttrBlock { attrs }), .. } = input.parse::()?; Ok(Self { name, attrs }) } } /// Block of arbitray tokens that are contained within { ... } struct AttrBlock { pub attrs: proc_macro2::TokenStream, } impl syn::parse::Parse for AttrBlock { fn parse(input: syn::parse::ParseStream) -> syn::Result { let content; let _ = syn::braced!(content in input); let attrs: proc_macro2::TokenStream = content.parse()?; Ok(Self { attrs }) } } ================================================ FILE: packages/wm-macros/src/subenum/mod.rs ================================================ use enum_attrs::{Subenum, SubenumDeclaration}; use crate::prelude::*; const SUBENUM_ATTR_NAME: &str = "subenum"; mod enum_attrs; mod variant_attr; pub fn sub_enum( input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); let attrs = &input.attrs; let sub_enums = match enum_attrs::collect_sub_enums( attrs.find_list_attrs(SUBENUM_ATTR_NAME), ) { Ok(sub_enums) => sub_enums, Err(err) => return err.to_compile_error().into(), }; let enum_data = match input.data { syn::Data::Enum(data) => data, _ => { return input .ident .error("This macro can only be used on enums") .to_compile_error() .into(); } }; let variants = match enum_data .variants .iter() .map(variant_attr::parse_variant) .collect::>>() { Ok(variants) => variants, Err(err) => return err.to_compile_error().into(), }; // Filter to get the default blocks and combine them into a single token // stream. let defaults = sub_enums .iter() .filter_map(|sub| match &sub { Subenum::Defaults(attrs) => Some(attrs.clone()), _ => None, }) .reduce(|mut acc, el| { acc.extend(el); acc }) .unwrap_or_default(); let sub_enums = sub_enums .into_iter() .filter_map(|sub| match sub { Subenum::Declaration(decl) => Some(decl), Subenum::Defaults(_) => None, }) .collect::>(); for variant in &variants { for enum_name in &variant.enums { if !sub_enums.iter().any(|sub_enum| sub_enum.name == *enum_name) { enum_name.emit_warning( "Variant references a subenum that is not defined.", ); } } } let sub_enums = combine_variants(sub_enums, variants, &defaults); let sub_enum_to_main_impls = sub_enums .iter() .map(|sub_enum| from_sub_to_main_impl(&input.ident, sub_enum)); let main_to_sub_impls = sub_enums .iter() .map(|sub| try_from_main_to_sub_impl(&input.ident, sub)); struct SharedVariant { sub_enum_a: syn::Ident, sub_enum_b: syn::Ident, variants: Vec, } // Find all sub enums that have a shared variant, so we can make // `TryFrom` impls between them. let mut shared_variants: Vec = Vec::new(); for i in 0..sub_enums.len() { for j in (i + 1)..sub_enums.len() { let sub_enum_a = &sub_enums[i]; let sub_enum_b = &sub_enums[j]; for variant_a in &sub_enum_a.variants { if sub_enum_b.variants.iter().any(|v| v.name == variant_a.name) { if let Some(shared) = shared_variants.iter_mut().find(|sv| { sv.sub_enum_a == sub_enum_a.name && sv.sub_enum_b == sub_enum_b.name }) { shared.variants.push(variant_a.clone()); } else { shared_variants.push(SharedVariant { sub_enum_a: sub_enum_a.name.clone(), sub_enum_b: sub_enum_b.name.clone(), variants: vec![variant_a.clone()], }); } } } } } let shared_variants = shared_variants.iter().map(|shared| { let a = &shared.sub_enum_a; let b = &shared.sub_enum_b; let variants_a_b = shared.variants.iter().map(|v| { let var_name = &v.name; quote::quote! { #a::#var_name(v) => Ok(#b::#var_name(v)) } }); let variants_b_a = shared.variants.iter().map(|v| { let var_name = &v.name; quote::quote! { #b::#var_name(v) => Ok(#a::#var_name(v)) } }); let eror_a_b = format!( "Cannot convert this variant of sub enum `{a}` to sub enum `{b}`." ); let eror_b_a = format!( "Cannot convert this variant of sub enum `{b}` to sub enum `{a}`." ); quote::quote! { impl TryFrom<#a> for #b { type Error = &'static str; fn try_from(value: #a) -> Result { match value { #(#variants_a_b),*, _ => Err(#eror_a_b), } } } impl TryFrom<#b> for #a { type Error = &'static str; fn try_from(value: #b) -> Result { match value { #(#variants_b_a),*, _ => Err(#eror_b_a), } } } } }); quote::quote! { #(#sub_enums)* #(#sub_enum_to_main_impls)* #(#main_to_sub_impls)* #(#shared_variants)* } .into() } /// Holds the final data for creating a sub enum. struct SubEnum<'a> { pub name: syn::Ident, pub defaults: &'a proc_macro2::TokenStream, pub attrs: proc_macro2::TokenStream, pub variants: Vec, } /// Final representation of a sub enum variant, which includes its name and /// the contained type. #[derive(Debug, Clone)] struct Variant { pub name: syn::Ident, pub contained: syn::Type, } impl<'a> quote::ToTokens for SubEnum<'a> { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let name = &self.name; let defaults = &self.defaults; let attrs = &self.attrs; let variants = &self.variants; tokens.extend(quote::quote! { #defaults #attrs pub enum #name { #(#variants),* } }); } } impl quote::ToTokens for Variant { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let variant_name = &self.name; let contained = &self.contained; tokens.extend(quote::quote! { #variant_name(#contained) }); } } /// Combine the sub enum declarations with its enum variants fn combine_variants( sub_enums: Vec, variants: Vec, defaults: &proc_macro2::TokenStream, ) -> Vec> { sub_enums .into_iter() .map(|sub_enum| { let mut combined_variants = Vec::new(); for variant in &variants { if variant.enums.contains(&sub_enum.name) { combined_variants.push(Variant { name: variant.name.clone(), contained: variant.contained.clone(), }); } } SubEnum { defaults, name: sub_enum.name, attrs: sub_enum.attrs, variants: combined_variants, } }) .collect() } /// Create a `impl From for name` block fn from_sub_to_main_impl( name: &syn::Ident, sub_enum: &SubEnum, ) -> proc_macro2::TokenStream { let sub_name = &sub_enum.name; let variants = sub_enum.variants.iter().map(|v| { let var_name = &v.name; quote::quote! { #sub_name::#var_name(v) => #name::#var_name(v) } }); quote::quote! { impl From<#sub_name> for #name { fn from(value: #sub_name) -> Self { match value { #(#variants),* } } } } } /// Create a `impl TryFrom for sub_enum` block fn try_from_main_to_sub_impl( name: &syn::Ident, sub_enum: &SubEnum, ) -> proc_macro2::TokenStream { let sub_name = &sub_enum.name; let variants = sub_enum.variants.iter().map(|v| { let var_name = &v.name; quote::quote! { #name::#var_name(v) => Ok(#sub_name::#var_name(v)) } }); let error = format!( "Cannot convert this variant of sub enum `{sub_name}` to main enum `{name}`." ); quote::quote! { impl TryFrom<#name> for #sub_name { type Error = &'static str; fn try_from(value: #name) -> Result { match value { #(#variants),*, _ => Err(#error), } } } } } ================================================ FILE: packages/wm-macros/src/subenum/variant_attr.rs ================================================ use syn::punctuated::Punctuated; use crate::prelude::*; /// Holds the parsed data for a single subenum variant. pub struct SubenumVariant { pub name: syn::Ident, pub contained: syn::Type, pub enums: Vec, } /// Parse a single subenum variant from a [syn::Variant]. pub fn parse_variant( variant: &syn::Variant, ) -> syn::Result { let name = variant.ident.clone(); let mut contained_iter = variant.fields.iter(); let contained = contained_iter .next() .map(|field| field.ty.clone()) .ok_or_else(|| { variant.error("Subenum variants must have a contained type") })?; contained_iter.next().is_some().then_error( variant.error("Subenum variants must have exactly one contained type"), )?; let enums = if let Some(enums) = variant .attrs .find_list_attrs(crate::subenum::SUBENUM_ATTR_NAME) .map(|attr| { attr.parse_args_with( Punctuated::::parse_terminated, ) }) .reduce(|acc, el| { acc.and_then(|mut acc| { el.map(|el| { acc.extend(el); acc }) }) }) { enums.map(|list| list.iter().cloned().collect::>())? } else { vec![] }; Ok(SubenumVariant { name, enums, contained, }) } ================================================ FILE: packages/wm-platform/Cargo.toml ================================================ [package] name = "wm-platform" version = "0.0.0" edition = "2021" [lib] path = "src/lib.rs" test = false [[test]] name = "test" harness = false path = "src/test.rs" [dependencies] home = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } serde = { workspace = true } regex = { workspace = true } [dev-dependencies] libtest-mimic-collect = "0.3.2" [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.52", features = [ "implement", "Win32_Devices_HumanInterfaceDevice", "Win32_Foundation", "Win32_Graphics_Dwm", "Win32_Graphics_Gdi", "Win32_Security", "Win32_System_Com", "Win32_System_Environment", "Win32_System_LibraryLoader", "Win32_System_Registry", "Win32_System_RemoteDesktop", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_UI_Accessibility", "Win32_UI_HiDpi", "Win32_UI_Input_Ime", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell_Common", "Win32_UI_TextServices", "Win32_UI_WindowsAndMessaging", ] } windows-interface = { version = "0.52" } [target.'cfg(target_os = "macos")'.dependencies] objc2-core-graphics = "0.3.2" objc2 = "0.6.4" objc2-app-kit = { version = "0.3.2", default-features = false, features = [ "NSAlert", "NSApplication", "NSEvent", "NSGraphics", "NSRunningApplication", "NSResponder", "NSScreen", "NSWindow", "NSWorkspace", "libc", "objc2-core-foundation", ] } objc2-application-services = "0.3.2" objc2-core-foundation = { version = "0.3.2", features = [ "CFCGTypes", "CFUUID", ] } objc2-foundation = { version = "0.3.2", default-features = false, features = [ "NSArray", "NSEnumerator", "NSNotification", "NSKeyValueCoding", "NSString", "NSThread", "NSValue", ] } ================================================ FILE: packages/wm-platform/build.rs ================================================ fn main() { #[cfg(target_os = "macos")] println!( "cargo:rustc-link-search=framework=/System/Library/PrivateFrameworks" ); } ================================================ FILE: packages/wm-platform/src/dispatcher.rs ================================================ use std::{ path::Path, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, thread::ThreadId, }; #[cfg(target_os = "macos")] use objc2::MainThreadMarker; #[cfg(target_os = "macos")] use objc2_app_kit::{NSAlert, NSAlertStyle, NSEvent}; #[cfg(target_os = "macos")] use objc2_application_services::{ kAXTrustedCheckOptionPrompt, AXIsProcessTrustedWithOptions, }; #[cfg(target_os = "macos")] use objc2_core_foundation::{CFBoolean, CFDictionary, CGPoint}; #[cfg(target_os = "macos")] use objc2_core_graphics::{CGError, CGEvent, CGWarpMouseCursorPosition}; #[cfg(target_os = "macos")] use objc2_foundation::NSString; #[cfg(target_os = "windows")] use windows::{ core::PCWSTR, Win32::{ Foundation::POINT, System::Environment::ExpandEnvironmentStringsW, UI::{ Input::KeyboardAndMouse::{ GetAsyncKeyState, VK_LBUTTON, VK_RBUTTON, }, Shell::{ ShellExecuteExW, SEE_MASK_NOASYNC, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW, }, WindowsAndMessaging::{ GetCursorPos, MessageBoxW, SetCursorPos, SystemParametersInfoW, ANIMATIONINFO, MB_ICONERROR, MB_OK, MB_SYSTEMMODAL, SPIF_SENDCHANGE, SPIF_UPDATEINIFILE, SPI_GETANIMATION, SPI_SETANIMATION, SW_HIDE, SW_NORMAL, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, }, }, }, }; #[cfg(target_os = "macos")] use crate::platform_impl::Application; use crate::{ platform_impl, Display, DisplayDevice, MouseButton, NativeWindow, Point, }; /// Type alias for a closure to be executed by the event loop. pub type DispatchFn = dyn FnOnce() + Send + 'static; /// A callback that pre-processes window procedure messages received by the /// event loop. /// /// Mirrors the Win32 [`WNDPROC`] signature. Returns `Some(lresult)` if /// the message was handled, or `None` to pass it along. /// /// [`WNDPROC`]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wndproc pub type WndProcCallback = dyn Fn(isize, u32, usize, isize) -> Option + Send + 'static; /// macOS-specific extension trait for [`Dispatcher`]. #[cfg(target_os = "macos")] pub trait DispatcherExtMacOs { /// Gets all running applications. /// /// # Platform-specific /// /// This method is only available on macOS. fn all_applications(&self) -> crate::Result>; /// Checks whether accessibility permissions are granted. /// /// If `prompt` is `true`, a dialog will be shown to the user to request /// accessibility permissions. /// /// # Platform-specific /// /// This method is only available on macOS. fn has_ax_permission(&self, prompt: bool) -> bool; } #[cfg(target_os = "macos")] impl DispatcherExtMacOs for Dispatcher { fn all_applications(&self) -> crate::Result> { platform_impl::all_applications(self) } fn has_ax_permission(&self, prompt: bool) -> bool { let options = CFDictionary::from_slices( &[unsafe { kAXTrustedCheckOptionPrompt }], &[CFBoolean::new(prompt)], ); unsafe { AXIsProcessTrustedWithOptions(Some(options.as_ref())) } } } /// Windows-specific extensions for `Dispatcher`. #[cfg(target_os = "windows")] pub trait DispatcherExtWindows { /// Returns the handle of the event loop's message window. /// /// # Platform-specific /// /// This method is only available on Windows. fn message_window_handle(&self) -> isize; /// Registers a callback to pre-process messages in the event loop's /// window procedure. /// /// Returns a unique ID that can be passed to /// `deregister_wndproc_callback` to remove the callback. /// /// # Platform-specific /// /// This method is only available on Windows. fn register_wndproc_callback( &self, callback: Box, ) -> crate::Result; /// Removes a previously registered window procedure callback by its ID. /// /// # Platform-specific /// /// This method is only available on Windows. fn deregister_wndproc_callback(&self, id: usize) -> crate::Result<()>; /// Gets whether system-wide window transition animations are enabled. /// /// # Platform-specific /// /// This method is only available on Windows. fn window_animations_enabled(&self) -> crate::Result; /// Enables or disables system-wide window transition animations. /// /// # Platform-specific /// /// This method is only available on Windows. fn set_window_animations_enabled( &self, enable: bool, ) -> crate::Result<()>; /// Expands `%VAR%` environment variable references in `input`. /// /// Returns the expanded string. /// /// # Platform-specific /// /// This method is only available on Windows. /// /// TODO: Remove this. Handle environment variable expansion in a /// unified, cross-platform way. fn expand_env_strings(&self, input: &str) -> crate::Result; /// Runs the specified program using `ShellExecuteExW`. /// /// If `hide_window` is `true`, the spawned process window is hidden. /// /// # Platform-specific /// /// This method is only available on Windows. /// /// TODO: Remove this. Use `shell_util::Shell::spawn` instead. fn shell_execute_ex( &self, program: &str, args: &str, directory: &Path, hide_window: bool, ) -> crate::Result<()>; } #[cfg(target_os = "windows")] impl DispatcherExtWindows for Dispatcher { fn message_window_handle(&self) -> isize { self.source.as_ref().unwrap().message_window_handle } fn register_wndproc_callback( &self, callback: Box, ) -> crate::Result { self .source .as_ref() .unwrap() .register_wndproc_callback(callback) } fn deregister_wndproc_callback(&self, id: usize) -> crate::Result<()> { self .source .as_ref() .unwrap() .deregister_wndproc_callback(id) } fn window_animations_enabled(&self) -> crate::Result { let mut animation_info = ANIMATIONINFO { #[allow(clippy::cast_possible_truncation)] cbSize: std::mem::size_of::() as u32, iMinAnimate: 0, }; unsafe { SystemParametersInfoW( SPI_GETANIMATION, animation_info.cbSize, Some(std::ptr::from_mut(&mut animation_info).cast()), SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), ) }?; Ok(animation_info.iMinAnimate != 0) } fn set_window_animations_enabled( &self, enable: bool, ) -> crate::Result<()> { let mut animation_info = ANIMATIONINFO { #[allow(clippy::cast_possible_truncation)] cbSize: std::mem::size_of::() as u32, iMinAnimate: i32::from(enable), }; unsafe { SystemParametersInfoW( SPI_SETANIMATION, animation_info.cbSize, Some(std::ptr::from_mut(&mut animation_info).cast()), SPIF_UPDATEINIFILE | SPIF_SENDCHANGE, ) }?; Ok(()) } fn expand_env_strings(&self, input: &str) -> crate::Result { let wide_input = input.encode_utf16().chain(Some(0)).collect::>(); let size = unsafe { ExpandEnvironmentStringsW(PCWSTR(wide_input.as_ptr()), None) }; if size == 0 { return Err(crate::Error::Platform(format!( "Failed to expand environment strings in '{input}'.", ))); } let mut buffer = vec![0u16; size as usize]; let size = unsafe { ExpandEnvironmentStringsW( PCWSTR(wide_input.as_ptr()), Some(&mut buffer), ) }; // The size includes the null terminator, so subtract one. Ok(String::from_utf16_lossy(&buffer[..(size - 1) as usize])) } fn shell_execute_ex( &self, program: &str, args: &str, directory: &Path, hide_window: bool, ) -> crate::Result<()> { let program_wide = program.encode_utf16().chain(Some(0)).collect::>(); let args_wide = args.encode_utf16().chain(Some(0)).collect::>(); let directory_wide = directory .to_string_lossy() .encode_utf16() .chain(Some(0)) .collect::>(); let mut exec_info = SHELLEXECUTEINFOW { #[allow(clippy::cast_possible_truncation)] cbSize: std::mem::size_of::() as u32, lpFile: PCWSTR(program_wide.as_ptr()), lpParameters: PCWSTR(args_wide.as_ptr()), lpDirectory: PCWSTR(directory_wide.as_ptr()), nShow: if hide_window { SW_HIDE } else { SW_NORMAL }.0 as _, fMask: SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC, ..Default::default() }; unsafe { ShellExecuteExW(&raw mut exec_info) } .map_err(crate::Error::from) } } /// A thread-safe dispatcher for cross-platform window management /// operations. /// /// On macOS, operations are automatically dispatched to the main thread /// whenever necessary. /// /// # Thread safety /// /// This type is `Send + Sync` and can be cheaply cloned and shared across /// threads. /// /// # Example usage /// /// ```rust,no_run /// use wm_platform::EventLoop; /// use std::thread; /// /// # fn main() -> wm_platform::Result<()> { /// let (event_loop, dispatcher) = EventLoop::new()?; /// /// // Dispatch from another thread. /// thread::spawn(move || { /// dispatcher.dispatch_async(|| { /// println!("This is running on the event loop thread!"); /// }).unwrap(); /// }); /// /// event_loop.run() /// # } /// ``` #[derive(Clone)] pub struct Dispatcher { source: Option, stopped: Arc, } impl Dispatcher { // TODO: Allow for source to be resolved after creation when used via // `EventLoopInstaller` (to be added). pub(crate) fn new( source: Option, stopped: Arc, ) -> Self { Self { source, stopped } } /// Stops the event loop gracefully from any thread. /// /// After calling this method, all subsequent calls to `dispatch_async()` /// and `dispatch_sync()` will return `Error::EventLoopStopped`. pub fn stop_event_loop(&self) -> crate::Result<()> { // Set stopped flag to prevent new dispatches. self.stopped.store(true, Ordering::SeqCst); // Signal platform-specific event loop to stop. if let Some(source) = &self.source { source.send_stop()?; } Ok(()) } /// Asynchronously executes a closure on the event loop thread. /// /// If the current thread is the event loop thread, the function is /// executed directly (synchronously). /// /// Returns `Ok(())` if the closure was successfully queued. No result is /// returned. pub fn dispatch_async(&self, dispatch_fn: F) -> crate::Result<()> where F: FnOnce() + Send + 'static, { // Check if stopped first. if self.stopped.load(Ordering::SeqCst) { return Err(crate::Error::EventLoopStopped); } // Execute the function directly if already on the event loop thread. if self.is_event_loop_thread() { dispatch_fn(); return Ok(()); } if let Some(source) = &self.source { // Platform-specific behavior: // * On Windows, this uses `PostMessageW` to send callbacks via // window messages. // * On macOS, this uses `CFRunLoopSourceSignal` to wake the run loop // and process callbacks. source.send_dispatch_async(dispatch_fn)?; } Ok(()) } /// Synchronously executes a closure on the event loop thread. /// /// If the current thread is the event loop thread, the function is /// executed directly. /// /// Returns a `Result` with the closure's return value. #[allow(clippy::missing_panics_doc)] pub fn dispatch_sync(&self, dispatch_fn: F) -> crate::Result where F: FnOnce() -> R + Send, R: Send, { // Check if stopped first. if self.stopped.load(Ordering::SeqCst) { return Err(crate::Error::EventLoopStopped); } // Execute the function directly if already on the event loop thread. if self.is_event_loop_thread() { return Ok(dispatch_fn()); } let (result_tx, result_rx) = std::sync::mpsc::channel(); // TODO: Block until event loop source is set. self.source.as_ref().unwrap().send_dispatch_sync(move || { let result = dispatch_fn(); if result_tx.send(result).is_err() { tracing::error!("Failed to send closure result."); } })?; result_rx .recv_timeout(std::time::Duration::from_secs(5)) .map_err(crate::Error::ChannelRecv) } /// Gets the thread ID of the event loop thread. #[allow(clippy::missing_panics_doc)] #[must_use] pub fn thread_id(&self) -> ThreadId { // TODO: Block until event loop source is set. self.source.as_ref().unwrap().thread_id } /// Gets whether the current thread is the event loop thread. #[must_use] fn is_event_loop_thread(&self) -> bool { std::thread::current().id() == self.thread_id() } /// Gets all active displays. /// /// NOTE: Does not guarantee a specific, consistent order. /// /// Returns all displays that are currently active and available for use. pub fn displays(&self) -> crate::Result> { platform_impl::all_displays(self) } /// Gets all active displays sorted left-to-right, top-to-bottom. /// /// Returns all displays that are currently active and available for /// use, sorted by their X coordinate (left edge), with ties broken /// by Y coordinate (top edge). /// /// TODO: Remove this. Instead, call `sort_monitors` after populating WM /// state. Need to assign workspaces after sorting monitors because of /// `bind_to_monitor`. pub fn sorted_displays(&self) -> crate::Result> { let displays = platform_impl::all_displays(self)?; let mut displays_with_bounds = displays .into_iter() .map(|display| { let bounds = display.bounds()?; crate::Result::Ok((display, bounds)) }) .try_collect::>()?; displays_with_bounds.sort_by(|(_, bounds_a), (_, bounds_b)| { if bounds_a.x() == bounds_b.x() { bounds_a.y().cmp(&bounds_b.y()) } else { bounds_a.x().cmp(&bounds_b.x()) } }); Ok( displays_with_bounds .into_iter() .map(|(display, _)| display) .collect(), ) } /// Gets all display devices. /// /// NOTE: Does not guarantee a specific, consistent order. /// /// Returns all display devices including active, inactive, and /// disconnected ones. pub fn display_devices(&self) -> crate::Result> { platform_impl::all_display_devices(self) } /// Gets the display containing the specified point. /// /// If no display contains the point, returns the primary display. pub fn display_from_point( &self, point: &Point, ) -> crate::Result { platform_impl::display_from_point(point, self) } /// Gets the primary display. pub fn primary_display(&self) -> crate::Result { platform_impl::primary_display(self) } /// Gets the nearest display to a window. /// /// Returns the display that contains the largest area of the window's /// frame. Defaults to the primary display if no overlap is found. pub fn nearest_display( &self, native_window: &NativeWindow, ) -> crate::Result { platform_impl::nearest_display(native_window, self) } /// Gets all visible windows from all running applications. /// /// NOTE: Does not guarantee a specific, consistent order. /// /// Returns a vector of `NativeWindow` instances for windows that are /// not hidden and on the current virtual desktop. pub fn visible_windows(&self) -> crate::Result> { platform_impl::visible_windows(self) } /// Gets the currently focused (foreground) window. /// /// This may be the desktop window if no window has focus. pub fn focused_window(&self) -> crate::Result { platform_impl::focused_window(self) } /// Gets the current cursor position. pub fn cursor_position(&self) -> crate::Result { #[cfg(target_os = "macos")] { let event = CGEvent::new(None); let point = CGEvent::location(event.as_deref()); #[allow(clippy::cast_possible_truncation)] Ok(Point { x: point.x as i32, y: point.y as i32, }) } #[cfg(target_os = "windows")] { let mut point = POINT { x: 0, y: 0 }; unsafe { GetCursorPos(&raw mut point) }?; Ok(Point { x: point.x, y: point.y, }) } } /// Gets whether the given mouse button is currently pressed. #[must_use] pub fn is_mouse_down(&self, button: &MouseButton) -> bool { #[cfg(target_os = "macos")] { let bit_index = match button { MouseButton::Left => 0usize, MouseButton::Right => 1usize, }; // Check if bit at corresponding index is set in the bitmask. let pressed_mask = NSEvent::pressedMouseButtons(); (pressed_mask & (1usize << bit_index)) != 0 } #[cfg(target_os = "windows")] { // Virtual-key codes for mouse buttons. let vk_code = match button { MouseButton::Left => VK_LBUTTON.0, MouseButton::Right => VK_RBUTTON.0, }; // High-order bit set indicates the key is currently down. let state = unsafe { GetAsyncKeyState(vk_code.into()) }; (state.cast_unsigned() & 0x8000u16) != 0 } } /// Gets the top-level window at the specified point. pub fn window_from_point( &self, point: &Point, ) -> crate::Result> { platform_impl::window_from_point(point, self) } /// Sets the cursor position to the specified coordinates. pub fn set_cursor_position(&self, point: &Point) -> crate::Result<()> { #[cfg(target_os = "macos")] { let point = CGPoint { x: f64::from(point.x), y: f64::from(point.y), }; if CGWarpMouseCursorPosition(point) != CGError::Success { return Err(crate::Error::Platform( "Failed to set cursor position.".to_string(), )); } } #[cfg(target_os = "windows")] { unsafe { SetCursorPos(point.x, point.y) }?; } Ok(()) } /// Removes focus from the current window and focuses the desktop. pub fn reset_focus(&self) -> crate::Result<()> { platform_impl::reset_focus(self) } /// Opens the operating system's file explorer at the given path. /// /// # Platform-specific /// /// - **Windows**: Uses `explorer` to open the file explorer. /// - **macOS**: Uses `open` to open the file explorer. pub fn open_file_explorer(&self, path: &Path) -> crate::Result<()> { #[cfg(target_os = "windows")] { let normalized_path = std::fs::canonicalize(path)?; std::process::Command::new("explorer") .arg(normalized_path) .spawn()?; } #[cfg(target_os = "macos")] { std::process::Command::new("open") .arg(path) .arg("-R") .spawn()?; } Ok(()) } /// Shows a modal error dialog with the given title and message. /// /// Blocks the current thread until the user dismisses the dialog. #[allow(clippy::missing_panics_doc)] pub fn show_error_dialog(&self, title: &str, message: &str) { #[cfg(target_os = "windows")] { let title_wide = title.encode_utf16().chain(Some(0)).collect::>(); let message_wide = message.encode_utf16().chain(Some(0)).collect::>(); unsafe { MessageBoxW( None, PCWSTR(message_wide.as_ptr()), PCWSTR(title_wide.as_ptr()), MB_ICONERROR | MB_OK | MB_SYSTEMMODAL, ); } } #[cfg(target_os = "macos")] { // TODO: This should block indefinitely. Currently, it gets timed out // after 5 seconds. let _ = self.dispatch_sync(|| { let mtm = MainThreadMarker::new().unwrap(); let alert = NSAlert::new(mtm); alert.setMessageText(&NSString::from_str(title)); alert.setInformativeText(&NSString::from_str(message)); alert.setAlertStyle(NSAlertStyle::Critical); alert.runModal(); }); } } } impl std::fmt::Debug for Dispatcher { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "EventLoopDispatcher") } } #[cfg(test)] mod tests { use std::sync::{Arc, Mutex}; use crate::EventLoop; #[test] fn dispatch_after_stop_fails() { let (_event_loop, dispatcher) = EventLoop::new().unwrap(); dispatcher .stop_event_loop() .expect("Failed to stop dispatcher."); // Try to dispatch asynchronously - should fail. let result = dispatcher.dispatch_sync(|| {}); assert!(matches!(result, Err(crate::Error::EventLoopStopped))); // Try dispatch synchronously - should fail. let sync_result: crate::Result = dispatcher.dispatch_sync(|| 69); assert!(matches!(sync_result, Err(crate::Error::EventLoopStopped))); } #[test] fn dispatch_sync_executes_in_order() { const ITERATIONS: usize = 5000; let (event_loop, dispatcher) = EventLoop::new().unwrap(); let order = Arc::new(Mutex::new(Vec::new())); let order_clone = order.clone(); std::thread::spawn(move || { for index in 1..=ITERATIONS { dispatcher .dispatch_sync(|| { order_clone.lock().unwrap().push(index); }) .unwrap(); } dispatcher.stop_event_loop().unwrap(); }); event_loop.run().unwrap(); assert_eq!( *order.lock().unwrap(), (1..=ITERATIONS).collect::>() ); } #[test] fn dispatch_sync_from_different_threads() { // Stress test with many threads calling `dispatch_sync` // simultaneously. Ensure that dispatching doesn't deadlock. const NUM_THREADS: usize = 10; const ITERATIONS: usize = 1000; let (event_loop, dispatcher) = EventLoop::new().unwrap(); let counter = Arc::new(Mutex::new(0)); let thread_handles: Vec<_> = (0..NUM_THREADS) .map(|_| { let counter = counter.clone(); let dispatcher = dispatcher.clone(); std::thread::spawn(move || { for _ in 0..ITERATIONS { dispatcher .dispatch_sync(|| { let mut count = counter.lock().unwrap(); *count += 1; }) .unwrap(); } }) }) .collect(); std::thread::spawn(move || { // Wait for all threads to finish. for handle in thread_handles { handle.join().unwrap(); } dispatcher.stop_event_loop().unwrap(); }); event_loop.run().unwrap(); assert_eq!(*counter.lock().unwrap(), NUM_THREADS * ITERATIONS); } #[test] fn dispatch_sync_with_nested() { // Test that calling `dispatch_sync` from within a `dispatch_sync` // callback works correctly (should execute directly without blocking). let (event_loop, dispatcher) = EventLoop::new().unwrap(); let result = Arc::new(Mutex::new(Vec::new())); let result_clone = result.clone(); std::thread::spawn(move || { dispatcher .dispatch_sync(|| { result_clone.lock().unwrap().push(1); // Nested `dispatch_sync` - should execute immediately since it's // already on the event loop thread. dispatcher .dispatch_sync(|| { result_clone.lock().unwrap().push(2); }) .unwrap(); result_clone.lock().unwrap().push(3); }) .unwrap(); dispatcher.stop_event_loop().unwrap(); }); event_loop.run().unwrap(); assert_eq!(*result.lock().unwrap(), vec![1, 2, 3]); } } ================================================ FILE: packages/wm-platform/src/display.rs ================================================ #[cfg(target_os = "macos")] use objc2::rc::Retained; #[cfg(target_os = "macos")] use objc2_app_kit::NSScreen; #[cfg(target_os = "macos")] use objc2_core_graphics::CGDirectDisplayID; #[cfg(target_os = "windows")] use windows::Win32::Graphics::Gdi::HMONITOR; #[cfg(target_os = "macos")] use crate::ThreadBound; use crate::{platform_impl, Rect}; /// Unique identifier for a display. /// /// Can be obtained with `display.id()`. /// /// # Platform-specific /// /// - **Windows**: `isize` (`HMONITOR`) /// - **macOS**: `u32` (`CGDirectDisplayID`) #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DisplayId( #[cfg(target_os = "windows")] pub isize, #[cfg(target_os = "macos")] pub u32, ); /// Unique identifier for a display device. /// /// Can be obtained with `display_device.id()`. /// /// # Platform-specific /// /// - **Windows**: Hardware ID string with fallback to adapter name. /// - **macOS**: UUID string from `CGDisplayCreateUUIDFromDisplayID`. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct DisplayDeviceId(pub String); /// macOS-specific extension trait for [`Display`]. #[cfg(target_os = "macos")] pub trait DisplayExtMacOs { /// Gets the Core Graphics display ID. fn cg_display_id(&self) -> CGDirectDisplayID; /// Gets the `NSScreen` instance for this display. /// /// # Platform-specific /// /// This method is only available on macOS. fn ns_screen(&self) -> &ThreadBound>; } #[cfg(target_os = "macos")] impl DisplayExtMacOs for Display { fn cg_display_id(&self) -> CGDirectDisplayID { self.inner.cg_display_id() } fn ns_screen(&self) -> &ThreadBound> { self.inner.ns_screen() } } /// Windows-specific extensions for [`Display`]. #[cfg(target_os = "windows")] pub trait DisplayExtWindows { /// Gets the monitor handle. /// /// # Platform-specific /// /// This method is only available on Windows. fn hmonitor(&self) -> HMONITOR; } #[cfg(target_os = "windows")] impl DisplayExtWindows for Display { fn hmonitor(&self) -> HMONITOR { self.inner.hmonitor() } } /// Represents a logical display space where windows can be placed. /// /// # Platform-specific /// /// - **Windows**: This corresponds to a Win32 "display monitor", each with /// a monitor handle (`HMONITOR`). /// - **macOS**: This corresponds to an `NSScreen`. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Display { pub(crate) inner: platform_impl::Display, } impl Display { /// Gets the unique identifier for this display. #[must_use] pub fn id(&self) -> DisplayId { self.inner.id() } /// Gets the display name. pub fn name(&self) -> crate::Result { self.inner.name() } /// Gets the full bounds rectangle of the display. pub fn bounds(&self) -> crate::Result { self.inner.bounds() } /// Gets the working area rectangle (excluding system UI). pub fn working_area(&self) -> crate::Result { self.inner.working_area() } /// Gets the scale factor for the display. pub fn scale_factor(&self) -> crate::Result { self.inner.scale_factor() } /// Gets the DPI for the display. pub fn dpi(&self) -> crate::Result { self.inner.dpi() } /// Returns whether this is the primary display. pub fn is_primary(&self) -> crate::Result { self.inner.is_primary() } /// Gets the display devices for this display. /// /// A single display can be associated with multiple display devices. For /// example, when mirroring a display or combining multiple displays /// (e.g. using NVIDIA Surround). pub fn devices(&self) -> crate::Result> { self.inner.devices() } /// Gets the main device (first non-mirroring device) for this display. pub fn main_device(&self) -> crate::Result { self.inner.main_device() } } /// Connection state of a display device. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ConnectionState { /// Device is connected and part of the desktop coordinate space. Active, /// Device is connected but inactive (i.e. on standby or in sleep mode). Inactive, /// Device is disconnected. Disconnected, } /// Mirroring state of a display device. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum MirroringState { /// This device is the source being mirrored. Source, /// This device is mirroring another (target). Target, } /// Display connection type for physical devices. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum OutputTechnology { /// Built-in display (laptop screen). Internal, /// VGA connection. VGA, /// DVI connection. DVI, /// HDMI connection. HDMI, /// DisplayPort connection. DisplayPort, /// Thunderbolt connection. Thunderbolt, /// USB connection. USB, /// Wireless connection. Wireless, /// Unknown connection type. Unknown, } /// macOS-specific extension trait for [`DisplayDevice`]. #[cfg(target_os = "macos")] pub trait DisplayDeviceExtMacOs { /// Gets the Core Graphics display ID. /// /// # Platform-specific /// /// This method is only available on macOS. fn cg_display_id(&self) -> CGDirectDisplayID; } #[cfg(target_os = "macos")] impl DisplayDeviceExtMacOs for DisplayDevice { fn cg_display_id(&self) -> CGDirectDisplayID { self.inner.cg_display_id() } } /// Windows-specific extensions for [`DisplayDevice`]. #[cfg(target_os = "windows")] pub trait DisplayDeviceExtWindows { /// Gets the device path. /// /// This can be an empty string for virtual display devices. /// /// # Platform-specific /// /// This method is only available on Windows. fn device_path(&self) -> Option; /// Gets the hardware ID from the device path. /// /// # Example usage /// /// ```rust,no_run /// device.device_path(); // "\\?\DISPLAY#DEL40A3#5&1234abcd&0&UID256#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}" /// device.hardware_id(); // Some("DEL40A3") /// ``` /// /// # Platform-specific /// /// This method is only available on Windows. fn hardware_id(&self) -> Option; /// Gets the output technology. /// /// # Platform-specific /// /// This method is only available on Windows. fn output_technology(&self) -> crate::Result>; } #[cfg(target_os = "windows")] impl DisplayDeviceExtWindows for DisplayDevice { fn device_path(&self) -> Option { self.inner.device_path.clone() } fn hardware_id(&self) -> Option { self.inner.hardware_id() } fn output_technology(&self) -> crate::Result> { self.inner.output_technology() } } /// Represents a display device (physical or virtual). /// /// This is typically a physical display device, such as a monitor or /// built-in laptop screen. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DisplayDevice { pub(crate) inner: platform_impl::DisplayDevice, } impl DisplayDevice { /// Gets the unique identifier for this display device. #[must_use] pub fn id(&self) -> DisplayDeviceId { self.inner.id() } /// Gets the rotation of the device in degrees. pub fn rotation(&self) -> crate::Result { self.inner.rotation() } /// Gets the refresh rate of the device in Hz. pub fn refresh_rate(&self) -> crate::Result { self.inner.refresh_rate() } /// Gets whether this is a built-in display. /// /// Returns `true` for embedded displays (like laptop screens). pub fn is_builtin(&self) -> crate::Result { self.inner.is_builtin() } /// Gets the connection state of the device. pub fn connection_state(&self) -> crate::Result { self.inner.connection_state() } /// Gets the mirroring state of the device. pub fn mirroring_state(&self) -> crate::Result> { self.inner.mirroring_state() } } #[cfg(test)] mod tests { use super::*; use crate::EventLoop; #[test] fn test_nearest_display() { let (event_loop, dispatcher) = EventLoop::new().unwrap(); let thread = std::thread::spawn(move || { let display = platform_impl::nearest_display( // Assumes that there is at least one window currently visible. &dispatcher.visible_windows().unwrap()[0], &dispatcher, ); dispatcher.stop_event_loop().unwrap(); display }); event_loop.run().unwrap(); let display = thread.join().unwrap(); assert!(display.is_ok()); } } ================================================ FILE: packages/wm-platform/src/display_listener.rs ================================================ use tokio::sync::mpsc; use crate::{platform_impl, Dispatcher}; /// A listener for system-wide display setting changes. /// /// Detects changes to display configuration including resolution changes, /// display connections/disconnections, and working area changes. pub struct DisplayListener { event_rx: mpsc::UnboundedReceiver<()>, /// Inner platform-specific display listener. inner: platform_impl::DisplayListener, } impl DisplayListener { /// Creates a new [`DisplayListener`]. pub fn new(dispatcher: &Dispatcher) -> crate::Result { let (event_tx, event_rx) = mpsc::unbounded_channel(); let inner = platform_impl::DisplayListener::new(event_tx, dispatcher)?; Ok(Self { event_rx, inner }) } /// Returns when the next display settings change is detected. /// /// Returns `None` if the channel has been closed. pub async fn next_event(&mut self) -> Option<()> { self.event_rx.recv().await } /// Terminates the display listener. pub fn terminate(&mut self) -> crate::Result<()> { self.inner.terminate() } } ================================================ FILE: packages/wm-platform/src/error.rs ================================================ #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] #[cfg(target_os = "windows")] Windows(#[from] windows::core::Error), #[error("Accessibility operation failed for attribute {0} with error code: {1}")] #[cfg(target_os = "macos")] Accessibility(String, i32), #[error(transparent)] Parse(#[from] ParseError), #[error("Invalid pointer: {0}")] InvalidPointer(String), #[error("AXValue creation failed: {0}")] #[cfg(target_os = "macos")] AXValueCreation(String), #[error(transparent)] ChannelRecv(#[from] std::sync::mpsc::RecvTimeoutError), #[error("Channel receive error")] OneshotRecv(#[from] tokio::sync::oneshot::error::RecvError), #[error(transparent)] IntConversion(#[from] std::num::TryFromIntError), #[error("Channel send error")] ChannelSend, #[error("Display enumeration failed")] DisplayEnumerationFailed, #[error("Display mode not found")] DisplayModeNotFound, #[error("Primary display not found")] PrimaryDisplayNotFound, #[error("Not main thread")] NotMainThread, #[error("Display not found")] DisplayNotFound, #[error("Display device not found")] DisplayDeviceNotFound, #[error("Hardware enumeration failed")] HardwareEnumerationFailed, #[error("Window enumeration failed")] WindowEnumerationFailed, #[error("Window not found")] WindowNotFound, #[error("Thread error: {0}")] Thread(String), #[error("Window message error: {0}")] WindowMessage(String), #[error("Platform error: {0}")] Platform(String), #[error("Event loop has been stopped")] EventLoopStopped, #[error("Keybinding is empty")] InvalidKeybinding, } #[derive(Debug, thiserror::Error)] pub enum ParseError { #[error( "Invalid length value '{0}': must be of format '10px' or '10%'." )] Length(String), #[error("Invalid keybinding: {0}")] Keybinding(String), #[error( "Invalid opacity value '{0}': must be of format '75%' or '0.75'." )] Opacity(String), #[error( "Invalid color '{0}': must be of format '#RRGGBB' or '#RRGGBBAA'." )] Color(String), #[error("Invalid delta value: {0}")] Delta(String), #[error("Invalid direction '{0}': must be one of 'left', 'right', 'up', or 'down'.")] Direction(String), } pub type Result = std::result::Result; ================================================ FILE: packages/wm-platform/src/event_loop.rs ================================================ use crate::{platform_impl, Dispatcher}; /// A cross-platform event loop that allows for remote dispatching via /// [`Dispatcher`]. /// /// Does not start pumping events until [`EventLoop::run`] is called. /// /// This type is `!Send` to ensure it stays on the thread where it was /// created, as the `Drop` implementation relies on thread-specific /// cleanup. /// /// # Platform-specific /// /// - **macOS**: Can be created on any thread. Runs `CFRunLoopRun()`. /// - **Windows**: Can be created on any thread. Runs a Win32 message loop. pub struct EventLoop { inner: platform_impl::EventLoop, /// Marker to ensure not `Send`. _marker: std::marker::PhantomData<*const ()>, } impl EventLoop { /// Creates a new event loop and dispatcher. pub fn new() -> crate::Result<(Self, Dispatcher)> { let (event_loop, dispatcher) = platform_impl::EventLoop::new()?; Ok(( Self { inner: event_loop, _marker: std::marker::PhantomData, }, dispatcher, )) } /// Runs the event loop, blocking the current thread until shutdown. /// /// # Platform-specific /// /// - **macOS**: Must be called from the main thread. Runs /// `CFRunLoopRun()`. /// - **Windows**: Can be called from any thread. Runs Win32 message /// loop. pub fn run(self) -> crate::Result<()> { self.inner.run() } } #[cfg(test)] mod tests { use std::time::Duration; use super::*; #[test] fn event_loop_start_stop() { let (event_loop, dispatcher) = EventLoop::new().expect("Failed to create event loop."); // Stop the event loop after a short delay. let handle = std::thread::spawn(move || { std::thread::sleep(Duration::from_millis(10)); dispatcher.stop_event_loop() }); event_loop.run().expect("Failed to run event loop."); // Ensure the event loop is stopped. assert!(handle.join().is_ok()); } } ================================================ FILE: packages/wm-platform/src/keybinding_listener.rs ================================================ use std::{ collections::HashMap, sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }, }; use tokio::sync::mpsc; use crate::{ platform_event::KeybindingEvent, platform_impl, Dispatcher, Key, }; /// Modifier key groups, where each entry maps a generic key (e.g. /// `Key::Shift`) to all its variants (e.g. `Key::LShift`, `Key::RShift`). /// /// `Cmd` and `Win` are treated as aliases within the same group. const MODIFIER_GROUPS: &[(Key, &[Key])] = &[ (Key::Shift, &[Key::Shift, Key::LShift, Key::RShift]), (Key::Ctrl, &[Key::Ctrl, Key::LCtrl, Key::RCtrl]), (Key::Alt, &[Key::Alt, Key::LAlt, Key::RAlt]), ( Key::Win, &[ Key::Win, Key::LWin, Key::RWin, Key::Cmd, Key::LCmd, Key::RCmd, ], ), ]; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Keybinding(Vec); impl Keybinding { /// Creates a new keybinding from a vector of keys. /// /// # Errors /// /// Returns [`Error::InvalidKeybinding`] if the keybinding is empty. pub fn new(keys: Vec) -> crate::Result { if keys.is_empty() { return Err(crate::Error::InvalidKeybinding); } Ok(Self(keys)) } /// Returns the keys in the keybinding. #[must_use] pub fn keys(&self) -> &[Key] { &self.0 } /// Returns the trigger key in the keybinding. #[must_use] #[allow(clippy::missing_panics_doc)] pub fn trigger_key(&self) -> &Key { // SAFETY: Keys vector is verified to be non-empty in // `Keybinding::new`. self.0.last().unwrap() } } /// A listener for system-wide keybindings. #[derive(Debug)] pub struct KeybindingListener { /// A receiver channel for outgoing keybinding events. event_rx: mpsc::UnboundedReceiver, /// A map of keybindings to their trigger key. /// /// The trigger key is the final key in a keybinding. For example, in /// the keybinding `[Key::Cmd, Key::Shift, Key::A]`, `Key::A` is the /// trigger key. keybinding_map: Arc>>>, /// Whether the listener is currently enabled. enabled: Arc, /// The underlying keyboard hook used to listen for key events. keyboard_hook: platform_impl::KeyboardHook, } impl KeybindingListener { /// Creates an instance of `KeybindingListener`. pub fn new( keybindings: &[Keybinding], dispatcher: &Dispatcher, ) -> crate::Result { let (event_tx, event_rx) = mpsc::unbounded_channel(); let keybinding_map = Arc::new(Mutex::new(Self::create_keybinding_map(keybindings))); let enabled = Arc::new(AtomicBool::new(true)); let keyboard_hook = Self::create_keyboard_hook( keybinding_map.clone(), enabled.clone(), event_tx, dispatcher, )?; Ok(Self { event_rx, keybinding_map, enabled, keyboard_hook, }) } /// Returns the next keybinding event from the listener. /// /// This will block until a keybinding event is available. pub async fn next_event(&mut self) -> Option { self.event_rx.recv().await } /// Updates the keybindings for the keybinding listener. /// /// # Panics /// /// If the internal mutex is poisoned. pub fn update(&self, keybindings: &[Keybinding]) { *self.keybinding_map.lock().unwrap() = Self::create_keybinding_map(keybindings); } /// Enables or disables the keybinding listener. pub fn enable(&mut self, enabled: bool) { self.enabled.store(enabled, Ordering::Relaxed); } /// Terminates the keybinding listener. pub fn terminate(&mut self) -> crate::Result<()> { self.keyboard_hook.terminate() } /// Creates and starts the keyboard hook with the given callback. fn create_keyboard_hook( keybinding_map: Arc>>>, enabled: Arc, event_tx: mpsc::UnboundedSender, dispatcher: &Dispatcher, ) -> crate::Result { platform_impl::KeyboardHook::new( move |event: platform_impl::KeyEvent| -> bool { if !enabled.load(Ordering::Relaxed) || !event.is_keypress { return false; } let Ok(keybinding_map) = keybinding_map.lock() else { tracing::error!("Failed to acquire lock on keybinding map."); return false; }; // Find keybinding candidates whose trigger key is the pressed key. let Some(candidates) = keybinding_map.get(&event.key) else { return false; }; let mut cached_key_states = HashMap::new(); // Find the matching keybindings based on the pressed keys. let matched_keybindings = candidates.iter().filter(|keybinding| { keybinding.keys().iter().all(|&key| { if key == event.key { return true; } *cached_key_states .entry(key) .or_insert_with(|| event.is_key_down(key)) }) }); // Find the longest matching keybinding. let Some(longest_keybinding) = matched_keybindings .max_by_key(|keybinding| keybinding.keys().len()) else { return false; }; // Reject if any modifier keys not in the keybinding are held. let has_extra_modifiers = MODIFIER_GROUPS .iter() // Filter out modifier groups that have keys in the keybinding. .filter(|(_, group_keys)| { !group_keys .iter() .any(|key| longest_keybinding.keys().contains(key)) }) // Use the group's "generic" key (e.g. `Key::Shift`) to check if // the modifier is held. This avoids lookups for `Key::LShift` // and `Key::RShift`. .any(|(generic_key, _)| { cached_key_states .get(generic_key) .copied() .unwrap_or_else(|| event.is_key_down(*generic_key)) }); if has_extra_modifiers { return false; } let _ = event_tx.send(KeybindingEvent(longest_keybinding.clone())); true }, dispatcher, ) } /// Builds the keybinding map from configs. fn create_keybinding_map( keybindings: &[Keybinding], ) -> HashMap> { let mut keybinding_map = HashMap::new(); for keybinding in keybindings { keybinding_map .entry(*keybinding.trigger_key()) .or_insert_with(Vec::new) .push(keybinding.clone()); } keybinding_map } } impl Drop for KeybindingListener { fn drop(&mut self) { let _ = self.terminate(); } } ================================================ FILE: packages/wm-platform/src/lib.rs ================================================ #![warn(clippy::all, clippy::pedantic)] #![allow(clippy::missing_errors_doc)] #![feature(iterator_try_collect)] mod dispatcher; mod display; mod display_listener; mod error; mod event_loop; mod keybinding_listener; mod models; mod mouse_listener; mod native_window; mod platform_event; mod platform_impl; mod single_instance; mod thread_bound; mod window_listener; pub use dispatcher::*; pub use display::*; pub use display_listener::*; pub use error::*; pub use event_loop::*; pub use keybinding_listener::*; pub use models::*; pub use mouse_listener::*; pub use native_window::*; pub use platform_event::*; pub use single_instance::*; pub use thread_bound::*; pub use window_listener::*; // TODO: Avoid exposing `windows` crate types in the public API. #[cfg(target_os = "windows")] pub use windows::Win32::UI::WindowsAndMessaging::{ SET_WINDOW_POS_FLAGS, SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOSENDCHANGING, WINDOW_EX_STYLE, WINDOW_STYLE, WS_CAPTION, WS_CHILD, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_MAXIMIZEBOX, }; ================================================ FILE: packages/wm-platform/src/models/color.rs ================================================ use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; #[derive(Debug, Clone, Serialize)] pub struct Color { pub r: u8, pub g: u8, pub b: u8, pub a: u8, } impl Color { #[must_use] #[allow(clippy::missing_panics_doc)] pub fn to_bgr(&self) -> u32 { let bgr = format!("{:02x}{:02x}{:02x}", self.b, self.g, self.r); // SAFETY: An invalid hex value is unrepresentable. u32::from_str_radix(&bgr, 16).unwrap() } } impl FromStr for Color { type Err = crate::ParseError; fn from_str(unparsed: &str) -> Result { let mut chars = unparsed.chars(); if chars.next() != Some('#') { return Err(crate::ParseError::Color(unparsed.to_string())); } let parse_hex = |slice: &str| -> Result { u8::from_str_radix(slice, 16) .map_err(|_| crate::ParseError::Color(unparsed.to_string())) }; let r = parse_hex(&unparsed[1..3])?; let g = parse_hex(&unparsed[3..5])?; let b = parse_hex(&unparsed[5..7])?; let a = match unparsed.len() { 9 => parse_hex(&unparsed[7..9])?, 7 => 255, _ => return Err(crate::ParseError::Color(unparsed.to_string())), }; Ok(Self { r, g, b, a }) } } /// Deserialize a `Color` from either a string or a struct. impl<'de> Deserialize<'de> for Color { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum ColorDe { Struct { r: u8, g: u8, b: u8, a: u8 }, String(String), } match ColorDe::deserialize(deserializer)? { ColorDe::Struct { r, g, b, a } => Ok(Self { r, g, b, a }), ColorDe::String(str) => { Self::from_str(&str).map_err(serde::de::Error::custom) } } } } ================================================ FILE: packages/wm-platform/src/models/corner_style.rs ================================================ use serde::{Deserialize, Serialize}; /// Corner style of a window's frame. /// /// # Platform-specific /// /// Only has an effect on Windows 11. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum CornerStyle { #[default] Default, Square, Rounded, SmallRounded, } ================================================ FILE: packages/wm-platform/src/models/delta.rs ================================================ use std::str::FromStr; use serde::Serialize; /// A wrapper that indicates a value should be interpreted as a delta /// (relative change). #[derive(Debug, Clone, Copy, PartialEq, Serialize)] pub struct Delta { pub inner: T, pub is_negative: bool, } impl> FromStr for Delta { type Err = crate::ParseError; fn from_str(unparsed: &str) -> Result { let unparsed = unparsed.trim(); let (raw, is_negative) = match unparsed.chars().next() { Some('+') => (&unparsed[1..], false), Some('-') => (&unparsed[1..], true), // No sign is interpreted as positive. _ => (unparsed, false), }; if raw.is_empty() { return Err(crate::ParseError::Delta(unparsed.to_string())); } let inner = T::from_str(raw)?; Ok(Self { inner, is_negative }) } } ================================================ FILE: packages/wm-platform/src/models/direction.rs ================================================ use std::str::FromStr; use serde::Serialize; #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum Direction { Left, Right, Up, Down, } impl Direction { /// Gets the inverse of a given direction. /// /// Example: /// ``` /// # use wm_platform::Direction; /// let dir = Direction::Left.inverse(); /// assert_eq!(dir, Direction::Right); /// ``` #[must_use] pub fn inverse(&self) -> Direction { match self { Direction::Left => Direction::Right, Direction::Right => Direction::Left, Direction::Up => Direction::Down, Direction::Down => Direction::Up, } } } impl FromStr for Direction { type Err = crate::ParseError; /// Parses a string into a direction. /// /// Example: /// ``` /// # use wm_platform::Direction; /// # use std::str::FromStr; /// let dir = Direction::from_str("left"); /// assert_eq!(dir.unwrap(), Direction::Left); /// ``` fn from_str(unparsed: &str) -> Result { match unparsed { "left" => Ok(Direction::Left), "right" => Ok(Direction::Right), "up" => Ok(Direction::Up), "down" => Ok(Direction::Down), _ => Err(crate::ParseError::Direction(unparsed.to_string())), } } } ================================================ FILE: packages/wm-platform/src/models/key.rs ================================================ use std::{fmt, str::FromStr}; use serde::{Deserialize, Serialize}; /// Platform-specific keyboard key code. /// /// Represents the raw key code from the underlying platform's keyboard /// API. /// /// # Platform-specific /// /// - **Windows**: `u16` (Virtual key code from Windows API). See /// - **macOS**: `i64` (Virtual key code from `CGEvent`). See #[derive( Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, )] pub struct KeyCode( #[cfg(target_os = "windows")] pub(crate) u16, #[cfg(target_os = "macos")] pub(crate) i64, ); impl fmt::Display for KeyCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[derive(Debug, thiserror::Error)] pub enum KeyParseError { #[error("Unknown key: {0}")] UnknownKey(String), } /// Cross-platform key representation. #[allow(clippy::unsafe_derive_deserialize)] #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, )] #[serde(rename_all = "snake_case")] pub enum Key { // Letter keys A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, // Number keys D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, // Function keys F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24, // Modifier keys Cmd, Ctrl, Alt, Shift, Win, LCmd, RCmd, LCtrl, RCtrl, LAlt, RAlt, LShift, RShift, LWin, RWin, // Special keys Space, Tab, Enter, Delete, Escape, Backspace, // Arrow keys Left, Right, Up, Down, // Navigation keys Home, End, PageUp, PageDown, Insert, // Lock keys NumLock, ScrollLock, CapsLock, // Numpad Numpad0, Numpad1, Numpad2, Numpad3, Numpad4, Numpad5, Numpad6, Numpad7, Numpad8, Numpad9, NumpadAdd, NumpadSubtract, NumpadMultiply, NumpadDivide, NumpadDecimal, // Media keys VolumeUp, VolumeDown, VolumeMute, MediaNextTrack, MediaPrevTrack, MediaStop, MediaPlayPause, PrintScreen, // Language-specific keys Muhenkan, Henkan, // OEM keys OemSemicolon, OemQuestion, OemTilde, OemOpenBrackets, OemPipe, OemCloseBrackets, OemQuotes, Oem8, Oem102, OemPlus, OemComma, OemMinus, OemPeriod, } impl Key { /// Attempts to parse a key from a literal string (e.g. `a`, `;`, `à`). /// /// # Platform-specific /// /// - **macOS**: Not implemented. Returns `KeyParseError::UnknownKey` for /// all keys. /// /// # Errors /// /// Returns `KeyParseError::UnknownKey` if the key is not found on the /// current keyboard layout. pub fn try_from_literal(key_str: &str) -> Result { #[cfg(target_os = "macos")] { Err(KeyParseError::UnknownKey(key_str.to_string())) } #[cfg(target_os = "windows")] { use windows::Win32::UI::Input::KeyboardAndMouse::{ GetKeyboardLayout, VkKeyScanExW, }; // Check if the key exists on the current keyboard layout. let utf16_key = key_str .encode_utf16() .next() .ok_or_else(|| KeyParseError::UnknownKey(key_str.to_string()))?; let layout = unsafe { GetKeyboardLayout(0) }; let vk_code = unsafe { VkKeyScanExW(utf16_key, layout) }; if vk_code == -1 { return Err(KeyParseError::UnknownKey(key_str.to_string())); } // The low-order byte contains the virtual-key code and the high- // order byte contains the shift state. let [high_order, low_order] = vk_code.to_be_bytes(); // Key is valid if it doesn't require shift or alt to be pressed. match high_order { 0 => Key::try_from(KeyCode(u16::from(low_order))) .map_err(|_| KeyParseError::UnknownKey(key_str.to_string())), _ => Err(KeyParseError::UnknownKey(key_str.to_string())), } } } } /// Generates `FromStr` and `Display` implementations for the `Key` enum. /// /// Each variant can have multiple string aliases, with the first used for /// the `Display` implementation. /// /// # Example /// ```no_run,compile_fail /// impl_key_parsing! { /// Enter => ["enter", "return", "cr"], /// Space => ["space", "spacebar", " "], /// } /// ``` macro_rules! impl_key_parsing { ($( $variant:ident => [$($str_name:literal),+ $(,)?]),* $(,)?) => { impl FromStr for Key { type Err = KeyParseError; fn from_str(key_str: &str) -> Result { match key_str.to_ascii_lowercase().as_str() { $($($str_name)|+ => Ok(Key::$variant),)* _ => Err(KeyParseError::UnknownKey(key_str.to_string())), } } } impl fmt::Display for Key { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { $(Key::$variant => { // Return the first string alias as the display name. let aliases = &[$($str_name),+]; write!(f, "{}", aliases[0]) },)* } } } impl Key { /// Returns all string aliases for this key variant. pub fn all_aliases(&self) -> Option<&'static [&'static str]> { match self { $(Key::$variant => Some(&[$($str_name),+]),)* } } } }; } impl_key_parsing! { // Letter keys A => ["a"], B => ["b"], C => ["c"], D => ["d"], E => ["e"], F => ["f"], G => ["g"], H => ["h"], I => ["i"], J => ["j"], K => ["k"], L => ["l"], M => ["m"], N => ["n"], O => ["o"], P => ["p"], Q => ["q"], R => ["r"], S => ["s"], T => ["t"], U => ["u"], V => ["v"], W => ["w"], X => ["x"], Y => ["y"], Z => ["z"], // Number keys D0 => ["0", "d0"], D1 => ["1", "d1"], D2 => ["2", "d2"], D3 => ["3", "d3"], D4 => ["4", "d4"], D5 => ["5", "d5"], D6 => ["6", "d6"], D7 => ["7", "d7"], D8 => ["8", "d8"], D9 => ["9", "d9"], // Function keys F1 => ["f1"], F2 => ["f2"], F3 => ["f3"], F4 => ["f4"], F5 => ["f5"], F6 => ["f6"], F7 => ["f7"], F8 => ["f8"], F9 => ["f9"], F10 => ["f10"], F11 => ["f11"], F12 => ["f12"], F13 => ["f13"], F14 => ["f14"], F15 => ["f15"], F16 => ["f16"], F17 => ["f17"], F18 => ["f18"], F19 => ["f19"], F20 => ["f20"], F21 => ["f21"], F22 => ["f22"], F23 => ["f23"], F24 => ["f24"], // Modifier keys Cmd => ["cmd"], Ctrl => ["ctrl", "control"], Alt => ["alt", "menu"], Shift => ["shift"], Win => ["win"], LCmd => ["lcmd"], RCmd => ["rcmd"], LCtrl => ["lctrl"], RCtrl => ["rctrl"], LAlt => ["lalt", "lmenu"], RAlt => ["ralt", "rmenu"], LShift => ["lshift"], RShift => ["rshift"], LWin => ["lwin"], RWin => ["rwin"], // Special keys Space => ["space"], Tab => ["tab"], Enter => ["enter", "return"], Delete => ["delete"], Escape => ["escape"], Backspace => ["backspace"], // Arrow keys Left => ["left"], Right => ["right"], Up => ["up"], Down => ["down"], // Navigation keys Home => ["home"], End => ["end"], PageUp => ["page_up"], PageDown => ["page_down"], Insert => ["insert"], // Lock keys NumLock => ["num_lock"], ScrollLock => ["scroll_lock"], CapsLock => ["caps_lock"], // Numpad Numpad0 => ["numpad0"], Numpad1 => ["numpad1"], Numpad2 => ["numpad2"], Numpad3 => ["numpad3"], Numpad4 => ["numpad4"], Numpad5 => ["numpad5"], Numpad6 => ["numpad6"], Numpad7 => ["numpad7"], Numpad8 => ["numpad8"], Numpad9 => ["numpad9"], NumpadAdd => ["numpad_add", "add"], NumpadSubtract => ["numpad_subtract", "subtract"], NumpadMultiply => ["numpad_multiply", "multiply"], NumpadDivide => ["numpad_divide", "divide"], NumpadDecimal => ["numpad_decimal", "decimal"], // Media keys VolumeUp => ["volume_up"], VolumeDown => ["volume_down"], VolumeMute => ["volume_mute"], MediaNextTrack => ["media_next_track"], MediaPrevTrack => ["media_prev_track"], MediaStop => ["media_stop"], MediaPlayPause => ["media_play_pause"], PrintScreen => ["print_screen"], // OEM keys OemSemicolon => ["oem_semicolon"], OemQuestion => ["oem_question"], OemTilde => ["oem_tilde"], OemOpenBrackets => ["oem_open_brackets"], OemPipe => ["oem_pipe"], OemCloseBrackets => ["oem_close_brackets"], OemQuotes => ["oem_quotes"], Oem8 => ["oem_8"], Oem102 => ["oem_102"], OemPlus => ["oem_plus"], OemComma => ["oem_comma"], OemMinus => ["oem_minus"], OemPeriod => ["oem_period"], // Language-specific keys Muhenkan => ["muhenkan"], Henkan => ["henkan"], } #[cfg(test)] mod tests { use super::*; #[test] fn test_key_parsing() { assert_eq!("a".parse::().unwrap(), Key::A); assert_eq!("cmd".parse::().unwrap(), Key::Cmd); assert_eq!("f1".parse::().unwrap(), Key::F1); assert_eq!("space".parse::().unwrap(), Key::Space); assert_eq!("enter".parse::().unwrap(), Key::Enter); assert_eq!("return".parse::().unwrap(), Key::Enter); // Should be case-insensitive. assert_eq!("Shift".parse::().unwrap(), Key::Shift); assert_eq!("CTRL".parse::().unwrap(), Key::Ctrl); assert_eq!("F1".parse::().unwrap(), Key::F1); // Should fail for unknown keys. assert!("invalid".parse::().is_err()); } #[test] fn test_key_display() { assert_eq!(Key::A.to_string(), "a"); assert_eq!(Key::Cmd.to_string(), "cmd"); assert_eq!(Key::F1.to_string(), "f1"); assert_eq!(Key::Space.to_string(), "space"); } } ================================================ FILE: packages/wm-platform/src/models/key_code.rs ================================================ #[cfg(target_os = "windows")] use windows::Win32::UI::Input::KeyboardAndMouse::{ VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_A, VK_ADD, VK_B, VK_BACK, VK_C, VK_CAPITAL, VK_CONVERT, VK_D, VK_DECIMAL, VK_DELETE, VK_DIVIDE, VK_DOWN, VK_E, VK_END, VK_ESCAPE, VK_F, VK_F1, VK_F10, VK_F11, VK_F12, VK_F13, VK_F14, VK_F15, VK_F16, VK_F17, VK_F18, VK_F19, VK_F2, VK_F20, VK_F21, VK_F22, VK_F23, VK_F24, VK_F3, VK_F4, VK_F5, VK_F6, VK_F7, VK_F8, VK_F9, VK_G, VK_H, VK_HOME, VK_I, VK_INSERT, VK_J, VK_K, VK_L, VK_LCONTROL, VK_LEFT, VK_LMENU, VK_LSHIFT, VK_LWIN, VK_M, VK_MEDIA_NEXT_TRACK, VK_MEDIA_PLAY_PAUSE, VK_MEDIA_PREV_TRACK, VK_MEDIA_STOP, VK_MULTIPLY, VK_N, VK_NEXT, VK_NONCONVERT, VK_NUMLOCK, VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7, VK_NUMPAD8, VK_NUMPAD9, VK_O, VK_OEM_1, VK_OEM_102, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_P, VK_PRIOR, VK_Q, VK_R, VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, VK_RWIN, VK_S, VK_SCROLL, VK_SNAPSHOT, VK_SPACE, VK_SUBTRACT, VK_T, VK_TAB, VK_U, VK_UP, VK_V, VK_VOLUME_DOWN, VK_VOLUME_MUTE, VK_VOLUME_UP, VK_W, VK_X, VK_Y, VK_Z, }; use crate::{Key, KeyCode}; #[derive(Debug, thiserror::Error)] pub enum KeyConversionError { #[error("Unknown key code: {0}")] UnknownKeyCode(KeyCode), } /// Generates `TryFrom` implementations for converting between `Key` and /// `KeyCode`. /// /// For Windows, the key code is assumed to be a `VK_*` constant (accessed /// via .0). /// /// # Example /// ```no_run,compile_fail /// impl_key_code_conversion! { /// Enter => { windows: VK_RETURN, macos: 0x24, }, /// Space => { windows: VK_SPACE, macos: 0x31, }, /// PrintScreen => { windows: VK_SNAPSHOT, }, // Only supported on Windows. /// } /// ``` macro_rules! impl_key_code_conversion { ( $( $variant:ident => { $(windows: $win_code:expr,)? $(macos: $mac_code:expr,)? } ),* $(,)? ) => { #[cfg(target_os = "windows")] impl TryFrom for Key { type Error = KeyConversionError; fn try_from(key_code: KeyCode) -> Result { let vk = VIRTUAL_KEY(key_code.0); $($(if vk == $win_code { return Ok(Key::$variant); })?)* Err(KeyConversionError::UnknownKeyCode(key_code)) } } #[cfg(target_os = "macos")] impl TryFrom for Key { type Error = KeyConversionError; fn try_from(key_code: KeyCode) -> Result { $($(if key_code.0 == $mac_code { return Ok(Key::$variant); })?)* Err(KeyConversionError::UnknownKeyCode(key_code)) } } impl TryFrom for KeyCode { type Error = KeyConversionError; fn try_from(key: Key) -> Result { match key { $(Key::$variant => { #[cfg(target_os = "windows")] { $(return Ok(KeyCode($win_code.0));)? #[allow(unreachable_code)] return Err(KeyConversionError::UnknownKeyCode(KeyCode(0))); } #[cfg(target_os = "macos")] { $(return Ok(KeyCode($mac_code));)? #[allow(unreachable_code)] return Err(KeyConversionError::UnknownKeyCode(KeyCode(0))); } }),* } } } }; } impl_key_code_conversion! { // Letter keys A => { windows: VK_A, macos: 0x00, }, B => { windows: VK_B, macos: 0x0B, }, C => { windows: VK_C, macos: 0x08, }, D => { windows: VK_D, macos: 0x02, }, E => { windows: VK_E, macos: 0x0E, }, F => { windows: VK_F, macos: 0x03, }, G => { windows: VK_G, macos: 0x05, }, H => { windows: VK_H, macos: 0x04, }, I => { windows: VK_I, macos: 0x22, }, J => { windows: VK_J, macos: 0x26, }, K => { windows: VK_K, macos: 0x28, }, L => { windows: VK_L, macos: 0x25, }, M => { windows: VK_M, macos: 0x2E, }, N => { windows: VK_N, macos: 0x2D, }, O => { windows: VK_O, macos: 0x1F, }, P => { windows: VK_P, macos: 0x23, }, Q => { windows: VK_Q, macos: 0x0C, }, R => { windows: VK_R, macos: 0x0F, }, S => { windows: VK_S, macos: 0x01, }, T => { windows: VK_T, macos: 0x11, }, U => { windows: VK_U, macos: 0x20, }, V => { windows: VK_V, macos: 0x09, }, W => { windows: VK_W, macos: 0x0D, }, X => { windows: VK_X, macos: 0x07, }, Y => { windows: VK_Y, macos: 0x10, }, Z => { windows: VK_Z, macos: 0x06, }, // Number keys D0 => { windows: VK_0, macos: 0x1D, }, D1 => { windows: VK_1, macos: 0x12, }, D2 => { windows: VK_2, macos: 0x13, }, D3 => { windows: VK_3, macos: 0x14, }, D4 => { windows: VK_4, macos: 0x15, }, D5 => { windows: VK_5, macos: 0x17, }, D6 => { windows: VK_6, macos: 0x16, }, D7 => { windows: VK_7, macos: 0x1A, }, D8 => { windows: VK_8, macos: 0x1C, }, D9 => { windows: VK_9, macos: 0x19, }, // Function keys F1 => { windows: VK_F1, macos: 0x7A, }, F2 => { windows: VK_F2, macos: 0x78, }, F3 => { windows: VK_F3, macos: 0x63, }, F4 => { windows: VK_F4, macos: 0x76, }, F5 => { windows: VK_F5, macos: 0x60, }, F6 => { windows: VK_F6, macos: 0x61, }, F7 => { windows: VK_F7, macos: 0x62, }, F8 => { windows: VK_F8, macos: 0x64, }, F9 => { windows: VK_F9, macos: 0x65, }, F10 => { windows: VK_F10, macos: 0x6D, }, F11 => { windows: VK_F11, macos: 0x67, }, F12 => { windows: VK_F12, macos: 0x6F, }, F13 => { windows: VK_F13, macos: 0x69, }, F14 => { windows: VK_F14, macos: 0x6B, }, F15 => { windows: VK_F15, macos: 0x71, }, F16 => { windows: VK_F16, macos: 0x6A, }, F17 => { windows: VK_F17, macos: 0x40, }, F18 => { windows: VK_F18, macos: 0x4F, }, F19 => { windows: VK_F19, macos: 0x50, }, F20 => { windows: VK_F20, macos: 0x5A, }, // Windows-only function keys; macOS has no F21-F24. F21 => { windows: VK_F21, }, F22 => { windows: VK_F22, }, F23 => { windows: VK_F23, }, F24 => { windows: VK_F24, }, // Modifier keys - use platform-specific primary variants LShift => { windows: VK_LSHIFT, macos: 0x38, }, RShift => { windows: VK_RSHIFT, macos: 0x3C, }, LCtrl => { windows: VK_LCONTROL, macos: 0x3B, }, RCtrl => { windows: VK_RCONTROL, macos: 0x3E, }, LAlt => { windows: VK_LMENU, macos: 0x3A, }, RAlt => { windows: VK_RMENU, macos: 0x3D, }, // General modifiers (canonical mapping) Shift => { windows: VK_LSHIFT, macos: 0x38, }, Ctrl => { windows: VK_LCONTROL, macos: 0x3B, }, Alt => { windows: VK_LMENU, macos: 0x3A, }, Cmd => { macos: 0x37, }, Win => { windows: VK_LWIN, }, // Platform-specific key mappings (aliases) LWin => { windows: VK_LWIN, }, RWin => { windows: VK_RWIN, }, LCmd => { macos: 0x37, }, RCmd => { macos: 0x36, }, // Special keys Space => { windows: VK_SPACE, macos: 0x31, }, Tab => { windows: VK_TAB, macos: 0x30, }, Enter => { windows: VK_RETURN, macos: 0x24, }, // macOS: Backspace == 0x33, Forward Delete == 0x75 Delete => { windows: VK_DELETE, macos: 0x75, }, Escape => { windows: VK_ESCAPE, macos: 0x35, }, Backspace => { windows: VK_BACK, macos: 0x33, }, // Arrow keys Left => { windows: VK_LEFT, macos: 0x7B, }, Right => { windows: VK_RIGHT, macos: 0x7C, }, Up => { windows: VK_UP, macos: 0x7E, }, Down => { windows: VK_DOWN, macos: 0x7D, }, // Navigation keys Home => { windows: VK_HOME, macos: 0x73, }, End => { windows: VK_END, macos: 0x77, }, PageUp => { windows: VK_PRIOR, macos: 0x74, }, PageDown => { windows: VK_NEXT, macos: 0x79, }, Insert => { windows: VK_INSERT, macos: 0x72, }, // Note: macOS 0x72 is Help // OEM keys OemSemicolon => { windows: VK_OEM_1, macos: 0x29, }, OemQuestion => { windows: VK_OEM_2, macos: 0x2C, }, OemTilde => { windows: VK_OEM_3, macos: 0x32, }, OemOpenBrackets => { windows: VK_OEM_4, macos: 0x21, }, OemPipe => { windows: VK_OEM_5, macos: 0x2A, }, OemCloseBrackets => { windows: VK_OEM_6, macos: 0x1E, }, OemQuotes => { windows: VK_OEM_7, macos: 0x27, }, Oem8 => { windows: VK_OEM_8, }, Oem102 => { windows: VK_OEM_102, }, OemPlus => { windows: VK_OEM_PLUS, macos: 0x18, }, OemComma => { windows: VK_OEM_COMMA, macos: 0x2B, }, OemMinus => { windows: VK_OEM_MINUS, macos: 0x1B, }, OemPeriod => { windows: VK_OEM_PERIOD, macos: 0x2F, }, // Numpad Numpad0 => { windows: VK_NUMPAD0, macos: 0x52, }, Numpad1 => { windows: VK_NUMPAD1, macos: 0x53, }, Numpad2 => { windows: VK_NUMPAD2, macos: 0x54, }, Numpad3 => { windows: VK_NUMPAD3, macos: 0x55, }, Numpad4 => { windows: VK_NUMPAD4, macos: 0x56, }, Numpad5 => { windows: VK_NUMPAD5, macos: 0x57, }, Numpad6 => { windows: VK_NUMPAD6, macos: 0x58, }, Numpad7 => { windows: VK_NUMPAD7, macos: 0x59, }, Numpad8 => { windows: VK_NUMPAD8, macos: 0x5B, }, Numpad9 => { windows: VK_NUMPAD9, macos: 0x5C, }, NumpadAdd => { windows: VK_ADD, macos: 0x45, }, NumpadSubtract => { windows: VK_SUBTRACT, macos: 0x4E, }, NumpadMultiply => { windows: VK_MULTIPLY, macos: 0x43, }, NumpadDivide => { windows: VK_DIVIDE, macos: 0x4B, }, NumpadDecimal => { windows: VK_DECIMAL, macos: 0x41, }, // Lock keys NumLock => { windows: VK_NUMLOCK, macos: 0x47, }, ScrollLock => { windows: VK_SCROLL, macos: 0x6B, }, CapsLock => { windows: VK_CAPITAL, macos: 0x39, }, // Media keys VolumeUp => { windows: VK_VOLUME_UP, macos: 0x48, }, VolumeDown => { windows: VK_VOLUME_DOWN, macos: 0x49, }, VolumeMute => { windows: VK_VOLUME_MUTE, macos: 0x4A, }, // TODO: Verify these media keys for macOS. MediaNextTrack => { windows: VK_MEDIA_NEXT_TRACK, macos: 0x42, }, MediaPrevTrack => { windows: VK_MEDIA_PREV_TRACK, macos: 0x4D, }, MediaStop => { windows: VK_MEDIA_STOP, macos: 0x4C, }, MediaPlayPause => { windows: VK_MEDIA_PLAY_PAUSE, macos: 0x34, }, PrintScreen => { windows: VK_SNAPSHOT, }, // Language-specific keys Muhenkan => { windows: VK_NONCONVERT, }, Henkan => { windows: VK_CONVERT, }, } #[cfg(test)] mod tests { use super::*; #[test] fn test_key_conversion_roundtrip() { let test_keys = [ Key::A, Key::S, Key::D, Key::F, Key::Cmd, Key::LAlt, Key::RCtrl, Key::LShift, Key::Space, Key::Tab, Key::Enter, Key::F1, Key::F12, Key::Left, Key::Right, ]; for key in test_keys { let code: KeyCode = key.try_into().unwrap(); let key2: Key = code.try_into().unwrap(); assert_eq!(key, key2, "Roundtrip failed for key: {key:?}"); } } #[test] fn test_platform_specific_key_code() { #[cfg(target_os = "windows")] { let code = KeyCode::try_from(Key::Win); assert!(code.is_ok()); let code2 = KeyCode::try_from(Key::Cmd); assert!(code2.is_err()); } #[cfg(target_os = "macos")] { let code = KeyCode::try_from(Key::Win); assert!(code.is_err()); let code2 = KeyCode::try_from(Key::Cmd); assert!(code2.is_ok()); } } } ================================================ FILE: packages/wm-platform/src/models/length_value.rs ================================================ use std::str::FromStr; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize}; #[derive(Debug, Clone, PartialEq, Serialize)] pub struct LengthValue { pub amount: f32, pub unit: LengthUnit, } #[derive(Debug, Deserialize, Clone, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum LengthUnit { Percentage, Pixel, } impl LengthValue { #[must_use] pub fn from_px(px: i32) -> Self { Self { #[allow(clippy::cast_precision_loss)] amount: px as f32, unit: LengthUnit::Pixel, } } #[must_use] pub fn to_px(&self, total_px: i32, scale_factor: Option) -> i32 { let scale_factor = scale_factor.unwrap_or(1.0); #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] match self.unit { LengthUnit::Percentage => (self.amount * total_px as f32) as i32, LengthUnit::Pixel => (self.amount * scale_factor) as i32, } } #[must_use] pub fn to_percentage(&self, total_px: i32) -> f32 { match self.unit { LengthUnit::Percentage => self.amount, #[allow(clippy::cast_precision_loss)] LengthUnit::Pixel => self.amount / total_px as f32, } } } impl FromStr for LengthValue { type Err = crate::ParseError; /// Parses a string containing a number followed by a unit (`px`, `%`). /// Allows for negative numbers. /// /// Example: /// ``` /// # use wm_platform::{LengthValue, LengthUnit}; /// # use std::str::FromStr; /// let check = LengthValue { /// amount: 100.0, /// unit: LengthUnit::Pixel, /// }; /// let parsed = LengthValue::from_str("100px"); /// assert_eq!(parsed.unwrap(), check); /// ``` fn from_str(unparsed: &str) -> Result { let units_regex = Regex::new(r"([+-]?\d+)(%|px)?").expect("Invalid regex."); let captures = units_regex .captures(unparsed) .ok_or(crate::ParseError::Length(unparsed.to_string()))?; let unit = match captures.get(2).map_or("", |m| m.as_str()) { "px" | "" => LengthUnit::Pixel, "%" => LengthUnit::Percentage, _ => return Err(crate::ParseError::Length(unparsed.to_string())), }; let amount = captures .get(1) .and_then(|m| m.as_str().parse::().ok()) // Store percentage units as a fraction of 1. .map(|amount| { if unit == LengthUnit::Percentage { amount / 100.0 } else { amount } }) .ok_or(crate::ParseError::Length(unparsed.to_string()))?; Ok(LengthValue { amount, unit }) } } /// Deserialize a `LengthValue` from either a string or a struct. impl<'de> Deserialize<'de> for LengthValue { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum LengthValueDe { Struct { amount: f32, unit: LengthUnit }, String(String), } match LengthValueDe::deserialize(deserializer)? { LengthValueDe::Struct { amount, unit } => Ok(Self { amount, unit }), LengthValueDe::String(str) => { Self::from_str(&str).map_err(serde::de::Error::custom) } } } } ================================================ FILE: packages/wm-platform/src/models/mod.rs ================================================ mod color; mod corner_style; mod delta; mod direction; mod key; mod key_code; mod length_value; mod opacity_value; mod point; mod rect; mod rect_delta; pub use color::*; pub use corner_style::*; pub use delta::*; pub use direction::*; pub use key::*; pub use key_code::*; pub use length_value::*; pub use opacity_value::*; pub use point::*; pub use rect::*; pub use rect_delta::*; ================================================ FILE: packages/wm-platform/src/models/opacity_value.rs ================================================ use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; #[derive(Debug, Clone, PartialEq, Serialize)] pub struct OpacityValue(pub f32); impl OpacityValue { #[must_use] pub fn to_alpha(&self) -> u8 { #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let alpha = (self.0 * 255.0).round() as u8; alpha } #[must_use] pub fn from_alpha(alpha: u8) -> Self { Self(f32::from(alpha) / 255.0) } } impl Default for OpacityValue { fn default() -> Self { Self(1.0) } } impl FromStr for OpacityValue { type Err = crate::ParseError; /// Parses a string for an opacity value. The string must be a percentage /// or a decimal number. /// /// Example: /// ``` /// # use wm_platform::{OpacityValue}; /// # use std::str::FromStr; /// let check = OpacityValue(0.75); /// let parsed = OpacityValue::from_str("75%"); /// assert_eq!(parsed.unwrap(), check); /// ``` fn from_str(unparsed: &str) -> Result { let unparsed = unparsed.trim(); if unparsed.ends_with('%') { let percentage = unparsed .trim_end_matches('%') .parse::() .map_err(|_| crate::ParseError::Opacity(unparsed.to_string()))?; Ok(Self(percentage / 100.0)) } else { unparsed .parse::() .map(Self) .map_err(|_| crate::ParseError::Opacity(unparsed.to_string())) } } } /// Deserialize an `OpacityValue` from either a number or a string. impl<'de> Deserialize<'de> for OpacityValue { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged, rename_all = "camelCase")] enum OpacityValueDe { Number(f32), String(String), } match OpacityValueDe::deserialize(deserializer)? { OpacityValueDe::Number(num) => Ok(Self(num)), OpacityValueDe::String(str) => { Self::from_str(&str).map_err(serde::de::Error::custom) } } } } ================================================ FILE: packages/wm-platform/src/models/point.rs ================================================ /// Represents an x-y coordinate. #[derive(Debug, Clone)] pub struct Point { pub x: i32, pub y: i32, } impl Point { /// Calculates the Euclidean distance between this point and another /// point. #[must_use] pub fn distance_between(&self, other: &Point) -> f32 { let dx = self.x - other.x; let dy = self.y - other.y; #[allow(clippy::cast_precision_loss)] ((dx * dx + dy * dy) as f32).sqrt() } } ================================================ FILE: packages/wm-platform/src/models/rect.rs ================================================ use serde::{Deserialize, Serialize}; use crate::{Direction, LengthValue, Point, RectDelta}; #[derive(Debug, Deserialize, Clone, Serialize, Eq, PartialEq)] pub enum Corner { TopLeft, TopRight, BottomLeft, BottomRight, } #[derive(Debug, Deserialize, Clone, Serialize, Eq, PartialEq)] pub struct Rect { /// X-coordinate of the left edge of the rectangle. pub left: i32, /// Y-coordinate of the top edge of the rectangle. pub top: i32, /// X-coordinate of the right edge of the rectangle. pub right: i32, /// Y-coordinate of the bottom edge of the rectangle. pub bottom: i32, } impl Rect { /// Creates a new `Rect` instance from the coordinates of its left, top, /// right, and bottom edges. #[must_use] pub fn from_ltrb(left: i32, top: i32, right: i32, bottom: i32) -> Self { Self { left, top, right, bottom, } } /// Creates a new `Rect` instance from its X/Y coordinates and size. #[must_use] pub fn from_xy(x: i32, y: i32, width: i32, height: i32) -> Self { Self { left: x, top: y, right: x + width, bottom: y + height, } } #[must_use] pub fn x(&self) -> i32 { self.left } #[must_use] pub fn y(&self) -> i32 { self.top } #[must_use] pub fn width(&self) -> i32 { self.right - self.left } #[must_use] pub fn height(&self) -> i32 { self.bottom - self.top } #[must_use] pub fn translate_to_coordinates(&self, x: i32, y: i32) -> Self { Self::from_xy(x, y, self.width(), self.height()) } #[must_use] pub fn translate_to_center(&self, outer_rect: &Rect) -> Self { Self::translate_to_coordinates( self, outer_rect.left + (outer_rect.width() / 2) - (self.width() / 2), outer_rect.top + (outer_rect.height() / 2) - (self.height() / 2), ) } #[must_use] pub fn translate_in_direction( &self, direction: &Direction, distance: i32, ) -> Rect { let (delta_x, delta_y) = match direction { Direction::Up => (0, -distance), Direction::Down => (0, distance), Direction::Left => (-distance, 0), Direction::Right => (distance, 0), }; Self::from_xy( self.x() + delta_x, self.y() + delta_y, self.width(), self.height(), ) } /// Returns a new `Rect` that is clamped within the bounds of the given /// outer rectangle. Attempts to preserve the width and height of the /// original rectangle. #[must_use] pub fn clamp(&self, outer_rect: &Rect) -> Self { Self::from_xy( self.left.max(outer_rect.left), self.top.max(outer_rect.top), self.width().min(outer_rect.width()), self.height().min(outer_rect.height()), ) } #[must_use] pub fn clamp_size(&self, width: i32, height: i32) -> Self { Self::from_xy( self.x(), self.y(), self.width().min(width), self.height().min(height), ) } #[must_use] pub fn center_point(&self) -> Point { Point { x: self.left + (self.width() / 2), y: self.top + (self.height() / 2), } } #[must_use] pub fn corner(&self, corner: &Corner) -> Point { match corner { Corner::TopLeft => Point { x: self.left, y: self.top, }, Corner::TopRight => Point { x: self.right, y: self.top, }, Corner::BottomLeft => Point { x: self.left, y: self.bottom, }, Corner::BottomRight => Point { x: self.right, y: self.bottom, }, } } /// Gets the delta between this rect and another rect. #[must_use] pub fn delta(&self, other: &Rect) -> RectDelta { RectDelta { left: LengthValue::from_px(other.left - self.left), top: LengthValue::from_px(other.top - self.top), right: LengthValue::from_px(self.right - other.right), bottom: LengthValue::from_px(self.bottom - other.bottom), } } #[must_use] pub fn apply_delta( &self, delta: &RectDelta, scale_factor: Option, ) -> Self { Self::from_ltrb( self.left - delta.left.to_px(self.width(), scale_factor), self.top - delta.top.to_px(self.height(), scale_factor), self.right + delta.right.to_px(self.width(), scale_factor), self.bottom + delta.bottom.to_px(self.height(), scale_factor), ) } // Gets the amount of overlap between the x-coordinates of the two rects. #[must_use] pub fn x_overlap(&self, other: &Rect) -> i32 { self.right.min(other.right) - self.x().max(other.x()) } // Gets the amount of overlap between the y-coordinates of the two rects. #[must_use] pub fn y_overlap(&self, other: &Rect) -> i32 { self.bottom.min(other.bottom) - self.y().max(other.y()) } /// Gets the intersection area of this rect and another rect. #[must_use] pub fn intersection_area(&self, other: &Rect) -> i32 { let x_overlap = self.x_overlap(other); let y_overlap = self.y_overlap(other); if x_overlap > 0 && y_overlap > 0 { x_overlap * y_overlap } else { 0 } } #[must_use] pub fn contains_point(&self, point: &Point) -> bool { let is_in_x = point.x >= self.left && point.x <= self.right; let is_in_y = point.y >= self.top && point.y <= self.bottom; is_in_x && is_in_y } /// Gets whether this rect fully encloses another rect. #[must_use] pub fn contains_rect(&self, other: &Rect) -> bool { self.left <= other.left && self.top <= other.top && self.right >= other.right && self.bottom >= other.bottom } /// Creates a new rect that is inset by the given amount of pixels on all /// sides. /// /// The `inset_px` can be a positive number to create a smaller rect /// (inset), or a negative number to create a larger rect (outset). #[must_use] pub fn inset(&self, inset_px: i32) -> Self { Self::from_ltrb( self.left + inset_px, self.top + inset_px, self.right - inset_px, self.bottom - inset_px, ) } #[must_use] pub fn distance_to_point(&self, point: &Point) -> f32 { let dx = (self.x() - point.x) .abs() .max((self.x() + self.width() - point.x).abs()); let dy = (self.y() - point.y) .abs() .max((self.y() + self.height() - point.y).abs()); #[allow(clippy::cast_precision_loss)] ((dx * dx + dy * dy) as f32).sqrt() } /// Returns the union of this rect and another rect. /// /// The union is the smallest rect that contains both rects, taking the /// minimum left/top and maximum right/bottom coordinates. #[must_use] pub fn union(&self, other: &Rect) -> Self { Self::from_ltrb( self.left.min(other.left), self.top.min(other.top), self.right.max(other.right), self.bottom.max(other.bottom), ) } } #[cfg(test)] mod tests { use super::*; #[test] fn intersection_area() { // Full overlap. let r1 = Rect::from_xy(0, 0, 100, 100); let r2 = Rect::from_xy(0, 0, 100, 100); assert_eq!(r1.intersection_area(&r2), 10000); // 100 * 100 // Partial overlap. let r1 = Rect::from_xy(0, 0, 100, 100); let r2 = Rect::from_xy(50, 50, 100, 100); assert_eq!(r1.intersection_area(&r2), 2500); // 50 * 50 // No overlap. let r1 = Rect::from_xy(0, 0, 100, 100); let r2 = Rect::from_xy(200, 200, 100, 100); assert_eq!(r1.intersection_area(&r2), 0); // No overlap (edges touching). let r1 = Rect::from_xy(0, 0, 100, 100); let r2 = Rect::from_xy(100, 0, 100, 100); assert_eq!(r1.intersection_area(&r2), 0); } } ================================================ FILE: packages/wm-platform/src/models/rect_delta.rs ================================================ use serde::{Deserialize, Serialize}; use super::LengthValue; #[derive(Debug, Deserialize, Clone, Serialize)] pub struct RectDelta { /// The delta in x-coordinates on the left of the rectangle. pub left: LengthValue, /// The delta in y-coordinates on the top of the rectangle. pub top: LengthValue, /// The delta in x-coordinates on the right of the rectangle. pub right: LengthValue, /// The delta in y-coordinates on the bottom of the rectangle. pub bottom: LengthValue, } impl RectDelta { #[must_use] pub fn new( left: LengthValue, top: LengthValue, right: LengthValue, bottom: LengthValue, ) -> Self { Self { left, top, right, bottom, } } /// Checks if the rectangle delta has a value greater than 1.0(px/%) for /// any of its sides. #[must_use] pub fn is_significant(&self) -> bool { self.bottom.amount > 1.0 || self.top.amount > 1.0 || self.left.amount > 1.0 || self.right.amount > 1.0 } /// Creates a new `RectDelta` with all sides set to 0px. #[must_use] pub fn zero() -> Self { Self::new( LengthValue::from_px(0), LengthValue::from_px(0), LengthValue::from_px(0), LengthValue::from_px(0), ) } /// Gets the inverse of this delta by negating all values. /// /// Returns a new `RectDelta` instance. #[must_use] pub fn inverse(&self) -> Self { RectDelta::new( LengthValue { amount: -self.left.amount, unit: self.left.unit.clone(), }, LengthValue { amount: -self.top.amount, unit: self.top.unit.clone(), }, LengthValue { amount: -self.right.amount, unit: self.right.unit.clone(), }, LengthValue { amount: -self.bottom.amount, unit: self.bottom.unit.clone(), }, ) } } ================================================ FILE: packages/wm-platform/src/mouse_listener.rs ================================================ use tokio::sync::mpsc; use crate::{platform_event::MouseEvent, platform_impl, Dispatcher}; /// Available mouse events that [`MouseListener`] can listen for. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum MouseEventKind { Move, LeftButtonDown, LeftButtonUp, RightButtonDown, RightButtonUp, } /// A listener for system-wide mouse events. pub struct MouseListener { /// Receiver for outgoing mouse events. event_rx: mpsc::UnboundedReceiver, /// Inner platform-specific mouse listener. inner: platform_impl::MouseListener, } impl MouseListener { /// Creates a new [`MouseListener`] with the specified enabled events. pub fn new( enabled_events: &[MouseEventKind], dispatcher: &Dispatcher, ) -> crate::Result { let (event_tx, event_rx) = mpsc::unbounded_channel(); let inner = platform_impl::MouseListener::new( enabled_events, event_tx, dispatcher, )?; Ok(Self { event_rx, inner }) } /// Returns the next mouse event from the listener. /// /// This will block until a mouse event is available. pub async fn next_event(&mut self) -> Option { self.event_rx.recv().await } /// Enables or disables the underlying mouse listener. pub fn enable(&mut self, enabled: bool) -> crate::Result<()> { self.inner.enable(enabled) } /// Updates the set of enabled mouse events to listen for. pub fn set_enabled_events( &mut self, enabled_events: &[MouseEventKind], ) -> crate::Result<()> { self.inner.set_enabled_events(enabled_events) } /// Terminates the mouse listener. pub fn terminate(&mut self) -> crate::Result<()> { self.inner.terminate() } } ================================================ FILE: packages/wm-platform/src/native_window.rs ================================================ #[cfg(target_os = "macos")] use objc2_application_services::AXUIElement; #[cfg(target_os = "macos")] use objc2_core_foundation::{CFBoolean, CFRetained, CFString}; #[cfg(target_os = "windows")] use windows::Win32::{ Foundation::HWND, UI::WindowsAndMessaging::{ SET_WINDOW_POS_FLAGS, WINDOW_EX_STYLE, WINDOW_STYLE, }, }; use crate::{platform_impl, Rect}; #[cfg(target_os = "macos")] use crate::{platform_impl::AXUIElementExt, ThreadBound}; #[cfg(target_os = "windows")] use crate::{Color, CornerStyle, Delta, OpacityValue, RectDelta}; /// Unique identifier of a window. /// /// Can be obtained with `window.id()`. /// /// # Platform-specific /// /// - **Windows**: `isize` (`HWND`) /// - **macOS**: `u32` (`CGWindowID`) #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct WindowId( #[cfg(target_os = "windows")] pub isize, #[cfg(target_os = "macos")] pub u32, ); impl WindowId { #[cfg(target_os = "macos")] pub(crate) fn from_window_element(el: &CFRetained) -> Self { let mut window_id = 0; unsafe { platform_impl::ffi::_AXUIElementGetWindow( CFRetained::as_ptr(el), &raw mut window_id, ) }; Self(window_id) } } #[derive(Clone, Debug, PartialEq, Eq)] pub enum WindowZOrder { Normal, AfterWindow(WindowId), Top, TopMost, } /// macOS-specific extension trait for [`NativeWindow`]. #[cfg(target_os = "macos")] pub trait NativeWindowExtMacOs { /// Gets the `AXUIElement` instance for this window. /// /// # Platform-specific /// /// This method is only available on macOS. fn ax_ui_element(&self) -> &ThreadBound>; /// Gets the bundle ID of the application that owns the window. /// /// # Platform-specific /// /// This method is only available on macOS. fn bundle_id(&self) -> Option; /// Gets the role of the window (e.g. `AXWindow`). /// /// # Platform-specific /// /// This method is only available on macOS. fn role(&self) -> crate::Result; /// Gets the sub-role of the window (e.g. `AXStandardWindow`). /// /// # Platform-specific /// /// This method is only available on macOS. fn subrole(&self) -> crate::Result; /// Whether the window is modal. /// /// # Platform-specific /// /// This method is only available on macOS. fn is_modal(&self) -> crate::Result; /// Whether the window is the main window for its application. /// /// # Platform-specific /// /// This method is only available on macOS. fn is_main(&self) -> crate::Result; } #[cfg(target_os = "macos")] impl NativeWindowExtMacOs for NativeWindow { fn ax_ui_element(&self) -> &ThreadBound> { &self.inner.element } fn bundle_id(&self) -> Option { self.inner.application.bundle_id() } fn role(&self) -> crate::Result { self.inner.element.with(|el| { el.get_attribute::("AXRole") .map(|cf_string| cf_string.to_string()) })? } fn subrole(&self) -> crate::Result { self.inner.element.with(|el| { el.get_attribute::("AXSubrole") .map(|cf_string| cf_string.to_string()) })? } fn is_modal(&self) -> crate::Result { self.inner.element.with(|el| { el.get_attribute::("AXModal") .map(|cf_bool| cf_bool.value()) })? } fn is_main(&self) -> crate::Result { self.inner.element.with(|el| { el.get_attribute::("AXMain") .map(|cf_bool| cf_bool.value()) })? } } /// Windows-specific extensions for [`NativeWindow`]. #[cfg(target_os = "windows")] pub trait NativeWindowWindowsExt { /// Creates a [`NativeWindow`] from a window handle. /// /// # Platform-specific /// /// This method is only available on Windows. fn from_handle(handle: isize) -> NativeWindow; /// Gets the window handle. /// /// # Platform-specific /// /// This method is only available on Windows. fn hwnd(&self) -> HWND; /// Gets the class name of the window. /// /// # Platform-specific /// /// This method is only available on Windows. fn class_name(&self) -> crate::Result; /// Gets the window's frame, including the window's shadow borders. /// /// # Platform-specific /// /// This method is only available on Windows. fn frame_with_shadows(&self) -> crate::Result; /// Gets the delta between the window's frame and the window's border. /// This represents the size of a window's shadow borders. /// /// # Platform-specific /// /// This method is only available on Windows. fn shadow_borders(&self) -> crate::Result; /// Whether the window has an owner window. /// /// # Platform-specific /// /// This method is only available on Windows. fn has_owner_window(&self) -> bool; /// Whether the window has the given window style flag(s) set. /// /// # Platform-specific /// /// This method is only available on Windows. fn has_window_style(&self, style: WINDOW_STYLE) -> bool; /// Whether the window has the given extended window style flag(s) set. /// /// # Platform-specific /// /// This method is only available on Windows. fn has_window_style_ex(&self, style: WINDOW_EX_STYLE) -> bool; /// Thin wrapper around [`SetWindowPos`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos). /// /// # Platform-specific /// /// This method is only available on Windows. fn set_window_pos( &self, z_order: &WindowZOrder, rect: &Rect, flags: SET_WINDOW_POS_FLAGS, ) -> crate::Result<()>; /// Shows the window asynchronously. /// /// NOTE: Cloaked windows do not get shown until uncloaked. /// /// # Platform-specific /// /// This method is only available on Windows. fn show(&self) -> crate::Result<()>; /// Hides the window asynchronously. /// /// # Platform-specific /// /// This method is only available on Windows. fn hide(&self) -> crate::Result<()>; /// Restores the window (unminimizes and unmaximizes). /// /// If `outer_frame` is provided, the window will be restored to the /// specified position. This avoids flickering compared to restoring /// and then repositioning the window. /// /// # Platform-specific /// /// This method is only available on Windows. fn restore(&self, outer_frame: Option<&Rect>) -> crate::Result<()>; /// Cloaks or uncloaks the window. /// /// # Platform-specific /// /// This method is only available on Windows. fn set_cloaked(&self, cloaked: bool) -> crate::Result<()>; /// Marks the window as fullscreen. /// /// Causes the native Windows taskbar to be moved to the bottom of the /// z-order when this window is active. /// /// # Platform-specific /// /// This method is only available on Windows. fn mark_fullscreen(&self, fullscreen: bool) -> crate::Result<()>; /// Adds or removes the window from the native taskbar. /// /// Cloaked windows are normally always shown in the taskbar, but can be /// manually toggled. Hidden windows (`SW_HIDE`) can never be shown in /// the taskbar. /// /// # Platform-specific /// /// This method is only available on Windows. fn set_taskbar_visibility(&self, visible: bool) -> crate::Result<()>; /// Adds the given extended window style flag(s) to the window. /// /// # Platform-specific /// /// This method is only available on Windows. fn add_window_style_ex(&self, style: WINDOW_EX_STYLE); /// Sets the window's z-order. /// /// # Platform-specific /// /// This method is only available on Windows. fn set_z_order(&self, zorder: &WindowZOrder) -> crate::Result<()>; /// Sets the visibility of the window's title bar. /// /// # Platform-specific /// /// This method is only available on Windows. fn set_title_bar_visibility(&self, visible: bool) -> crate::Result<()>; /// Sets the color of the window's border. /// /// # Platform-specific /// /// This method is only available on Windows. fn set_border_color(&self, color: Option<&Color>) -> crate::Result<()>; /// Sets the corner style of the window. /// /// # Platform-specific /// /// This method is only available on Windows. fn set_corner_style( &self, corner_style: &CornerStyle, ) -> crate::Result<()>; /// Sets the transparency of the window. /// /// # Platform-specific /// /// This method is only available on Windows. fn set_transparency( &self, opacity_value: &OpacityValue, ) -> crate::Result<()>; /// Adjusts the window's transparency by a relative delta. /// /// # Platform-specific /// /// This method is only available on Windows. fn adjust_transparency( &self, opacity_delta: &Delta, ) -> crate::Result<()>; } #[cfg(target_os = "windows")] impl NativeWindowWindowsExt for NativeWindow { fn from_handle(handle: isize) -> Self { platform_impl::NativeWindow::new(handle).into() } fn hwnd(&self) -> HWND { self.inner.hwnd() } fn class_name(&self) -> crate::Result { self.inner.class_name() } fn frame_with_shadows(&self) -> crate::Result { self.inner.frame_with_shadows() } fn shadow_borders(&self) -> crate::Result { self.inner.shadow_borders() } fn has_owner_window(&self) -> bool { self.inner.has_owner_window() } fn has_window_style(&self, style: WINDOW_STYLE) -> bool { self.inner.has_window_style(style) } fn has_window_style_ex(&self, style: WINDOW_EX_STYLE) -> bool { self.inner.has_window_style_ex(style) } fn set_window_pos( &self, z_order: &WindowZOrder, rect: &Rect, flags: SET_WINDOW_POS_FLAGS, ) -> crate::Result<()> { self.inner.set_window_pos(z_order, rect, flags) } fn show(&self) -> crate::Result<()> { self.inner.show() } fn hide(&self) -> crate::Result<()> { self.inner.hide() } fn restore(&self, outer_frame: Option<&Rect>) -> crate::Result<()> { self.inner.restore(outer_frame) } fn set_cloaked(&self, cloaked: bool) -> crate::Result<()> { self.inner.set_cloaked(cloaked) } fn mark_fullscreen(&self, fullscreen: bool) -> crate::Result<()> { self.inner.mark_fullscreen(fullscreen) } fn set_taskbar_visibility(&self, visible: bool) -> crate::Result<()> { self.inner.set_taskbar_visibility(visible) } fn add_window_style_ex(&self, style: WINDOW_EX_STYLE) { self.inner.add_window_style_ex(style); } fn set_z_order(&self, z_order: &WindowZOrder) -> crate::Result<()> { self.inner.set_z_order(z_order) } fn set_title_bar_visibility(&self, visible: bool) -> crate::Result<()> { self.inner.set_title_bar_visibility(visible) } fn set_border_color(&self, color: Option<&Color>) -> crate::Result<()> { self.inner.set_border_color(color) } fn set_corner_style( &self, corner_style: &CornerStyle, ) -> crate::Result<()> { self.inner.set_corner_style(corner_style) } fn set_transparency( &self, opacity_value: &OpacityValue, ) -> crate::Result<()> { self.inner.set_transparency(opacity_value) } fn adjust_transparency( &self, opacity_delta: &Delta, ) -> crate::Result<()> { self.inner.adjust_transparency(opacity_delta) } } #[derive(Clone, Debug)] pub struct NativeWindow { pub(crate) inner: platform_impl::NativeWindow, } impl NativeWindow { /// Gets the unique identifier for this window. #[must_use] pub fn id(&self) -> WindowId { self.inner.id() } /// Gets the window's title. /// /// Note that empty strings are valid window titles. /// /// # Errors /// /// Returns [`Error::WindowNotFound`] if the window is invalid. pub fn title(&self) -> crate::Result { self.inner.title() } pub fn process_name(&self) -> crate::Result { self.inner.process_name() } /// Gets a rectangle of the window's size and position. /// /// # Platform-specific /// /// - **Windows**: Includes the window's shadow borders. /// - **macOS**: If the window was previously resized to a value outside /// of the window's allowed min/max width & height (e.g. via calling /// `set_frame`), this can return those invalid values and might not /// reflect the actual window size. pub fn frame(&self) -> crate::Result { self.inner.frame() } /// Gets the window's position as (x, y) coordinates. pub fn position(&self) -> crate::Result<(f64, f64)> { self.inner.position() } /// Gets the window's size as (width, height). pub fn size(&self) -> crate::Result<(f64, f64)> { self.inner.size() } /// Whether the window is still valid. /// /// Returns `true` if the underlying window is still alive. #[must_use] pub fn is_valid(&self) -> bool { self.inner.is_valid() } /// Whether the window is actually visible. pub fn is_visible(&self) -> crate::Result { self.inner.is_visible() } /// Whether the window is minimized. pub fn is_minimized(&self) -> crate::Result { self.inner.is_minimized() } /// Whether the window is maximized. pub fn is_maximized(&self) -> crate::Result { self.inner.is_maximized() } /// Whether the window can be resized. pub fn is_resizable(&self) -> crate::Result { self.inner.is_resizable() } /// Whether the window is the OS's desktop window. pub fn is_desktop_window(&self) -> crate::Result { self.inner.is_desktop_window() } /// Repositions and resizes the window to the specified rectangle. /// /// # Platform-specific /// /// - **Windows**: Automatically adjusts the `rect` prior to calling [`SetWindowPos`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos) /// to include the window's shadow borders. To set the window's /// position directly, use [`NativeWindowWindowsExt::set_window_pos`]. pub fn set_frame(&self, rect: &Rect) -> crate::Result<()> { self.inner.set_frame(rect) } /// Resizes the window to the specified size. pub fn resize(&self, width: i32, height: i32) -> crate::Result<()> { self.inner.resize(width, height) } /// Repositions the window to the specified position. pub fn reposition(&self, x: i32, y: i32) -> crate::Result<()> { self.inner.reposition(x, y) } pub fn minimize(&self) -> crate::Result<()> { self.inner.minimize() } pub fn maximize(&self) -> crate::Result<()> { self.inner.maximize() } /// Sets focus to the window and raises it to the top of the z-order. pub fn focus(&self) -> crate::Result<()> { self.inner.focus() } /// Closes the window. /// /// # Platform-specific /// /// - **Windows**: This sends a `WM_CLOSE` message to the window. /// - **macOS**: This simulates pressing the close button on the window's /// title bar. pub fn close(&self) -> crate::Result<()> { self.inner.close() } } impl PartialEq for NativeWindow { fn eq(&self, other: &Self) -> bool { self.inner.id() == other.inner.id() } } impl Eq for NativeWindow {} ================================================ FILE: packages/wm-platform/src/platform_event.rs ================================================ use super::NativeWindow; use crate::{ platform_impl::WindowEventNotificationInner, Keybinding, MouseEventKind, Point, WindowId, }; #[derive(Clone, Debug)] pub enum PlatformEvent { Window(WindowEvent), Keybinding(KeybindingEvent), Mouse(MouseEvent), DisplaySettingsChanged, } #[derive(Clone, Debug)] pub enum WindowEvent { /// Window gained focus. Focused { window: NativeWindow, notification: WindowEventNotification, }, /// Window was hidden. Hidden { window: NativeWindow, notification: WindowEventNotification, }, /// Size or position of window has changed. /// /// `is_interactive_start` and `is_interactive_end` indicate whether the /// move or resize was initiated via manual interaction with the /// window's drag handles. /// /// # Platform-specific /// /// - **Windows**: Corresponds to `EVENT_OBJECT_LOCATIONCHANGE`, /// `EVENT_SYSTEM_MOVESIZESTART`, and `EVENT_SYSTEM_MOVESIZEEND`. /// - **macOS**: Corresponds to `AXWindowMoved` and `AXWindowResized`. /// The `is_interactive_start` and `is_interactive_end` flags are /// always `false`. MovedOrResized { window: NativeWindow, is_interactive_start: bool, is_interactive_end: bool, notification: WindowEventNotification, }, /// Window was minimized. Minimized { window: NativeWindow, notification: WindowEventNotification, }, /// Window was restored from minimized state. MinimizeEnded { window: NativeWindow, notification: WindowEventNotification, }, /// Window became visible. Shown { window: NativeWindow, notification: WindowEventNotification, }, /// Window title changed. TitleChanged { window: NativeWindow, notification: WindowEventNotification, }, /// Window was destroyed. Destroyed { window_id: WindowId, notification: WindowEventNotification, }, } impl WindowEvent { /// Get the window handle if available (not available for /// `WindowEvent::Destroyed`). #[must_use] pub fn window(&self) -> Option<&NativeWindow> { match self { Self::Focused { window, .. } | Self::Hidden { window, .. } | Self::MovedOrResized { window, .. } | Self::Minimized { window, .. } | Self::MinimizeEnded { window, .. } | Self::Shown { window, .. } | Self::TitleChanged { window, .. } => Some(window), Self::Destroyed { .. } => None, } } /// Returns the platform-specific window event notification. #[must_use] pub fn notification(&self) -> &WindowEventNotification { match self { Self::Focused { notification, .. } | Self::Hidden { notification, .. } | Self::MovedOrResized { notification, .. } | Self::Minimized { notification, .. } | Self::MinimizeEnded { notification, .. } | Self::Shown { notification, .. } | Self::TitleChanged { notification, .. } | Self::Destroyed { notification, .. } => notification, } } } /// Platform-specific window event notification. /// /// Some events are "synthetic" and do not have a corresponding /// notification (represented by `None`). /// /// Synthetic events can occur when: /// * On macOS, `WindowEvent::Shown` is emitted for new visible windows /// even if a different notification is received first. #[derive(Clone, Debug)] pub struct WindowEventNotification( pub Option, ); #[derive(Clone, Debug)] pub struct KeybindingEvent(pub Keybinding); #[derive(Clone, Debug, Eq, PartialEq)] pub enum MouseButton { Left, Right, } /// Tracks which mouse buttons are currently pressed. #[derive(Clone, Copy, Debug, Default)] pub struct PressedButtons { left: bool, right: bool, } impl PressedButtons { /// Returns whether the given button is currently pressed. #[must_use] pub fn contains(&self, button: &MouseButton) -> bool { match button { MouseButton::Left => self.left, MouseButton::Right => self.right, } } /// Updates button state based on a mouse event. pub(crate) fn update(&mut self, event: MouseEventKind) { match event { MouseEventKind::LeftButtonDown => self.left = true, MouseEventKind::LeftButtonUp => self.left = false, MouseEventKind::RightButtonDown => self.right = true, MouseEventKind::RightButtonUp => self.right = false, MouseEventKind::Move => {} } } } #[derive(Clone, Debug)] pub enum MouseEvent { /// Mouse cursor moved. Move { position: Point, pressed_buttons: PressedButtons, /// Window under cursor. /// /// # Platform-specific /// /// - **macOS**: Sourced from the `CGEvent` field. Unreliable; often /// `None`, with the real window ID appearing sporadically. /// - **Windows**: Always `None`. window_below_cursor: Option, }, /// A mouse button was pressed. ButtonDown { position: Point, button: MouseButton, pressed_buttons: PressedButtons, }, /// A mouse button was released. ButtonUp { position: Point, button: MouseButton, pressed_buttons: PressedButtons, }, } impl MouseEvent { /// Returns the cursor position at the time of the event. /// /// `0,0` is the top-left corner of the primary monitor. #[must_use] pub fn position(&self) -> &Point { match self { Self::Move { position, .. } | Self::ButtonDown { position, .. } | Self::ButtonUp { position, .. } => position, } } /// Returns which mouse buttons were pressed at the time of the event. #[must_use] pub fn pressed_buttons(&self) -> &PressedButtons { match self { Self::Move { pressed_buttons, .. } | Self::ButtonDown { pressed_buttons, .. } | Self::ButtonUp { pressed_buttons, .. } => pressed_buttons, } } } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/application.rs ================================================ use std::sync::Arc; use objc2::rc::Retained; use objc2_app_kit::{ NSApplicationActivationPolicy, NSRunningApplication, NSWorkspace, }; use objc2_application_services::AXUIElement; use objc2_core_foundation::{CFArray, CFRetained}; use objc2_foundation::NSString; use crate::{ platform_impl::{ffi, AXUIElementExt, NativeWindow}, Dispatcher, ThreadBound, WindowId, }; pub type ProcessId = i32; /// Represents a running macOS application. #[derive(Clone, Debug)] pub struct Application { pub(crate) pid: ProcessId, pub(crate) dispatcher: Dispatcher, pub(crate) ns_app: Retained, pub(crate) ax_element: Arc>>, } impl Application { /// Creates an instance of `Application`. pub(crate) fn new( ns_app: Retained, dispatcher: Dispatcher, ) -> Self { let pid = ns_app.processIdentifier(); let ax_element = Arc::new(ThreadBound::new( // Creation of `AXUIElement` for an application does not fail even // if the PID is invalid. Instead, subsequent operations on // the returned `AXUIElement` will error. unsafe { AXUIElement::new_application(pid) }, dispatcher.clone(), )); Self { pid, dispatcher, ns_app, ax_element, } } pub fn focused_window( &self, ) -> crate::Result> { self.ax_element.with(|el| { let focused_window = el.get_attribute::("AXFocusedWindow"); focused_window.map(|window_el| { let window_id = WindowId::from_window_element(&window_el); let window_el = ThreadBound::new(window_el, self.dispatcher.clone()); Some(NativeWindow::new(window_id, window_el, self.clone()).into()) }) })? } pub fn windows(&self) -> crate::Result> { self.ax_element.with(|el| { let windows = el.get_attribute::>("AXWindows"); windows.map(|windows| { windows .iter() .map(|window_el| { let window_id = WindowId::from_window_element(&window_el); let window_el = ThreadBound::new(window_el, self.dispatcher.clone()); NativeWindow::new(window_id, window_el, self.clone()).into() }) .collect() }) })? } pub fn psn(&self) -> crate::Result { let mut psn = ffi::ProcessSerialNumber::default(); if unsafe { ffi::GetProcessForPID(self.pid, &raw mut psn) } != 0 { return Err(crate::Error::Platform( "Failed to get process serial number.".to_string(), )); } Ok(psn) } pub fn bundle_id(&self) -> Option { self .ns_app .bundleIdentifier() .map(|ns_string| ns_string.to_string()) } pub fn process_name(&self) -> Option { self .ns_app .localizedName() .map(|ns_string| ns_string.to_string()) } /// Whether the application is an XPC service. /// /// Windows from XPC services are non-standard and cannot be managed. /// Though XPC services are not intended to accept UI interaction, some /// of Apple's own services have windows (e.g. `QuickLookUIService`, /// used for Finder previews). pub fn is_xpc(&self) -> crate::Result { let psn = self.psn()?; #[allow(clippy::cast_possible_truncation)] let mut process_info = { let mut info = ffi::ProcessInfo::default(); info.info_length = std::mem::size_of::() as u32; info }; if unsafe { ffi::GetProcessInformation(&raw const psn, &raw mut process_info) } != 0 { return Err(crate::Error::Platform( "Failed to get process information.".to_string(), )); } Ok(process_info.r#type.to_be_bytes() == *b"XPC!") } pub fn activation_policy(&self) -> NSApplicationActivationPolicy { self.ns_app.activationPolicy() } /// Whether the application should be observed. pub(crate) fn should_observe(&self) -> bool { if self.activation_policy() == NSApplicationActivationPolicy::Prohibited { return false; } !self.is_xpc().unwrap_or(false) } pub(crate) fn is_hidden(&self) -> bool { self.ns_app.isHidden() } } pub(crate) fn all_applications( dispatcher: &Dispatcher, ) -> crate::Result> { dispatcher.dispatch_sync(|| { let running_apps = NSWorkspace::sharedWorkspace().runningApplications(); running_apps .iter() .map(|app| Application::new(app, dispatcher.clone())) .collect() }) } pub(crate) fn application_for_bundle_id( bundle_id: &str, dispatcher: &Dispatcher, ) -> crate::Result> { let bundle_id = bundle_id.to_owned(); dispatcher.dispatch_sync(|| { let apps = NSRunningApplication::runningApplicationsWithBundleIdentifier( &NSString::from_str(&bundle_id), ); apps .into_iter() .next() .map(|app| Application::new(app, dispatcher.clone())) }) } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/application_observer.rs ================================================ use std::{ ptr::NonNull, sync::{Arc, Mutex}, }; use objc2_application_services::{AXError, AXObserver, AXUIElement}; use objc2_core_foundation::{ kCFRunLoopDefaultMode, CFRetained, CFRunLoop, CFRunLoopSource, CFString, }; use tokio::sync::mpsc; use crate::{ platform_impl::{ Application, NativeWindow, ProcessId, WindowEventNotificationInner, }, NativeWindowExtMacOs, ThreadBound, WindowEvent, WindowId, }; /// Notifications to register for the `AXUIElement` of an application. const AX_APP_NOTIFICATIONS: &[&str] = &["AXFocusedWindowChanged", "AXWindowCreated"]; /// Notifications to register for the `AXUIElement` of a window. const AX_WINDOW_NOTIFICATIONS: &[&str] = &[ "AXTitleChanged", "AXUIElementDestroyed", "AXWindowMoved", "AXWindowResized", "AXWindowDeminiaturized", "AXWindowMiniaturized", ]; /// Context passed to the application event callback. #[derive(Debug)] struct ApplicationEventContext { application: Application, events_tx: mpsc::UnboundedSender, app_windows: Arc>>, observer: CFRetained, } /// Represents an accessibility observer for a specific application. #[derive(Debug)] pub(crate) struct ApplicationObserver { pub(crate) pid: ProcessId, app_windows: Arc>>, events_tx: mpsc::UnboundedSender, _observer: CFRetained, observer_source: CFRetained, } // TODO: Remove this. unsafe impl Send for ApplicationObserver {} impl ApplicationObserver { /// Creates a new `ApplicationObserver` for the given application. /// /// If `is_startup` is `true`, the observer will not emit /// `WindowEvent::Shown` for windows already running on startup. pub fn new( app: &Application, events_tx: mpsc::UnboundedSender, is_startup: bool, ) -> crate::Result { let observer = unsafe { let mut observer = std::ptr::null_mut(); let result = AXObserver::create( app.pid, Some(Self::window_event_callback), // SAFETY: Stack address of `observer` is guaranteed to be // non-null. NonNull::new(&raw mut observer).unwrap(), ); if result != AXError::Success { return Err(crate::Error::Accessibility( "AXObserverCreate".to_string(), result.0, )); } CFRetained::retain(NonNull::new(observer).ok_or_else(|| { crate::Error::InvalidPointer("AXObserver is null.".to_string()) })?) }; let app_windows = Arc::new(Mutex::new(app.windows()?)); let context = Box::into_raw(Box::new(ApplicationEventContext { application: app.clone(), events_tx: events_tx.clone(), app_windows: app_windows.clone(), observer: observer.clone(), })); let runloop = CFRunLoop::current().ok_or(crate::Error::EventLoopStopped)?; let observer_source = unsafe { observer.run_loop_source() }; runloop.add_source(Some(&observer_source), unsafe { kCFRunLoopDefaultMode }); // Register for all window notifications. // TODO: Remove from runloop if registration fails. Self::register_app_notifications(app, &observer, context)?; // Emit `WindowEvent::Shown` for all existing windows. for window in app_windows.lock().unwrap().iter() { if let Err(err) = Self::register_window_notifications(window, &observer, context) { tracing::warn!( "Failed to register window notifications for PID {}: {}", app.pid, err ); } // Don't emit `WindowEvent::Shown` for windows that are already // running on startup. if !is_startup { if let Err(err) = events_tx.send(WindowEvent::Shown { window: window.clone(), notification: crate::WindowEventNotification(None), }) { tracing::warn!( "Failed to send window event for PID {}: {}", app.pid, err ); } } } Ok(Self { pid: app.pid, app_windows, events_tx, _observer: observer, observer_source, }) } fn register_app_notifications( app: &Application, observer: &CFRetained, context: *mut ApplicationEventContext, ) -> crate::Result<()> { for notification in AX_APP_NOTIFICATIONS { unsafe { let notification_cfstr = CFString::from_static_str(notification); let result = observer.add_notification( app.ax_element.get_ref()?, ¬ification_cfstr, context.cast::(), ); if result != AXError::Success { return Err(crate::Error::Platform(format!( "Failed to add notification {} for PID {}: {:?}", notification, app.pid, result ))); } } } Ok(()) } fn register_window_notifications( window: &crate::NativeWindow, observer: &CFRetained, context: *mut ApplicationEventContext, ) -> crate::Result<()> { for notification in AX_WINDOW_NOTIFICATIONS { unsafe { let notification_cfstr = CFString::from_static_str(notification); let result = observer.add_notification( window.ax_ui_element().get_ref()?, ¬ification_cfstr, context.cast::(), ); if result != AXError::Success { return Err(crate::Error::Platform(format!( "Failed to add notification {} for window {}: {:?}", notification, window.id().0, result ))); } } } Ok(()) } pub(crate) fn emit_all_windows_destroyed(&self) { for window in self.app_windows.lock().unwrap().iter() { if let Err(err) = self.events_tx.send(WindowEvent::Destroyed { window_id: window.id(), notification: crate::WindowEventNotification(None), }) { tracing::warn!( "Failed to send window event for PID {}: {}", self.pid, err ); } } } pub(crate) fn emit_all_windows_hidden(&self) { for window in self.app_windows.lock().unwrap().iter() { if let Err(err) = self.events_tx.send(WindowEvent::Hidden { window: window.clone(), notification: crate::WindowEventNotification(None), }) { tracing::warn!( "Failed to send window event for PID {}: {}", self.pid, err ); } } } pub(crate) fn emit_all_windows_shown(&self) { for window in self.app_windows.lock().unwrap().iter() { if let Err(err) = self.events_tx.send(WindowEvent::Shown { window: window.clone(), notification: crate::WindowEventNotification(None), }) { tracing::warn!( "Failed to send window event for PID {}: {}", self.pid, err ); } } } /// Callback function for accessibility window events. #[allow(clippy::too_many_lines)] unsafe extern "C-unwind" fn window_event_callback( _observer: NonNull, element: NonNull, notification_name: NonNull, context: *mut std::ffi::c_void, ) { if context.is_null() { tracing::error!("Window event callback received null context."); return; } let context = &mut *context.cast::(); let ax_element = unsafe { CFRetained::retain(element) }; let notification = WindowEventNotificationInner { name: notification_name.as_ref().to_string(), ax_element_ptr: element.as_ptr().cast::(), }; tracing::debug!( "Received window event: {} for PID: {}", notification.name, context.application.pid ); let found_window = { let app_windows = context.app_windows.lock().unwrap(); app_windows .iter() .find(|window| { window.ax_ui_element().get_ref().ok() == Some(&ax_element) }) .cloned() }; if notification.name.as_str() == "AXUIElementDestroyed" { if let Some(window) = &found_window { context .app_windows .lock() .unwrap() .retain(|w| w.id() != window.id()); if let Err(err) = context.events_tx.send(WindowEvent::Destroyed { window_id: window.id(), notification: crate::WindowEventNotification(Some(notification)), }) { tracing::warn!( "Failed to send window event for PID {}: {}", context.application.pid, err ); } } return; } let is_new_window = found_window.is_none(); let window = found_window.unwrap_or_else(|| { let window_id = WindowId::from_window_element(&ax_element); let ax_element = ThreadBound::new( ax_element, context.application.dispatcher.clone(), ); NativeWindow::new(window_id, ax_element, context.application.clone()) .into() }); if is_new_window { context.app_windows.lock().unwrap().push(window.clone()); let _ = Self::register_window_notifications( &window, &context.observer.clone(), context, ); if let Err(err) = context.events_tx.send(WindowEvent::Shown { window: window.clone(), notification: crate::WindowEventNotification(Some( notification.clone(), )), }) { tracing::warn!( "Failed to send window event for PID {}: {}", context.application.pid, err ); } } let window_event = match notification.name.as_str() { "AXFocusedWindowChanged" => WindowEvent::Focused { window, notification: crate::WindowEventNotification(Some(notification)), }, "AXWindowMoved" | "AXWindowResized" => WindowEvent::MovedOrResized { window, is_interactive_start: false, is_interactive_end: false, notification: crate::WindowEventNotification(Some(notification)), }, "AXWindowMiniaturized" => WindowEvent::Minimized { window, notification: crate::WindowEventNotification(Some(notification)), }, "AXWindowDeminiaturized" => WindowEvent::MinimizeEnded { window, notification: crate::WindowEventNotification(Some(notification)), }, "AXTitleChanged" => WindowEvent::TitleChanged { window, notification: crate::WindowEventNotification(Some(notification)), }, _ => { tracing::debug!( "Unhandled window notification: {} for PID: {}", notification.name, context.application.pid ); return; } }; if let Err(err) = context.events_tx.send(window_event) { tracing::warn!( "Failed to send window event for PID {}: {}", context.application.pid, err ); } } } impl Drop for ApplicationObserver { fn drop(&mut self) { // Invalidate the runloop source. This is thread-safe and is OK to call // after the run loop is stopped. self.observer_source.invalidate(); } } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/ax_ui_element.rs ================================================ use std::ptr::{self, NonNull}; pub use objc2_application_services::{AXError, AXUIElement}; use objc2_core_foundation::{CFRetained, CFString, CFType}; use crate::Error; /// Extension trait for [`AXUIElement`]. pub trait AXUIElementExt { /// Retrieves the value of an accessibility attribute. /// /// # Errors /// /// Returns an error if: /// - The accessibility operation fails (e.g. invalid attribute name). /// - The attribute value cannot be cast to the requested type. fn get_attribute( &self, attribute: &str, ) -> crate::Result>; /// Sets the value of an accessibility attribute. /// /// # Errors /// /// Returns an error if the accessibility operation fails. fn set_attribute>( &self, attribute: &str, value: &CFRetained, ) -> crate::Result<()>; } impl AXUIElementExt for AXUIElement { fn get_attribute( &self, attribute: &str, ) -> crate::Result> { let mut value: *const CFType = ptr::null(); let result = unsafe { self.copy_attribute_value( &CFString::from_str(attribute), // SAFETY: Stack address of `value` is guaranteed to be // non-null. NonNull::new(&raw mut value).unwrap(), ) }; if result != AXError::Success { return Err(Error::Accessibility(attribute.to_string(), result.0)); } NonNull::new(value.cast_mut()) .map(|ptr| unsafe { CFRetained::from_raw(ptr.cast()) }) .ok_or_else(|| { Error::InvalidPointer( "copy_attribute_value returned success but null pointer" .to_string(), ) }) } fn set_attribute>( &self, attribute: &str, value: &CFRetained, ) -> crate::Result<()> { let cf_attribute = CFString::from_str(attribute); let result = unsafe { self.set_attribute_value(&cf_attribute, value.as_ref()) }; if result != AXError::Success { return Err(Error::Accessibility(attribute.to_string(), result.0)); } Ok(()) } } #[cfg(test)] mod tests { use objc2_core_foundation::CFString; use super::*; #[test] fn get_attribute_invalid_attribute_is_err() { let pid = i32::try_from(std::process::id()).expect("pid overflow"); let el = unsafe { AXUIElement::new_application(pid) }; let result = el.get_attribute::("AXDefinitelyNotARealAttribute"); assert!(result.is_err()); } #[test] fn set_attribute_invalid_attribute_is_err() { let pid = i32::try_from(std::process::id()).expect("pid overflow"); let el = unsafe { AXUIElement::new_application(pid) }; let value = CFString::from_str("dummy"); let result = el.set_attribute("AXDefinitelyNotARealAttribute", &value); assert!(result.is_err()); } } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/ax_value.rs ================================================ use std::{ffi::c_void, mem::MaybeUninit, ptr::NonNull}; use objc2_application_services::{AXValue, AXValueType}; use objc2_core_foundation::{ CFRange, CFRetained, CGPoint, CGRect, CGSize, }; /// Trait for types that can be converted to and from `AXValue`. pub trait AXValueTypeMarker: Sized + Copy { /// The `AXValueType` constant for this type. const AX_TYPE: AXValueType; } impl AXValueTypeMarker for CGPoint { const AX_TYPE: AXValueType = AXValueType::CGPoint; } impl AXValueTypeMarker for CGSize { const AX_TYPE: AXValueType = AXValueType::CGSize; } impl AXValueTypeMarker for CGRect { const AX_TYPE: AXValueType = AXValueType::CGRect; } impl AXValueTypeMarker for CFRange { const AX_TYPE: AXValueType = AXValueType::CFRange; } /// Extension trait for [`AXValue`]. pub trait AXValueExt { /// Creates a new `AXValue` from the given value. /// /// This is a wrapper over `AXValue::new` from `objc2`. /// /// # Errors /// /// Returns an error if the `AXValue` creation fails. fn new_strict( val: &T, ) -> crate::Result>; /// Extracts the value from this `AXValue`. /// /// This is a wrapper over `AXValue::value` from `objc2`. /// /// # Errors /// /// Returns an error if: /// - The `AXValue` type doesn't match the requested type `T`. /// - The accessibility framework fails to extract the value. fn value_strict(&self) -> crate::Result; } impl AXValueExt for AXValue { fn new_strict( val: &T, ) -> crate::Result> { let ptr = NonNull::new(std::ptr::from_ref::(val) as *mut c_void) .ok_or_else(|| { crate::Error::InvalidPointer("Value pointer is null".to_string()) })?; unsafe { AXValue::new(T::AX_TYPE, ptr) }.ok_or_else(|| { crate::Error::AXValueCreation(format!( "Failed to create AXValue for type with AX_TYPE {:?}", T::AX_TYPE )) }) } fn value_strict(&self) -> crate::Result { let mut value = MaybeUninit::::uninit(); let ptr = NonNull::new(value.as_mut_ptr().cast::()) .ok_or_else(|| { crate::Error::InvalidPointer( "Value buffer pointer is null".to_string(), ) })?; let success = unsafe { self.value(T::AX_TYPE, ptr) }; if success { Ok(unsafe { value.assume_init() }) } else { Err(crate::Error::AXValueCreation(format!( "Failed to extract value from AXValue for type with AX_TYPE {:?}", T::AX_TYPE ))) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_ax_value_creation_and_extraction() { let point = CGPoint { x: 10.0, y: 20.0 }; let ax_value = AXValue::new_strict(&point).expect("Failed to create AXValue."); let extracted_point: CGPoint = ax_value .value_strict() .expect("Failed to extract value from AXValue."); assert!((point.x - extracted_point.x).abs() < f64::EPSILON); assert!((point.y - extracted_point.y).abs() < f64::EPSILON); } #[test] fn test_ax_value_wrong_type_extraction() { let point = CGPoint { x: 10.0, y: 20.0 }; let ax_value = AXValue::new_strict(&point).expect("Failed to create AXValue."); // Try to extract as `CGSize` instead of `CGPoint`. let result = ax_value.value_strict::(); assert!(result.is_err()); } } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/display.rs ================================================ use std::sync::Arc; use objc2::{rc::Retained, MainThreadMarker}; use objc2_app_kit::NSScreen; use objc2_core_foundation::{CFRetained, CFUUID}; use objc2_core_graphics::{ CGDirectDisplayID, CGDisplayBounds, CGDisplayCopyDisplayMode, CGDisplayMirrorsDisplay, CGDisplayMode, CGDisplayRotation, CGError, CGGetActiveDisplayList, CGGetOnlineDisplayList, CGMainDisplayID, }; use objc2_foundation::{ns_string, NSNumber, NSRect}; use crate::{ platform_impl::ffi, ConnectionState, Dispatcher, DisplayDeviceId, DisplayId, MirroringState, Point, Rect, ThreadBound, }; /// Platform-specific implementation of [`Display`]. #[derive(Clone, Debug)] pub(crate) struct Display { cg_display_id: CGDirectDisplayID, ns_screen: Arc>>, } impl Display { /// Creates an instance of `Display`. pub(crate) fn new( ns_screen: ThreadBound>, ) -> crate::Result { let cg_display_id = ns_screen .with(|screen| { let device_description = screen.deviceDescription(); device_description .objectForKey(ns_string!("NSScreenNumber")) .and_then(|val| { val.downcast_ref::().map(NSNumber::as_u32) }) })? .ok_or(crate::Error::DisplayNotFound)?; Ok(Self { cg_display_id, ns_screen: Arc::new(ns_screen), }) } /// Implements [`Display::id`]. pub(crate) fn id(&self) -> DisplayId { DisplayId(self.cg_display_id) } /// Implements [`Display::name`]. pub(crate) fn name(&self) -> crate::Result { self.ns_screen.with(|screen| { let name = screen.localizedName(); Ok(name.to_string()) })? } /// Implements [`Display::bounds`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn bounds(&self) -> crate::Result { let cg_rect = CGDisplayBounds(self.cg_display_id); #[allow(clippy::cast_possible_truncation)] Ok(Rect::from_xy( cg_rect.origin.x as i32, cg_rect.origin.y as i32, cg_rect.size.width as i32, cg_rect.size.height as i32, )) } /// Implements [`Display::working_area`]. pub(crate) fn working_area(&self) -> crate::Result { let primary_display_bounds = { let bounds = CGDisplayBounds(CGMainDisplayID()); #[allow(clippy::cast_possible_truncation)] Rect::from_xy( bounds.origin.x as i32, bounds.origin.y as i32, bounds.size.width as i32, bounds.size.height as i32, ) }; self.ns_screen.with(|screen| { // Convert `NSScreen.visibleFrame` into the same coordinate space as // `CGDisplayBounds`. Ok(appkit_rect_to_cg_rect( screen.visibleFrame(), &primary_display_bounds, )) })? } /// Implements [`Display::scale_factor`]. pub(crate) fn scale_factor(&self) -> crate::Result { #[allow(clippy::cast_possible_truncation)] self .ns_screen .with(|screen| screen.backingScaleFactor() as f32) } /// Implements [`Display::dpi`]. pub(crate) fn dpi(&self) -> crate::Result { let scale_factor = self.scale_factor()?; #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] Ok((72.0 * scale_factor) as u32) } /// Implements [`Display::is_primary`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn is_primary(&self) -> crate::Result { let main_display_id = CGMainDisplayID(); Ok(self.cg_display_id == main_display_id) } /// Implements [`Display::devices`]. pub(crate) fn devices( &self, ) -> crate::Result> { let main_device = DisplayDevice::new( self.cg_display_id, cg_display_uuid(self.cg_display_id)?, ); // TODO: Get devices that are mirroring this display as well. Ok(vec![main_device.into()]) } /// Implements [`Display::main_device`]. pub(crate) fn main_device(&self) -> crate::Result { self .devices()? .into_iter() .find(|device| { matches!( device.mirroring_state(), Ok(None | Some(MirroringState::Source)) ) }) .ok_or(crate::Error::DisplayNotFound) } /// Implements [`DisplayExtMacOs::cg_display_id`]. pub(crate) fn cg_display_id(&self) -> CGDirectDisplayID { self.cg_display_id } /// Implements [`DisplayExtMacOs::ns_screen`]. pub(crate) fn ns_screen(&self) -> &ThreadBound> { &self.ns_screen } } /// Transforms an AppKit screen rectangle (e.g. `NSScreen.visibleFrame`) /// into Core Graphics coordinate space (e.g. `CGDisplayBounds`). /// /// AppKit has (0,0) at the bottom-left corner of the primary display, /// whereas Core Graphics has it at the top-left corner. So we can convert /// between the two by offsetting the Y-axis by the primary display's /// height. fn appkit_rect_to_cg_rect( appkit_rect: NSRect, primary_display_bounds: &Rect, ) -> Rect { let adjusted_y = f64::from(primary_display_bounds.height()) - (appkit_rect.origin.y + appkit_rect.size.height); #[allow(clippy::cast_possible_truncation)] Rect::from_xy( appkit_rect.origin.x as i32, adjusted_y as i32, appkit_rect.size.width as i32, appkit_rect.size.height as i32, ) } impl From for crate::Display { fn from(display: Display) -> Self { crate::Display { inner: display } } } impl PartialEq for Display { fn eq(&self, other: &Self) -> bool { self.id() == other.id() } } impl Eq for Display {} /// Platform-specific implementation of [`DisplayDevice`]. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct DisplayDevice { cg_display_id: CGDirectDisplayID, uuid: CFRetained, } impl DisplayDevice { /// Creates an instance of `DisplayDevice`. #[must_use] pub(crate) fn new( cg_display_id: CGDirectDisplayID, uuid: CFRetained, ) -> Self { Self { cg_display_id, uuid, } } /// Implements [`DisplayDevice::id`]. pub(crate) fn id(&self) -> DisplayDeviceId { // SAFETY: Can assume that the `CFUUID` is valid regardless of whether // the underlying display device is still alive. let uuid_string = CFUUID::new_string(None, Some(&self.uuid)) .unwrap() .to_string(); DisplayDeviceId(uuid_string) } /// Implements [`DisplayDevice::rotation`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn rotation(&self) -> crate::Result { #[allow(clippy::cast_possible_truncation)] Ok(CGDisplayRotation(self.cg_display_id) as f32) } /// Implements [`DisplayDevice::refresh_rate`]. pub(crate) fn refresh_rate(&self) -> crate::Result { // NOTE: Calling `CGDisplayModeRelease` on cleanup is not needed, since // it's equivalent to `CFRelease` in this case. Ref: https://developer.apple.com/documentation/coregraphics/cgdisplaymoderelease let display_mode = CGDisplayCopyDisplayMode(self.cg_display_id) .ok_or(crate::Error::DisplayModeNotFound)?; let refresh_rate = CGDisplayMode::refresh_rate(Some(&display_mode)); #[allow(clippy::cast_possible_truncation)] Ok(refresh_rate as f32) } /// Implements [`DisplayDevice::is_builtin`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn is_builtin(&self) -> crate::Result { // TODO: Implement this properly. let main_display_id = CGMainDisplayID(); Ok(self.cg_display_id == main_display_id) } /// Implements [`DisplayDevice::connection_state`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn connection_state(&self) -> crate::Result { let display_mode = CGDisplayCopyDisplayMode(self.cg_display_id); // TODO: Implement this properly. if display_mode.is_none() { Ok(ConnectionState::Disconnected) } else { Ok(ConnectionState::Active) } } /// Implements [`DisplayDevice::mirroring_state`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn mirroring_state( &self, ) -> crate::Result> { let mirrored_display = CGDisplayMirrorsDisplay(self.cg_display_id); // TODO: Clean this up. if mirrored_display == 0 { // This display is not mirroring another display // Check if another display is mirroring this one by querying active // displays let mut displays: Vec = vec![0; 32]; let mut display_count: u32 = 0; #[allow(clippy::cast_possible_truncation)] let result = unsafe { CGGetActiveDisplayList( displays.len() as u32, displays.as_mut_ptr(), &raw mut display_count, ) }; if result == CGError::Success { displays.truncate(display_count as usize); for &display_id in &displays { if display_id == self.cg_display_id { continue; // Skip self } let other_mirrored = CGDisplayMirrorsDisplay(display_id); if other_mirrored == self.cg_display_id { // Another display is mirroring this one, so this is the source return Ok(Some(MirroringState::Source)); } } } Ok(None) } else { // This display is mirroring another display, so it's a target Ok(Some(MirroringState::Target)) } } /// Implements [`DisplayDeviceExtMacOs::cg_display_id`]. pub(crate) fn cg_display_id(&self) -> CGDirectDisplayID { self.cg_display_id } } impl From for crate::DisplayDevice { fn from(device: DisplayDevice) -> Self { crate::DisplayDevice { inner: device } } } /// Gets the UUID for a display device from its `CGDirectDisplayID`. /// /// This UUID is stable across reboots, whereas `CGDirectDisplayID` is not. fn cg_display_uuid( cg_display_id: CGDirectDisplayID, ) -> crate::Result> { let ptr = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(cg_display_id) }; ptr.map(|ptr| unsafe { CFRetained::from_raw(ptr) }).ok_or( crate::Error::InvalidPointer( "Failed to create UUID for display device".to_string(), ), ) } /// Implements [`Dispatcher::displays`]. pub(crate) fn all_displays( dispatcher: &Dispatcher, ) -> crate::Result> { dispatcher.dispatch_sync(|| { let mtm = MainThreadMarker::new().ok_or(crate::Error::NotMainThread)?; let mut displays = Vec::new(); for screen in NSScreen::screens(mtm) { let ns_screen = ThreadBound::new(screen, dispatcher.clone()); displays.push(Display::new(ns_screen)?.into()); } Ok(displays) })? } /// Implements [`Dispatcher::display_devices`]. pub(crate) fn all_display_devices( _: &Dispatcher, ) -> crate::Result> { let mut cg_display_ids: Vec = vec![0; 32]; // Max 32 displays let mut display_count: u32 = 0; #[allow(clippy::cast_possible_truncation)] let result = unsafe { CGGetOnlineDisplayList( cg_display_ids.len() as u32, cg_display_ids.as_mut_ptr(), &raw mut display_count, ) }; if result != CGError::Success { return Err(crate::Error::DisplayEnumerationFailed); } cg_display_ids.truncate(display_count as usize); cg_display_ids .into_iter() .map(|cg_display_id| { Ok( DisplayDevice::new(cg_display_id, cg_display_uuid(cg_display_id)?) .into(), ) }) .collect() } /// Implements [`Dispatcher::display_from_point`]. pub(crate) fn display_from_point( point: &Point, dispatcher: &Dispatcher, ) -> crate::Result { let displays = all_displays(dispatcher)?; for display in displays { let bounds = display.bounds()?; if bounds.contains_point(point) { return Ok(display); } } Err(crate::Error::DisplayNotFound) } /// Implements [`Dispatcher::primary_display`]. pub(crate) fn primary_display( dispatcher: &Dispatcher, ) -> crate::Result { dispatcher.dispatch_sync(|| { let mtm = MainThreadMarker::new().ok_or(crate::Error::NotMainThread)?; let ns_screen = ThreadBound::new( NSScreen::mainScreen(mtm).ok_or(crate::Error::DisplayNotFound)?, dispatcher.clone(), ); Display::new(ns_screen).map(Into::into) })? } /// Implements [`Dispatcher::nearest_display`]. /// /// NOTE: This was benchmarked to be 400-600µs on initial retrieval and /// 150-300µs on subsequent retrievals. Using `CGGetDisplaysWithRect` and /// getting the corresponding `NSScreen` was found to be slightly slower /// (700-800µs and then 200-300µs on subsequent retrievals). pub(crate) fn nearest_display( native_window: &crate::NativeWindow, dispatcher: &Dispatcher, ) -> crate::Result { dispatcher.dispatch_sync(|| { // Get the window's frame in screen coordinates. let window_frame = native_window.frame()?; let screens = all_displays(dispatcher)?; let mut best_screen = None; let mut max_intersection_area = 0; // TODO: Clean this up. // Iterate through all screens to find the one with the largest // intersection with the window. for screen in screens { let screen_frame = screen.bounds()?; // Calculate intersection area. let intersection_x = i32::max(window_frame.x(), screen_frame.x()); let intersection_y = i32::max(window_frame.y(), screen_frame.y()); let intersection_width = i32::min( window_frame.x() + window_frame.width(), screen_frame.x() + screen_frame.width(), ) - intersection_x; let intersection_height = i32::min( window_frame.y() + window_frame.height(), screen_frame.y() + screen_frame.height(), ) - intersection_y; // If there's a valid intersection, calculate its area. if intersection_width > 0 && intersection_height > 0 { let area = intersection_width * intersection_height; if area > max_intersection_area { max_intersection_area = area; best_screen = Some(screen); } } } // If we found a screen with intersection, use it. Otherwise, if the // window is off-screen, use the main screen. best_screen .or_else(|| primary_display(dispatcher).ok()) .ok_or(crate::Error::DisplayNotFound) })? } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/display_listener.rs ================================================ use std::time::{Duration, Instant}; use objc2::rc::Retained; use tokio::sync::mpsc; use crate::{ platform_impl::{ NotificationCenter, NotificationEvent, NotificationName, NotificationObserver, }, Dispatcher, ThreadBound, }; /// Platform-specific implementation of [`DisplayListener`]. pub(crate) struct DisplayListener { /// Notification observer bound to the main thread. observer: Option>>, } impl DisplayListener { /// Creates an instance of `DisplayListener`. pub(crate) fn new( event_tx: mpsc::UnboundedSender<()>, dispatcher: &Dispatcher, ) -> crate::Result { let dispatcher_clone = dispatcher.clone(); let observer = dispatcher.dispatch_sync(move || { Self::add_observers(event_tx, dispatcher_clone) })?; Ok(Self { observer: Some(observer), }) } /// Implements [`DisplayListener::terminate`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn terminate(&mut self) -> crate::Result<()> { // On macOS 10.11+, observer subscriptions are cleaned up automatically // without calling `removeObserver`. // Ref: https://developer.apple.com/documentation/foundation/notificationcenter/removeobserver(_:name:object:) // // Dropping the `NotificationObserver` also drops its channel sender, // causing the listener thread to exit. self.observer.take(); Ok(()) } /// Registers notification observers on the main thread. fn add_observers( event_tx: mpsc::UnboundedSender<()>, dispatcher: Dispatcher, ) -> ThreadBound> { let (observer, mut events_rx) = NotificationObserver::new(); let mut default_center = NotificationCenter::default_center(); let mut workspace_center = NotificationCenter::workspace_center(); // Add observer which will fire when displays are connected and // disconnected, resolution changes, or arrangement changes. unsafe { default_center.add_observer( NotificationName::ApplicationDidChangeScreenParameters, &observer, None, ); } // Add observers for system sleep and wake events. unsafe { workspace_center.add_observer( NotificationName::WorkspaceWillSleep, &observer, None, ); workspace_center.add_observer( NotificationName::WorkspaceDidWake, &observer, None, ); } std::thread::spawn(move || { // Duration to suppress display change events after wake. macOS fires // several notifications after waking from sleep, and displays can // take 1-2 seconds to be reported as online. const WAKE_COALESCE_DURATION: Duration = Duration::from_secs(5); let mut is_asleep = false; let mut wake_time: Option = None; // Loop exits when the sender is dropped in `Self::terminate`. while let Some(event) = events_rx.blocking_recv() { match event { NotificationEvent::WorkspaceWillSleep => { is_asleep = true; } NotificationEvent::WorkspaceDidWake => { is_asleep = false; wake_time = Some(Instant::now()); // Send a single display change event after the coalesce // duration to pick up any changes that occurred during wake. let event_tx = event_tx.clone(); std::thread::spawn(move || { std::thread::sleep(WAKE_COALESCE_DURATION); if let Err(err) = event_tx.send(()) { tracing::warn!( "Failed to send display change event: {}", err ); } }); } NotificationEvent::ApplicationDidChangeScreenParameters => { // Ignore display change events while asleep or within the // coalesce duration after wake. if is_asleep || wake_time .is_some_and(|t| t.elapsed() < WAKE_COALESCE_DURATION) { continue; } // Coalesce duration has passed; clear it. if wake_time.is_some() { wake_time = None; } if let Err(err) = event_tx.send(()) { tracing::warn!( "Failed to send display change event: {}", err ); break; } } _ => {} } } tracing::debug!("Display listener thread exited."); }); ThreadBound::new(observer, dispatcher) } } impl Drop for DisplayListener { fn drop(&mut self) { let _ = self.terminate(); } } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/event_loop.rs ================================================ use std::sync::{ atomic::{AtomicBool, Ordering}, mpsc, Arc, }; use objc2::MainThreadMarker; use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy}; use objc2_core_foundation::{ kCFRunLoopDefaultMode, CFRetained, CFRunLoop, CFRunLoopSource, CFRunLoopSourceContext, }; use crate::{DispatchFn, Dispatcher}; /// Source for dispatching callbacks onto the event loop thread. #[derive(Clone)] pub(crate) struct EventLoopSource { dispatch_tx: mpsc::Sender>, source: CFRetained, run_loop: CFRetained, pub(crate) thread_id: std::thread::ThreadId, } impl EventLoopSource { pub(crate) fn send_dispatch_async( &self, dispatch_fn: F, ) -> crate::Result<()> where F: FnOnce() + Send + 'static, { // TODO: Avoid duplicate check in `dispatch_sync`. if std::thread::current().id() == self.thread_id { dispatch_fn(); return Ok(()); } self .dispatch_tx .send(Box::new(dispatch_fn)) .map_err(|_| crate::Error::ChannelSend)?; // Signal the run loop source, which schedules the `perform` callback // to be invoked. If signaled multiple times in a short period, this // gets coalesced into a single signal. self.source.signal(); // Wake up the run loop to process the signal. self.run_loop.wake_up(); Ok(()) } pub(crate) fn send_dispatch_sync( &self, dispatch_fn: F, ) -> crate::Result<()> where F: FnOnce() + Send, { // SAFETY: Usage of this function needs to be in a synchronous // context where the dispatch function will be executed before the // caller's stack frame is dropped. let dispatch_fn_static = unsafe { std::mem::transmute::< Box, Box, >(Box::new(dispatch_fn)) }; self.send_dispatch_async(dispatch_fn_static) } pub(crate) fn send_stop(&self) -> crate::Result<()> { let (result_tx, result_rx) = std::sync::mpsc::channel(); self.send_dispatch_sync(|| { let mtm = unsafe { MainThreadMarker::new_unchecked() }; // Call `stop()` to mark the run loop for termination. let ns_app = NSApplication::sharedApplication(mtm); ns_app.stop(None); // `stop()` only takes effect after processing a subsequent UI event. // Post a dummy event so the application actually exits. ns_app.abortModal(); let _ = result_tx.send(()); })?; result_rx .recv_timeout(std::time::Duration::from_secs(3)) .map_err(crate::Error::ChannelRecv) } } // SAFETY: `CFRunLoop` and `CFRunLoopSource` are thread-safe types. The // `objc2` bindings don't implement `Send + Sync`. unsafe impl Send for EventLoopSource {} unsafe impl Sync for EventLoopSource {} /// Platform-specific implementation of [`EventLoop`]. pub(crate) struct EventLoop { source: EventLoopSource, stopped: Arc, } impl EventLoop { /// Implements [`EventLoop::new`]. pub fn new() -> crate::Result<(Self, Dispatcher)> { // Add a new run loop source that allows dispatching from any thread. let source = Self::add_dispatch_source()?; let stopped = Arc::new(AtomicBool::new(false)); let dispatcher = Dispatcher::new(Some(source.clone()), stopped.clone()); Ok(( Self { source: source.clone(), stopped, }, dispatcher, )) } /// Implements [`EventLoop::run`]. #[allow(clippy::unused_self)] pub fn run(self) -> crate::Result<()> { let mtm = MainThreadMarker::new().ok_or(crate::Error::NotMainThread)?; tracing::info!("Starting macOS event loop."); NSApplication::sharedApplication(mtm).run(); tracing::info!("macOS event loop exiting."); Ok(()) } /// Adds a source (`CFRunLoopSource`) for allowing dispatches to /// the current run loop. /// /// Can only be called on the main thread. pub(crate) fn add_dispatch_source() -> crate::Result { let mtm = MainThreadMarker::new().ok_or(crate::Error::NotMainThread)?; // Initialize `NSApplication` on the main thread. This is necessary for // some AppKit components (e.g. system tray) to be functional. // TODO: Skip this if not on the main thread, and instead run a normal // run loop. let ns_app = NSApplication::sharedApplication(mtm); ns_app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); let (dispatch_tx, dispatch_rx) = mpsc::channel(); let dispatch_rx_ptr = Box::into_raw(Box::new(dispatch_rx)).cast::(); // Create `CFRunLoopSource` context. let mut context = CFRunLoopSourceContext { version: 0, info: dispatch_rx_ptr, retain: None, release: Some(Self::runloop_source_released_callback), copyDescription: None, equal: None, hash: None, schedule: None, cancel: None, perform: Some(Self::runloop_signaled_callback), }; // Create the run loop source. let source = unsafe { CFRunLoopSource::new(None, 0, &raw mut context) }.ok_or( crate::Error::Platform( "Failed to create run loop source.".to_string(), ), )?; let run_loop = CFRunLoop::current().ok_or(crate::Error::EventLoopStopped)?; run_loop.add_source(Some(&source), unsafe { kCFRunLoopDefaultMode }); Ok(EventLoopSource { dispatch_tx, source, run_loop, thread_id: std::thread::current().id(), }) } // This function is called by the `CFRunLoopSource` when signaled. extern "C-unwind" fn runloop_signaled_callback( info: *mut std::ffi::c_void, ) { let callbacks = unsafe { &*(info as *const mpsc::Receiver>) }; // Process any pending dispatched callbacks. Multiple run loop signals // may be coalesced together (calling `perform` only once), so it's // important to drain all pending callbacks. for callback in callbacks.try_iter() { callback(); } } // This function is called when the `CFRunLoopSource` is released. extern "C-unwind" fn runloop_source_released_callback( info: *const std::ffi::c_void, ) { // SAFETY: This pointer was created with `Box::into_raw` in // `add_dispatch_source`, so it can safely be converted back to a `Box` // and dropped. let _ = unsafe { Box::from_raw(info as *mut mpsc::Receiver>) }; } } impl Drop for EventLoop { fn drop(&mut self) { tracing::info!("Shutting down event loop."); // Stop the run loop if not already stopped. if !self.stopped.load(Ordering::SeqCst) { let _ = self.source.send_stop(); } // Invalidate the runloop source to trigger its release callback. This // is thread-safe and is OK to call after the run loop is stopped. self.source.source.invalidate(); } } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/ffi.rs ================================================ use std::{ffi::c_void, ptr::NonNull}; use objc2_application_services::{AXError, AXUIElement}; use objc2_core_foundation::CFUUID; use objc2_core_graphics::{CGDirectDisplayID, CGError, CGWindowID}; use crate::platform_impl::ProcessId; /// Carbon process serial number (PSN), used to uniquely identify a /// process. #[derive(Clone, Debug, Default)] #[repr(C)] pub struct ProcessSerialNumber { high: u32, low: u32, } /// Carbon process information, populated by `GetProcessInformation`. #[derive(Default)] #[repr(C, packed(2))] pub(crate) struct ProcessInfo { pub(crate) info_length: u32, name: *const u8, psn: ProcessSerialNumber, pub(crate) r#type: u32, signature: u32, mode: u32, location: *const u8, size: u32, free_mem: u32, launcher: ProcessSerialNumber, launch_date: u32, active_time: u32, app_ref: *const u8, } pub const CPS_USER_GENERATED: u32 = 0x200; #[link(name = "ApplicationServices", kind = "framework")] unsafe extern "C" { // Deprecated in macOS 10.9 in late 2014, but still works fine. pub(crate) fn GetProcessForPID( pid: ProcessId, psn: *mut ProcessSerialNumber, ) -> u32; // Deprecated in macOS 10.9 in late 2014, but still works fine. pub(crate) fn GetProcessInformation( psn: *const ProcessSerialNumber, process_info: *mut ProcessInfo, ) -> u32; // `CGDisplayCreateUUIDFromDisplayID` comes from the `ColorSync` // framework, which is a subframework of `ApplicationServices`. pub(crate) fn CGDisplayCreateUUIDFromDisplayID( display: CGDirectDisplayID, ) -> Option>; } unsafe extern "C" { pub(crate) fn _AXUIElementGetWindow( elem: NonNull, window_id: *mut CGWindowID, ) -> AXError; } #[link(name = "SkyLight", kind = "framework")] unsafe extern "C" { pub(crate) fn _SLPSSetFrontProcessWithOptions( psn: &ProcessSerialNumber, window_id: i32, mode: u32, ) -> CGError; pub(crate) fn SLPSPostEventRecordTo( psn: &ProcessSerialNumber, event: *const c_void, ) -> CGError; } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/keyboard_hook.rs ================================================ use std::{os::raw::c_void, ptr::NonNull}; use objc2_core_foundation::{ kCFRunLoopCommonModes, CFMachPort, CFRetained, CFRunLoop, }; use objc2_core_graphics::{ CGEvent, CGEventField, CGEventFlags, CGEventMask, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventTapProxy, CGEventType, }; use crate::{Dispatcher, Error, Key, KeyCode, ThreadBound}; /// A key event received from the keyboard hook. #[derive(Clone, Debug)] pub struct KeyEvent { /// The key that was pressed or released. pub key: Key, /// Key code that generated this event. #[allow(dead_code)] pub key_code: KeyCode, /// Whether the event is for a key press or release. pub is_keypress: bool, /// Modifier key flags at the time of the event. event_flags: CGEventFlags, } impl KeyEvent { /// Gets whether the specified key is currently pressed. pub fn is_key_down(&self, key: Key) -> bool { match key { Key::Cmd | Key::LCmd | Key::RCmd | Key::Win | Key::LWin | Key::RWin => { self.event_flags & CGEventFlags::MaskCommand != CGEventFlags::empty() } Key::Alt | Key::LAlt | Key::RAlt => { self.event_flags & CGEventFlags::MaskAlternate != CGEventFlags::empty() } Key::Ctrl | Key::LCtrl | Key::RCtrl => { self.event_flags & CGEventFlags::MaskControl != CGEventFlags::empty() } Key::Shift | Key::LShift | Key::RShift => { self.event_flags & CGEventFlags::MaskShift != CGEventFlags::empty() } _ => { // TODO: For non-modifier keys, check using CGEventSourceStateID. false } } } } /// Data shared with the `CGEventTap` callback. struct CallbackData { callback: Box bool + Send + Sync + 'static>, } /// A system-wide low-level keyboard hook. #[derive(Debug)] pub struct KeyboardHook { /// Mach port for the created `CGEventTap`. tap_port: Option>>, /// Pointer to [`CallbackData`], used by the `CGEventTap` callback. callback_ptr: Option, } impl KeyboardHook { /// Creates an instance of `KeyboardHook`. /// /// The callback is called for every keyboard event and returns `true` if /// the event should be intercepted. pub fn new( callback: F, dispatcher: &Dispatcher, ) -> crate::Result where F: Fn(KeyEvent) -> bool + Send + Sync + 'static, { let callback_ptr = { let data = Box::new(CallbackData { callback: Box::new(callback), }); Box::into_raw(data) as usize }; let tap_port = dispatcher .dispatch_sync(|| Self::create_event_tap(callback_ptr, dispatcher)) .flatten() .inspect_err(|_| { // Clean up the callback data if event tap creation fails. let _ = unsafe { Box::from_raw(callback_ptr as *mut CallbackData) }; })?; Ok(Self { tap_port: Some(tap_port), callback_ptr: Some(callback_ptr), }) } /// Terminates the keyboard hook by invalidating the event tap. #[allow(clippy::unnecessary_wraps)] pub fn terminate(&mut self) -> crate::Result<()> { if let Some(tap) = self.tap_port.take() { // Invalidate the event tap to stop it from receiving events. This // also invalidates the run loop source. // See: https://developer.apple.com/documentation/corefoundation/cfmachportinvalidate(_:) let _ = tap.with(|tap| CFMachPort::invalidate(tap)); } // Clean up the callback data if it exists. if let Some(ptr) = self.callback_ptr.take() { let _ = unsafe { Box::from_raw(ptr as *mut CallbackData) }; } Ok(()) } /// Creates a `CGEventTap` object. fn create_event_tap( callback_ptr: usize, dispatcher: &Dispatcher, ) -> crate::Result>> { let mask: CGEventMask = (1u64 << u64::from(CGEventType::KeyDown.0)) | (1u64 << u64::from(CGEventType::KeyUp.0)); let tap_port = unsafe { CGEvent::tap_create( CGEventTapLocation::SessionEventTap, CGEventTapPlacement::HeadInsertEventTap, CGEventTapOptions::Default, mask, Some(Self::keyboard_event_callback), callback_ptr as *mut c_void, ) .ok_or_else(|| { Error::Platform( "Failed to create `CGEventTap`. Accessibility permissions may be required." .to_string(), ) }) }?; let loop_source = CFMachPort::new_run_loop_source(None, Some(&tap_port), 0) .ok_or_else(|| { Error::Platform("Failed to create loop source".to_string()) })?; let current_loop = CFRunLoop::current().ok_or_else(|| { Error::Platform("Failed to get current run loop".to_string()) })?; current_loop .add_source(Some(&loop_source), unsafe { kCFRunLoopCommonModes }); CGEvent::tap_enable(&tap_port, true); Ok(ThreadBound::new(tap_port, dispatcher.clone())) } /// Callback function for keyboard events. /// /// For use with `CGEventTap`. extern "C-unwind" fn keyboard_event_callback( _proxy: CGEventTapProxy, event_type: CGEventType, mut event: NonNull, user_info: *mut c_void, ) -> *mut CGEvent { if user_info.is_null() { tracing::error!("Null pointer passed to keyboard event callback."); return unsafe { event.as_mut() }; } // Extract the key code of the pressed/released key. let key_code = KeyCode(unsafe { CGEvent::integer_value_field( Some(event.as_ref()), CGEventField::KeyboardEventKeycode, ) }); // Try to convert the key code to a known key. let Ok(key) = Key::try_from(key_code) else { return unsafe { event.as_mut() }; }; let event_flags = unsafe { CGEvent::flags(Some(event.as_ref())) }; let key_event = KeyEvent { key, key_code, is_keypress: event_type == CGEventType::KeyDown, event_flags, }; // Get callback from user data and invoke it. let data = unsafe { &*(user_info as *const CallbackData) }; let should_intercept = (data.callback)(key_event); if should_intercept { std::ptr::null_mut() } else { unsafe { event.as_mut() } } } } impl Drop for KeyboardHook { fn drop(&mut self) { let _ = self.terminate(); } } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/mod.rs ================================================ mod application; mod application_observer; mod ax_ui_element; mod ax_value; mod display; mod display_listener; mod event_loop; pub(crate) mod ffi; mod keyboard_hook; mod mouse_listener; mod native_window; mod notification_center; mod single_instance; mod window_listener; pub(crate) use application::*; pub(crate) use application_observer::*; pub(crate) use ax_ui_element::*; pub(crate) use ax_value::*; pub(crate) use display::*; pub(crate) use display_listener::*; pub(crate) use event_loop::*; pub(crate) use keyboard_hook::*; pub(crate) use mouse_listener::*; pub(crate) use native_window::*; pub(crate) use notification_center::*; pub(crate) use single_instance::*; pub(crate) use window_listener::*; ================================================ FILE: packages/wm-platform/src/platform_impl/macos/mouse_listener.rs ================================================ use std::{ os::raw::c_void, ptr::NonNull, time::{Duration, Instant}, }; use objc2_core_foundation::{ kCFRunLoopCommonModes, CFMachPort, CFRetained, CFRunLoop, }; use objc2_core_graphics::{ CGEvent, CGEventField, CGEventMask, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventTapProxy, CGEventType, }; use tokio::sync::mpsc; use crate::{ mouse_listener::MouseEventKind, platform_event::{MouseButton, MouseEvent, PressedButtons}, Dispatcher, Error, Point, ThreadBound, WindowId, }; /// Data shared with the `CGEventTap` callback. struct CallbackData { event_tx: mpsc::UnboundedSender, /// Pressed button state tracked from events. pressed_buttons: PressedButtons, /// Timestamp of the last emitted `Move` event for throttling. last_move_emission: Option, } impl CallbackData { fn new(event_tx: mpsc::UnboundedSender) -> Self { Self { event_tx, pressed_buttons: PressedButtons::default(), last_move_emission: None, } } } /// Platform-specific implementation of [`MouseListener`]. #[derive(Debug)] pub(crate) struct MouseListener { dispatcher: Dispatcher, event_tx: mpsc::UnboundedSender, /// Mach port for the created `CGEventTap`. tap_port: Option>>, /// Pointer to [`CallbackData`], used by the `CGEventTap` callback. callback_data_ptr: Option, } impl MouseListener { /// Implements [`MouseListener::new`]. pub(crate) fn new( enabled_events: &[MouseEventKind], event_tx: mpsc::UnboundedSender, dispatcher: &Dispatcher, ) -> crate::Result { let callback_data_ptr = { let data = Box::new(CallbackData::new(event_tx.clone())); Box::into_raw(data) as usize }; let tap_port = dispatcher .dispatch_sync(|| { Self::create_event_tap( enabled_events, callback_data_ptr, dispatcher, ) }) .flatten() .inspect_err(|_| { // Clean up the callback data if event tap creation fails. let _ = unsafe { Box::from_raw(callback_data_ptr as *mut CallbackData) }; })?; Ok(Self { tap_port: Some(tap_port), callback_data_ptr: Some(callback_data_ptr), dispatcher: dispatcher.clone(), event_tx, }) } /// Implements [`MouseListener::enable`]. pub(crate) fn enable(&mut self, enabled: bool) -> crate::Result<()> { if let Some(tap_port) = &self.tap_port { tap_port.with(|tap| CGEvent::tap_enable(tap, enabled))?; } Ok(()) } /// Implements [`MouseListener::set_enabled_events`]. pub(crate) fn set_enabled_events( &mut self, enabled_events: &[MouseEventKind], ) -> crate::Result<()> { let _ = self.terminate(); let callback_data_ptr = { let data = Box::new(CallbackData::new(self.event_tx.clone())); Box::into_raw(data) as usize }; let tap_port = self .dispatcher .dispatch_sync(|| { Self::create_event_tap( enabled_events, callback_data_ptr, &self.dispatcher, ) }) .flatten() .inspect_err(|_| { // Clean up the callback data if event tap creation fails. let _ = unsafe { Box::from_raw(callback_data_ptr as *mut CallbackData) }; })?; self.callback_data_ptr = Some(callback_data_ptr); self.tap_port = Some(tap_port); Ok(()) } /// Implements [`MouseListener::terminate`]. pub(crate) fn terminate(&mut self) -> crate::Result<()> { if let Some(tap) = self.tap_port.take() { // Invalidate the tap to stop it from receiving events. This also // invalidates the run loop source. // See: https://developer.apple.com/documentation/corefoundation/cfmachportinvalidate(_:) tap.with(|tap| CFMachPort::invalidate(tap))?; } // Clean up the callback data if it exists. if let Some(ptr) = self.callback_data_ptr.take() { let _ = unsafe { Box::from_raw(ptr as *mut CallbackData) }; } Ok(()) } /// Creates and registers a `CGEventTap` for mouse events. fn create_event_tap( enabled_events: &[MouseEventKind], callback_data_ptr: usize, dispatcher: &Dispatcher, ) -> crate::Result>> { let mask = Self::event_mask_from_enabled(enabled_events); let tap_port = unsafe { CGEvent::tap_create( CGEventTapLocation::AnnotatedSessionEventTap, CGEventTapPlacement::HeadInsertEventTap, CGEventTapOptions::Default, mask, Some(Self::mouse_event_callback), callback_data_ptr as *mut c_void, ) .ok_or_else(|| { Error::Platform( "Failed to create `CGEventTap`. Accessibility permissions may be required.".to_string(), ) }) }?; let loop_source = CFMachPort::new_run_loop_source(None, Some(&tap_port), 0) .ok_or_else(|| { Error::Platform("Failed to create loop source".to_string()) })?; let current_loop = CFRunLoop::current().ok_or_else(|| { Error::Platform("Failed to get current run loop".to_string()) })?; current_loop .add_source(Some(&loop_source), unsafe { kCFRunLoopCommonModes }); CGEvent::tap_enable(&tap_port, true); Ok(ThreadBound::new(tap_port, dispatcher.clone())) } /// Gets the `CGEvent` mask for the enabled mouse events. fn event_mask_from_enabled( enabled_events: &[MouseEventKind], ) -> CGEventMask { let mut mask = 0u64; for event in enabled_events { match event { MouseEventKind::Move => { // NOTE: `MouseMoved` doesn't get triggered when clicking and // dragging. Therefore, we also listen for `LeftMouseDragged` // and `RightMouseDragged` events. mask |= 1u64 << u64::from(CGEventType::MouseMoved.0); mask |= 1u64 << u64::from(CGEventType::LeftMouseDragged.0); mask |= 1u64 << u64::from(CGEventType::RightMouseDragged.0); } MouseEventKind::LeftButtonDown => { mask |= 1u64 << u64::from(CGEventType::LeftMouseDown.0); } MouseEventKind::RightButtonDown => { mask |= 1u64 << u64::from(CGEventType::RightMouseDown.0); } MouseEventKind::LeftButtonUp => { mask |= 1u64 << u64::from(CGEventType::LeftMouseUp.0); } MouseEventKind::RightButtonUp => { mask |= 1u64 << u64::from(CGEventType::RightMouseUp.0); } } } mask } /// Callback for the `CGEventTap`. extern "C-unwind" fn mouse_event_callback( _: CGEventTapProxy, cg_event_type: CGEventType, mut cg_event: NonNull, user_info: *mut c_void, ) -> *mut CGEvent { if user_info.is_null() { tracing::error!("Null pointer passed to mouse event callback."); return unsafe { cg_event.as_mut() }; } let data = unsafe { &mut *user_info.cast::() }; // Map a `CGEventType` to a `MouseEventKind`. let event_kind = match cg_event_type { CGEventType::LeftMouseDown => MouseEventKind::LeftButtonDown, CGEventType::LeftMouseUp => MouseEventKind::LeftButtonUp, CGEventType::RightMouseDown => MouseEventKind::RightButtonDown, CGEventType::RightMouseUp => MouseEventKind::RightButtonUp, _ => MouseEventKind::Move, }; // Extract the cursor position from the `CGEvent`. let cg_event_ref = unsafe { cg_event.as_ref() }; let position = { let cg_point = CGEvent::location(Some(cg_event_ref)); #[allow(clippy::cast_possible_truncation)] Point { x: cg_point.x as i32, y: cg_point.y as i32, } }; // NOTE: Unfortunately quite unreliable. Returns 0 in most cases with // the real window ID interspersed every so often. Often a 100–200ms // delay before the real window ID is returned. let window_below_cursor = { let window_id = CGEvent::integer_value_field( Some(cg_event_ref), CGEventField::MouseEventWindowUnderMousePointer, ); if window_id == 0 { None } else { #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] Some(WindowId(window_id as u32)) } }; data.pressed_buttons.update(event_kind); // Throttle mouse move events so that there's a minimum of 50ms between // each emission. State change events (button down/up) always get // emitted. let should_emit = match event_kind { MouseEventKind::Move => { let has_elapsed_throttle = data.last_move_emission.is_none_or(|timestamp| { timestamp.elapsed() >= Duration::from_millis(50) }); // TODO: This is a hack to let through mouse move events when // they contain a window ID. macOS sporadically includes the // window ID on mouse events. has_elapsed_throttle || window_below_cursor.is_some() } _ => true, }; let mouse_event = match event_kind { MouseEventKind::LeftButtonDown => MouseEvent::ButtonDown { position, button: MouseButton::Left, pressed_buttons: data.pressed_buttons, }, MouseEventKind::LeftButtonUp => MouseEvent::ButtonUp { position, button: MouseButton::Left, pressed_buttons: data.pressed_buttons, }, MouseEventKind::RightButtonDown => MouseEvent::ButtonDown { position, button: MouseButton::Right, pressed_buttons: data.pressed_buttons, }, MouseEventKind::RightButtonUp => MouseEvent::ButtonUp { position, button: MouseButton::Right, pressed_buttons: data.pressed_buttons, }, MouseEventKind::Move => MouseEvent::Move { position, pressed_buttons: data.pressed_buttons, window_below_cursor, }, }; if should_emit { let _ = data.event_tx.send(mouse_event); if event_kind == MouseEventKind::Move { data.last_move_emission = Some(Instant::now()); } } unsafe { cg_event.as_mut() } } } impl Drop for MouseListener { fn drop(&mut self) { if let Err(err) = self.terminate() { tracing::warn!("Failed to terminate mouse listener: {}", err); } } } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/native_window.rs ================================================ use std::sync::Arc; use objc2::MainThreadMarker; use objc2_app_kit::{ NSApplicationActivationOptions, NSWindow, NSWorkspace, }; use objc2_application_services::{AXError, AXValue}; use objc2_core_foundation::{ CFBoolean, CFRetained, CFString, CGPoint, CGSize, }; use objc2_core_graphics::{CGDisplayIsAsleep, CGError}; use crate::{ platform_impl::{ self, ffi, AXUIElement, AXUIElementExt, AXValueExt, Application, }, Dispatcher, Point, Rect, ThreadBound, WindowId, }; /// Platform-specific implementation of [`NativeWindow`]. #[derive(Clone, Debug)] pub(crate) struct NativeWindow { pub(crate) id: WindowId, pub(crate) element: Arc>>, pub(crate) application: Application, } impl NativeWindow { /// Creates an instance of `NativeWindow`. #[must_use] pub(crate) fn new( id: WindowId, element: ThreadBound>, application: Application, ) -> Self { Self { element: Arc::new(element), id, application, } } /// Implements [`NativeWindow::id`]. pub(crate) fn id(&self) -> WindowId { self.id } /// Implements [`NativeWindow::title`]. pub(crate) fn title(&self) -> crate::Result { self.element.with(|el| { el.get_attribute::("AXTitle") .map(|cf_string| cf_string.to_string()) })? } /// Implements [`NativeWindow::process_name`]. pub(crate) fn process_name(&self) -> crate::Result { self .application .process_name() .ok_or(crate::Error::Platform( "Failed to get application process name.".to_string(), )) } /// Implements [`NativeWindow::frame`]. pub(crate) fn frame(&self) -> crate::Result { // TODO: Consider refactoring this to use a single dispatch. // TODO: Would `AXFrame` work instead? let size = self.size()?; let position = self.position()?; #[allow(clippy::cast_possible_truncation)] Ok(Rect::from_xy( position.0 as i32, position.1 as i32, size.0 as i32, size.1 as i32, )) } /// Implements [`NativeWindow::position`]. pub(crate) fn position(&self) -> crate::Result<(f64, f64)> { self.element.with(move |el| { el.get_attribute::("AXPosition") .and_then(|ax_value| ax_value.value_strict::()) .map(|point| (point.x, point.y)) })? } /// Implements [`NativeWindow::size`]. pub(crate) fn size(&self) -> crate::Result<(f64, f64)> { self.element.with(move |el| { el.get_attribute::("AXSize") .and_then(|ax_value| ax_value.value_strict::()) .map(|size| (size.width, size.height)) })? } /// Implements [`NativeWindow::is_valid`]. pub(crate) fn is_valid(&self) -> bool { // Query `AXRole`, which is present on all valid `AXUIElement`s. self .element .with(|el| match el.get_attribute::("AXRole") { Err(crate::Error::Accessibility(_, code)) if code == AXError::InvalidUIElement.0 => { let has_login_window = NSWorkspace::sharedWorkspace() .frontmostApplication() .and_then(|app| app.bundleIdentifier()) .is_some_and(|id| id.to_string() == "com.apple.loginwindow"); // AX calls transiently fail with `InvalidUIElement` during // sleep/wake. The window should still be considered valid. // // Events during sleep: // 1. Display goes asleep. // 2. AX calls fail with `InvalidUIElement`. // 3. Login window activates. // // Events during wake: // 1. Display wakes up. // 2. Login window deactivates and AX calls succeed again. // // Perf: `CGDisplayIsAsleep` ~1-5µs, login window check ~1-2ms. CGDisplayIsAsleep(0) || has_login_window } _ => true, }) .unwrap_or(false) } /// Implements [`NativeWindow::is_visible`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn is_visible(&self) -> crate::Result { Ok(!self.application.is_hidden()) } /// Implements [`NativeWindow::is_minimized`]. pub(crate) fn is_minimized(&self) -> crate::Result { self.element.with(|el| { el.get_attribute::("AXMinimized") .map(|cf_bool| cf_bool.value()) })? } /// Implements [`NativeWindow::is_maximized`]. pub(crate) fn is_maximized(&self) -> crate::Result { self.element.with(|el| { el.get_attribute::("AXFullScreen") .map(|cf_bool| cf_bool.value()) })? } /// Implements [`NativeWindow::is_resizable`]. #[allow(clippy::unnecessary_wraps, clippy::unused_self)] pub(crate) fn is_resizable(&self) -> crate::Result { // TODO: Not sure if this is even available via the AX API. Ok(true) } /// Implements [`NativeWindow::is_desktop_window`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn is_desktop_window(&self) -> crate::Result { Ok( self.application.bundle_id() == Some("com.apple.finder".to_string()), ) } /// Implements [`NativeWindow::set_frame`]. pub(crate) fn set_frame(&self, rect: &Rect) -> crate::Result<()> { // TODO: Consider adding a separate `set_frame_async` method which // spawns a thread. Calling blocking AXUIElement methods from different // threads supposedly works fine. // TODO: Refactor the repeated `set_attribute` calls. let rect = rect.clone(); self.with_enhanced_ui_disabled(move |el| -> crate::Result<()> { let ax_size = CGSize::new(rect.width().into(), rect.height().into()); let ax_value = AXValue::new_strict(&ax_size)?; el.set_attribute("AXSize", &ax_value)?; let ax_point = CGPoint::new(rect.x().into(), rect.y().into()); let ax_value = AXValue::new_strict(&ax_point)?; el.set_attribute("AXPosition", &ax_value)?; let ax_size = CGSize::new(rect.width().into(), rect.height().into()); let ax_value = AXValue::new_strict(&ax_size)?; el.set_attribute("AXSize", &ax_value) }) } /// Implements [`NativeWindow::resize`]. pub(crate) fn resize( &self, width: i32, height: i32, ) -> crate::Result<()> { self.with_enhanced_ui_disabled(move |el| -> crate::Result<()> { let ax_size = CGSize::new(width.into(), height.into()); let ax_value = AXValue::new_strict(&ax_size)?; el.set_attribute("AXSize", &ax_value) }) } /// Implements [`NativeWindow::reposition`]. pub(crate) fn reposition(&self, x: i32, y: i32) -> crate::Result<()> { self.with_enhanced_ui_disabled(move |el| -> crate::Result<()> { let ax_point = CGPoint::new(x.into(), y.into()); let ax_value = AXValue::new_strict(&ax_point)?; el.set_attribute("AXPosition", &ax_value) }) } /// Implements [`NativeWindow::minimize`]. pub(crate) fn minimize(&self) -> crate::Result<()> { self.element.with(move |el| -> crate::Result<()> { let ax_bool = CFBoolean::new(true); el.set_attribute::("AXMinimized", &ax_bool.into()) })? } /// Implements [`NativeWindow::maximize`]. pub(crate) fn maximize(&self) -> crate::Result<()> { self.element.with(move |el| -> crate::Result<()> { let ax_bool = CFBoolean::new(true); el.set_attribute::("AXFullScreen", &ax_bool.into()) })? } /// Implements [`NativeWindow::focus`]. pub(crate) fn focus(&self) -> crate::Result<()> { let psn = self.application.psn()?; self.set_front_process(&psn)?; self.set_key_window(&psn)?; self.raise() } /// Implements [`NativeWindow::close`]. pub(crate) fn close(&self) -> crate::Result<()> { self.element.with(|el| -> crate::Result<()> { let close_button = el.get_attribute::("AXCloseButton")?; // Simulate pressing the window's close button. let result = unsafe { close_button.perform_action(&CFString::from_str("AXPress")) }; if result != AXError::Success { return Err(crate::Error::Accessibility( "AXPress".to_string(), result.0, )); } Ok(()) })? } /// Executes a callback with the `AXEnhancedUserInterface` attribute /// temporarily disabled on the application `AXUIElement`. /// /// This is to prevent inconsistent window resizing and repositioning /// for certain applications (e.g. Firefox). /// /// References: /// - /// - fn with_enhanced_ui_disabled( &self, callback: F, ) -> crate::Result where F: FnOnce(&CFRetained) -> crate::Result + Send, R: Send, { self.application.ax_element.with(|app_el| { // Get whether enhanced UI is currently enabled. let was_enabled = app_el .get_attribute::("AXEnhancedUserInterface") .is_ok_and(|cf_bool| cf_bool.value()); // Disable enhanced UI if it was enabled. if was_enabled { let ax_bool = CFBoolean::new(false); let _ = app_el.set_attribute::( "AXEnhancedUserInterface", &ax_bool.into(), ); } // Execute the callback with the window element. let result = self.element.with(callback); // Restore enhanced UI if it was originally enabled. if was_enabled { let ax_bool = CFBoolean::new(true); let _ = app_el.set_attribute::( "AXEnhancedUserInterface", &ax_bool.into(), ); } result })?? } fn raise(&self) -> crate::Result<()> { self.element.with(move |el| -> crate::Result<()> { // This has a couple of caveats: // - Some windows do not get raised without first calling // `_SLPSSetFrontProcessWithOptions`. // - This changes focus if raising a window of the frontmost (active) // application. For example, if 2 Chrome windows are open and one // is focused, raising the other will change focus to the other // window. // // Because of these caveats, this method is not exposed as a public // API. It's also the reason why the GlazeWM feature of bringing all // tiling/floating windows to the front on focus change is not // implemented for macOS. let result = unsafe { el.perform_action(&CFString::from_str("AXRaise")) }; if result != AXError::Success { return Err(crate::Error::Accessibility( "AXRaise".to_string(), result.0, )); } Ok(()) })? } fn set_front_process( &self, psn: &ffi::ProcessSerialNumber, ) -> crate::Result<()> { let result = unsafe { #[allow(clippy::cast_possible_wrap)] ffi::_SLPSSetFrontProcessWithOptions( psn, self.id.0 as i32, ffi::CPS_USER_GENERATED, ) }; if result != CGError::Success { return Err(crate::Error::Platform( "Failed to set front process.".to_string(), )); } Ok(()) } fn set_key_window( &self, psn: &ffi::ProcessSerialNumber, ) -> crate::Result<()> { // Ref: https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468 let window_id = self.id.0.to_ne_bytes(); let mut event1 = [0u8; 0xf8]; event1[0x04] = 0xf8; event1[0x08] = 0x01; event1[0x3a] = 0x10; event1[0x3c..(0x3c + window_id.len())].copy_from_slice(&window_id); event1[0x20..(0x20 + 0x10)].fill(0xff); let mut event2 = event1; event2[0x08] = 0x02; for event in [event1, event2] { let result = unsafe { ffi::SLPSPostEventRecordTo(psn, event.as_ptr().cast()) }; if result != CGError::Success { return Err(crate::Error::Platform( "Failed to set key window.".to_string(), )); } } Ok(()) } } impl From for crate::NativeWindow { fn from(window: NativeWindow) -> Self { crate::NativeWindow { inner: window } } } /// Implements [`Dispatcher::visible_windows`]. pub(crate) fn visible_windows( dispatcher: &Dispatcher, ) -> crate::Result> { Ok( platform_impl::all_applications(dispatcher)? .iter() .filter_map(|app| app.windows().ok()) .flat_map(std::iter::IntoIterator::into_iter) .collect(), ) } /// Implements [`Dispatcher::window_by_id`]. pub(crate) fn window_by_id( id: WindowId, dispatcher: &Dispatcher, ) -> crate::Result> { // TODO: The performance of this is terrible. A better solution would be // to have a cache of window ID <-> `NativeWindow` instances. for app in platform_impl::all_applications(dispatcher)? { if let Ok(windows) = app.windows() { if let Some(win) = windows.into_iter().find(|w| w.id() == id) { return Ok(Some(win)); } } } Ok(None) } /// Implements [`Dispatcher::window_from_point`]. pub(crate) fn window_from_point( point: &Point, dispatcher: &Dispatcher, ) -> crate::Result> { // Get the top-most window ID at the given point. let window_id = dispatcher.dispatch_sync(|| { let cg_point = CGPoint { x: f64::from(point.x), y: f64::from(point.y), }; let window_id = unsafe { NSWindow::windowNumberAtPoint_belowWindowWithWindowNumber( cg_point, // 0 for all windows. 0, MainThreadMarker::new_unchecked(), ) }; #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] WindowId(window_id as u32) })?; // No window found at the given point. if window_id.0 == 0 { return Ok(None); } window_by_id(window_id, dispatcher) .map_err(|_| crate::Error::WindowNotFound) } /// Implements [`Dispatcher::focused_window`]. pub(crate) fn focused_window( dispatcher: &Dispatcher, ) -> crate::Result { dispatcher .dispatch_sync(|| { // Get the frontmost (active) application. let frontmost_app = NSWorkspace::sharedWorkspace() .frontmostApplication() .map(|app| Application::new(app, dispatcher.clone())); // Get the focused window of the frontmost application. frontmost_app.and_then(|app| app.focused_window().ok().flatten()) })? .ok_or(crate::Error::WindowNotFound) } /// Implements [`Dispatcher::reset_focus`]. // TODO: Move this to a better-suited module. pub(crate) fn reset_focus(dispatcher: &Dispatcher) -> crate::Result<()> { let Some(application) = platform_impl::application_for_bundle_id( "com.apple.finder", dispatcher, )? else { return Err(crate::Error::Platform( "Failed to get desktop application.".to_string(), )); }; let success = application.ns_app.activateWithOptions( NSApplicationActivationOptions::ActivateAllWindows, ); if !success { return Err(crate::Error::Platform( "Failed to activate desktop application.".to_string(), )); } Ok(()) } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/notification_center.rs ================================================ use objc2::{ define_class, msg_send, rc::Retained, runtime::AnyObject, sel, AnyThread, DefinedClass, }; use objc2_app_kit::{ NSApplicationDidChangeScreenParametersNotification, NSRunningApplication, NSWorkspace, NSWorkspaceActiveSpaceDidChangeNotification, NSWorkspaceDidActivateApplicationNotification, NSWorkspaceDidHideApplicationNotification, NSWorkspaceDidLaunchApplicationNotification, NSWorkspaceDidTerminateApplicationNotification, NSWorkspaceDidUnhideApplicationNotification, NSWorkspaceDidWakeNotification, NSWorkspaceWillSleepNotification, }; use objc2_foundation::{ ns_string, NSNotification, NSNotificationCenter, NSNotificationName, NSObject, NSString, }; use tokio::sync::mpsc; /// Notification names for observing macOS workspace and screen events. #[derive(Debug)] pub(crate) enum NotificationName { WorkspaceActiveSpaceDidChange, WorkspaceDidActivateApplication, WorkspaceDidLaunchApplication, WorkspaceDidTerminateApplication, WorkspaceDidHideApplication, WorkspaceDidUnhideApplication, WorkspaceDidWake, WorkspaceWillSleep, ApplicationDidChangeScreenParameters, } impl From<&NSNotificationName> for NotificationName { fn from(name: &NSNotificationName) -> Self { if name == unsafe { NSWorkspaceDidLaunchApplicationNotification } { Self::WorkspaceDidLaunchApplication } else if name == unsafe { NSWorkspaceDidActivateApplicationNotification } { Self::WorkspaceDidActivateApplication } else if name == unsafe { NSWorkspaceDidTerminateApplicationNotification } { Self::WorkspaceDidTerminateApplication } else if name == unsafe { NSWorkspaceActiveSpaceDidChangeNotification } { Self::WorkspaceActiveSpaceDidChange } else if name == unsafe { NSWorkspaceDidHideApplicationNotification } { Self::WorkspaceDidHideApplication } else if name == unsafe { NSWorkspaceDidUnhideApplicationNotification } { Self::WorkspaceDidUnhideApplication } else if name == unsafe { NSWorkspaceDidWakeNotification } { Self::WorkspaceDidWake } else if name == unsafe { NSWorkspaceWillSleepNotification } { Self::WorkspaceWillSleep } else if name == unsafe { NSApplicationDidChangeScreenParametersNotification } { Self::ApplicationDidChangeScreenParameters } else { panic!("Unknown notification name: {name}"); } } } impl From for &NSString { fn from(name: NotificationName) -> Self { match name { NotificationName::WorkspaceActiveSpaceDidChange => unsafe { NSWorkspaceActiveSpaceDidChangeNotification }, NotificationName::WorkspaceDidActivateApplication => unsafe { NSWorkspaceDidActivateApplicationNotification }, NotificationName::WorkspaceDidLaunchApplication => unsafe { NSWorkspaceDidLaunchApplicationNotification }, NotificationName::WorkspaceDidTerminateApplication => unsafe { NSWorkspaceDidTerminateApplicationNotification }, NotificationName::WorkspaceDidHideApplication => unsafe { NSWorkspaceDidHideApplicationNotification }, NotificationName::WorkspaceDidUnhideApplication => unsafe { NSWorkspaceDidUnhideApplicationNotification }, NotificationName::WorkspaceDidWake => unsafe { NSWorkspaceDidWakeNotification }, NotificationName::WorkspaceWillSleep => unsafe { NSWorkspaceWillSleepNotification }, NotificationName::ApplicationDidChangeScreenParameters => unsafe { NSApplicationDidChangeScreenParametersNotification }, } } } /// Events received from macOS notification center observers. #[derive(Debug)] pub(crate) enum NotificationEvent { WorkspaceActiveSpaceDidChange, WorkspaceDidActivateApplication(Retained), WorkspaceDidLaunchApplication(Retained), WorkspaceDidTerminateApplication(Retained), WorkspaceDidHideApplication(Retained), WorkspaceDidUnhideApplication(Retained), WorkspaceWillSleep, WorkspaceDidWake, ApplicationDidChangeScreenParameters, } /// Instance variables for `NotificationObserver`. #[repr(C)] pub(crate) struct NotificationObserverIvars { events_tx: mpsc::UnboundedSender, } define_class! { // SAFETY: // - The superclass `NSObject` does not have any subclassing requirements. // - `NotificationObserver` does not implement `Drop`. #[unsafe(super(NSObject))] #[ivars = Box] pub(crate) struct NotificationObserver; // SAFETY: Each of these method signatures must match their invocations. impl NotificationObserver { #[unsafe(method(onEvent:))] fn on_event(&self, notif: &NSNotification) { self.handle_event(notif); } } } impl NotificationObserver { pub fn new( ) -> (Retained, mpsc::UnboundedReceiver) { let (events_tx, events_rx) = mpsc::unbounded_channel(); let instance = Self::alloc() .set_ivars(Box::new(NotificationObserverIvars { events_tx })); // SAFETY: The signature of `NSObject`'s `init` method is correct. (unsafe { msg_send![super(instance), init] }, events_rx) } fn handle_event(&self, notif: &NSNotification) { tracing::debug!("Received notification: {notif:#?}"); match NotificationName::from(&*notif.name()) { NotificationName::WorkspaceActiveSpaceDidChange => { self.emit_event(NotificationEvent::WorkspaceActiveSpaceDidChange); } NotificationName::WorkspaceDidActivateApplication => { if let Some(app) = unsafe { app_from_notification(notif) } { self.emit_event( NotificationEvent::WorkspaceDidActivateApplication(app), ); } else { tracing::warn!( "Failed to extract application from activate notification" ); } } NotificationName::WorkspaceDidLaunchApplication => { if let Some(app) = unsafe { app_from_notification(notif) } { self.emit_event( NotificationEvent::WorkspaceDidLaunchApplication(app), ); } else { tracing::warn!( "Failed to extract application from launch notification" ); } } NotificationName::WorkspaceDidTerminateApplication => { if let Some(app) = unsafe { app_from_notification(notif) } { self.emit_event( NotificationEvent::WorkspaceDidTerminateApplication(app), ); } else { tracing::warn!( "Failed to extract application from terminate notification" ); } } NotificationName::WorkspaceDidHideApplication => { if let Some(app) = unsafe { app_from_notification(notif) } { self.emit_event(NotificationEvent::WorkspaceDidHideApplication( app, )); } } NotificationName::WorkspaceDidUnhideApplication => { if let Some(app) = unsafe { app_from_notification(notif) } { self.emit_event( NotificationEvent::WorkspaceDidUnhideApplication(app), ); } } NotificationName::WorkspaceDidWake => { self.emit_event(NotificationEvent::WorkspaceDidWake); } NotificationName::WorkspaceWillSleep => { self.emit_event(NotificationEvent::WorkspaceWillSleep); } NotificationName::ApplicationDidChangeScreenParameters => { self.emit_event( NotificationEvent::ApplicationDidChangeScreenParameters, ); } } } fn emit_event(&self, event: NotificationEvent) { if let Err(err) = self.ivars().events_tx.send(event) { tracing::warn!("Failed to send event: {err}"); } } } /// Wrapper around `NSNotificationCenter` for registering event observers. #[derive(Debug)] pub(crate) struct NotificationCenter { inner: Retained, } impl NotificationCenter { pub fn workspace_center() -> Self { let center = NSWorkspace::sharedWorkspace().notificationCenter(); Self { inner: center } } pub fn default_center() -> Self { let center = NSNotificationCenter::defaultCenter(); Self { inner: center } } pub unsafe fn add_observer( &mut self, notification_name: NotificationName, observer: &NotificationObserver, object: Option<&AnyObject>, ) { tracing::info!("Adding observer for {notification_name:?}."); self.inner.addObserver_selector_name_object( observer, sel!(onEvent:), Some(notification_name.into()), object, ); } } pub unsafe fn app_from_notification( notification: &NSNotification, ) -> Option> { notification .userInfo()? .objectForKey(ns_string!("NSWorkspaceApplicationKey")) .map(|app| Retained::::cast_unchecked(app)) } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/single_instance.rs ================================================ use std::{ fs::{self, File, TryLockError}, path::PathBuf, }; /// Platform-specific implementation of [`SingleInstance`]. pub(crate) struct SingleInstance { /// File that holds the lock. /// /// The lock is automatically released when the [`File`] is dropped. _file: File, } impl SingleInstance { /// Implements [`SingleInstance::new`]. pub(crate) fn new() -> crate::Result { let path = Self::lock_file_path()?; if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(crate::Error::Io)?; } let file = File::create(&path).map_err(crate::Error::Io)?; // Acquire exclusive file lock. file.try_lock().map_err(|err| match err { TryLockError::WouldBlock => crate::Error::Platform( "Another instance of the application is already running." .to_string(), ), TryLockError::Error(io_err) => crate::Error::Io(io_err), })?; Ok(Self { _file: file }) } /// Implements [`SingleInstance::is_running`]. #[must_use] pub(crate) fn is_running() -> bool { let Ok(file) = Self::lock_file_path() .and_then(|path| File::open(&path).map_err(crate::Error::Io)) else { return false; }; // If `try_lock` fails with `WouldBlock`, the lock is held by another // process. file .try_lock() .is_err_and(|err| matches!(err, TryLockError::WouldBlock)) } /// Returns the path to the lock file: /// `~/Library/Application Support/glazewm/.lock`. fn lock_file_path() -> crate::Result { let home = home::home_dir().ok_or_else(|| { crate::Error::Platform( "Unable to determine home directory.".to_string(), ) })?; Ok(home.join("Library/Application Support/glazewm/.lock")) } } ================================================ FILE: packages/wm-platform/src/platform_impl/macos/window_listener.rs ================================================ use std::collections::HashMap; use objc2::rc::Retained; use objc2_app_kit::NSWorkspace; use tokio::sync::mpsc; use crate::{ platform_impl::{ self, Application, ApplicationObserver, NotificationCenter, NotificationEvent, NotificationName, NotificationObserver, ProcessId, }, Dispatcher, ThreadBound, WindowEvent, }; /// Platform-specific implementation of [`WindowEventNotification`]. #[derive(Clone, Debug)] pub struct WindowEventNotificationInner { /// Name of the notification (e.g. `AXWindowMoved`). pub name: String, /// Pointer to the `AXUIElement` that triggered the notification. pub ax_element_ptr: *mut std::ffi::c_void, } unsafe impl Send for WindowEventNotificationInner {} /// Platform-specific implementation of [`WindowListener`]. #[derive(Debug)] pub(crate) struct WindowListener { /// Workspace notification observer, bound to the main thread. observer: Option>>, } impl WindowListener { /// Implements [`WindowListener::new`]. pub(crate) fn new( events_tx: mpsc::UnboundedSender, dispatcher: &Dispatcher, ) -> crate::Result { let observer = dispatcher .dispatch_sync(|| Self::init(events_tx, dispatcher.clone()))??; Ok(Self { observer: Some(observer), }) } /// Implements [`WindowListener::terminate`]. pub(crate) fn terminate(&mut self) { // On macOS 10.11+, observer subscriptions are cleaned up automatically // without calling `removeObserver`. // Ref: https://developer.apple.com/documentation/foundation/notificationcenter/removeobserver(_:name:object:) // // Dropping the `NotificationObserver` also drops its channel sender, // causing the listener thread to exit. self.observer.take(); } fn init( events_tx: mpsc::UnboundedSender, dispatcher: Dispatcher, ) -> crate::Result>> { let (observer, events_rx) = NotificationObserver::new(); let workspace = NSWorkspace::sharedWorkspace(); let mut workspace_center = NotificationCenter::workspace_center(); for notification in [ NotificationName::WorkspaceActiveSpaceDidChange, NotificationName::WorkspaceDidLaunchApplication, NotificationName::WorkspaceDidActivateApplication, NotificationName::WorkspaceDidTerminateApplication, NotificationName::WorkspaceDidHideApplication, NotificationName::WorkspaceDidUnhideApplication, ] { unsafe { workspace_center.add_observer( notification, &observer, Some(&workspace), ); } } let running_apps = platform_impl::all_applications(&dispatcher)?; // Create observers for all running applications. let app_observers = running_apps .into_iter() .filter_map(|app| { Self::create_app_observer(&app, events_tx.clone()).ok() }) .collect::>(); tracing::info!( "Registered observers for {} existing applications.", app_observers.len() ); let dispatcher_clone = dispatcher.clone(); std::thread::spawn(move || { Self::listen_workspace_events( app_observers, events_rx, &events_tx, &dispatcher_clone, ); }); Ok(ThreadBound::new(observer, dispatcher)) } fn listen_workspace_events( app_observers: Vec, mut events_rx: mpsc::UnboundedReceiver, events_tx: &mpsc::UnboundedSender, dispatcher: &Dispatcher, ) { // Track window observers for each application by PID. let mut app_observers: HashMap = app_observers .into_iter() .map(|observer| (observer.pid, observer)) .collect(); // Loop exits when the sender is dropped in `Self::terminate`. while let Some(event) = events_rx.blocking_recv() { tracing::debug!("Received workspace event: {event:?}"); match event { NotificationEvent::WorkspaceDidLaunchApplication(running_app) => { let events_tx = events_tx.clone(); let Ok(Ok(app_observer)) = dispatcher.dispatch_sync(|| { let app = Application::new(running_app, dispatcher.clone()); if !app.should_observe() { return Err(crate::Error::Platform(format!( "Skipped observer registration for PID {} (should ignore).", app.pid, ))); } ApplicationObserver::new(&app, events_tx.clone(), false) }) else { continue; }; if app_observers.contains_key(&app_observer.pid) { tracing::debug!( "Observer already exists for PID {}.", app_observer.pid ); continue; } app_observers.insert(app_observer.pid, app_observer); } NotificationEvent::WorkspaceDidTerminateApplication( running_app, ) => { let pid = running_app.processIdentifier(); if let Some(observer) = app_observers.remove(&pid) { tracing::info!( "Removed window observer for terminated PID: {}", pid ); observer.emit_all_windows_destroyed(); } } NotificationEvent::WorkspaceDidActivateApplication( running_app, ) => { let Ok(Ok(Some(focused_window))) = dispatcher.dispatch_sync(|| { let app = Application::new(running_app, dispatcher.clone()); app.focused_window() }) else { continue; }; let _ = events_tx.send(WindowEvent::Focused { window: focused_window, notification: crate::WindowEventNotification(None), }); } NotificationEvent::WorkspaceDidHideApplication(running_app) => { if let Some(app_observer) = app_observers.get(&running_app.processIdentifier()) { app_observer.emit_all_windows_hidden(); } } NotificationEvent::WorkspaceDidUnhideApplication(running_app) => { if let Some(app_observer) = app_observers.get(&running_app.processIdentifier()) { app_observer.emit_all_windows_shown(); } } _ => {} } } tracing::debug!("Window listener thread exited."); } fn create_app_observer( app: &Application, events_tx: mpsc::UnboundedSender, ) -> crate::Result { if !app.should_observe() { return Err(crate::Error::Platform(format!( "Skipped observer registration for PID {} (should ignore).", app.pid, ))); } let app_observer_res = ApplicationObserver::new(app, events_tx, true); if let Err(err) = &app_observer_res { tracing::debug!( "Skipped observer registration for PID {}: {}", app.pid, err ); } app_observer_res } } impl Drop for WindowListener { fn drop(&mut self) { self.terminate(); } } ================================================ FILE: packages/wm-platform/src/platform_impl/mod.rs ================================================ #[cfg(target_os = "windows")] #[path = "windows/mod.rs"] mod platform; #[cfg(target_os = "macos")] #[path = "macos/mod.rs"] mod platform; pub(crate) use platform::*; #[cfg(all(not(target_os = "windows"), not(target_os = "macos"),))] compile_error!("The platform you're compiling for is not supported."); ================================================ FILE: packages/wm-platform/src/platform_impl/windows/com.rs ================================================ use std::cell::RefCell; use windows::{ core::{ComInterface, IUnknown, IUnknown_Vtbl, GUID, HRESULT}, Win32::{ System::Com::{ CoCreateInstance, CoInitializeEx, CoUninitialize, IServiceProvider, CLSCTX_ALL, CLSCTX_SERVER, COINIT_APARTMENTTHREADED, }, UI::Shell::{ITaskbarList2, TaskbarList}, }, }; /// COM class identifier (CLSID) for the Windows Shell that implements the /// `IServiceProvider` interface. const CLSID_IMMERSIVE_SHELL: GUID = GUID::from_u128(0xC2F03A33_21F5_47FA_B4BB_156362A2F239); thread_local! { /// Manages per-thread COM initialization. COM must be initialized on each /// thread that uses it, so we store this in thread-local storage to handle /// the setup and cleanup automatically. /// /// Wrapped in `RefCell` to allow mutation via `COM_INIT.borrow_mut()`. pub(crate) static COM_INIT: RefCell = RefCell::new(ComInit::new()); } pub(crate) struct ComInit { service_provider: Option, application_view_collection: Option, taskbar_list: Option, } impl ComInit { /// Initializes COM on the current thread with apartment threading model. /// `COINIT_APARTMENTTHREADED` is required for shell COM objects. /// /// # Panics /// /// Panics if COM initialization fails. This is typically only possible /// if COM is already initialized with an incompatible threading model. #[must_use] pub(crate) fn new() -> Self { unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) } .expect("Unable to initialize COM."); let service_provider = unsafe { CoCreateInstance(&CLSID_IMMERSIVE_SHELL, None, CLSCTX_ALL) } .ok(); let application_view_collection = service_provider.as_ref().and_then( |provider: &IServiceProvider| unsafe { provider.QueryService(&IApplicationViewCollection::IID).ok() }, ); let taskbar_list = unsafe { CoCreateInstance(&TaskbarList, None, CLSCTX_SERVER) }.ok(); Self { service_provider, application_view_collection, taskbar_list, } } /// Returns an instance of `IApplicationViewCollection`. pub(crate) fn application_view_collection( &self, ) -> crate::Result<&IApplicationViewCollection> { self.application_view_collection.as_ref().ok_or_else(|| { crate::Error::Platform( "Failed to query for `IApplicationViewCollection` instance." .to_string(), ) }) } /// Returns an instance of `ITaskbarList2`. pub(crate) fn taskbar_list(&self) -> crate::Result<&ITaskbarList2> { self.taskbar_list.as_ref().ok_or_else(|| { crate::Error::Platform( "Unable to create `ITaskbarList2` instance.".to_string(), ) }) } /// Refreshes cached COM interfaces. /// /// Called automatically by `with_retry` when COM operations fail due to /// stale interface pointers (e.g. after Explorer restarts). pub(crate) fn refresh(&mut self) { // Re-create the service provider. self.service_provider = unsafe { CoCreateInstance(&CLSID_IMMERSIVE_SHELL, None, CLSCTX_ALL) } .ok(); // Re-create the application view collection. self.application_view_collection = self .service_provider .as_ref() .and_then(|provider: &IServiceProvider| unsafe { provider.QueryService(&IApplicationViewCollection::IID).ok() }); // Re-create the taskbar list. self.taskbar_list = unsafe { CoCreateInstance(&TaskbarList, None, CLSCTX_SERVER) }.ok(); } /// Executes a COM operation, refreshing interfaces on failure and /// retrying once. Use this for operations that may fail due to stale /// COM interfaces. pub fn with_retry(&mut self, op: F) -> crate::Result where F: Fn(&Self) -> crate::Result, { if let Ok(result) = op(self) { Ok(result) } else { self.refresh(); op(self) } } } impl Default for ComInit { fn default() -> Self { Self::new() } } impl Drop for ComInit { fn drop(&mut self) { // Explicitly drop COM interfaces first. drop(self.taskbar_list.take()); drop(self.application_view_collection.take()); drop(self.service_provider.take()); unsafe { CoUninitialize() }; } } /// Undocumented COM interface for Windows shell functionality. /// /// Note that filler methods are added to match the vtable layout. #[windows_interface::interface("1841c6d7-4f9d-42c0-af41-8747538f10e5")] pub unsafe trait IApplicationViewCollection: IUnknown { pub unsafe fn m1(&self); pub unsafe fn m2(&self); pub unsafe fn m3(&self); pub unsafe fn get_view_for_hwnd( &self, window: isize, application_view: *mut Option, ) -> HRESULT; } /// Undocumented COM interface for managing views in the Windows shell. /// /// Note that filler methods are added to match the vtable layout. #[windows_interface::interface("372E1D3B-38D3-42E4-A15B-8AB2B178F513")] pub unsafe trait IApplicationView: IUnknown { pub unsafe fn m1(&self); pub unsafe fn m2(&self); pub unsafe fn m3(&self); pub unsafe fn m4(&self); pub unsafe fn m5(&self); pub unsafe fn m6(&self); pub unsafe fn m7(&self); pub unsafe fn m8(&self); pub unsafe fn m9(&self); pub unsafe fn set_cloak( &self, cloak_type: u32, cloak_flag: i32, ) -> HRESULT; } ================================================ FILE: packages/wm-platform/src/platform_impl/windows/display.rs ================================================ use windows::{ core::PCWSTR, Win32::{ Foundation::{BOOL, LPARAM, POINT, RECT}, Graphics::Gdi::{ EnumDisplayDevicesW, EnumDisplayMonitors, EnumDisplaySettingsW, GetMonitorInfoW, MonitorFromPoint, MonitorFromWindow, DEVMODEW, DISPLAY_DEVICEW, DISPLAY_DEVICE_ACTIVE, ENUM_CURRENT_SETTINGS, HDC, HMONITOR, MONITORINFO, MONITORINFOEXW, MONITOR_DEFAULTTONEAREST, MONITOR_DEFAULTTOPRIMARY, }, UI::{ HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI}, WindowsAndMessaging::EDD_GET_DEVICE_INTERFACE_NAME, }, }, }; use crate::{ display::{ ConnectionState, DisplayDeviceId, DisplayId, MirroringState, OutputTechnology, }, Dispatcher, NativeWindow, Point, Rect, }; /// Platform-specific implementation of [`Display`]. #[derive(Clone, Debug)] pub(crate) struct Display { pub(crate) monitor_handle: isize, } impl Display { /// Creates an instance of `Display`. #[must_use] pub(crate) fn new(monitor_handle: isize) -> Self { Self { monitor_handle } } /// Implements [`Display::id`]. pub(crate) fn id(&self) -> DisplayId { DisplayId(self.monitor_handle) } /// Implements [`Display::name`]. pub(crate) fn name(&self) -> crate::Result { Ok( String::from_utf16_lossy(&self.monitor_info_ex()?.szDevice) .trim_end_matches('\0') .to_string(), ) } /// Implements [`Display::bounds`]. pub(crate) fn bounds(&self) -> crate::Result { let rc = self.monitor_info_ex()?.monitorInfo.rcMonitor; Ok(Rect::from_ltrb(rc.left, rc.top, rc.right, rc.bottom)) } /// Implements [`Display::working_area`]. pub(crate) fn working_area(&self) -> crate::Result { let rc = self.monitor_info_ex()?.monitorInfo.rcWork; Ok(Rect::from_ltrb(rc.left, rc.top, rc.right, rc.bottom)) } /// Implements [`Display::scale_factor`]. pub(crate) fn scale_factor(&self) -> crate::Result { let dpi = self.dpi()?; #[allow(clippy::cast_precision_loss)] Ok(dpi as f32 / 96.0) } /// Implements [`Display::dpi`]. pub(crate) fn dpi(&self) -> crate::Result { let mut dpi_x = u32::default(); let mut dpi_y = u32::default(); unsafe { GetDpiForMonitor( HMONITOR(self.monitor_handle), MDT_EFFECTIVE_DPI, &raw mut dpi_x, &raw mut dpi_y, ) }?; // Arbitrarily choose the Y DPI. Ok(dpi_y) } /// Implements [`Display::is_primary`]. pub(crate) fn is_primary(&self) -> crate::Result { // Check for `MONITORINFOF_PRIMARY` flag (`0x1`). Ok(self.monitor_info_ex()?.monitorInfo.dwFlags & 0x1 != 0) } /// Implements [`Display::devices`]. pub(crate) fn devices( &self, ) -> crate::Result> { let monitor_info = self.monitor_info_ex()?; let adapter_name = String::from_utf16_lossy(&monitor_info.szDevice) .trim_end_matches('\0') .to_string(); // Get the display devices associated with the display's adapter. let devices = (0u32..) .map_while(|index| { #[allow(clippy::cast_possible_truncation)] let mut device = DISPLAY_DEVICEW { cb: std::mem::size_of::() as u32, ..Default::default() }; // When passing the `EDD_GET_DEVICE_INTERFACE_NAME` flag, the // returned `DISPLAY_DEVICEW` will contain the device path in the // `DeviceID` field. unsafe { EnumDisplayDevicesW( PCWSTR(monitor_info.szDevice.as_ptr()), index, &raw mut device, EDD_GET_DEVICE_INTERFACE_NAME, ) } .as_bool() .then_some(device) }) // Filter out any devices that are not active. .filter(|device| device.StateFlags & DISPLAY_DEVICE_ACTIVE != 0) .map(|device| { DisplayDevice::new(adapter_name.clone(), &device.DeviceID).into() }) .collect(); Ok(devices) } /// Implements [`Display::main_device`]. pub(crate) fn main_device(&self) -> crate::Result { self .devices()? .into_iter() .find(|device| { matches!( device.mirroring_state(), Ok(None | Some(MirroringState::Source)) ) }) .ok_or(crate::Error::DisplayDeviceNotFound) } /// Implements [`DisplayExtWindows::hmonitor`]. pub(crate) fn hmonitor(&self) -> HMONITOR { HMONITOR(self.monitor_handle) } /// Ref: fn monitor_info_ex(&self) -> crate::Result { let mut monitor_info = MONITORINFOEXW { monitorInfo: MONITORINFO { #[allow(clippy::cast_possible_truncation)] cbSize: std::mem::size_of::() as u32, ..Default::default() }, ..Default::default() }; unsafe { GetMonitorInfoW( HMONITOR(self.monitor_handle), std::ptr::from_mut(&mut monitor_info).cast(), ) } .ok()?; Ok(monitor_info) } } impl From for crate::Display { fn from(display: Display) -> Self { crate::Display { inner: display } } } impl PartialEq for Display { fn eq(&self, other: &Self) -> bool { self.monitor_handle == other.monitor_handle } } impl Eq for Display {} /// Platform-specific implementation of [`DisplayDevice`]. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct DisplayDevice { /// Display adapter name (e.g. `\\.\DISPLAY1`). adapter_name: String, /// Device interface path (e.g. /// `\\?\DISPLAY#DEL40A3#5&1234abcd&0&UID256# /// {e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}`). pub(crate) device_path: Option, } impl DisplayDevice { /// Creates an instance of `DisplayDevice`. #[must_use] pub(crate) fn new(adapter_name: String, device_path: &[u16]) -> Self { // NOTE: This may be an empty string for virtual display devices. let device_path = String::from_utf16_lossy(device_path) .trim_end_matches('\0') .to_string(); // Check that the device path is valid. If not, set it to `None`. let device_path = (device_path.split('#').count() >= 4).then_some(device_path); Self { adapter_name, device_path, } } /// Implements [`DisplayDevice::id`]. pub(crate) fn id(&self) -> DisplayDeviceId { // TODO: Display adapter name might not be unique. DisplayDeviceId( self .hardware_id() .unwrap_or_else(|| self.adapter_name.clone()), ) } /// Implements [`DisplayDevice::rotation`]. pub(crate) fn rotation(&self) -> crate::Result { let orientation = unsafe { self .current_device_mode()? .Anonymous1 .Anonymous2 .dmDisplayOrientation }; Ok(match orientation.0 { 1 => 90.0, 2 => 180.0, 3 => 270.0, _ => 0.0, }) } /// Implements [`DisplayDevice::refresh_rate`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn refresh_rate(&self) -> crate::Result { #[allow(clippy::cast_precision_loss)] Ok(self.current_device_mode()?.dmDisplayFrequency as f32) } /// Implements [`DisplayDevice::is_builtin`]. #[allow(clippy::unnecessary_wraps, clippy::unused_self)] pub(crate) fn is_builtin(&self) -> crate::Result { // TODO: Use `DisplayConfigGetDeviceInfo` to determine whether the // output technology is internal. Ok(false) } /// Implements [`DisplayDevice::connection_state`]. #[allow(clippy::unnecessary_wraps, clippy::unused_self)] pub(crate) fn connection_state(&self) -> crate::Result { // TODO: Detect disconnected state. Ok(ConnectionState::Active) } /// Implements [`DisplayDevice::mirroring_state`]. #[allow(clippy::unnecessary_wraps, clippy::unused_self)] pub(crate) fn mirroring_state( &self, ) -> crate::Result> { // TODO: Implement mirroring detection using // `DisplayConfigGetDeviceInfo`. Ok(None) } /// Implements [`DisplayDeviceExtWindows::hardware_id`]. pub(crate) fn hardware_id(&self) -> Option { self .device_path .as_deref()? .split('#') .nth(1) .map(ToString::to_string) } /// Implements [`DisplayDeviceExtWindows::output_technology`]. #[allow(clippy::unnecessary_wraps, clippy::unused_self)] pub(crate) fn output_technology( &self, ) -> crate::Result> { // TODO: Use `DisplayConfigGetDeviceInfo` to get the output technology. Ok(Some(OutputTechnology::Unknown)) } /// Gets the current device mode. fn current_device_mode(&self) -> crate::Result { #[allow(clippy::cast_possible_truncation)] let mut device_mode = DEVMODEW { dmSize: std::mem::size_of::() as u16, ..Default::default() }; let wide_adapter_name = self .adapter_name .encode_utf16() .chain(std::iter::once(0)) .collect::>(); unsafe { EnumDisplaySettingsW( PCWSTR(wide_adapter_name.as_ptr()), ENUM_CURRENT_SETTINGS, &raw mut device_mode, ) } .ok()?; Ok(device_mode) } } impl From for crate::DisplayDevice { fn from(device: DisplayDevice) -> Self { crate::DisplayDevice { inner: device } } } /// Implements [`Dispatcher::displays`]. pub(crate) fn all_displays( _: &Dispatcher, ) -> crate::Result> { let mut monitor_handles: Vec = Vec::new(); // Callback for `EnumDisplayMonitors` to collect monitor handles. #[allow(clippy::items_after_statements)] extern "system" fn monitor_enum_proc( handle: HMONITOR, _hdc: HDC, _clip: *mut RECT, data: LPARAM, ) -> BOOL { let handles = data.0 as *mut Vec; // SAFETY: `data` is a valid pointer to the `monitor_handles` vec, // which outlives this callback. unsafe { (*handles).push(handle.0) }; true.into() } unsafe { EnumDisplayMonitors( HDC::default(), None, Some(monitor_enum_proc), LPARAM(std::ptr::from_mut(&mut monitor_handles) as _), ) } .ok()?; Ok( monitor_handles .into_iter() .map(|handle| Display::new(handle).into()) .collect(), ) } /// Implements [`Dispatcher::display_devices`]. pub(crate) fn all_display_devices( dispatcher: &Dispatcher, ) -> crate::Result> { all_displays(dispatcher)? .into_iter() .map(|display| display.devices()) .collect::>>() .map(|vecs| vecs.into_iter().flatten().collect()) } /// Implements [`Dispatcher::display_from_point`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn display_from_point( point: &Point, _: &Dispatcher, ) -> crate::Result { let handle = unsafe { MonitorFromPoint( POINT { x: point.x, y: point.y, }, MONITOR_DEFAULTTOPRIMARY, ) }; Ok(Display::new(handle.0).into()) } /// Implements [`Dispatcher::primary_display`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn primary_display( _: &Dispatcher, ) -> crate::Result { let handle = unsafe { MonitorFromPoint(POINT { x: 0, y: 0 }, MONITOR_DEFAULTTOPRIMARY) }; Ok(Display::new(handle.0).into()) } /// Implements [`Dispatcher::nearest_display`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn nearest_display( native_window: &NativeWindow, _: &Dispatcher, ) -> crate::Result { let handle = unsafe { MonitorFromWindow(native_window.inner.hwnd(), MONITOR_DEFAULTTONEAREST) }; Ok(Display::new(handle.0).into()) } ================================================ FILE: packages/wm-platform/src/platform_impl/windows/display_listener.rs ================================================ use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use tokio::sync::mpsc; use tracing::warn; use windows::Win32::UI::WindowsAndMessaging::{ DBT_DEVNODES_CHANGED, PBT_APMRESUMEAUTOMATIC, PBT_APMRESUMESUSPEND, PBT_APMSUSPEND, SPI_SETWORKAREA, WM_DEVICECHANGE, WM_DISPLAYCHANGE, WM_POWERBROADCAST, WM_SETTINGCHANGE, }; use crate::{Dispatcher, DispatcherExtWindows}; /// Platform-specific implementation of [`DisplayListener`]. pub(crate) struct DisplayListener { callback_id: Option, dispatcher: Dispatcher, } impl DisplayListener { /// Implements [`DisplayListener::new`]. pub(crate) fn new( event_tx: mpsc::UnboundedSender<()>, dispatcher: &Dispatcher, ) -> crate::Result { let is_system_suspended = Arc::new(AtomicBool::new(false)); let callback_id = dispatcher.register_wndproc_callback(Box::new( move |_hwnd, message, wparam, _lparam| { match message { WM_POWERBROADCAST => { #[allow(clippy::cast_possible_truncation)] match wparam as u32 { // System is resuming from sleep/hibernation. PBT_APMRESUMEAUTOMATIC | PBT_APMRESUMESUSPEND => { is_system_suspended.store(false, Ordering::Relaxed); } // System is entering sleep/hibernation. PBT_APMSUSPEND => { is_system_suspended.store(true, Ordering::Relaxed); } _ => {} } Some(0) } WM_DISPLAYCHANGE | WM_SETTINGCHANGE | WM_DEVICECHANGE => { let should_emit = { // Ignore display change messages if the system hasn't fully // resumed from sleep. if is_system_suspended.load(Ordering::Relaxed) { false } else { #[allow(clippy::cast_possible_truncation)] match message { // Received when displays are connected and disconnected, // resolution changes, or arrangement changes. WM_DISPLAYCHANGE => true, // Received when the working area has changed. Fires when // the Windows taskbar is changed or an appbar is // registered or changed. 3rd-party apps like // ButteryTaskbar can trigger this message by calling // `SystemParametersInfo(SPI_SETWORKAREA, ...)`. WM_SETTINGCHANGE => wparam as u32 == SPI_SETWORKAREA.0, // Received when any device is connected or disconnected // (including non-display devices). // TODO: Check if this is actually needed. Previous C# // implementation did not use this. WM_DEVICECHANGE => wparam as u32 == DBT_DEVNODES_CHANGED, _ => unreachable!(), } } }; if should_emit { let _ = event_tx.send(()); } Some(0) } _ => None, } }, ))?; Ok(Self { callback_id: Some(callback_id), dispatcher: dispatcher.clone(), }) } /// Implements [`DisplayListener::terminate`]. pub(crate) fn terminate(&mut self) -> crate::Result<()> { if let Some(id) = self.callback_id.take() { self.dispatcher.deregister_wndproc_callback(id)?; } Ok(()) } } impl Drop for DisplayListener { fn drop(&mut self) { if let Err(err) = self.terminate() { warn!("Failed to terminate display listener: {}", err); } } } ================================================ FILE: packages/wm-platform/src/platform_impl/windows/event_loop.rs ================================================ use std::{ cell::RefCell, collections::HashMap, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, }, thread::{self, ThreadId}, }; use windows::{ core::w, Win32::{ Foundation::{HWND, LPARAM, LRESULT, WPARAM}, System::Threading::GetCurrentThreadId, UI::WindowsAndMessaging::{ CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetMessageW, PostMessageW, PostThreadMessageW, RegisterClassW, RegisterWindowMessageW, SendMessageW, TranslateMessage, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, MSG, WINDOW_EX_STYLE, WM_QUIT, WNDCLASSW, WNDPROC, WS_OVERLAPPEDWINDOW, }, }, }; use crate::{DispatchFn, Dispatcher, WndProcCallback}; thread_local! { /// Custom message ID for dispatching closures to be run on the event /// loop thread. /// /// `WPARAM` contains a `Box>` that must be retrieved /// with `Box::from_raw`. `LPARAM` is unused. /// /// This message is sent using `PostMessageW` and handled in /// [`EventLoop::window_proc`]. static WM_DISPATCH_CALLBACK: u32 = unsafe { RegisterWindowMessageW(w!("GlazeWM:Dispatch")) }; /// Registered callbacks that pre-process messages in the event loop's /// window procedure. /// /// Keyed by a unique callback ID for later deregistration. static WNDPROC_CALLBACKS: RefCell>> = RefCell::new(HashMap::new()); } /// Source for dispatching callbacks onto the event loop thread. #[derive(Clone)] pub(crate) struct EventLoopSource { pub(crate) message_window_handle: isize, pub(crate) thread_id: ThreadId, os_thread_id: u32, next_callback_id: Arc, } impl EventLoopSource { pub(crate) fn send_dispatch_async( &self, dispatch_fn: F, ) -> crate::Result<()> where F: FnOnce() + Send + 'static, { // Double box the callback to avoid `STATUS_ACCESS_VIOLATION` on // Windows. Ref: https://github.com/tauri-apps/tao/blob/dev/src/platform_impl/windows/event_loop.rs#L596 let dispatch_fn: Box> = Box::new(Box::new(dispatch_fn)); // Leak to a raw pointer to then be passed as `WPARAM` in the message. let callback_ptr = Box::into_raw(dispatch_fn); unsafe { if PostMessageW( HWND(self.message_window_handle), WM_DISPATCH_CALLBACK.with(|v| *v), WPARAM(callback_ptr as _), LPARAM(0), ) .is_ok() { Ok(()) } else { // If `PostMessage` fails, we need to clean up the callback. let _ = Box::from_raw(callback_ptr); Err(crate::Error::WindowMessage( "Failed to post message".to_string(), )) } } } #[allow(clippy::unnecessary_wraps)] pub(crate) fn send_dispatch_sync( &self, dispatch_fn: F, ) -> crate::Result<()> where F: FnOnce() + Send, { let dispatch_fn: Box> = Box::new(Box::new(dispatch_fn)); let callback_ptr = Box::into_raw(dispatch_fn); // `SendMessageW` blocks the calling thread until the window procedure // processes the message and executes the closure. This guarantees the // closure's lifetime remains valid. unsafe { SendMessageW( HWND(self.message_window_handle), WM_DISPATCH_CALLBACK.with(|v| *v), WPARAM(callback_ptr as _), LPARAM(0), ); } Ok(()) } pub(crate) fn send_stop(&self) -> crate::Result<()> { unsafe { PostThreadMessageW(self.os_thread_id, WM_QUIT, WPARAM(0), LPARAM(0)) } .map_err(|_| { crate::Error::WindowMessage( "Failed to post quit message".to_string(), ) }) } pub(crate) fn register_wndproc_callback( &self, callback: Box, ) -> crate::Result { let id = self.next_callback_id.fetch_add(1, Ordering::Relaxed); // The callback is installed asynchronously on the event loop thread. self.send_dispatch_async(move || { WNDPROC_CALLBACKS.with(|cbs| { cbs.borrow_mut().insert(id, callback); }); })?; Ok(id) } pub(crate) fn deregister_wndproc_callback( &self, id: usize, ) -> crate::Result<()> { self.send_dispatch_async(move || { WNDPROC_CALLBACKS.with(|cbs| { cbs.borrow_mut().remove(&id); }); }) } } /// Platform-specific implementation of [`EventLoop`]. pub(crate) struct EventLoop { source: EventLoopSource, } impl EventLoop { /// Implements [`EventLoop::new`]. pub(crate) fn new() -> crate::Result<(Self, Dispatcher)> { // Create a hidden message window on the current thread. let window_handle = Self::create_message_window(Some(Self::window_proc))?; let source = EventLoopSource { message_window_handle: window_handle, thread_id: thread::current().id(), os_thread_id: unsafe { GetCurrentThreadId() }, next_callback_id: Arc::new(AtomicUsize::new(0)), }; let stopped = Arc::new(AtomicBool::new(false)); let dispatcher = Dispatcher::new(Some(source.clone()), stopped); Ok((Self { source }, dispatcher)) } /// Implements [`EventLoop::run`]. pub(crate) fn run(&self) -> crate::Result<()> { tracing::info!("Starting event loop."); let mut msg = MSG::default(); // Start the message loop. Blocks until `WM_QUIT` is received. loop { if unsafe { GetMessageW(&raw mut msg, None, 0, 0) }.as_bool() { unsafe { TranslateMessage(&raw const msg); DispatchMessageW(&raw const msg); } } else { break; } } tracing::info!("Event loop thread exiting."); unsafe { DestroyWindow(HWND(self.source.message_window_handle)) }?; Ok(()) } /// Shuts down the event loop gracefully. pub(crate) fn shutdown(&mut self) -> crate::Result<()> { tracing::info!("Shutting down event loop."); self.source.send_stop()?; Ok(()) } /// Creates a hidden message window. /// /// Returns a handle to the created window. fn create_message_window( window_procedure: WNDPROC, ) -> crate::Result { let wnd_class = WNDCLASSW { lpszClassName: w!("MessageWindow"), style: CS_HREDRAW | CS_VREDRAW, lpfnWndProc: window_procedure, ..Default::default() }; unsafe { RegisterClassW(&raw const wnd_class) }; let handle = unsafe { CreateWindowExW( WINDOW_EX_STYLE::default(), w!("MessageWindow"), w!("MessageWindow"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, None, None, wnd_class.hInstance, None, ) }; if handle.0 == 0 { return Err(crate::Error::Platform( "Creation of message window failed.".to_string(), )); } Ok(handle.0) } /// Window procedure for handling messages. unsafe extern "system" fn window_proc( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { // Handle dispatch callbacks first. if msg == WM_DISPATCH_CALLBACK.with(|v| *v) { // Convert the `WPARAM` fn pointer back to a double-boxed function. let dispatch_fn: Box> = Box::from_raw(wparam.0 as *mut _); dispatch_fn(); return LRESULT(0); } // Let registered callbacks pre-process the message. let handled = WNDPROC_CALLBACKS.with(|cbs| { for callback in cbs.borrow().values() { if let Some(result) = callback(hwnd.0, msg, wparam.0, lparam.0) { return Some(LRESULT(result)); } } None }); if let Some(result) = handled { return result; } // `WM_QUIT` is handled by the message loop and should be forwarded // along with other messages. DefWindowProcW(hwnd, msg, wparam, lparam) } } impl Drop for EventLoop { fn drop(&mut self) { if let Err(err) = self.shutdown() { tracing::warn!("Failed to shut down event loop: {err}"); } } } ================================================ FILE: packages/wm-platform/src/platform_impl/windows/keyboard_hook.rs ================================================ use std::cell::Cell; use windows::Win32::{ Foundation::{HINSTANCE, LPARAM, LRESULT, WPARAM}, UI::{ Input::KeyboardAndMouse::{ GetKeyState, VK_LCONTROL, VK_LMENU, VK_LSHIFT, VK_LWIN, VK_RCONTROL, VK_RMENU, VK_RSHIFT, VK_RWIN, }, WindowsAndMessaging::{ CallNextHookEx, SetWindowsHookExW, UnhookWindowsHookEx, HHOOK, KBDLLHOOKSTRUCT, WH_KEYBOARD_LL, WM_KEYDOWN, WM_SYSKEYDOWN, }, }, }; use crate::{Dispatcher, Key, KeyCode}; /// Callback stored in [`HOOK`] for intercepting keyboard events. type HookCallback = Box bool>; thread_local! { /// Stores the hook callback for the current thread. /// /// The hook callback is called for every keyboard event and returns /// `true` if the event should be intercepted. static HOOK: Cell> = Cell::default(); } /// A key event received from the keyboard hook. #[derive(Clone, Debug)] pub struct KeyEvent { /// The key that was pressed or released. pub key: Key, /// Key code that generated this event. #[allow(dead_code)] pub key_code: KeyCode, /// Whether the event is for a key press or release. pub is_keypress: bool, } impl KeyEvent { /// Gets whether the specified key is currently pressed. #[allow(clippy::unused_self)] pub fn is_key_down(&self, key: Key) -> bool { match key { Key::Cmd | Key::Win => { Self::is_key_down_raw(VK_LWIN.0) || Self::is_key_down_raw(VK_RWIN.0) } Key::Alt => { Self::is_key_down_raw(VK_LMENU.0) || Self::is_key_down_raw(VK_RMENU.0) } Key::Ctrl => { Self::is_key_down_raw(VK_LCONTROL.0) || Self::is_key_down_raw(VK_RCONTROL.0) } Key::Shift => { Self::is_key_down_raw(VK_LSHIFT.0) || Self::is_key_down_raw(VK_RSHIFT.0) } _ => { if let Ok(key_code) = KeyCode::try_from(key) { Self::is_key_down_raw(key_code.0) } else { false } } } } /// Gets whether the specified key is currently down using the raw key /// code. fn is_key_down_raw(key: u16) -> bool { unsafe { (GetKeyState(key.into()) & 0x80) == 0x80 } } } /// A system-wide low-level keyboard hook. #[derive(Debug)] pub struct KeyboardHook { handle: HHOOK, dispatcher: Dispatcher, } impl KeyboardHook { /// Creates an instance of `KeyboardHook`. /// /// The callback is called for every keyboard event and returns `true` if /// the event should be intercepted. /// /// # Panics /// /// Panics when attempting to register multiple hooks on the dispatcher's /// thread. pub fn new( callback: F, dispatcher: &Dispatcher, ) -> crate::Result where F: Fn(KeyEvent) -> bool + Send + Sync + 'static, { let handle = dispatcher.dispatch_sync(move || { HOOK.with(|state| { assert!( state.take().is_none(), "Only one keyboard hook can be registered on the dispatcher's thread." ); state.set(Some(Box::new(callback))); }); unsafe { SetWindowsHookExW( WH_KEYBOARD_LL, Some(Self::hook_proc), HINSTANCE::default(), 0, ) } })??; Ok(Self { handle, dispatcher: dispatcher.clone(), }) } /// Terminates the keyboard hook by unregistering it. pub fn terminate(&mut self) -> crate::Result<()> { unsafe { UnhookWindowsHookEx(self.handle) }?; // Dispatch cleanup to the event loop thread since the callback // is stored in a thread-local on that thread. let _ = self.dispatcher.dispatch_async(|| { HOOK.with(|state| { state.take(); }); }); Ok(()) } /// Hook procedure for keyboard events. /// /// For use with `SetWindowsHookExW`. extern "system" fn hook_proc( code: i32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { // If the code is less than zero, the hook procedure must pass the hook // notification directly to other applications. if code != 0 { return unsafe { CallNextHookEx(None, code, wparam, lparam) }; } // Get struct with the keyboard input event. let input = unsafe { *(lparam.0 as *const KBDLLHOOKSTRUCT) }; #[allow(clippy::cast_possible_truncation)] let key_code = KeyCode(input.vkCode as u16); #[allow(clippy::cast_possible_truncation)] let is_keypress = wparam.0 as u32 == WM_KEYDOWN || wparam.0 as u32 == WM_SYSKEYDOWN; let Ok(key) = Key::try_from(key_code) else { return unsafe { CallNextHookEx(None, code, wparam, lparam) }; }; let key_event = KeyEvent { key, key_code, is_keypress, }; let should_intercept = HOOK.with(|state| { if let Some(callback) = state.take() { let result = callback(key_event); state.set(Some(callback)); result } else { false } }); if should_intercept { return LRESULT(1); } unsafe { CallNextHookEx(None, code, wparam, lparam) } } } impl Drop for KeyboardHook { fn drop(&mut self) { let _ = self.terminate(); } } ================================================ FILE: packages/wm-platform/src/platform_impl/windows/mod.rs ================================================ pub(crate) mod com; mod display; mod display_listener; mod event_loop; mod keyboard_hook; mod mouse_listener; mod native_window; mod single_instance; mod window_listener; pub(crate) use display::*; pub(crate) use display_listener::*; pub(crate) use event_loop::*; pub(crate) use keyboard_hook::*; pub(crate) use mouse_listener::*; pub(crate) use native_window::*; pub(crate) use single_instance::*; pub(crate) use window_listener::*; ================================================ FILE: packages/wm-platform/src/platform_impl/windows/mouse_listener.rs ================================================ use std::{ sync::{Arc, Mutex}, time::{Duration, Instant}, }; use tokio::sync::mpsc; use windows::Win32::{ Devices::HumanInterfaceDevice::{ HID_USAGE_GENERIC_MOUSE, HID_USAGE_PAGE_GENERIC, }, Foundation::{HWND, POINT}, UI::{ Input::{ GetRawInputData, RegisterRawInputDevices, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTHEADER, RIDEV_INPUTSINK, RIDEV_REMOVE, RID_INPUT, RIM_TYPEMOUSE, }, WindowsAndMessaging::{ GetCursorPos, RI_MOUSE_LEFT_BUTTON_DOWN, RI_MOUSE_LEFT_BUTTON_UP, RI_MOUSE_RIGHT_BUTTON_DOWN, RI_MOUSE_RIGHT_BUTTON_UP, WM_INPUT, }, }, }; use super::FOREGROUND_INPUT_IDENTIFIER; use crate::{ mouse_listener::MouseEventKind, platform_event::{MouseButton, MouseEvent, PressedButtons}, Dispatcher, DispatcherExtWindows, Point, }; /// Data shared with the window procedure callback. struct CallbackData { event_tx: mpsc::UnboundedSender, /// Pressed button state tracked from events. pressed: PressedButtons, /// Timestamp of the last emitted `Move` event for throttling. last_move_emission: Option, } /// Platform-specific implementation of [`MouseListener`]. pub(crate) struct MouseListener { callback_id: Option, callback_data: Arc>, dispatcher: Dispatcher, } impl MouseListener { /// Implements [`MouseListener::new`]. pub(crate) fn new( enabled_events: &[MouseEventKind], event_tx: mpsc::UnboundedSender, dispatcher: &Dispatcher, ) -> crate::Result { let callback_data = Arc::new(Mutex::new(CallbackData { event_tx, pressed: PressedButtons::default(), last_move_emission: None, })); let callback_id = Self::register_callback( enabled_events, Arc::clone(&callback_data), dispatcher, )?; Ok(Self { callback_id: Some(callback_id), dispatcher: dispatcher.clone(), callback_data, }) } /// Implements [`MouseListener::enable`]. pub(crate) fn enable(&mut self, enabled: bool) -> crate::Result<()> { if self.callback_id.is_some() { let handle = self.dispatcher.message_window_handle(); self.dispatcher.dispatch_sync(move || { Self::enable_raw_input(handle, enabled) })??; } Ok(()) } /// Implements [`MouseListener::set_enabled_events`]. pub(crate) fn set_enabled_events( &mut self, enabled_events: &[MouseEventKind], ) -> crate::Result<()> { let _ = self.terminate(); let callback_id = Self::register_callback( enabled_events, Arc::clone(&self.callback_data), &self.dispatcher, )?; self.callback_id = Some(callback_id); Ok(()) } /// Implements [`MouseListener::terminate`]. pub(crate) fn terminate(&mut self) -> crate::Result<()> { self.enable(false)?; if let Some(id) = self.callback_id.take() { self.dispatcher.deregister_wndproc_callback(id)?; } Ok(()) } /// Registers a window procedure callback for `WM_INPUT` and enables raw /// input. /// /// Returns the ID of the created callback. fn register_callback( enabled_events: &[MouseEventKind], callback_data: Arc>, dispatcher: &Dispatcher, ) -> crate::Result { let enabled_events: Arc<[MouseEventKind]> = Arc::from(enabled_events.to_vec().into_boxed_slice()); let callback_id = dispatcher.register_wndproc_callback(Box::new( move |_hwnd, msg, _wparam, lparam| { if msg != WM_INPUT { return None; } let mut callback_data = callback_data .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if let Err(err) = Self::handle_wm_input( lparam, &enabled_events, &mut callback_data, ) { tracing::warn!("Failed to handle WM_INPUT message: {}", err); } Some(0) }, ))?; // Register raw input devices, which will then deliver `WM_INPUT` // messages to the event loop's message window. let handle = dispatcher.message_window_handle(); dispatcher .dispatch_sync(move || Self::enable_raw_input(handle, true))??; Ok(callback_id) } /// Processes a `WM_INPUT` message, extracting raw input data and /// sending the appropriate [`MouseEvent`] on the channel. fn handle_wm_input( lparam: isize, enabled_events: &[MouseEventKind], callback_data: &mut CallbackData, ) -> crate::Result<()> { let mut raw_input: RAWINPUT = unsafe { std::mem::zeroed() }; #[allow(clippy::cast_possible_truncation)] let mut raw_input_size = std::mem::size_of::() as u32; let res_size = unsafe { #[allow(clippy::cast_possible_truncation)] GetRawInputData( HRAWINPUT(lparam), RID_INPUT, Some(std::ptr::from_mut(&mut raw_input).cast()), &raw mut raw_input_size, std::mem::size_of::() as u32, ) }; // Ignore if input is invalid or not a mouse event. Inputs from our own // process are also ignored, since `NativeWindow::focus` simulates // mouse input. #[allow(clippy::cast_possible_truncation)] if res_size == 0 || raw_input_size == u32::MAX || raw_input.header.dwType != RIM_TYPEMOUSE.0 || unsafe { raw_input.data.mouse.ulExtraInformation } as u32 == FOREGROUND_INPUT_IDENTIFIER { return Ok(()); } // Map button flags to a `MouseEventKind`. let event_kind = { let button_flags = u32::from(unsafe { raw_input.data.mouse.Anonymous.Anonymous.usButtonFlags }); // Button flags indicate a transition in mouse button state. // Ref: https://learn.microsoft.com/en-us/windows/win32/api/ntddmou/ns-ntddmou-mouse_input_data#members if button_flags & RI_MOUSE_LEFT_BUTTON_DOWN != 0 { MouseEventKind::LeftButtonDown } else if button_flags & RI_MOUSE_LEFT_BUTTON_UP != 0 { MouseEventKind::LeftButtonUp } else if button_flags & RI_MOUSE_RIGHT_BUTTON_DOWN != 0 { MouseEventKind::RightButtonDown } else if button_flags & RI_MOUSE_RIGHT_BUTTON_UP != 0 { MouseEventKind::RightButtonUp } else { MouseEventKind::Move } }; if !enabled_events.contains(&event_kind) { return Ok(()); } // Throttle mouse move events so that there's a minimum of 50ms between // each emission. State change events (button down/up) always get // emitted. let should_emit = match event_kind { MouseEventKind::Move => { callback_data.last_move_emission.is_none_or(|timestamp| { timestamp.elapsed() >= Duration::from_millis(50) }) } _ => true, }; if !should_emit { return Ok(()); } callback_data.pressed.update(event_kind); let mouse_event = match event_kind { MouseEventKind::LeftButtonDown => MouseEvent::ButtonDown { position: Self::cursor_pos()?, button: MouseButton::Left, pressed_buttons: callback_data.pressed, }, MouseEventKind::LeftButtonUp => MouseEvent::ButtonUp { position: Self::cursor_pos()?, button: MouseButton::Left, pressed_buttons: callback_data.pressed, }, MouseEventKind::RightButtonDown => MouseEvent::ButtonDown { position: Self::cursor_pos()?, button: MouseButton::Right, pressed_buttons: callback_data.pressed, }, MouseEventKind::RightButtonUp => MouseEvent::ButtonUp { position: Self::cursor_pos()?, button: MouseButton::Right, pressed_buttons: callback_data.pressed, }, MouseEventKind::Move => MouseEvent::Move { position: Self::cursor_pos()?, pressed_buttons: callback_data.pressed, window_below_cursor: None, }, }; let _ = callback_data.event_tx.send(mouse_event); if event_kind == MouseEventKind::Move { callback_data.last_move_emission = Some(Instant::now()); } Ok(()) } /// Gets the current cursor position. fn cursor_pos() -> crate::Result { let mut point = POINT { x: 0, y: 0 }; unsafe { GetCursorPos(&raw mut point) }?; Ok(Point { x: point.x, y: point.y, }) } /// Registers or deregisters the raw input device for mouse events. fn enable_raw_input( target_handle: isize, enabled: bool, ) -> crate::Result<()> { let mode_flag = if enabled { RIDEV_INPUTSINK } else { RIDEV_REMOVE }; let target_hwnd = if enabled { HWND(target_handle) } else { HWND::default() }; let rid = RAWINPUTDEVICE { usUsagePage: HID_USAGE_PAGE_GENERIC, usUsage: HID_USAGE_GENERIC_MOUSE, dwFlags: mode_flag, hwndTarget: target_hwnd, }; unsafe { #[allow(clippy::cast_possible_truncation)] RegisterRawInputDevices( &[rid], std::mem::size_of::() as u32, ) } .map_err(crate::Error::from) } } impl Drop for MouseListener { fn drop(&mut self) { if let Err(err) = self.terminate() { tracing::warn!("Failed to terminate mouse listener: {}", err); } } } ================================================ FILE: packages/wm-platform/src/platform_impl/windows/native_window.rs ================================================ use std::time::Duration; use tokio::task; use tracing::warn; use windows::{ core::PWSTR, Win32::{ Foundation::{CloseHandle, BOOL, HWND, LPARAM, POINT, RECT}, Graphics::Dwm::{ DwmGetWindowAttribute, DwmSetWindowAttribute, DWMWA_BORDER_COLOR, DWMWA_CLOAKED, DWMWA_COLOR_NONE, DWMWA_EXTENDED_FRAME_BOUNDS, DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_DEFAULT, DWMWCP_DONOTROUND, DWMWCP_ROUND, DWMWCP_ROUNDSMALL, }, System::Threading::{ OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, }, UI::{ Input::KeyboardAndMouse::{ SendInput, INPUT, INPUT_0, INPUT_MOUSE, MOUSEINPUT, }, WindowsAndMessaging::{ EnumWindows, GetAncestor, GetClassNameW, GetDesktopWindow, GetForegroundWindow, GetLayeredWindowAttributes, GetShellWindow, GetWindow, GetWindowLongPtrW, GetWindowRect, GetWindowTextW, GetWindowThreadProcessId, IsIconic, IsWindow, IsWindowVisible, IsZoomed, SendNotifyMessageW, SetForegroundWindow, SetLayeredWindowAttributes, SetWindowLongPtrW, SetWindowPlacement, SetWindowPos, ShowWindowAsync, WindowFromPoint, GA_ROOT, GWL_EXSTYLE, GWL_STYLE, GW_OWNER, HWND_NOTOPMOST, HWND_TOP, HWND_TOPMOST, LAYERED_WINDOW_ATTRIBUTES_FLAGS, LWA_ALPHA, LWA_COLORKEY, SET_WINDOW_POS_FLAGS, SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOMOVE, SWP_NOOWNERZORDER, SWP_NOSENDCHANGING, SWP_NOSIZE, SWP_NOZORDER, SWP_SHOWWINDOW, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOWNA, WINDOWPLACEMENT, WINDOW_EX_STYLE, WINDOW_STYLE, WM_CLOSE, WPF_ASYNCWINDOWPLACEMENT, WS_DLGFRAME, WS_EX_LAYERED, WS_THICKFRAME, }, }, }, }; use super::com::{IApplicationView, COM_INIT}; use crate::{ Color, CornerStyle, Delta, Dispatcher, LengthValue, OpacityValue, Point, Rect, RectDelta, WindowId, WindowZOrder, }; /// Magic number used to identify programmatic mouse inputs from our own /// process. pub(crate) const FOREGROUND_INPUT_IDENTIFIER: u32 = 6379; /// Platform-specific implementation of [`NativeWindow`]. #[derive(Clone, Debug)] pub(crate) struct NativeWindow { pub(crate) handle: isize, } impl NativeWindow { /// Creates an instance of `NativeWindow`. #[must_use] pub(crate) fn new(handle: isize) -> Self { Self { handle } } /// Implements [`NativeWindow::id`]. #[must_use] pub(crate) fn id(&self) -> WindowId { WindowId(self.handle) } /// Implements [`NativeWindow::title`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn title(&self) -> crate::Result { let mut text: [u16; 512] = [0; 512]; let length = unsafe { GetWindowTextW(self.hwnd(), &mut text) }; #[allow(clippy::cast_sign_loss)] Ok(String::from_utf16_lossy(&text[..length as usize])) } /// Implements [`NativeWindow::process_name`]. pub(crate) fn process_name(&self) -> crate::Result { let mut process_id = 0u32; unsafe { GetWindowThreadProcessId(self.hwnd(), Some(&raw mut process_id)); } let process_handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, process_id) }?; let mut buffer = [0u16; 256]; let mut length = u32::try_from(buffer.len())?; unsafe { let query_res = QueryFullProcessImageNameW( process_handle, PROCESS_NAME_WIN32, PWSTR(buffer.as_mut_ptr()), &raw mut length, ); // Always close the process handle regardless of the query result. CloseHandle(process_handle)?; query_res }?; let exe_path = String::from_utf16_lossy(&buffer[..length as usize]); exe_path .split('\\') .next_back() .map(|file_name| { file_name.split('.').next().unwrap_or(file_name).to_string() }) .ok_or_else(|| { crate::Error::Platform("Failed to parse process name.".to_string()) }) } /// Implements [`NativeWindow::frame`]. pub(crate) fn frame(&self) -> crate::Result { let mut rect = RECT::default(); let dwm_res = unsafe { #[allow(clippy::cast_possible_truncation)] DwmGetWindowAttribute( self.hwnd(), DWMWA_EXTENDED_FRAME_BOUNDS, std::ptr::from_mut(&mut rect).cast(), std::mem::size_of::() as u32, ) }; if let Ok(()) = dwm_res { Ok(Rect::from_ltrb( rect.left, rect.top, rect.right, rect.bottom, )) } else { warn!("Failed to get window's frame position. Falling back to border position."); self.frame_with_shadows() } } /// Implements [`NativeWindow::position`]. pub(crate) fn position(&self) -> crate::Result<(f64, f64)> { let frame = self.frame()?; Ok((f64::from(frame.left), f64::from(frame.top))) } /// Implements [`NativeWindow::size`]. pub(crate) fn size(&self) -> crate::Result<(f64, f64)> { let frame = self.frame()?; Ok((f64::from(frame.width()), f64::from(frame.height()))) } /// Implements [`NativeWindow::is_valid`]. pub(crate) fn is_valid(&self) -> bool { unsafe { IsWindow(self.hwnd()) }.as_bool() } /// Implements [`NativeWindow::is_visible`]. pub(crate) fn is_visible(&self) -> crate::Result { let is_visible = unsafe { IsWindowVisible(self.hwnd()) }.as_bool(); Ok(is_visible && !self.is_cloaked()?) } /// Implements [`NativeWindow::is_minimized`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn is_minimized(&self) -> crate::Result { Ok(unsafe { IsIconic(self.hwnd()) }.as_bool()) } /// Implements [`NativeWindow::is_maximized`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn is_maximized(&self) -> crate::Result { Ok(unsafe { IsZoomed(self.hwnd()) }.as_bool()) } /// Implements [`NativeWindow::is_resizable`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn is_resizable(&self) -> crate::Result { Ok(self.has_window_style(WS_THICKFRAME)) } /// Implements [`NativeWindow::is_desktop_window`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn is_desktop_window(&self) -> crate::Result { Ok(*self == desktop_window()) } /// Implements [`NativeWindow::set_frame`]. pub(crate) fn set_frame(&self, rect: &Rect) -> crate::Result<()> { unsafe { SetWindowPos( self.hwnd(), HWND_NOTOPMOST, rect.x(), rect.y(), rect.width(), rect.height(), SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOCOPYBITS | SWP_NOSENDCHANGING | SWP_ASYNCWINDOWPOS | SWP_FRAMECHANGED, ) }?; Ok(()) } /// Implements [`NativeWindow::resize`]. pub(crate) fn resize( &self, width: i32, height: i32, ) -> crate::Result<()> { unsafe { SetWindowPos( self.hwnd(), HWND_NOTOPMOST, 0, 0, width, height, SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOMOVE | SWP_NOCOPYBITS | SWP_NOSENDCHANGING | SWP_ASYNCWINDOWPOS | SWP_FRAMECHANGED, ) }?; Ok(()) } /// Implements [`NativeWindow::reposition`]. pub(crate) fn reposition(&self, x: i32, y: i32) -> crate::Result<()> { unsafe { SetWindowPos( self.hwnd(), HWND_NOTOPMOST, x, y, 0, 0, SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOSIZE | SWP_NOCOPYBITS | SWP_NOSENDCHANGING | SWP_ASYNCWINDOWPOS | SWP_FRAMECHANGED, ) }?; Ok(()) } /// Implements [`NativeWindow::minimize`]. pub(crate) fn minimize(&self) -> crate::Result<()> { unsafe { ShowWindowAsync(self.hwnd(), SW_MINIMIZE).ok() }?; Ok(()) } /// Implements [`NativeWindow::maximize`]. pub(crate) fn maximize(&self) -> crate::Result<()> { unsafe { ShowWindowAsync(self.hwnd(), SW_MAXIMIZE).ok() }?; Ok(()) } /// Implements [`NativeWindow::focus`]. pub(crate) fn focus(&self) -> crate::Result<()> { let input = [INPUT { r#type: INPUT_MOUSE, Anonymous: INPUT_0 { mi: MOUSEINPUT { dwExtraInfo: FOREGROUND_INPUT_IDENTIFIER as usize, ..Default::default() }, }, }]; // Bypass restriction for setting the foreground window by sending an // input to our own process first. #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] unsafe { SendInput(&input, std::mem::size_of::() as i32) }; // Set as the foreground window. unsafe { SetForegroundWindow(self.hwnd()) }.ok()?; Ok(()) } /// Implements [`NativeWindow::close`]. pub(crate) fn close(&self) -> crate::Result<()> { unsafe { SendNotifyMessageW(self.hwnd(), WM_CLOSE, None, None) }?; Ok(()) } /// Implements [`NativeWindowWindowsExt::hwnd`]. pub(crate) fn hwnd(&self) -> HWND { HWND(self.handle) } /// Implements [`NativeWindowWindowsExt::class_name`]. pub(crate) fn class_name(&self) -> crate::Result { let mut buffer = [0u16; 256]; let result = unsafe { GetClassNameW(self.hwnd(), &mut buffer) }; if result == 0 { return Err(windows::core::Error::from_win32().into()); } #[allow(clippy::cast_sign_loss)] let class_name = String::from_utf16_lossy(&buffer[..result as usize]); Ok(class_name) } /// Implements [`NativeWindowWindowsExt::frame_with_shadows`]. pub(crate) fn frame_with_shadows(&self) -> crate::Result { let mut rect = RECT::default(); unsafe { GetWindowRect(self.hwnd(), std::ptr::from_mut(&mut rect).cast()) }?; Ok(Rect::from_ltrb( rect.left, rect.top, rect.right, rect.bottom, )) } /// Implements [`NativeWindowWindowsExt::shadow_borders`]. // TODO: Return tuple of (left, top, right, bottom) instead of // `RectDelta`. pub(crate) fn shadow_borders(&self) -> crate::Result { let border_pos = self.frame_with_shadows()?; let frame_pos = self.frame()?; Ok(RectDelta::new( LengthValue::from_px(frame_pos.left - border_pos.left), LengthValue::from_px(frame_pos.top - border_pos.top), LengthValue::from_px(border_pos.right - frame_pos.right), LengthValue::from_px(border_pos.bottom - frame_pos.bottom), )) } /// Implements [`NativeWindowWindowsExt::has_owner_window`]. pub(crate) fn has_owner_window(&self) -> bool { unsafe { GetWindow(self.hwnd(), GW_OWNER) }.0 != 0 } /// Implements [`NativeWindowWindowsExt::has_window_style`]. pub(crate) fn has_window_style(&self, style: WINDOW_STYLE) -> bool { let current_style = unsafe { GetWindowLongPtrW(self.hwnd(), GWL_STYLE) }; #[allow(clippy::cast_possible_wrap)] let style = style.0 as isize; (current_style & style) != 0 } /// Implements [`NativeWindowWindowsExt::has_window_style_ex`]. pub(crate) fn has_window_style_ex( &self, style: WINDOW_EX_STYLE, ) -> bool { let current_style = unsafe { GetWindowLongPtrW(self.hwnd(), GWL_EXSTYLE) }; #[allow(clippy::cast_possible_wrap)] let style = style.0 as isize; (current_style & style) != 0 } /// Implements [`NativeWindowWindowsExt::set_window_pos`]. pub(crate) fn set_window_pos( &self, z_order: &WindowZOrder, rect: &Rect, flags: SET_WINDOW_POS_FLAGS, ) -> crate::Result<()> { let z_order_hwnd = match z_order { WindowZOrder::TopMost => HWND_TOPMOST, WindowZOrder::Top => HWND_TOP, WindowZOrder::Normal => HWND_NOTOPMOST, WindowZOrder::AfterWindow(window_id) => HWND(window_id.0), }; unsafe { SetWindowPos( self.hwnd(), z_order_hwnd, rect.x(), rect.y(), rect.width(), rect.height(), flags, ) }?; Ok(()) } /// Implements [`NativeWindowWindowsExt::show`]. pub(crate) fn show(&self) -> crate::Result<()> { unsafe { ShowWindowAsync(self.hwnd(), SW_SHOWNA) }.ok()?; Ok(()) } /// Implements [`NativeWindowWindowsExt::hide`]. pub(crate) fn hide(&self) -> crate::Result<()> { unsafe { ShowWindowAsync(self.hwnd(), SW_HIDE) }.ok()?; Ok(()) } /// Implements [`NativeWindowWindowsExt::restore`]. pub(crate) fn restore( &self, outer_frame: Option<&Rect>, ) -> crate::Result<()> { match outer_frame { None => { unsafe { ShowWindowAsync(self.hwnd(), SW_RESTORE) }.ok()?; Ok(()) } Some(rect) => { let placement = WINDOWPLACEMENT { #[allow(clippy::cast_possible_truncation)] length: std::mem::size_of::() as u32, flags: WPF_ASYNCWINDOWPLACEMENT, showCmd: SW_RESTORE.0 as u32, rcNormalPosition: RECT { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, }, ..Default::default() }; unsafe { SetWindowPlacement(self.hwnd(), &raw const placement) }?; Ok(()) } } } /// Implements [`NativeWindowWindowsExt::set_cloaked`]. pub(crate) fn set_cloaked(&self, cloaked: bool) -> crate::Result<()> { COM_INIT.with(|com_init| -> crate::Result<()> { com_init.borrow_mut().with_retry(|com| { let view_collection = com.application_view_collection()?; let mut view: Option = None; unsafe { view_collection.get_view_for_hwnd(self.hwnd().0, &raw mut view) } .ok()?; let view = view.ok_or_else(|| { crate::Error::Platform( "Unable to get application view by window handle.".to_string(), ) })?; // Ref: https://github.com/Ciantic/AltTabAccessor/issues/1#issuecomment-1426877843 unsafe { view.set_cloak(1, if cloaked { 2 } else { 0 }) } .ok() .map_err(|_| { crate::Error::Platform("Failed to cloak window.".to_string()) }) }) }) } /// Implements [`NativeWindowWindowsExt::mark_fullscreen`]. pub(crate) fn mark_fullscreen( &self, fullscreen: bool, ) -> crate::Result<()> { COM_INIT.with(|com_init| -> crate::Result<()> { com_init.borrow_mut().with_retry(|com| { let taskbar_list = com.taskbar_list()?; unsafe { taskbar_list.MarkFullscreenWindow(self.hwnd(), fullscreen) }?; Ok(()) }) }) } /// Implements [`NativeWindowWindowsExt::set_taskbar_visibility`]. pub(crate) fn set_taskbar_visibility( &self, visible: bool, ) -> crate::Result<()> { COM_INIT.with(|com_init| -> crate::Result<()> { com_init.borrow_mut().with_retry(|com| { let taskbar_list = com.taskbar_list()?; if visible { unsafe { taskbar_list.AddTab(self.hwnd())? }; } else { unsafe { taskbar_list.DeleteTab(self.hwnd())? }; } Ok(()) }) }) } /// Implements [`NativeWindowWindowsExt::add_window_style_ex`]. pub(crate) fn add_window_style_ex(&self, style: WINDOW_EX_STYLE) { let current_style = unsafe { GetWindowLongPtrW(self.hwnd(), GWL_EXSTYLE) }; #[allow(clippy::cast_possible_wrap)] if current_style & style.0 as isize == 0 { let new_style = current_style | style.0 as isize; unsafe { SetWindowLongPtrW(self.hwnd(), GWL_EXSTYLE, new_style) }; } } /// Implements [`NativeWindowWindowsExt::set_z_order`]. pub(crate) fn set_z_order( &self, z_order: &WindowZOrder, ) -> crate::Result<()> { let z_order_hwnd = match z_order { WindowZOrder::TopMost => HWND_TOPMOST, WindowZOrder::Top => HWND_TOP, WindowZOrder::Normal => HWND_NOTOPMOST, WindowZOrder::AfterWindow(window_id) => HWND(window_id.0), }; let flags = SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_ASYNCWINDOWPOS | SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE; unsafe { SetWindowPos(self.hwnd(), z_order_hwnd, 0, 0, 0, 0, flags) }?; // Z-order can sometimes still be incorrect after the above call. let handle = self.handle; task::spawn(async move { tokio::time::sleep(Duration::from_millis(10)).await; let _ = unsafe { SetWindowPos(HWND(handle), z_order_hwnd, 0, 0, 0, 0, flags) }; }); Ok(()) } /// Implements [`NativeWindowWindowsExt::set_title_bar_visibility`]. pub(crate) fn set_title_bar_visibility( &self, visible: bool, ) -> crate::Result<()> { let style = unsafe { GetWindowLongPtrW(self.hwnd(), GWL_STYLE) }; #[allow(clippy::cast_possible_wrap)] let new_style = if visible { style | (WS_DLGFRAME.0 as isize) } else { style & !(WS_DLGFRAME.0 as isize) }; if new_style != style { unsafe { SetWindowLongPtrW(self.hwnd(), GWL_STYLE, new_style); SetWindowPos( self.hwnd(), HWND_NOTOPMOST, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOSENDCHANGING | SWP_ASYNCWINDOWPOS, )?; } } Ok(()) } /// Implements [`NativeWindowWindowsExt::set_border_color`]. pub(crate) fn set_border_color( &self, color: Option<&Color>, ) -> crate::Result<()> { let bgr = match color { Some(color) => color.to_bgr(), None => DWMWA_COLOR_NONE, }; unsafe { #[allow(clippy::cast_possible_truncation)] DwmSetWindowAttribute( self.hwnd(), DWMWA_BORDER_COLOR, std::ptr::from_ref(&bgr).cast(), std::mem::size_of::() as u32, )?; } Ok(()) } /// Implements [`NativeWindowWindowsExt::set_corner_style`]. pub(crate) fn set_corner_style( &self, corner_style: &CornerStyle, ) -> crate::Result<()> { let corner_preference = match corner_style { CornerStyle::Default => DWMWCP_DEFAULT, CornerStyle::Square => DWMWCP_DONOTROUND, CornerStyle::Rounded => DWMWCP_ROUND, CornerStyle::SmallRounded => DWMWCP_ROUNDSMALL, }; unsafe { #[allow(clippy::cast_possible_truncation)] DwmSetWindowAttribute( self.hwnd(), DWMWA_WINDOW_CORNER_PREFERENCE, std::ptr::from_ref(&(corner_preference.0)).cast(), std::mem::size_of::() as u32, )?; } Ok(()) } /// Implements [`NativeWindowWindowsExt::set_transparency`]. pub(crate) fn set_transparency( &self, opacity_value: &OpacityValue, ) -> crate::Result<()> { // Make the window layered if it isn't already. self.add_window_style_ex(WS_EX_LAYERED); unsafe { SetLayeredWindowAttributes( self.hwnd(), None, opacity_value.to_alpha(), LWA_ALPHA, )?; } Ok(()) } /// Implements [`NativeWindowWindowsExt::adjust_transparency`]. pub(crate) fn adjust_transparency( &self, opacity_delta: &Delta, ) -> crate::Result<()> { let mut alpha = u8::MAX; let mut flag = LAYERED_WINDOW_ATTRIBUTES_FLAGS::default(); unsafe { GetLayeredWindowAttributes( self.hwnd(), None, Some(&raw mut alpha), Some(&raw mut flag), )?; } if flag.contains(LWA_COLORKEY) { return Err(crate::Error::Platform( "Window uses color key for its transparency and cannot be adjusted." .to_string(), )); } let target_alpha = if opacity_delta.is_negative { alpha.saturating_sub(opacity_delta.inner.to_alpha()) } else { alpha.saturating_add(opacity_delta.inner.to_alpha()) }; self.set_transparency(&OpacityValue::from_alpha(target_alpha)) } /// Whether the window is cloaked. For some UWP apps, `WS_VISIBLE` will /// be present even if the window isn't actually visible. The /// `DWMWA_CLOAKED` attribute is used to check whether these apps are /// visible. fn is_cloaked(&self) -> crate::Result { let mut cloaked = 0u32; unsafe { #[allow(clippy::cast_possible_truncation)] DwmGetWindowAttribute( self.hwnd(), DWMWA_CLOAKED, std::ptr::from_mut::(&mut cloaked).cast(), std::mem::size_of::() as u32, ) }?; Ok(cloaked != 0) } } impl PartialEq for NativeWindow { fn eq(&self, other: &Self) -> bool { self.handle == other.handle } } impl Eq for NativeWindow {} impl From for crate::NativeWindow { fn from(window: NativeWindow) -> Self { crate::NativeWindow { inner: window } } } /// Implements [`Dispatcher::visible_windows`]. pub(crate) fn visible_windows( _: &Dispatcher, ) -> crate::Result> { let mut handles: Vec = Vec::new(); #[allow(clippy::items_after_statements)] extern "system" fn visible_windows_proc( handle: HWND, data: LPARAM, ) -> BOOL { let handles = data.0 as *mut Vec; unsafe { (*handles).push(handle.0) }; true.into() } unsafe { EnumWindows( Some(visible_windows_proc), LPARAM(std::ptr::from_mut(&mut handles) as _), ) }?; Ok( handles .into_iter() .map(NativeWindow::new) .filter(|window| window.is_visible().unwrap_or(false)) .map(Into::into) .collect(), ) } /// Implements [`Dispatcher::focused_window`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn focused_window( _: &Dispatcher, ) -> crate::Result { let handle = unsafe { GetForegroundWindow() }; Ok(NativeWindow::new(handle.0).into()) } /// Implements [`Dispatcher::window_from_point`]. #[allow(clippy::unnecessary_wraps)] pub(crate) fn window_from_point( point: &Point, _: &Dispatcher, ) -> crate::Result> { let point = POINT { x: point.x, y: point.y, }; let handle = unsafe { WindowFromPoint(point) }; if handle.0 == 0 { return Ok(None); } let root = unsafe { GetAncestor(handle, GA_ROOT) }; if root.0 == 0 { return Ok(None); } Ok(Some(NativeWindow::new(root.0).into())) } /// Implements [`Dispatcher::reset_focus`]. pub(crate) fn reset_focus(_dispatcher: &Dispatcher) -> crate::Result<()> { desktop_window().focus() } /// Gets the `NativeWindow` instance of the desktop window. /// /// This is the explorer.exe wallpaper window (i.e. "Progman"). If /// explorer.exe isn't running, then default to the desktop window below /// the wallpaper window. #[must_use] fn desktop_window() -> NativeWindow { let handle = match unsafe { GetShellWindow() } { HWND(0) => unsafe { GetDesktopWindow() }, handle => handle, }; NativeWindow::new(handle.0) } ================================================ FILE: packages/wm-platform/src/platform_impl/windows/single_instance.rs ================================================ use windows::{ core::{w, PCWSTR}, Win32::{ Foundation::{ CloseHandle, GetLastError, ERROR_ALREADY_EXISTS, ERROR_FILE_NOT_FOUND, HANDLE, }, System::Threading::{ CreateMutexW, OpenMutexW, ReleaseMutex, SYNCHRONIZATION_ACCESS_RIGHTS, }, }, }; /// Arbitrary GUID to uniquely identify the application. const APP_GUID: PCWSTR = w!("Global\\325d0ed7-7f60-4925-8d1b-aa287b26b218"); /// Platform-specific implementation of [`SingleInstance`]. pub struct SingleInstance { handle: HANDLE, } impl SingleInstance { /// Implements [`SingleInstance::new`]. pub(crate) fn new() -> crate::Result { // Create a named system-wide mutex. let handle = unsafe { CreateMutexW(None, true, APP_GUID) }?; if let Err(err) = unsafe { GetLastError() } { if err == ERROR_ALREADY_EXISTS.into() { return Err(crate::Error::Platform( "Another instance of the application is already running." .to_string(), )); } } Ok(Self { handle }) } /// Implements [`SingleInstance::is_running`]. #[must_use] pub(crate) fn is_running() -> bool { let res = unsafe { OpenMutexW(SYNCHRONIZATION_ACCESS_RIGHTS::default(), false, APP_GUID) }; // If the mutex exists, then another instance is running. match res { Ok(_) => false, Err(err) => err == ERROR_FILE_NOT_FOUND.into(), } } } impl Drop for SingleInstance { fn drop(&mut self) { unsafe { let _ = ReleaseMutex(self.handle); let _ = CloseHandle(self.handle); } } } ================================================ FILE: packages/wm-platform/src/platform_impl/windows/window_listener.rs ================================================ use std::sync::OnceLock; use tokio::sync::mpsc; use windows::Win32::{ Foundation::HWND, UI::{ Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK}, WindowsAndMessaging::{ EVENT_OBJECT_CLOAKED, EVENT_OBJECT_DESTROY, EVENT_OBJECT_HIDE, EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_SHOW, EVENT_OBJECT_UNCLOAKED, EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_MINIMIZEEND, EVENT_SYSTEM_MINIMIZESTART, EVENT_SYSTEM_MOVESIZEEND, EVENT_SYSTEM_MOVESIZESTART, OBJID_WINDOW, WINEVENT_OUTOFCONTEXT, WINEVENT_SKIPOWNPROCESS, }, }, }; use super::NativeWindow; use crate::{Dispatcher, WindowEvent, WindowId}; thread_local! { /// Sender for window events. For use with hook procedure. static EVENT_TX: OnceLock> = const { OnceLock::new() }; } /// Platform-specific implementation of [`WindowEventNotification`]. #[derive(Clone, Debug)] pub struct WindowEventNotificationInner; /// Platform-specific implementation of [`WindowListener`]. #[derive(Debug)] pub(crate) struct WindowListener { hook_handles: Vec, } impl WindowListener { /// Implements [`WindowListener::new`]. pub(crate) fn new( event_tx: mpsc::UnboundedSender, dispatcher: &Dispatcher, ) -> crate::Result { let hook_handles = dispatcher.dispatch_sync(move || { EVENT_TX.with(|lock| lock.set(event_tx)).map_err(|_| { crate::Error::Platform( "Window event sender already set.".to_string(), ) })?; Self::hook_win_events() })??; Ok(Self { hook_handles }) } /// Implements [`WindowListener::terminate`]. pub(crate) fn terminate(&mut self) { for handle in self.hook_handles.drain(..) { let _ = unsafe { UnhookWinEvent(handle) }; } } /// Creates several window event hooks via `SetWinEventHook`. /// /// Separate hooks are created per event range, which is more performant /// than a single hook covering all events. fn hook_win_events() -> crate::Result> { let event_ranges = [ (EVENT_OBJECT_DESTROY, EVENT_OBJECT_HIDE), (EVENT_SYSTEM_MINIMIZESTART, EVENT_SYSTEM_MINIMIZEEND), (EVENT_SYSTEM_MOVESIZESTART, EVENT_SYSTEM_MOVESIZEEND), (EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND), (EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_NAMECHANGE), (EVENT_OBJECT_CLOAKED, EVENT_OBJECT_UNCLOAKED), ]; event_ranges .iter() .try_fold(Vec::new(), |mut handles, (min, max)| { // Create a window hook for the event range. let hook_handle = unsafe { SetWinEventHook( *min, *max, None, Some(Self::window_event_proc), 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS, ) }; if hook_handle.is_invalid() { return Err(crate::Error::Platform( "Failed to set window event hook.".to_string(), )); } handles.push(hook_handle); Ok(handles) }) } /// Callback passed to `SetWinEventHook`. /// /// This function is called on selected window events, and forwards them /// through an MPSC channel. extern "system" fn window_event_proc( _hook: HWINEVENTHOOK, event_type: u32, handle: HWND, id_object: i32, id_child: i32, _event_thread: u32, _event_time: u32, ) { // Check whether the event is associated with a window object rather // than a UI control. let is_window_event = id_object == OBJID_WINDOW.0 && id_child == 0 && handle != HWND(0); if !is_window_event { return; } let Some(event_tx) = EVENT_TX.with(|lock| lock.get().cloned()) else { return; }; let notification = crate::WindowEventNotification(None); let event = match event_type { EVENT_OBJECT_DESTROY => WindowEvent::Destroyed { window_id: WindowId(handle.0), notification, }, EVENT_SYSTEM_FOREGROUND => WindowEvent::Focused { window: NativeWindow::new(handle.0).into(), notification, }, EVENT_OBJECT_HIDE | EVENT_OBJECT_CLOAKED => WindowEvent::Hidden { window: NativeWindow::new(handle.0).into(), notification, }, EVENT_OBJECT_LOCATIONCHANGE => WindowEvent::MovedOrResized { window: NativeWindow::new(handle.0).into(), is_interactive_start: false, is_interactive_end: false, notification, }, EVENT_SYSTEM_MINIMIZESTART => WindowEvent::Minimized { window: NativeWindow::new(handle.0).into(), notification, }, EVENT_SYSTEM_MINIMIZEEND => WindowEvent::MinimizeEnded { window: NativeWindow::new(handle.0).into(), notification, }, EVENT_SYSTEM_MOVESIZESTART => WindowEvent::MovedOrResized { window: NativeWindow::new(handle.0).into(), is_interactive_start: true, is_interactive_end: false, notification, }, EVENT_SYSTEM_MOVESIZEEND => WindowEvent::MovedOrResized { window: NativeWindow::new(handle.0).into(), is_interactive_start: false, is_interactive_end: true, notification, }, EVENT_OBJECT_SHOW | EVENT_OBJECT_UNCLOAKED => WindowEvent::Shown { window: NativeWindow::new(handle.0).into(), notification, }, EVENT_OBJECT_NAMECHANGE => WindowEvent::TitleChanged { window: NativeWindow::new(handle.0).into(), notification, }, _ => return, }; if let Err(err) = event_tx.send(event) { tracing::warn!("Failed to send window event: {}.", err); } } } impl Drop for WindowListener { fn drop(&mut self) { self.terminate(); } } ================================================ FILE: packages/wm-platform/src/single_instance.rs ================================================ use crate::platform_impl; /// Ensures only one instance of the application is running at a time. /// /// # Platform-specific /// /// - **Windows**: Uses a named system-wide mutex. /// - **macOS**: Uses an exclusive file lock. pub struct SingleInstance { /// Inner platform-specific single instance implementation. _inner: platform_impl::SingleInstance, } impl SingleInstance { /// Creates a new [`SingleInstance`], acquiring the platform-specific /// lock or mutex. /// /// # Errors /// /// Returns [`Error::Platform`] if another instance is already running. pub fn new() -> crate::Result { let inner = platform_impl::SingleInstance::new()?; Ok(Self { _inner: inner }) } /// Returns whether another instance of the application is currently /// running. #[must_use] pub fn is_running() -> bool { platform_impl::SingleInstance::is_running() } } ================================================ FILE: packages/wm-platform/src/test.rs ================================================ #![feature(iterator_try_collect)] #[macro_use] extern crate libtest_mimic_collect; mod dispatcher; mod display; mod error; mod event_loop; mod keybinding_listener; mod models; mod mouse_listener; mod native_window; mod platform_event; mod platform_impl; mod thread_bound; mod window_listener; pub use dispatcher::*; pub use display::*; pub use error::*; pub use event_loop::*; pub use keybinding_listener::*; pub use models::*; pub use mouse_listener::*; pub use native_window::*; pub use platform_event::*; pub use thread_bound::*; pub use window_listener::*; pub fn main() { // Due to macOS requiring the main thread for some UI APIs, these // tests must execute on the main thread. Until this is natively // supported via cargo's test harness, we use `libtest_mimic_collect`. // // To run these tests, run `cargo test <...args> -- --test-threads=1`. // // Ref: https://github.com/rust-lang/rust/issues/104053 libtest_mimic_collect::TestCollection::run(); } ================================================ FILE: packages/wm-platform/src/thread_bound.rs ================================================ use core::{ fmt, mem::{self, ManuallyDrop}, }; use std::thread::ThreadId; use crate::Dispatcher; /// Binds a value to the current event loop thread. /// /// `ThreadBound` wraps a value created on an event loop thread and /// guarantees that all access and destruction of that value happens on /// the same thread, using the provided [`Dispatcher`]. This allows the /// wrapper to be used across threads (`Send + Sync`) even when `T` itself /// is not thread-safe. /// /// Inspired by: /// - `threadbound::ThreadBound` /// - `dispatch2::MainThreadBound` /// /// NOTE: Dropping the wrapper schedules the inner value to be dropped on /// the event loop thread. If the event loop has already stopped, the drop /// is skipped to avoid running `T`'s destructor on the wrong thread, /// potentially leaking the value. /// /// # Example usage /// /// ```no_run /// use wm_platform::{EventLoop, Dispatcher, ThreadBound}; /// /// # fn main() -> wm_platform::Result<()> { /// let (event_loop, dispatcher) = EventLoop::new()?; /// /// // Create the value on the event loop thread. /// let bound = dispatcher.dispatch_sync(|| { /// ThreadBound::new(String::from("hello"), dispatcher.clone()) /// })?; /// /// // Access from any thread via the dispatcher. /// let len = bound.with(|s| s.len())?; /// assert_eq!(len, 5); /// /// // Direct access only works on the original thread. /// assert!(bound.get_ref().is_ok()); /// /// drop(bound); // Drop is scheduled on the event loop thread. /// # Ok(()) } /// ``` #[derive(Clone)] pub struct ThreadBound { value: ManuallyDrop, thread_id: ThreadId, dispatcher: Dispatcher, } // SAFETY: Access to the inner value is only exposed on the event loop // thread. unsafe impl Send for ThreadBound {} unsafe impl Sync for ThreadBound {} impl ThreadBound { /// Binds a value to the current event loop thread. /// /// # Panics /// /// Panics if `dispatcher` is not tied to an event loop running on the /// current thread. #[inline] pub fn new(inner: T, dispatcher: Dispatcher) -> Self { let thread_id = std::thread::current().id(); // Ensure the dispatcher is tied to the same thread. assert_eq!(thread_id, dispatcher.thread_id()); Self { value: ManuallyDrop::new(inner), thread_id, dispatcher, } } /// Returns `Ok(&T)` if called on the event loop thread. /// /// # Errors /// /// Returns `Error::NotMainThread` if called from a different thread. #[inline] pub fn get_ref(&self) -> crate::Result<&T> { if self.is_event_loop_thread() { Ok(&self.value) } else { Err(crate::Error::NotMainThread) } } /// Returns `Ok(&mut T)` if called on the event loop thread. /// /// # Errors /// /// Returns `Error::NotMainThread` if called from a different thread. #[inline] pub fn get_mut(&mut self) -> crate::Result<&mut T> { if self.is_event_loop_thread() { Ok(&mut self.value) } else { Err(crate::Error::NotMainThread) } } /// Consumes the wrapper and returns `Ok(T)` if called on the event loop /// thread. /// /// # Errors /// /// Returns `Error::NotMainThread` if called from a different thread. #[inline] pub fn into_inner(self) -> crate::Result { if self.is_event_loop_thread() { // Prevent `Drop` from running. let mut this = ManuallyDrop::new(self); // SAFETY: `self` is consumed by this function, and wrapped in // `ManuallyDrop`, so the item's destructor is never run. Ok(unsafe { ManuallyDrop::take(&mut this.value) }) } else { Err(crate::Error::NotMainThread) } } /// Execute a closure with `&T` on the event loop thread. /// /// Runs synchronously and returns the closure's result. #[inline] pub fn with(&self, f: F) -> crate::Result where F: Send + FnOnce(&T) -> R, R: Send, { self.dispatcher.dispatch_sync(|| f(&self.value)) } /// Execute a closure with `&mut T` on the event loop thread. /// /// Runs synchronously and returns the closure's result. #[inline] #[allow( clippy::borrow_as_ptr, clippy::ptr_as_ptr, clippy::as_conversions )] pub fn with_mut(&mut self, f: F) -> crate::Result where F: Send + FnOnce(&mut T) -> R, R: Send, { // TODO: This is pretty cursed. Should be a better way. let value_ptr = std::ptr::from_mut::>(&mut self.value) as usize; self.dispatcher.dispatch_sync(|| unsafe { // SAFETY: The closure executes on the event loop thread where the // value was created, and we only create a unique mutable reference. let value_mut: &mut T = &mut *(value_ptr as *mut T); f(value_mut) }) } /// Returns `true` if called on the event loop thread. #[inline] #[must_use] pub fn is_event_loop_thread(&self) -> bool { std::thread::current().id() == self.thread_id } } impl fmt::Debug for ThreadBound { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ThreadBound").finish_non_exhaustive() } } impl Drop for ThreadBound { #[allow( clippy::borrow_as_ptr, clippy::ptr_as_ptr, clippy::as_conversions, clippy::ref_as_ptr )] fn drop(&mut self) { if mem::needs_drop::() { // TODO: This is pretty cursed. Should be a better way. let value_ptr = std::ptr::from_mut::>(&mut self.value) as usize; let _ = self.dispatcher.dispatch_sync(|| unsafe { // SAFETY: The value is dropped on the event loop thread, which is // the same thread that it originated from (guaranteed by `new`). // Additionally, the value is never used again after this point. ManuallyDrop::drop(&mut *(value_ptr as *mut ManuallyDrop)); }); } } } ================================================ FILE: packages/wm-platform/src/window_listener.rs ================================================ use tokio::sync::mpsc; use crate::{platform_impl, Dispatcher, WindowEvent}; /// A listener for system-wide window events. pub struct WindowListener { event_rx: mpsc::UnboundedReceiver, /// Inner platform-specific window listener. inner: platform_impl::WindowListener, } impl WindowListener { /// Creates a new window listener. pub fn new(dispatcher: &Dispatcher) -> crate::Result { let (event_tx, event_rx) = mpsc::unbounded_channel(); let inner = platform_impl::WindowListener::new(event_tx, dispatcher)?; Ok(Self { event_rx, inner }) } /// Returns the next window event from the listener. /// /// This will block until a window event is available. pub async fn next_event(&mut self) -> Option { self.event_rx.recv().await } /// Terminates the window listener. pub fn terminate(&mut self) { self.inner.terminate(); } } ================================================ FILE: packages/wm-watcher/Cargo.toml ================================================ [package] name = "wm-watcher" version = "0.0.0" edition = "2021" [[bin]] name = "glazewm-watcher" path = "src/main.rs" [build-dependencies] tauri-winres = { workspace = true } [dependencies] anyhow = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } wm-common = { path = "../wm-common" } wm-ipc-client = { path = "../wm-ipc-client" } wm-platform = { path = "../wm-platform" } ================================================ FILE: packages/wm-watcher/build.rs ================================================ use tauri_winres::VersionInfo; fn main() { if cfg!(not(target_os = "windows")) { panic!("wm-watcher is only supported on Windows."); } println!("cargo:rerun-if-env-changed=VERSION_NUMBER"); let mut res = tauri_winres::WindowsResource::new(); res.set_icon("../../resources/assets/icon.ico"); // Set language to English (US). res.set_language(0x0409); res.set("OriginalFilename", "glazewm-watcher.exe"); res.set("ProductName", "GlazeWM Watcher"); res.set("FileDescription", "GlazeWM Watcher"); let version_parts = env!("VERSION_NUMBER") .split('.') .take(3) .map(|part| part.parse().unwrap_or(0)) .collect::>(); let [major, minor, patch] = <[u16; 3]>::try_from(version_parts).unwrap_or([0, 0, 0]); let version_str = format!("{major}.{minor}.{patch}.0"); res.set("FileVersion", &version_str); res.set("ProductVersion", &version_str); let version_u64 = (u64::from(major) << 48) | (u64::from(minor) << 32) | (u64::from(patch) << 16); res.set_version_info(VersionInfo::FILEVERSION, version_u64); res.set_version_info(VersionInfo::PRODUCTVERSION, version_u64); res.compile().unwrap(); } ================================================ FILE: packages/wm-watcher/src/main.rs ================================================ // The `windows` or `console` subsystem (default is `console`) determines // whether a console window is spawned on launch, if not already ran // through a console. The following prevents this additional console window // in release mode. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![warn(clippy::all, clippy::pedantic)] use anyhow::Context; use wm_common::{ClientResponseData, ContainerDto, WindowDto, WmEvent}; use wm_ipc_client::IpcClient; use wm_platform::{NativeWindow, NativeWindowWindowsExt, OpacityValue}; #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt().init(); let mut client = IpcClient::connect().await?; // Get handles to windows that are already open on watcher launch. let mut managed_handles = query_initial_windows(&mut client) .await? .into_iter() .map(|window| window.handle) .collect::>(); // Update window handles on window manage/unmanage events. let subscribe_res = watch_managed_handles(&mut client, &mut managed_handles).await; match subscribe_res { Ok(()) => { tracing::info!("WM exited successfully. Skipping watcher cleanup."); } Err(err) => { tracing::info!( "Running watcher cleanup. WM exited unexpectedly: {}", err ); let managed_windows = managed_handles.into_iter().map(NativeWindow::from_handle); for window in managed_windows { if let Err(err) = window.show() { tracing::warn!("Failed to show window: {:?}", err); } let _ = window.set_taskbar_visibility(true); let _ = window.set_border_color(None); let _ = window.set_transparency(&OpacityValue::from_alpha(u8::MAX)); } } } Ok(()) } async fn query_initial_windows( client: &mut IpcClient, ) -> anyhow::Result> { let query_message = "query windows"; client .send(query_message) .await .context("Failed to send window query command.")?; client .client_response(query_message) .await .and_then(|response| match response.data { Some(ClientResponseData::Windows(data)) => Some(data), _ => None, }) .map(|data| { data .windows .into_iter() .filter_map(|container| match container { ContainerDto::Window(window) => Some(window), _ => None, }) .collect::>() }) .context("Invalid data in windows query response.") } async fn watch_managed_handles( client: &mut IpcClient, handles: &mut Vec, ) -> anyhow::Result<()> { let subscription_message = "sub -e window_managed window_unmanaged application_exiting"; client .send(subscription_message) .await .context("Failed to send subscribe command to IPC server.")?; let subscription_id = client .client_response(subscription_message) .await .and_then(|response| match response.data { Some(ClientResponseData::EventSubscribe(data)) => { Some(data.subscription_id) } _ => None, }) .context("No subscription ID in watcher event subscription.")?; loop { let event_data = client .event_subscription(&subscription_id) .await .and_then(|event| event.data); match event_data { Some(WmEvent::WindowManaged { managed_window }) => { if let ContainerDto::Window(window) = managed_window { tracing::info!("Watcher added handle: {}.", window.handle); handles.push(window.handle); } } Some(WmEvent::WindowUnmanaged { unmanaged_handle, .. }) => { tracing::info!("Watcher removed handle: {}.", unmanaged_handle); handles.retain(|&handle| handle != unmanaged_handle); } Some(WmEvent::ApplicationExiting) => { return Ok(()); } Some(_) => unreachable!(), None => { anyhow::bail!("IPC connection closed unexpectedly.") } } } } ================================================ FILE: resources/Info.plist ================================================ CFBundleName GlazeWM CFBundleDisplayName GlazeWM CFBundleIdentifier io.glzr.glazewm CFBundleVersion ${VERSION} CFBundleShortVersionString ${VERSION} CFBundlePackageType APPL CFBundleSignature ???? CFBundleExecutable glazewm CFBundleIconFile icon CFBundleInfoDictionaryVersion 6.0 LSMinimumSystemVersion 10.13 NSHighResolutionCapable LSApplicationCategoryType public.app-category.utilities NSHumanReadableCopyright Copyright © glzr-io. All rights reserved. LSUIElement NSAccessibilityUsageDescription GlazeWM requires accessibility permissions to manage and arrange windows. LSArchitecturePriority arm64 x86_64 ================================================ FILE: resources/assets/sample-config.yaml ================================================ general: # Commands to run when the WM has started. This is useful for running a # script or launching another application. # Example: The below command launches Zebar. startup_commands: ['shell-exec zebar'] # Commands to run just before the WM is shutdown. # Example: The below command kills Zebar. shutdown_commands: ['shell-exec taskkill /IM zebar.exe /F'] # Commands to run after the WM config is reloaded. config_reload_commands: [] # Whether to automatically focus windows underneath the cursor. focus_follows_cursor: false # Whether to switch back and forth between the previously focused # workspace when focusing the current workspace. toggle_workspace_on_refocus: false cursor_jump: # Whether to automatically move the cursor on the specified trigger. enabled: true # Trigger for cursor jump: # - 'monitor_focus': Jump when focus changes between monitors. # - 'window_focus': Jump when focus changes between windows. trigger: 'monitor_focus' # How windows should be hidden when switching workspaces. # - 'cloak': (Windows-only) Recommended option for Windows. # - 'hide': (Windows-only) Legacy option for Windows. Has stability issues with some apps. # - 'place_in_corner': Artifically hides the window by placing it in the corner of the # monitor. On macOS, this is always used instead of cloak/hide. hide_method: 'cloak' # Affects which windows get shown in the native Windows taskbar. Has no # effect if `hide_method: 'hide'`. # - 'true': Show all windows (regardless of workspace). # - 'false': Only show windows from the currently shown workspaces. show_all_in_taskbar: false gaps: # Whether to scale the gaps with the DPI of the monitor. scale_with_dpi: true # Gap between adjacent windows. inner_gap: '20px' # Gap between windows and the screen edge. outer_gap: top: '60px' right: '20px' bottom: '20px' left: '20px' window_effects: # Visual effects to apply to the focused window. focused_window: # Highlight the window with a colored border. # ** Exclusive to Windows 11 due to API limitations. border: enabled: true color: '#8dbcff' # Remove the title bar from the window's frame. Note that this can # cause rendering issues for some applications. hide_title_bar: enabled: false # Change the corner style of the window's frame. # ** Exclusive to Windows 11 due to API limitations. corner_style: enabled: false # Allowed values: 'square', 'rounded', 'small_rounded'. style: 'square' # Change the transparency of the window. transparency: enabled: false # Can be something like '95%' or '0.95' for slightly transparent windows. # '0' or '0%' is fully transparent (and, by consequence, unfocusable). opacity: '95%' # Visual effects to apply to non-focused windows. other_windows: border: enabled: true color: '#a1a1a1' hide_title_bar: enabled: false corner_style: enabled: false style: 'square' transparency: enabled: false opacity: '0%' window_behavior: # New windows are created in this state whenever possible. # Allowed values: 'tiling', 'floating'. initial_state: 'tiling' # Sets the default options for when a new window is created. This also # changes the defaults for when the state change commands, like # `set-floating`, are used without any flags. state_defaults: floating: # Whether to center floating windows by default. centered: true # Whether to show floating windows as always on top. shown_on_top: false fullscreen: # Maximize the window if possible. If the window doesn't have a # maximize button, then it'll be fullscreen'ed normally instead. maximized: false # Whether to show fullscreen windows as always on top. shown_on_top: false workspaces: - name: '1' - name: '2' - name: '3' - name: '4' - name: '5' - name: '6' - name: '7' - name: '8' - name: '9' window_rules: - commands: ['ignore'] match: # Ignores any Zebar windows. - window_process: { equals: 'zebar' } # Ignores picture-in-picture windows for browsers. - window_title: { regex: '[Pp]icture.in.[Pp]icture' } window_class: { regex: 'Chrome_WidgetWin_1|MozillaDialogClass' } # Ignore rules for various 3rd-party apps. - window_process: { equals: 'PowerToys' } window_class: { regex: 'HwndWrapper\[PowerToys\.PowerAccent.*?\]' } - window_title: { equals: 'Command Palette' } window_class: { equals: 'WinUIDesktopWin32WindowClass' } - window_process: { equals: 'PowerToys' } window_title: { regex: '.*? - Peek' } - window_process: { equals: 'Lively' } window_class: { regex: 'HwndWrapper' } - window_process: { equals: 'EXCEL' } window_class: { not_regex: 'XLMAIN' } - window_process: { equals: 'WINWORD' } window_class: { not_regex: 'OpusApp' } - window_process: { equals: 'POWERPNT' } window_class: { not_regex: 'PPTFrameClass' } binding_modes: # When enabled, the focused window can be resized via arrow keys or HJKL. - name: 'resize' keybindings: - commands: ['resize --width -2%'] bindings: ['h', 'left'] - commands: ['resize --width +2%'] bindings: ['l', 'right'] - commands: ['resize --height +2%'] bindings: ['k', 'up'] - commands: ['resize --height -2%'] bindings: ['j', 'down'] # Press enter/escape to return to default keybindings. - commands: ['wm-disable-binding-mode --name resize'] bindings: ['escape', 'enter'] keybindings: # Shift focus in a given direction. - commands: ['focus --direction left'] bindings: ['alt+h', 'alt+left'] - commands: ['focus --direction right'] bindings: ['alt+l', 'alt+right'] - commands: ['focus --direction up'] bindings: ['alt+k', 'alt+up'] - commands: ['focus --direction down'] bindings: ['alt+j', 'alt+down'] # Move focused window in a given direction. - commands: ['move --direction left'] bindings: ['alt+shift+h', 'alt+shift+left'] - commands: ['move --direction right'] bindings: ['alt+shift+l', 'alt+shift+right'] - commands: ['move --direction up'] bindings: ['alt+shift+k', 'alt+shift+up'] - commands: ['move --direction down'] bindings: ['alt+shift+j', 'alt+shift+down'] # Resize focused window by a percentage or pixel amount. - commands: ['resize --width -2%'] bindings: ['alt+u'] - commands: ['resize --width +2%'] bindings: ['alt+p'] - commands: ['resize --height +2%'] bindings: ['alt+o'] - commands: ['resize --height -2%'] bindings: ['alt+i'] # As an alternative to the resize keybindings above, resize mode enables # resizing via arrow keys or HJKL. The binding mode is defined above with # the name 'resize'. - commands: ['wm-enable-binding-mode --name resize'] bindings: ['alt+r'] # Disables window management and all other keybindings until alt+shift+p # is pressed again. - commands: ['wm-toggle-pause'] bindings: ['alt+shift+p'] # Change tiling direction. This determines where new tiling windows will # be inserted. - commands: ['toggle-tiling-direction'] bindings: ['alt+v'] # Change focus from tiling windows -> floating -> fullscreen. - commands: ['wm-cycle-focus'] bindings: ['alt+space'] # Change the focused window to be floating. - commands: ['toggle-floating --centered'] bindings: ['alt+shift+space'] # Change the focused window to be tiling. - commands: ['toggle-tiling'] bindings: ['alt+t'] # Change the focused window to be fullscreen. - commands: ['toggle-fullscreen'] bindings: ['alt+f'] # Minimize focused window. - commands: ['toggle-minimized'] bindings: ['alt+m'] # Close focused window. - commands: ['close'] bindings: ['alt+shift+q'] # Kill GlazeWM process safely. - commands: ['wm-exit'] bindings: ['alt+shift+e'] # Re-evaluate configuration file. - commands: ['wm-reload-config'] bindings: ['alt+shift+r'] # Redraw all windows. - commands: ['wm-redraw'] bindings: ['alt+shift+w'] # Launch CMD terminal. Alternatively, use `shell-exec wt` or # `shell-exec %ProgramFiles%/Git/git-bash.exe` to start Windows # Terminal and Git Bash respectively. - commands: ['shell-exec cmd'] bindings: ['alt+enter'] # Focus the next/previous active workspace defined in `workspaces` config. - commands: ['focus --next-active-workspace'] bindings: ['alt+s'] - commands: ['focus --prev-active-workspace'] bindings: ['alt+a'] # Focus the workspace that last had focus. - commands: ['focus --recent-workspace'] bindings: ['alt+d'] # Change focus to a workspace defined in `workspaces` config. - commands: ['focus --workspace 1'] bindings: ['alt+1'] - commands: ['focus --workspace 2'] bindings: ['alt+2'] - commands: ['focus --workspace 3'] bindings: ['alt+3'] - commands: ['focus --workspace 4'] bindings: ['alt+4'] - commands: ['focus --workspace 5'] bindings: ['alt+5'] - commands: ['focus --workspace 6'] bindings: ['alt+6'] - commands: ['focus --workspace 7'] bindings: ['alt+7'] - commands: ['focus --workspace 8'] bindings: ['alt+8'] - commands: ['focus --workspace 9'] bindings: ['alt+9'] # Move the focused window's parent workspace to a monitor in a given # direction. - commands: ['move-workspace --direction left'] bindings: ['alt+shift+a'] - commands: ['move-workspace --direction right'] bindings: ['alt+shift+f'] - commands: ['move-workspace --direction up'] bindings: ['alt+shift+d'] - commands: ['move-workspace --direction down'] bindings: ['alt+shift+s'] # Move focused window to a workspace defined in `workspaces` config. - commands: ['move --workspace 1', 'focus --workspace 1'] bindings: ['alt+shift+1'] - commands: ['move --workspace 2', 'focus --workspace 2'] bindings: ['alt+shift+2'] - commands: ['move --workspace 3', 'focus --workspace 3'] bindings: ['alt+shift+3'] - commands: ['move --workspace 4', 'focus --workspace 4'] bindings: ['alt+shift+4'] - commands: ['move --workspace 5', 'focus --workspace 5'] bindings: ['alt+shift+5'] - commands: ['move --workspace 6', 'focus --workspace 6'] bindings: ['alt+shift+6'] - commands: ['move --workspace 7', 'focus --workspace 7'] bindings: ['alt+shift+7'] - commands: ['move --workspace 8', 'focus --workspace 8'] bindings: ['alt+shift+8'] - commands: ['move --workspace 9', 'focus --workspace 9'] bindings: ['alt+shift+9'] ================================================ FILE: resources/scripts/package.ps1 ================================================ # Usage: ./resources/scripts/package.ps1 -VersionNumber 1.0.0 param( [Parameter(Mandatory=$true)] [string]$VersionNumber ) function ExitOnError() { if ($LASTEXITCODE -ne 0) { Exit 1 } } function SignFiles() { param( [Parameter(Mandatory)] [string[]]$filePaths ) if (!(Get-Command "azuresigntool" -ErrorAction SilentlyContinue)) { Write-Output "Skipping signing because AzureSignTool is not installed." Return } $secrets = @( "AZ_VAULT_URL", "AZ_CERT_NAME", "AZ_CLIENT_ID", "AZ_CLIENT_SECRET", "AZ_TENANT_ID", "RFC3161_TIMESTAMP_URL" ) foreach ($secret in $secrets) { if (!(Test-Path "env:$secret")) { Write-Output "Skipping signing due to missing secret '$secret'." Return } } Write-Output "Signing $filePaths." azuresigntool sign -kvu $ENV:AZ_VAULT_URL ` -kvc $ENV:AZ_CERT_NAME ` -kvi $ENV:AZ_CLIENT_ID ` -kvs $ENV:AZ_CLIENT_SECRET ` -kvt $ENV:AZ_TENANT_ID ` -tr $ENV:RFC3161_TIMESTAMP_URL ` -td sha256 $filePaths ExitOnError } function DownloadZebarInstallers() { Write-Output "Downloading latest Zebar MSI's" $latestRelease = 'https://api.github.com/repos/glzr-io/zebar/releases/latest' $latestInstallers = Invoke-RestMethod $latestRelease | % assets | ? name -like "*.msi" $latestInstallers | ForEach-Object { $outFile = Join-Path "out" $_.name # Rename the MSI files (e.g. `zebar-1.5.0-opt1-x64.msi` -> `zebar-x64.msi`). if ($_.name -like "*-x64.msi") { $outFile = "out/zebar-x64.msi" } elseif ($_.name -like "*-arm64.msi") { $outFile = "out/zebar-arm64.msi" } Invoke-WebRequest $_.browser_download_url -OutFile $outFile } } function BuildExes() { # Rust targets to build for (x64 and arm64). $rustTargets = @("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc") # Set the version number as an environment variable for `cargo build`. $env:VERSION_NUMBER = $VersionNumber foreach ($target in $rustTargets) { $outDir = if ($target -eq "x86_64-pc-windows-msvc") { "out/x64" } else { "out/arm64" } $sourceDir = "target/$target/release" $requiredExes = @("glazewm.exe", "glazewm-cli.exe", "glazewm-watcher.exe") $sourcePaths = $requiredExes | ForEach-Object { "$sourceDir/$_" } # Build for the target if the executables do not exist. if (($sourcePaths | Where-Object { !(Test-Path $_) }).Count -gt 0) { Write-Output "Build artifact not found for target '$target'. Building now..." cargo build --locked --release --target $target --features ui_access ExitOnError Write-Output "Build completed successfully for target '$target'." } Write-Output "Moving built executables from $sourceDir to $outDir" New-Item -ItemType Directory -Force -Path $outDir Move-Item -Force -Path $sourcePaths -Destination $outDir $outPaths = $requiredExes | ForEach-Object { "$outDir/$_" } SignFiles $outPaths } } function BuildInstallers() { # WiX architectures to create installers for (x64 and arm64). $wixArchs = @("x64", "arm64") foreach ($arch in $wixArchs) { Write-Output "Creating MSI installer ($arch)" wix build -arch $arch -ext WixToolset.UI.wixext -ext WixToolset.Util.wixext ` -out "./out/installer-$arch.msi" "./resources/wix/standalone.wxs" "./resources/wix/standalone-ui.wxs" ` -d VERSION_NUMBER="$VersionNumber" ` -d EXE_DIR="out/$arch" } SignFiles @("out/installer-x64.msi", "out/installer-arm64.msi") Write-Output "Creating universal installer" wix build -arch "x64" -ext WixToolset.BootstrapperApplications.wixext ` -out "./out/unsigned-installer-universal.exe" "./resources/wix/bundle.wxs" ` -d VERSION_NUMBER="$VersionNumber" Write-Output "Detaching & reattaching Burn engine for signing" wix burn detach "./out/unsigned-installer-universal.exe" -engine "./out/engine.exe" SignFiles @("out/engine.exe") wix burn reattach "./out/unsigned-installer-universal.exe" ` -engine "./out/engine.exe" ` -o "./out/installer-universal.exe" SignFiles @("out/installer-universal.exe") } function Package() { Write-Output "Packaging with version number: $VersionNumber" Write-Output "Creating output directory" New-Item -ItemType Directory -Force -Path "out" DownloadZebarInstallers BuildExes BuildInstallers } Package ================================================ FILE: resources/wix/bundle-theme.wxl ================================================ ================================================ FILE: resources/wix/bundle-theme.xml ================================================ Segoe UI Segoe UI Segoe UI Segoe UI Segoe UI Segoe UI Segoe UI Segoe UI ================================================ FILE: resources/wix/bundle.wxs ================================================ ================================================ FILE: resources/wix/standalone-ui.wxs ================================================ ================================================ FILE: resources/wix/standalone.wxs ================================================ ================================================ FILE: rust-toolchain.toml ================================================ [toolchain] channel = "nightly" ================================================ FILE: rustfmt.toml ================================================ tab_spaces = 2 max_width = 75 imports_granularity = "Crate" group_imports = "StdExternalCrate" use_field_init_shorthand = true wrap_comments = true