├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── shell.nix └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── build.rs ├── doc └── mixxc.1 ├── src ├── accent.rs ├── anchor.rs ├── app.rs ├── error.rs ├── label.rs ├── main.rs ├── proto │ ├── mod.rs │ ├── wayland.rs │ └── x.rs ├── server │ ├── error.rs │ ├── mod.rs │ ├── pipewire.rs │ └── pulse.rs ├── style.rs ├── widgets │ ├── fill.rs │ ├── mod.rs │ ├── sliderbox.rs │ └── switchbox.rs └── xdg.rs └── style └── default.scss /.gitattributes: -------------------------------------------------------------------------------- 1 | Cargo.lock -diff 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: When something doesn't seem quite right. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Description** 11 | A clear and concise description of what the problem is. 12 | 13 | **To Reproduce** 14 | 1. Full command: 15 | ```sh 16 | GSK_RENDERER=cairo setsid -f mixxc -P -a bottom -a left -m 20 -m 20 -b v -i -M -A 17 | ``` 18 | 2. Open browser with a video playback. 19 | 3. Move the slider for the browser window in Mixxc very fast. 20 | 21 | **Environment:** 22 | - WM or DE: ... 23 | [ e.g. Hyprland (0.44.1) ] 24 | 25 | - Version: ... 26 | [ e.g. 0.2.3 ] 27 | 28 | - Graphics Driver: ... 29 | [ e.g. NVidia 550.120 ] 30 | -------------------------------------------------------------------------------- /.github/shell.nix: -------------------------------------------------------------------------------- 1 | { target }: 2 | 3 | let 4 | rust_overlay = import (fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"); 5 | pkgs = import { overlays = [ rust_overlay ]; }; 6 | rustVersion = "1.82.0"; 7 | rust = pkgs.rust-bin.stable.${rustVersion}.minimal.override { 8 | targets = [ target ]; 9 | }; 10 | in 11 | pkgs.mkShell { 12 | buildInputs = [ rust ] ++ (with pkgs; [ 13 | pkg-config 14 | mold 15 | clang 16 | patchelf 17 | gtk4.dev 18 | gtk4-layer-shell.dev 19 | libpulseaudio.dev 20 | ]); 21 | 22 | CARGO_BUILD_TARGET = target; 23 | CARGO_INCREMENTAL = "0"; 24 | CARGO_PROFILE_RELEASE_DEBUG = "none"; 25 | CARGO_PROFILE_RELEASE_LTO = "true"; 26 | CARGO_PROFILE_RELEASE_PANIC = "abort"; 27 | CARGO_PROFILE_RELEASE_STRIP = "symbols"; 28 | 29 | CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = "clang"; 30 | CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS = "-C link-arg=-fuse-ld=mold"; 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9]+\.[0-9]+\.[0-9]+' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | target: [ x86_64-unknown-linux-gnu ] 14 | steps: 15 | - name: Prepare Environment 16 | uses: cachix/install-nix-action@v30 17 | with: 18 | nix_path: nixpkgs=channel:nixos-23.11-small 19 | 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Build 24 | run: | 25 | nix-shell .github/shell.nix --argstr target ${{ matrix.target }} --run " 26 | cargo build --locked --profile release --features Sass,Wayland,X11 27 | " 28 | 29 | - name: Find Executable 30 | run: | 31 | find target/${{ matrix.target }}/release -maxdepth 1 -type f -executable -exec echo binary={} \; -quit >> "$GITHUB_ENV" 32 | 33 | - name: Find Style 34 | run: | 35 | find target/${{ matrix.target }}/release -type f -name '*.css' -exec echo style={} \; -quit >> "$GITHUB_ENV" 36 | 37 | - name: Patch ELF 38 | run: | 39 | nix-shell .github/shell.nix --argstr target ${{ matrix.target }} --run " 40 | patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 ${{ env.binary }} 41 | patchelf --remove-rpath ${{ env.binary }} 42 | " 43 | 44 | - name: Report MD5 45 | run: | 46 | md5sum -b ${{ env.binary }} | head -c 32 47 | 48 | - name: Upload Style 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: default.css 52 | path: ${{ env.style }} 53 | if-no-files-found: error 54 | overwrite: false 55 | 56 | - name: Upload Artifact 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: ${{ matrix.target }} 60 | path: ${{ env.binary }} 61 | if-no-files-found: error 62 | overwrite: true 63 | 64 | release: 65 | needs: build 66 | runs-on: ubuntu-latest 67 | permissions: 68 | contents: write 69 | steps: 70 | - name: Checkout 71 | uses: actions/checkout@v4 72 | 73 | - name: Download Artifact 74 | uses: actions/download-artifact@v4 75 | with: 76 | path: artifacts 77 | 78 | - name: Package 79 | run: | 80 | mkdir -p packages 81 | for dir in artifacts/*; do 82 | tar czf "packages/${dir##*/}.tar.gz" doc/*.1 style/* LICENSE -C $dir . 83 | done 84 | 85 | - name: Release 86 | uses: docker://antonyurchenko/git-release:v6 87 | env: 88 | DRAFT_RELEASE: true 89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | with: 91 | args: packages/*.tar.gz 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | - [Makefile](/Makefile) to make installation and build process a bit easier. 11 | 12 | ### Fixed 13 | - Active clients will take priority over muted and paused ones with `-P` `--per-process` flag as intended. 14 | 15 | ### Changed 16 | - (Breaking) Flag `-k` `--keep` was removed in favor of the user provided delay option for closing `-c` `--close`. 17 | - Window will no longer close itself without a notice by default to not confuse new users. 18 | 19 | ## [0.2.4] - 2025-01-14 20 | 21 | ### Added 22 | - Integration with system accent color setting through [XDG Desktop Portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html). 23 | Requires `Accent` feature to be included at compile time, compatible `xdg-desktop-portal` set and running for `org.freedesktop.impl.portal.Settings` (like `xdg-desktop-portal-kde`) and `-C` or `--accent` flag. 24 | - New sidebar to quickly swap between audio outputs. 25 | - `.sass` and `.scss` styles will be compiled using the system `sass` compiler binary if `Sass` feature was not enabled at the compile time. Style compilation time is much longer, but the resulting binary is around 2mb smaller in size. (https://sass-lang.com/install) 26 | 27 | ### Changed 28 | - Version reported by the `-v` `--version` flag will now include git commit hash if the commit used for build wasn't tagged. 29 | - Default `.css` style is now automatically compiled from `.scss` to reduce the amount of syntax errors and ease the maintenance. 30 | - (CSS) Deprecated`@define-colors` and SCSS color definitions in favor of the CSS variables. 31 | - (CSS) New boolean flags for `.scss` to toggle visibility of some elements. 32 | - (CSS) Dimmed border accent color, reduced font size and changed volume bar color into a gradient, this should provide a slightly more interesting result with different accents. 33 | 34 | ### Fixed 35 | - Layershell initialization before window is realized, which could prevent a successful launch under certain conditions. 36 | - Missing bracket in the default `.scss` style. 37 | - Audio server connection is now cleanly terminated when window is closed or if process recieves SIGINT signal. (should cure the sound popping) 38 | 39 | ## [0.2.3] - 2024-10-22 40 | 41 | ### Added 42 | - New flag `-A` `--active` that hides paused clients. 43 | - New flag `-P` `--per-process` that combines sinks from the same process into a single one. 44 | (This should help with WINE and browser applications, but might have unexpected side effects depending on the software) 45 | - (CSS) Default style now has an animation when hovering over or clicking a volume knob. 46 | 47 | ### Fixed 48 | - Excessive number of updates on unrelated client fields, caused by a function that lowers peak. 49 | - Unsynchronized communications with pulse audio server that could lead to issues. 50 | 51 | ### Changed 52 | - (CSS) Default foreground color is now less eye burning. (#FFFFFF -> #DDDDDD) 53 | - GTK log messages will not appear if `GTK_DEBUG` variable is not set. 54 | 55 | ## [0.2.2] - 2024-04-13 56 | 57 | ### Added 58 | - Added a man page. 59 | - Vertical orientation for volume sliders is now available via `-b v` or `--bar vertical`. 60 | - Added a short flag for `--max-volume` -> `-x`. 61 | - Added an optional master slider for current device volume under `-M`, `--master` flags. 62 | 63 | ### Fixed 64 | - (CSS) Icon wasn't affected by style changes in `default.css`, because class selector was invalid. 65 | - (CSS) Name and description used white color instead of the foreground. 66 | - Peakers will always start unmuted, in case something forced them to mute and state was saved by audio server. 67 | - Experimental fix for X when window flickers in the middle of the screen for a single frame on startup. 68 | 69 | ## [0.2.1] - 2024-03-22 70 | 71 | ### Fixed 72 | - Animation no longer plays multiple times when a new client is addded. 73 | 74 | ## [0.2.0] - 2024-03-21 75 | 76 | ### Added 77 | - Audio client icons can now be desplayed with `-i` `--icon` flag. 78 | - Automated dynamically linked release builds for general linux distributions (not NixOS) with `glibc` that include all features. 79 | 80 | ### Fixed 81 | - (CSS) GTK system theme was unintentionally affecting style. 82 | - Window quickly resizing because sink buffer was not populated fast enough. 83 | - Peaker no longer breaks when volume slider is set to 0. 84 | - Window no longer steals keyboard focus if `--keep` was provided and it's not necessarily. 85 | 86 | ## [0.1.10] - 2024-03-03 87 | 88 | ### Deprecated 89 | - (CSS) `.client { animation: ... }` is now deprecated in favor of `.client.new { animation: ... }` 90 | 91 | ### Added 92 | 93 | - (CSS) Animation for removed sinks `.client.removed { animation: ... }`. 94 | - Added a warning for Wayland compositors that don't support [wlr-layer-shell-unstable-v1](https://wayland.app/protocols/wlr-layer-shell-unstable-v1) protocol. 95 | - Added a [CHANGELOG.md](/CHANGELOG.md). 96 | 97 | ### Fixed 98 | - `X11` and `Wayland` features now can both be included into the binary with runtime checks. 99 | 100 | ### Changed 101 | 102 | - Window now has a fixed default size of `350x30` instead of being dynamic. 103 | - Window autoclosing is now more reliable and closes only when window looses focus. 104 | - List of sinks will now grow from bottom to top if window is anchored to bottom. 105 | - Minimal `rustc` version for compilation is now `1.75.0` due to [FileTimes](https://doc.rust-lang.org/std/fs/struct.FileTimes.html) stabilization. 106 | 107 | [unreleased]: https://github.com/Elvyria/Mixxc/compare/0.2.4...HEAD 108 | [0.2.4]: https://github.com/Elvyria/Mixxc/compare/0.2.3...0.2.4 109 | [0.2.3]: https://github.com/Elvyria/Mixxc/compare/0.2.2...0.2.3 110 | [0.2.2]: https://github.com/Elvyria/Mixxc/compare/0.2.1...0.2.2 111 | [0.2.1]: https://github.com/Elvyria/Mixxc/compare/0.2.0...0.2.1 112 | [0.2.0]: https://github.com/Elvyria/Mixxc/compare/0.1.10...0.2.0 113 | [0.1.10]: https://github.com/Elvyria/Mixxc/compare/0.1.9...0.1.10 114 | [0.1.9]: https://github.com/Elvyria/Mixxc/compare/0.1.8...0.1.9 115 | [0.1.8]: https://github.com/Elvyria/Mixxc/compare/0.1.7...0.1.8 116 | [0.1.7]: https://github.com/Elvyria/Mixxc/compare/0.1.6...0.1.7 117 | [0.1.6]: https://github.com/Elvyria/Mixxc/compare/0.1.5...0.1.6 118 | [0.1.5]: https://github.com/Elvyria/Mixxc/compare/0.1.4...0.1.5 119 | [0.1.4]: https://github.com/Elvyria/Mixxc/compare/0.1.3...0.1.4 120 | [0.1.3]: https://github.com/Elvyria/Mixxc/compare/0.1.2...0.1.3 121 | [0.1.2]: https://github.com/Elvyria/Mixxc/compare/0.1.1...0.1.2 122 | [0.1.1]: https://github.com/Elvyria/Mixxc/compare/0.1.0...0.1.1 123 | [0.1.0]: https://github.com/Elvyria/Mixxc/releases/tag/0.1.0 124 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "allocator-api2" 34 | version = "0.2.18" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 37 | 38 | [[package]] 39 | name = "anyhow" 40 | version = "1.0.92" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" 43 | 44 | [[package]] 45 | name = "argh" 46 | version = "0.1.12" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219" 49 | dependencies = [ 50 | "argh_derive", 51 | "argh_shared", 52 | ] 53 | 54 | [[package]] 55 | name = "argh_derive" 56 | version = "0.1.12" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a" 59 | dependencies = [ 60 | "argh_shared", 61 | "proc-macro2", 62 | "quote", 63 | "syn 2.0.82", 64 | ] 65 | 66 | [[package]] 67 | name = "argh_shared" 68 | version = "0.1.12" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531" 71 | dependencies = [ 72 | "serde", 73 | ] 74 | 75 | [[package]] 76 | name = "async-broadcast" 77 | version = "0.7.1" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" 80 | dependencies = [ 81 | "event-listener", 82 | "event-listener-strategy", 83 | "futures-core", 84 | "pin-project-lite", 85 | ] 86 | 87 | [[package]] 88 | name = "async-channel" 89 | version = "2.3.1" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" 92 | dependencies = [ 93 | "concurrent-queue", 94 | "event-listener-strategy", 95 | "futures-core", 96 | "pin-project-lite", 97 | ] 98 | 99 | [[package]] 100 | name = "async-io" 101 | version = "2.3.4" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" 104 | dependencies = [ 105 | "async-lock", 106 | "cfg-if", 107 | "concurrent-queue", 108 | "futures-io", 109 | "futures-lite", 110 | "parking", 111 | "polling", 112 | "rustix", 113 | "slab", 114 | "tracing", 115 | "windows-sys 0.59.0", 116 | ] 117 | 118 | [[package]] 119 | name = "async-lock" 120 | version = "3.4.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" 123 | dependencies = [ 124 | "event-listener", 125 | "event-listener-strategy", 126 | "pin-project-lite", 127 | ] 128 | 129 | [[package]] 130 | name = "async-process" 131 | version = "2.3.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" 134 | dependencies = [ 135 | "async-channel", 136 | "async-io", 137 | "async-lock", 138 | "async-signal", 139 | "async-task", 140 | "blocking", 141 | "cfg-if", 142 | "event-listener", 143 | "futures-lite", 144 | "rustix", 145 | "tracing", 146 | ] 147 | 148 | [[package]] 149 | name = "async-recursion" 150 | version = "1.1.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" 153 | dependencies = [ 154 | "proc-macro2", 155 | "quote", 156 | "syn 2.0.82", 157 | ] 158 | 159 | [[package]] 160 | name = "async-signal" 161 | version = "0.2.10" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" 164 | dependencies = [ 165 | "async-io", 166 | "async-lock", 167 | "atomic-waker", 168 | "cfg-if", 169 | "futures-core", 170 | "futures-io", 171 | "rustix", 172 | "signal-hook-registry", 173 | "slab", 174 | "windows-sys 0.59.0", 175 | ] 176 | 177 | [[package]] 178 | name = "async-task" 179 | version = "4.7.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 182 | 183 | [[package]] 184 | name = "async-trait" 185 | version = "0.1.83" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" 188 | dependencies = [ 189 | "proc-macro2", 190 | "quote", 191 | "syn 2.0.82", 192 | ] 193 | 194 | [[package]] 195 | name = "atomic-waker" 196 | version = "1.1.2" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 199 | 200 | [[package]] 201 | name = "autocfg" 202 | version = "1.4.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 205 | 206 | [[package]] 207 | name = "backtrace" 208 | version = "0.3.74" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 211 | dependencies = [ 212 | "addr2line", 213 | "cfg-if", 214 | "libc", 215 | "miniz_oxide", 216 | "object", 217 | "rustc-demangle", 218 | "windows-targets 0.52.6", 219 | ] 220 | 221 | [[package]] 222 | name = "bitflags" 223 | version = "1.3.2" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 226 | 227 | [[package]] 228 | name = "bitflags" 229 | version = "2.6.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 232 | 233 | [[package]] 234 | name = "blocking" 235 | version = "1.6.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" 238 | dependencies = [ 239 | "async-channel", 240 | "async-task", 241 | "futures-io", 242 | "futures-lite", 243 | "piper", 244 | ] 245 | 246 | [[package]] 247 | name = "bumpalo" 248 | version = "3.16.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 251 | 252 | [[package]] 253 | name = "byteorder" 254 | version = "1.5.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 257 | 258 | [[package]] 259 | name = "bytes" 260 | version = "1.8.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" 263 | 264 | [[package]] 265 | name = "cairo-rs" 266 | version = "0.20.1" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "e8a0ea147c94108c9613235388f540e4d14c327f7081c9e471fc8ee8a2533e69" 269 | dependencies = [ 270 | "bitflags 2.6.0", 271 | "cairo-sys-rs", 272 | "glib", 273 | "libc", 274 | ] 275 | 276 | [[package]] 277 | name = "cairo-sys-rs" 278 | version = "0.20.0" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "428290f914b9b86089f60f5d8a9f6e440508e1bcff23b25afd51502b0a2da88f" 281 | dependencies = [ 282 | "glib-sys", 283 | "libc", 284 | "system-deps", 285 | ] 286 | 287 | [[package]] 288 | name = "cfg-expr" 289 | version = "0.17.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "d0890061c4d3223e7267f3bad2ec40b997d64faac1c2815a4a9d95018e2b9e9c" 292 | dependencies = [ 293 | "smallvec", 294 | "target-lexicon", 295 | ] 296 | 297 | [[package]] 298 | name = "cfg-if" 299 | version = "1.0.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 302 | 303 | [[package]] 304 | name = "cfg_aliases" 305 | version = "0.2.1" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 308 | 309 | [[package]] 310 | name = "codemap" 311 | version = "0.1.3" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" 314 | 315 | [[package]] 316 | name = "color-print" 317 | version = "0.3.6" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "1ee543c60ff3888934877a5671f45494dd27ed4ba25c6670b9a7576b7ed7a8c0" 320 | dependencies = [ 321 | "color-print-proc-macro", 322 | ] 323 | 324 | [[package]] 325 | name = "color-print-proc-macro" 326 | version = "0.3.6" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "77ff1a80c5f3cb1ca7c06ffdd71b6a6dd6d8f896c42141fbd43f50ed28dcdb93" 329 | dependencies = [ 330 | "nom", 331 | "proc-macro2", 332 | "quote", 333 | "syn 2.0.82", 334 | ] 335 | 336 | [[package]] 337 | name = "concurrent-queue" 338 | version = "2.5.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 341 | dependencies = [ 342 | "crossbeam-utils", 343 | ] 344 | 345 | [[package]] 346 | name = "crossbeam-utils" 347 | version = "0.8.20" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 350 | 351 | [[package]] 352 | name = "derive_more" 353 | version = "1.0.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 356 | dependencies = [ 357 | "derive_more-impl", 358 | ] 359 | 360 | [[package]] 361 | name = "derive_more-impl" 362 | version = "1.0.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" 365 | dependencies = [ 366 | "proc-macro2", 367 | "quote", 368 | "syn 2.0.82", 369 | "unicode-xid", 370 | ] 371 | 372 | [[package]] 373 | name = "endi" 374 | version = "1.1.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" 377 | 378 | [[package]] 379 | name = "enum_dispatch" 380 | version = "0.3.13" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" 383 | dependencies = [ 384 | "once_cell", 385 | "proc-macro2", 386 | "quote", 387 | "syn 2.0.82", 388 | ] 389 | 390 | [[package]] 391 | name = "enumflags2" 392 | version = "0.7.10" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" 395 | dependencies = [ 396 | "enumflags2_derive", 397 | "serde", 398 | ] 399 | 400 | [[package]] 401 | name = "enumflags2_derive" 402 | version = "0.7.10" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" 405 | dependencies = [ 406 | "proc-macro2", 407 | "quote", 408 | "syn 2.0.82", 409 | ] 410 | 411 | [[package]] 412 | name = "equivalent" 413 | version = "1.0.1" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 416 | 417 | [[package]] 418 | name = "errno" 419 | version = "0.3.9" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 422 | dependencies = [ 423 | "libc", 424 | "windows-sys 0.52.0", 425 | ] 426 | 427 | [[package]] 428 | name = "event-listener" 429 | version = "5.3.1" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" 432 | dependencies = [ 433 | "concurrent-queue", 434 | "parking", 435 | "pin-project-lite", 436 | ] 437 | 438 | [[package]] 439 | name = "event-listener-strategy" 440 | version = "0.5.2" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" 443 | dependencies = [ 444 | "event-listener", 445 | "pin-project-lite", 446 | ] 447 | 448 | [[package]] 449 | name = "fastrand" 450 | version = "2.1.1" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 453 | 454 | [[package]] 455 | name = "field-offset" 456 | version = "0.3.6" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" 459 | dependencies = [ 460 | "memoffset", 461 | "rustc_version", 462 | ] 463 | 464 | [[package]] 465 | name = "flume" 466 | version = "0.11.1" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 469 | dependencies = [ 470 | "futures-core", 471 | "futures-sink", 472 | "nanorand", 473 | "spin", 474 | ] 475 | 476 | [[package]] 477 | name = "fragile" 478 | version = "2.0.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" 481 | 482 | [[package]] 483 | name = "futures" 484 | version = "0.3.31" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 487 | dependencies = [ 488 | "futures-channel", 489 | "futures-core", 490 | "futures-executor", 491 | "futures-io", 492 | "futures-sink", 493 | "futures-task", 494 | "futures-util", 495 | ] 496 | 497 | [[package]] 498 | name = "futures-channel" 499 | version = "0.3.31" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 502 | dependencies = [ 503 | "futures-core", 504 | "futures-sink", 505 | ] 506 | 507 | [[package]] 508 | name = "futures-core" 509 | version = "0.3.31" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 512 | 513 | [[package]] 514 | name = "futures-executor" 515 | version = "0.3.31" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 518 | dependencies = [ 519 | "futures-core", 520 | "futures-task", 521 | "futures-util", 522 | ] 523 | 524 | [[package]] 525 | name = "futures-io" 526 | version = "0.3.31" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 529 | 530 | [[package]] 531 | name = "futures-lite" 532 | version = "2.3.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" 535 | dependencies = [ 536 | "fastrand", 537 | "futures-core", 538 | "futures-io", 539 | "parking", 540 | "pin-project-lite", 541 | ] 542 | 543 | [[package]] 544 | name = "futures-macro" 545 | version = "0.3.31" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 548 | dependencies = [ 549 | "proc-macro2", 550 | "quote", 551 | "syn 2.0.82", 552 | ] 553 | 554 | [[package]] 555 | name = "futures-sink" 556 | version = "0.3.31" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 559 | 560 | [[package]] 561 | name = "futures-task" 562 | version = "0.3.31" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 565 | 566 | [[package]] 567 | name = "futures-util" 568 | version = "0.3.31" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 571 | dependencies = [ 572 | "futures-channel", 573 | "futures-core", 574 | "futures-io", 575 | "futures-macro", 576 | "futures-sink", 577 | "futures-task", 578 | "memchr", 579 | "pin-project-lite", 580 | "pin-utils", 581 | "slab", 582 | ] 583 | 584 | [[package]] 585 | name = "gdk-pixbuf" 586 | version = "0.20.4" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "c4c29071a9e92337d8270a85cb0510cda4ac478be26d09ad027cc1d081911b19" 589 | dependencies = [ 590 | "gdk-pixbuf-sys", 591 | "gio", 592 | "glib", 593 | "libc", 594 | ] 595 | 596 | [[package]] 597 | name = "gdk-pixbuf-sys" 598 | version = "0.20.4" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "687343b059b91df5f3fbd87b4307038fa9e647fcc0461d0d3f93e94fee20bf3d" 601 | dependencies = [ 602 | "gio-sys", 603 | "glib-sys", 604 | "gobject-sys", 605 | "libc", 606 | "system-deps", 607 | ] 608 | 609 | [[package]] 610 | name = "gdk4" 611 | version = "0.9.2" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "c121aeeb0cf7545877ae615dac6bfd088b739d8abee4d55e7143b06927d16a31" 614 | dependencies = [ 615 | "cairo-rs", 616 | "gdk-pixbuf", 617 | "gdk4-sys", 618 | "gio", 619 | "gl", 620 | "glib", 621 | "libc", 622 | "pango", 623 | ] 624 | 625 | [[package]] 626 | name = "gdk4-sys" 627 | version = "0.9.2" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "7d3c03d1ea9d5199f14f060890fde68a3b5ec5699144773d1fa6abf337bfbc9c" 630 | dependencies = [ 631 | "cairo-sys-rs", 632 | "gdk-pixbuf-sys", 633 | "gio-sys", 634 | "glib-sys", 635 | "gobject-sys", 636 | "libc", 637 | "pango-sys", 638 | "pkg-config", 639 | "system-deps", 640 | ] 641 | 642 | [[package]] 643 | name = "gdk4-x11" 644 | version = "0.9.2" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "a84f16ccd1e7fad79c1613411a041ba154371d1fa68ab15cd973fe889879efa5" 647 | dependencies = [ 648 | "gdk4", 649 | "gdk4-x11-sys", 650 | "gio", 651 | "glib", 652 | "libc", 653 | "x11", 654 | ] 655 | 656 | [[package]] 657 | name = "gdk4-x11-sys" 658 | version = "0.9.2" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "e91004338e548c4774ec7aa32100557ee905e6ba8114151414a5c34644790380" 661 | dependencies = [ 662 | "gdk4-sys", 663 | "glib-sys", 664 | "libc", 665 | "system-deps", 666 | ] 667 | 668 | [[package]] 669 | name = "gethostname" 670 | version = "0.4.3" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" 673 | dependencies = [ 674 | "libc", 675 | "windows-targets 0.48.5", 676 | ] 677 | 678 | [[package]] 679 | name = "getrandom" 680 | version = "0.2.15" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 683 | dependencies = [ 684 | "cfg-if", 685 | "js-sys", 686 | "libc", 687 | "wasi", 688 | "wasm-bindgen", 689 | ] 690 | 691 | [[package]] 692 | name = "gimli" 693 | version = "0.31.1" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 696 | 697 | [[package]] 698 | name = "gio" 699 | version = "0.20.4" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "b8d999e8fb09583e96080867e364bc1e701284ad206c76a5af480d63833ad43c" 702 | dependencies = [ 703 | "futures-channel", 704 | "futures-core", 705 | "futures-io", 706 | "futures-util", 707 | "gio-sys", 708 | "glib", 709 | "libc", 710 | "pin-project-lite", 711 | "smallvec", 712 | ] 713 | 714 | [[package]] 715 | name = "gio-sys" 716 | version = "0.20.4" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "4f7efc368de04755344f0084104835b6bb71df2c1d41e37d863947392a894779" 719 | dependencies = [ 720 | "glib-sys", 721 | "gobject-sys", 722 | "libc", 723 | "system-deps", 724 | "windows-sys 0.52.0", 725 | ] 726 | 727 | [[package]] 728 | name = "gl" 729 | version = "0.14.0" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" 732 | dependencies = [ 733 | "gl_generator", 734 | ] 735 | 736 | [[package]] 737 | name = "gl_generator" 738 | version = "0.14.0" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" 741 | dependencies = [ 742 | "khronos_api", 743 | "log", 744 | "xml-rs", 745 | ] 746 | 747 | [[package]] 748 | name = "glib" 749 | version = "0.20.4" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "adcf1ec6d3650bf9fdbc6cee242d4fcebc6f6bfd9bea5b929b6a8b7344eb85ff" 752 | dependencies = [ 753 | "bitflags 2.6.0", 754 | "futures-channel", 755 | "futures-core", 756 | "futures-executor", 757 | "futures-task", 758 | "futures-util", 759 | "gio-sys", 760 | "glib-macros", 761 | "glib-sys", 762 | "gobject-sys", 763 | "libc", 764 | "memchr", 765 | "smallvec", 766 | ] 767 | 768 | [[package]] 769 | name = "glib-macros" 770 | version = "0.20.4" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "a6bf88f70cd5720a6197639dcabcb378dd528d0cb68cb1f45e3b358bcb841cd7" 773 | dependencies = [ 774 | "heck", 775 | "proc-macro-crate", 776 | "proc-macro2", 777 | "quote", 778 | "syn 2.0.82", 779 | ] 780 | 781 | [[package]] 782 | name = "glib-sys" 783 | version = "0.20.4" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "5f9eca5d88cfa6a453b00d203287c34a2b7cac3a7831779aa2bb0b3c7233752b" 786 | dependencies = [ 787 | "libc", 788 | "system-deps", 789 | ] 790 | 791 | [[package]] 792 | name = "gobject-sys" 793 | version = "0.20.4" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "a4c674d2ff8478cf0ec29d2be730ed779fef54415a2fb4b565c52def62696462" 796 | dependencies = [ 797 | "glib-sys", 798 | "libc", 799 | "system-deps", 800 | ] 801 | 802 | [[package]] 803 | name = "graphene-rs" 804 | version = "0.20.4" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "1f53144c7fe78292705ff23935f1477d511366fb2f73c43d63b37be89076d2fe" 807 | dependencies = [ 808 | "glib", 809 | "graphene-sys", 810 | "libc", 811 | ] 812 | 813 | [[package]] 814 | name = "graphene-sys" 815 | version = "0.20.4" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "e741797dc5081e59877a4d72c442c72d61efdd99161a0b1c1b29b6b988934b99" 818 | dependencies = [ 819 | "glib-sys", 820 | "libc", 821 | "pkg-config", 822 | "system-deps", 823 | ] 824 | 825 | [[package]] 826 | name = "grass_compiler" 827 | version = "0.13.4" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6" 830 | dependencies = [ 831 | "codemap", 832 | "indexmap", 833 | "lasso", 834 | "once_cell", 835 | "phf", 836 | "rand", 837 | ] 838 | 839 | [[package]] 840 | name = "gsk4" 841 | version = "0.9.2" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "aa21a2f7c51ee1c6cc1242c2faf3aae2b7566138f182696759987bde8219e922" 844 | dependencies = [ 845 | "cairo-rs", 846 | "gdk4", 847 | "glib", 848 | "graphene-rs", 849 | "gsk4-sys", 850 | "libc", 851 | "pango", 852 | ] 853 | 854 | [[package]] 855 | name = "gsk4-sys" 856 | version = "0.9.2" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "0f9fb607554f9f4e8829eb7ea301b0fde051e1dbfd5d16b143a8a9c2fac6c01b" 859 | dependencies = [ 860 | "cairo-sys-rs", 861 | "gdk4-sys", 862 | "glib-sys", 863 | "gobject-sys", 864 | "graphene-sys", 865 | "libc", 866 | "pango-sys", 867 | "system-deps", 868 | ] 869 | 870 | [[package]] 871 | name = "gtk4" 872 | version = "0.9.2" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "31e2d105ce672f5cdcb5af2602e91c2901e91c72da15ab76f613ad57ecf04c6d" 875 | dependencies = [ 876 | "cairo-rs", 877 | "field-offset", 878 | "futures-channel", 879 | "gdk-pixbuf", 880 | "gdk4", 881 | "gio", 882 | "glib", 883 | "graphene-rs", 884 | "gsk4", 885 | "gtk4-macros", 886 | "gtk4-sys", 887 | "libc", 888 | "pango", 889 | ] 890 | 891 | [[package]] 892 | name = "gtk4-layer-shell" 893 | version = "0.4.0" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "f3e1e1b1516be3d7ca089dfa6a1e688e268c74aef50c0c25fe8c46b1ba8ed1cc" 896 | dependencies = [ 897 | "bitflags 2.6.0", 898 | "gdk4", 899 | "glib", 900 | "glib-sys", 901 | "gtk4", 902 | "gtk4-layer-shell-sys", 903 | "libc", 904 | ] 905 | 906 | [[package]] 907 | name = "gtk4-layer-shell-sys" 908 | version = "0.3.0" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "e3057dc117db2d664a9b45f1956568701914e80cf9f2c8cef0a755af4c1c8105" 911 | dependencies = [ 912 | "gdk4-sys", 913 | "glib-sys", 914 | "gtk4-sys", 915 | "libc", 916 | "system-deps", 917 | ] 918 | 919 | [[package]] 920 | name = "gtk4-macros" 921 | version = "0.9.1" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "e9e7b362c8fccd2712297903717d65d30defdab2b509bc9d209cbe5ffb9fabaf" 924 | dependencies = [ 925 | "proc-macro-crate", 926 | "proc-macro2", 927 | "quote", 928 | "syn 2.0.82", 929 | ] 930 | 931 | [[package]] 932 | name = "gtk4-sys" 933 | version = "0.9.2" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "cbe4325908b1c1642dbb48e9f49c07a73185babf43e8b2065b0f881a589f55b8" 936 | dependencies = [ 937 | "cairo-sys-rs", 938 | "gdk-pixbuf-sys", 939 | "gdk4-sys", 940 | "gio-sys", 941 | "glib-sys", 942 | "gobject-sys", 943 | "graphene-sys", 944 | "gsk4-sys", 945 | "libc", 946 | "pango-sys", 947 | "system-deps", 948 | ] 949 | 950 | [[package]] 951 | name = "hashbrown" 952 | version = "0.14.5" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 955 | dependencies = [ 956 | "ahash", 957 | "allocator-api2", 958 | ] 959 | 960 | [[package]] 961 | name = "hashbrown" 962 | version = "0.15.0" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 965 | 966 | [[package]] 967 | name = "heck" 968 | version = "0.5.0" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 971 | 972 | [[package]] 973 | name = "hermit-abi" 974 | version = "0.3.9" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 977 | 978 | [[package]] 979 | name = "hermit-abi" 980 | version = "0.4.0" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 983 | 984 | [[package]] 985 | name = "hex" 986 | version = "0.4.3" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 989 | 990 | [[package]] 991 | name = "indexmap" 992 | version = "2.6.0" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 995 | dependencies = [ 996 | "equivalent", 997 | "hashbrown 0.15.0", 998 | ] 999 | 1000 | [[package]] 1001 | name = "js-sys" 1002 | version = "0.3.72" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 1005 | dependencies = [ 1006 | "wasm-bindgen", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "khronos_api" 1011 | version = "3.1.0" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" 1014 | 1015 | [[package]] 1016 | name = "lasso" 1017 | version = "0.7.3" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "6e14eda50a3494b3bf7b9ce51c52434a761e383d7238ce1dd5dcec2fbc13e9fb" 1020 | dependencies = [ 1021 | "hashbrown 0.14.5", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "libc" 1026 | version = "0.2.161" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 1029 | 1030 | [[package]] 1031 | name = "libpulse-binding" 1032 | version = "2.28.1" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff" 1035 | dependencies = [ 1036 | "bitflags 1.3.2", 1037 | "libc", 1038 | "libpulse-sys", 1039 | "num-derive", 1040 | "num-traits", 1041 | "winapi", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "libpulse-sys" 1046 | version = "1.21.0" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" 1049 | dependencies = [ 1050 | "libc", 1051 | "num-derive", 1052 | "num-traits", 1053 | "pkg-config", 1054 | "winapi", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "linux-raw-sys" 1059 | version = "0.4.14" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 1062 | 1063 | [[package]] 1064 | name = "lock_api" 1065 | version = "0.4.12" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 1068 | dependencies = [ 1069 | "autocfg", 1070 | "scopeguard", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "log" 1075 | version = "0.4.22" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 1078 | 1079 | [[package]] 1080 | name = "memchr" 1081 | version = "2.7.4" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1084 | 1085 | [[package]] 1086 | name = "memoffset" 1087 | version = "0.9.1" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 1090 | dependencies = [ 1091 | "autocfg", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "minimal-lexical" 1096 | version = "0.2.1" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1099 | 1100 | [[package]] 1101 | name = "miniz_oxide" 1102 | version = "0.8.0" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 1105 | dependencies = [ 1106 | "adler2", 1107 | ] 1108 | 1109 | [[package]] 1110 | name = "mio" 1111 | version = "1.0.2" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 1114 | dependencies = [ 1115 | "hermit-abi 0.3.9", 1116 | "libc", 1117 | "wasi", 1118 | "windows-sys 0.52.0", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "mixxc" 1123 | version = "0.2.4" 1124 | dependencies = [ 1125 | "anyhow", 1126 | "argh", 1127 | "bitflags 2.6.0", 1128 | "color-print", 1129 | "derive_more", 1130 | "enum_dispatch", 1131 | "gdk4-x11", 1132 | "glib", 1133 | "grass_compiler", 1134 | "gtk4", 1135 | "gtk4-layer-shell", 1136 | "libpulse-binding", 1137 | "num-traits", 1138 | "parking_lot", 1139 | "regex", 1140 | "relm4", 1141 | "smallvec", 1142 | "thiserror", 1143 | "tokio", 1144 | "tokio-util", 1145 | "tracker", 1146 | "x11rb", 1147 | "zbus", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "nanorand" 1152 | version = "0.7.0" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 1155 | dependencies = [ 1156 | "getrandom", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "nix" 1161 | version = "0.29.0" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 1164 | dependencies = [ 1165 | "bitflags 2.6.0", 1166 | "cfg-if", 1167 | "cfg_aliases", 1168 | "libc", 1169 | "memoffset", 1170 | ] 1171 | 1172 | [[package]] 1173 | name = "nom" 1174 | version = "7.1.3" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1177 | dependencies = [ 1178 | "memchr", 1179 | "minimal-lexical", 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "num-derive" 1184 | version = "0.3.3" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" 1187 | dependencies = [ 1188 | "proc-macro2", 1189 | "quote", 1190 | "syn 1.0.109", 1191 | ] 1192 | 1193 | [[package]] 1194 | name = "num-traits" 1195 | version = "0.2.19" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1198 | dependencies = [ 1199 | "autocfg", 1200 | ] 1201 | 1202 | [[package]] 1203 | name = "object" 1204 | version = "0.36.5" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 1207 | dependencies = [ 1208 | "memchr", 1209 | ] 1210 | 1211 | [[package]] 1212 | name = "once_cell" 1213 | version = "1.20.2" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 1216 | 1217 | [[package]] 1218 | name = "ordered-stream" 1219 | version = "0.2.0" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" 1222 | dependencies = [ 1223 | "futures-core", 1224 | "pin-project-lite", 1225 | ] 1226 | 1227 | [[package]] 1228 | name = "pango" 1229 | version = "0.20.4" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "aa26aa54b11094d72141a754901cd71d9356432bb8147f9cace8d9c7ba95f356" 1232 | dependencies = [ 1233 | "gio", 1234 | "glib", 1235 | "libc", 1236 | "pango-sys", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "pango-sys" 1241 | version = "0.20.4" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "84fd65917bf12f06544ae2bbc200abf9fc0a513a5a88a0fa81013893aef2b838" 1244 | dependencies = [ 1245 | "glib-sys", 1246 | "gobject-sys", 1247 | "libc", 1248 | "system-deps", 1249 | ] 1250 | 1251 | [[package]] 1252 | name = "parking" 1253 | version = "2.2.1" 1254 | source = "registry+https://github.com/rust-lang/crates.io-index" 1255 | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1256 | 1257 | [[package]] 1258 | name = "parking_lot" 1259 | version = "0.12.3" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1262 | dependencies = [ 1263 | "lock_api", 1264 | "parking_lot_core", 1265 | ] 1266 | 1267 | [[package]] 1268 | name = "parking_lot_core" 1269 | version = "0.9.10" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1272 | dependencies = [ 1273 | "cfg-if", 1274 | "libc", 1275 | "redox_syscall", 1276 | "smallvec", 1277 | "windows-targets 0.52.6", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "phf" 1282 | version = "0.11.2" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 1285 | dependencies = [ 1286 | "phf_macros", 1287 | "phf_shared", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "phf_generator" 1292 | version = "0.11.2" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" 1295 | dependencies = [ 1296 | "phf_shared", 1297 | "rand", 1298 | ] 1299 | 1300 | [[package]] 1301 | name = "phf_macros" 1302 | version = "0.11.2" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" 1305 | dependencies = [ 1306 | "phf_generator", 1307 | "phf_shared", 1308 | "proc-macro2", 1309 | "quote", 1310 | "syn 2.0.82", 1311 | ] 1312 | 1313 | [[package]] 1314 | name = "phf_shared" 1315 | version = "0.11.2" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 1318 | dependencies = [ 1319 | "siphasher", 1320 | ] 1321 | 1322 | [[package]] 1323 | name = "pin-project-lite" 1324 | version = "0.2.14" 1325 | source = "registry+https://github.com/rust-lang/crates.io-index" 1326 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 1327 | 1328 | [[package]] 1329 | name = "pin-utils" 1330 | version = "0.1.0" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1333 | 1334 | [[package]] 1335 | name = "piper" 1336 | version = "0.2.4" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 1339 | dependencies = [ 1340 | "atomic-waker", 1341 | "fastrand", 1342 | "futures-io", 1343 | ] 1344 | 1345 | [[package]] 1346 | name = "pkg-config" 1347 | version = "0.3.31" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 1350 | 1351 | [[package]] 1352 | name = "polling" 1353 | version = "3.7.3" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" 1356 | dependencies = [ 1357 | "cfg-if", 1358 | "concurrent-queue", 1359 | "hermit-abi 0.4.0", 1360 | "pin-project-lite", 1361 | "rustix", 1362 | "tracing", 1363 | "windows-sys 0.59.0", 1364 | ] 1365 | 1366 | [[package]] 1367 | name = "ppv-lite86" 1368 | version = "0.2.20" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 1371 | dependencies = [ 1372 | "zerocopy", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "proc-macro-crate" 1377 | version = "3.2.0" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" 1380 | dependencies = [ 1381 | "toml_edit", 1382 | ] 1383 | 1384 | [[package]] 1385 | name = "proc-macro2" 1386 | version = "1.0.88" 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" 1388 | checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" 1389 | dependencies = [ 1390 | "unicode-ident", 1391 | ] 1392 | 1393 | [[package]] 1394 | name = "quote" 1395 | version = "1.0.37" 1396 | source = "registry+https://github.com/rust-lang/crates.io-index" 1397 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 1398 | dependencies = [ 1399 | "proc-macro2", 1400 | ] 1401 | 1402 | [[package]] 1403 | name = "rand" 1404 | version = "0.8.5" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1407 | dependencies = [ 1408 | "libc", 1409 | "rand_chacha", 1410 | "rand_core", 1411 | ] 1412 | 1413 | [[package]] 1414 | name = "rand_chacha" 1415 | version = "0.3.1" 1416 | source = "registry+https://github.com/rust-lang/crates.io-index" 1417 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1418 | dependencies = [ 1419 | "ppv-lite86", 1420 | "rand_core", 1421 | ] 1422 | 1423 | [[package]] 1424 | name = "rand_core" 1425 | version = "0.6.4" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1428 | dependencies = [ 1429 | "getrandom", 1430 | ] 1431 | 1432 | [[package]] 1433 | name = "redox_syscall" 1434 | version = "0.5.7" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 1437 | dependencies = [ 1438 | "bitflags 2.6.0", 1439 | ] 1440 | 1441 | [[package]] 1442 | name = "regex" 1443 | version = "1.11.1" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1446 | dependencies = [ 1447 | "regex-automata", 1448 | "regex-syntax", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "regex-automata" 1453 | version = "0.4.8" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 1456 | dependencies = [ 1457 | "regex-syntax", 1458 | ] 1459 | 1460 | [[package]] 1461 | name = "regex-syntax" 1462 | version = "0.8.5" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1465 | 1466 | [[package]] 1467 | name = "relm4" 1468 | version = "0.9.1" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "30837553c1a8cfea1a404c83ec387c5c8ff9358e1060b057c274c5daa5035ad1" 1471 | dependencies = [ 1472 | "flume", 1473 | "fragile", 1474 | "futures", 1475 | "gtk4", 1476 | "once_cell", 1477 | "relm4-macros", 1478 | "tokio", 1479 | "tracing", 1480 | ] 1481 | 1482 | [[package]] 1483 | name = "relm4-macros" 1484 | version = "0.9.1" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "5a895a7455441a857d100ca679bd24a92f91d28b5e3df63296792ac1af2eddde" 1487 | dependencies = [ 1488 | "proc-macro2", 1489 | "quote", 1490 | "syn 2.0.82", 1491 | ] 1492 | 1493 | [[package]] 1494 | name = "rustc-demangle" 1495 | version = "0.1.24" 1496 | source = "registry+https://github.com/rust-lang/crates.io-index" 1497 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1498 | 1499 | [[package]] 1500 | name = "rustc_version" 1501 | version = "0.4.1" 1502 | source = "registry+https://github.com/rust-lang/crates.io-index" 1503 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 1504 | dependencies = [ 1505 | "semver", 1506 | ] 1507 | 1508 | [[package]] 1509 | name = "rustix" 1510 | version = "0.38.37" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 1513 | dependencies = [ 1514 | "bitflags 2.6.0", 1515 | "errno", 1516 | "libc", 1517 | "linux-raw-sys", 1518 | "windows-sys 0.52.0", 1519 | ] 1520 | 1521 | [[package]] 1522 | name = "scopeguard" 1523 | version = "1.2.0" 1524 | source = "registry+https://github.com/rust-lang/crates.io-index" 1525 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1526 | 1527 | [[package]] 1528 | name = "semver" 1529 | version = "1.0.23" 1530 | source = "registry+https://github.com/rust-lang/crates.io-index" 1531 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 1532 | 1533 | [[package]] 1534 | name = "serde" 1535 | version = "1.0.211" 1536 | source = "registry+https://github.com/rust-lang/crates.io-index" 1537 | checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793" 1538 | dependencies = [ 1539 | "serde_derive", 1540 | ] 1541 | 1542 | [[package]] 1543 | name = "serde_derive" 1544 | version = "1.0.211" 1545 | source = "registry+https://github.com/rust-lang/crates.io-index" 1546 | checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff" 1547 | dependencies = [ 1548 | "proc-macro2", 1549 | "quote", 1550 | "syn 2.0.82", 1551 | ] 1552 | 1553 | [[package]] 1554 | name = "serde_repr" 1555 | version = "0.1.19" 1556 | source = "registry+https://github.com/rust-lang/crates.io-index" 1557 | checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" 1558 | dependencies = [ 1559 | "proc-macro2", 1560 | "quote", 1561 | "syn 2.0.82", 1562 | ] 1563 | 1564 | [[package]] 1565 | name = "serde_spanned" 1566 | version = "0.6.8" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 1569 | dependencies = [ 1570 | "serde", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "signal-hook-registry" 1575 | version = "1.4.2" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1578 | dependencies = [ 1579 | "libc", 1580 | ] 1581 | 1582 | [[package]] 1583 | name = "siphasher" 1584 | version = "0.3.11" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 1587 | 1588 | [[package]] 1589 | name = "slab" 1590 | version = "0.4.9" 1591 | source = "registry+https://github.com/rust-lang/crates.io-index" 1592 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1593 | dependencies = [ 1594 | "autocfg", 1595 | ] 1596 | 1597 | [[package]] 1598 | name = "smallvec" 1599 | version = "1.13.2" 1600 | source = "registry+https://github.com/rust-lang/crates.io-index" 1601 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1602 | 1603 | [[package]] 1604 | name = "socket2" 1605 | version = "0.5.7" 1606 | source = "registry+https://github.com/rust-lang/crates.io-index" 1607 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1608 | dependencies = [ 1609 | "libc", 1610 | "windows-sys 0.52.0", 1611 | ] 1612 | 1613 | [[package]] 1614 | name = "spin" 1615 | version = "0.9.8" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1618 | dependencies = [ 1619 | "lock_api", 1620 | ] 1621 | 1622 | [[package]] 1623 | name = "static_assertions" 1624 | version = "1.1.0" 1625 | source = "registry+https://github.com/rust-lang/crates.io-index" 1626 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1627 | 1628 | [[package]] 1629 | name = "syn" 1630 | version = "1.0.109" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1633 | dependencies = [ 1634 | "proc-macro2", 1635 | "quote", 1636 | "unicode-ident", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "syn" 1641 | version = "2.0.82" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" 1644 | dependencies = [ 1645 | "proc-macro2", 1646 | "quote", 1647 | "unicode-ident", 1648 | ] 1649 | 1650 | [[package]] 1651 | name = "system-deps" 1652 | version = "7.0.3" 1653 | source = "registry+https://github.com/rust-lang/crates.io-index" 1654 | checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" 1655 | dependencies = [ 1656 | "cfg-expr", 1657 | "heck", 1658 | "pkg-config", 1659 | "toml", 1660 | "version-compare", 1661 | ] 1662 | 1663 | [[package]] 1664 | name = "target-lexicon" 1665 | version = "0.12.16" 1666 | source = "registry+https://github.com/rust-lang/crates.io-index" 1667 | checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" 1668 | 1669 | [[package]] 1670 | name = "tempfile" 1671 | version = "3.13.0" 1672 | source = "registry+https://github.com/rust-lang/crates.io-index" 1673 | checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" 1674 | dependencies = [ 1675 | "cfg-if", 1676 | "fastrand", 1677 | "once_cell", 1678 | "rustix", 1679 | "windows-sys 0.59.0", 1680 | ] 1681 | 1682 | [[package]] 1683 | name = "thiserror" 1684 | version = "1.0.64" 1685 | source = "registry+https://github.com/rust-lang/crates.io-index" 1686 | checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 1687 | dependencies = [ 1688 | "thiserror-impl", 1689 | ] 1690 | 1691 | [[package]] 1692 | name = "thiserror-impl" 1693 | version = "1.0.64" 1694 | source = "registry+https://github.com/rust-lang/crates.io-index" 1695 | checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 1696 | dependencies = [ 1697 | "proc-macro2", 1698 | "quote", 1699 | "syn 2.0.82", 1700 | ] 1701 | 1702 | [[package]] 1703 | name = "tokio" 1704 | version = "1.40.0" 1705 | source = "registry+https://github.com/rust-lang/crates.io-index" 1706 | checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" 1707 | dependencies = [ 1708 | "backtrace", 1709 | "bytes", 1710 | "libc", 1711 | "mio", 1712 | "pin-project-lite", 1713 | "signal-hook-registry", 1714 | "socket2", 1715 | "tokio-macros", 1716 | "tracing", 1717 | "windows-sys 0.52.0", 1718 | ] 1719 | 1720 | [[package]] 1721 | name = "tokio-macros" 1722 | version = "2.4.0" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 1725 | dependencies = [ 1726 | "proc-macro2", 1727 | "quote", 1728 | "syn 2.0.82", 1729 | ] 1730 | 1731 | [[package]] 1732 | name = "tokio-util" 1733 | version = "0.7.12" 1734 | source = "registry+https://github.com/rust-lang/crates.io-index" 1735 | checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" 1736 | dependencies = [ 1737 | "bytes", 1738 | "futures-core", 1739 | "futures-sink", 1740 | "pin-project-lite", 1741 | "tokio", 1742 | ] 1743 | 1744 | [[package]] 1745 | name = "toml" 1746 | version = "0.8.19" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 1749 | dependencies = [ 1750 | "serde", 1751 | "serde_spanned", 1752 | "toml_datetime", 1753 | "toml_edit", 1754 | ] 1755 | 1756 | [[package]] 1757 | name = "toml_datetime" 1758 | version = "0.6.8" 1759 | source = "registry+https://github.com/rust-lang/crates.io-index" 1760 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1761 | dependencies = [ 1762 | "serde", 1763 | ] 1764 | 1765 | [[package]] 1766 | name = "toml_edit" 1767 | version = "0.22.22" 1768 | source = "registry+https://github.com/rust-lang/crates.io-index" 1769 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 1770 | dependencies = [ 1771 | "indexmap", 1772 | "serde", 1773 | "serde_spanned", 1774 | "toml_datetime", 1775 | "winnow", 1776 | ] 1777 | 1778 | [[package]] 1779 | name = "tracing" 1780 | version = "0.1.40" 1781 | source = "registry+https://github.com/rust-lang/crates.io-index" 1782 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1783 | dependencies = [ 1784 | "pin-project-lite", 1785 | "tracing-attributes", 1786 | "tracing-core", 1787 | ] 1788 | 1789 | [[package]] 1790 | name = "tracing-attributes" 1791 | version = "0.1.27" 1792 | source = "registry+https://github.com/rust-lang/crates.io-index" 1793 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1794 | dependencies = [ 1795 | "proc-macro2", 1796 | "quote", 1797 | "syn 2.0.82", 1798 | ] 1799 | 1800 | [[package]] 1801 | name = "tracing-core" 1802 | version = "0.1.32" 1803 | source = "registry+https://github.com/rust-lang/crates.io-index" 1804 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1805 | dependencies = [ 1806 | "once_cell", 1807 | ] 1808 | 1809 | [[package]] 1810 | name = "tracker" 1811 | version = "0.2.2" 1812 | source = "registry+https://github.com/rust-lang/crates.io-index" 1813 | checksum = "ce5c98457ff700aaeefcd4a4a492096e78a2af1dd8523c66e94a3adb0fdbd415" 1814 | dependencies = [ 1815 | "tracker-macros", 1816 | ] 1817 | 1818 | [[package]] 1819 | name = "tracker-macros" 1820 | version = "0.2.2" 1821 | source = "registry+https://github.com/rust-lang/crates.io-index" 1822 | checksum = "dc19eb2373ccf3d1999967c26c3d44534ff71ae5d8b9dacf78f4b13132229e48" 1823 | dependencies = [ 1824 | "proc-macro2", 1825 | "quote", 1826 | "syn 2.0.82", 1827 | ] 1828 | 1829 | [[package]] 1830 | name = "uds_windows" 1831 | version = "1.1.0" 1832 | source = "registry+https://github.com/rust-lang/crates.io-index" 1833 | checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" 1834 | dependencies = [ 1835 | "memoffset", 1836 | "tempfile", 1837 | "winapi", 1838 | ] 1839 | 1840 | [[package]] 1841 | name = "unicode-ident" 1842 | version = "1.0.13" 1843 | source = "registry+https://github.com/rust-lang/crates.io-index" 1844 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 1845 | 1846 | [[package]] 1847 | name = "unicode-xid" 1848 | version = "0.2.5" 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" 1850 | checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" 1851 | 1852 | [[package]] 1853 | name = "version-compare" 1854 | version = "0.2.0" 1855 | source = "registry+https://github.com/rust-lang/crates.io-index" 1856 | checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" 1857 | 1858 | [[package]] 1859 | name = "version_check" 1860 | version = "0.9.5" 1861 | source = "registry+https://github.com/rust-lang/crates.io-index" 1862 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1863 | 1864 | [[package]] 1865 | name = "wasi" 1866 | version = "0.11.0+wasi-snapshot-preview1" 1867 | source = "registry+https://github.com/rust-lang/crates.io-index" 1868 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1869 | 1870 | [[package]] 1871 | name = "wasm-bindgen" 1872 | version = "0.2.95" 1873 | source = "registry+https://github.com/rust-lang/crates.io-index" 1874 | checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" 1875 | dependencies = [ 1876 | "cfg-if", 1877 | "once_cell", 1878 | "wasm-bindgen-macro", 1879 | ] 1880 | 1881 | [[package]] 1882 | name = "wasm-bindgen-backend" 1883 | version = "0.2.95" 1884 | source = "registry+https://github.com/rust-lang/crates.io-index" 1885 | checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" 1886 | dependencies = [ 1887 | "bumpalo", 1888 | "log", 1889 | "once_cell", 1890 | "proc-macro2", 1891 | "quote", 1892 | "syn 2.0.82", 1893 | "wasm-bindgen-shared", 1894 | ] 1895 | 1896 | [[package]] 1897 | name = "wasm-bindgen-macro" 1898 | version = "0.2.95" 1899 | source = "registry+https://github.com/rust-lang/crates.io-index" 1900 | checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" 1901 | dependencies = [ 1902 | "quote", 1903 | "wasm-bindgen-macro-support", 1904 | ] 1905 | 1906 | [[package]] 1907 | name = "wasm-bindgen-macro-support" 1908 | version = "0.2.95" 1909 | source = "registry+https://github.com/rust-lang/crates.io-index" 1910 | checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" 1911 | dependencies = [ 1912 | "proc-macro2", 1913 | "quote", 1914 | "syn 2.0.82", 1915 | "wasm-bindgen-backend", 1916 | "wasm-bindgen-shared", 1917 | ] 1918 | 1919 | [[package]] 1920 | name = "wasm-bindgen-shared" 1921 | version = "0.2.95" 1922 | source = "registry+https://github.com/rust-lang/crates.io-index" 1923 | checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" 1924 | 1925 | [[package]] 1926 | name = "winapi" 1927 | version = "0.3.9" 1928 | source = "registry+https://github.com/rust-lang/crates.io-index" 1929 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1930 | dependencies = [ 1931 | "winapi-i686-pc-windows-gnu", 1932 | "winapi-x86_64-pc-windows-gnu", 1933 | ] 1934 | 1935 | [[package]] 1936 | name = "winapi-i686-pc-windows-gnu" 1937 | version = "0.4.0" 1938 | source = "registry+https://github.com/rust-lang/crates.io-index" 1939 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1940 | 1941 | [[package]] 1942 | name = "winapi-x86_64-pc-windows-gnu" 1943 | version = "0.4.0" 1944 | source = "registry+https://github.com/rust-lang/crates.io-index" 1945 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1946 | 1947 | [[package]] 1948 | name = "windows-sys" 1949 | version = "0.52.0" 1950 | source = "registry+https://github.com/rust-lang/crates.io-index" 1951 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1952 | dependencies = [ 1953 | "windows-targets 0.52.6", 1954 | ] 1955 | 1956 | [[package]] 1957 | name = "windows-sys" 1958 | version = "0.59.0" 1959 | source = "registry+https://github.com/rust-lang/crates.io-index" 1960 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1961 | dependencies = [ 1962 | "windows-targets 0.52.6", 1963 | ] 1964 | 1965 | [[package]] 1966 | name = "windows-targets" 1967 | version = "0.48.5" 1968 | source = "registry+https://github.com/rust-lang/crates.io-index" 1969 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1970 | dependencies = [ 1971 | "windows_aarch64_gnullvm 0.48.5", 1972 | "windows_aarch64_msvc 0.48.5", 1973 | "windows_i686_gnu 0.48.5", 1974 | "windows_i686_msvc 0.48.5", 1975 | "windows_x86_64_gnu 0.48.5", 1976 | "windows_x86_64_gnullvm 0.48.5", 1977 | "windows_x86_64_msvc 0.48.5", 1978 | ] 1979 | 1980 | [[package]] 1981 | name = "windows-targets" 1982 | version = "0.52.6" 1983 | source = "registry+https://github.com/rust-lang/crates.io-index" 1984 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1985 | dependencies = [ 1986 | "windows_aarch64_gnullvm 0.52.6", 1987 | "windows_aarch64_msvc 0.52.6", 1988 | "windows_i686_gnu 0.52.6", 1989 | "windows_i686_gnullvm", 1990 | "windows_i686_msvc 0.52.6", 1991 | "windows_x86_64_gnu 0.52.6", 1992 | "windows_x86_64_gnullvm 0.52.6", 1993 | "windows_x86_64_msvc 0.52.6", 1994 | ] 1995 | 1996 | [[package]] 1997 | name = "windows_aarch64_gnullvm" 1998 | version = "0.48.5" 1999 | source = "registry+https://github.com/rust-lang/crates.io-index" 2000 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2001 | 2002 | [[package]] 2003 | name = "windows_aarch64_gnullvm" 2004 | version = "0.52.6" 2005 | source = "registry+https://github.com/rust-lang/crates.io-index" 2006 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2007 | 2008 | [[package]] 2009 | name = "windows_aarch64_msvc" 2010 | version = "0.48.5" 2011 | source = "registry+https://github.com/rust-lang/crates.io-index" 2012 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2013 | 2014 | [[package]] 2015 | name = "windows_aarch64_msvc" 2016 | version = "0.52.6" 2017 | source = "registry+https://github.com/rust-lang/crates.io-index" 2018 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2019 | 2020 | [[package]] 2021 | name = "windows_i686_gnu" 2022 | version = "0.48.5" 2023 | source = "registry+https://github.com/rust-lang/crates.io-index" 2024 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2025 | 2026 | [[package]] 2027 | name = "windows_i686_gnu" 2028 | version = "0.52.6" 2029 | source = "registry+https://github.com/rust-lang/crates.io-index" 2030 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2031 | 2032 | [[package]] 2033 | name = "windows_i686_gnullvm" 2034 | version = "0.52.6" 2035 | source = "registry+https://github.com/rust-lang/crates.io-index" 2036 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2037 | 2038 | [[package]] 2039 | name = "windows_i686_msvc" 2040 | version = "0.48.5" 2041 | source = "registry+https://github.com/rust-lang/crates.io-index" 2042 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2043 | 2044 | [[package]] 2045 | name = "windows_i686_msvc" 2046 | version = "0.52.6" 2047 | source = "registry+https://github.com/rust-lang/crates.io-index" 2048 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2049 | 2050 | [[package]] 2051 | name = "windows_x86_64_gnu" 2052 | version = "0.48.5" 2053 | source = "registry+https://github.com/rust-lang/crates.io-index" 2054 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2055 | 2056 | [[package]] 2057 | name = "windows_x86_64_gnu" 2058 | version = "0.52.6" 2059 | source = "registry+https://github.com/rust-lang/crates.io-index" 2060 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2061 | 2062 | [[package]] 2063 | name = "windows_x86_64_gnullvm" 2064 | version = "0.48.5" 2065 | source = "registry+https://github.com/rust-lang/crates.io-index" 2066 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2067 | 2068 | [[package]] 2069 | name = "windows_x86_64_gnullvm" 2070 | version = "0.52.6" 2071 | source = "registry+https://github.com/rust-lang/crates.io-index" 2072 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2073 | 2074 | [[package]] 2075 | name = "windows_x86_64_msvc" 2076 | version = "0.48.5" 2077 | source = "registry+https://github.com/rust-lang/crates.io-index" 2078 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2079 | 2080 | [[package]] 2081 | name = "windows_x86_64_msvc" 2082 | version = "0.52.6" 2083 | source = "registry+https://github.com/rust-lang/crates.io-index" 2084 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2085 | 2086 | [[package]] 2087 | name = "winnow" 2088 | version = "0.6.20" 2089 | source = "registry+https://github.com/rust-lang/crates.io-index" 2090 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 2091 | dependencies = [ 2092 | "memchr", 2093 | ] 2094 | 2095 | [[package]] 2096 | name = "x11" 2097 | version = "2.21.0" 2098 | source = "registry+https://github.com/rust-lang/crates.io-index" 2099 | checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" 2100 | dependencies = [ 2101 | "libc", 2102 | "pkg-config", 2103 | ] 2104 | 2105 | [[package]] 2106 | name = "x11rb" 2107 | version = "0.13.1" 2108 | source = "registry+https://github.com/rust-lang/crates.io-index" 2109 | checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" 2110 | dependencies = [ 2111 | "gethostname", 2112 | "rustix", 2113 | "x11rb-protocol", 2114 | ] 2115 | 2116 | [[package]] 2117 | name = "x11rb-protocol" 2118 | version = "0.13.1" 2119 | source = "registry+https://github.com/rust-lang/crates.io-index" 2120 | checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" 2121 | 2122 | [[package]] 2123 | name = "xdg-home" 2124 | version = "1.3.0" 2125 | source = "registry+https://github.com/rust-lang/crates.io-index" 2126 | checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" 2127 | dependencies = [ 2128 | "libc", 2129 | "windows-sys 0.59.0", 2130 | ] 2131 | 2132 | [[package]] 2133 | name = "xml-rs" 2134 | version = "0.8.22" 2135 | source = "registry+https://github.com/rust-lang/crates.io-index" 2136 | checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" 2137 | 2138 | [[package]] 2139 | name = "zbus" 2140 | version = "5.0.1" 2141 | source = "registry+https://github.com/rust-lang/crates.io-index" 2142 | checksum = "333be40ef37976542e10832ba961e3e44ea215a6b1e2673066b303ee3e0ede10" 2143 | dependencies = [ 2144 | "async-broadcast", 2145 | "async-process", 2146 | "async-recursion", 2147 | "async-trait", 2148 | "enumflags2", 2149 | "event-listener", 2150 | "futures-core", 2151 | "futures-util", 2152 | "hex", 2153 | "nix", 2154 | "ordered-stream", 2155 | "serde", 2156 | "serde_repr", 2157 | "static_assertions", 2158 | "tokio", 2159 | "tracing", 2160 | "uds_windows", 2161 | "windows-sys 0.59.0", 2162 | "xdg-home", 2163 | "zbus_macros", 2164 | "zbus_names", 2165 | "zvariant", 2166 | ] 2167 | 2168 | [[package]] 2169 | name = "zbus_macros" 2170 | version = "5.0.1" 2171 | source = "registry+https://github.com/rust-lang/crates.io-index" 2172 | checksum = "381be624000c82e716c2a45d9213fabacf82177591fa8a6ff655d2825450601a" 2173 | dependencies = [ 2174 | "proc-macro-crate", 2175 | "proc-macro2", 2176 | "quote", 2177 | "syn 2.0.82", 2178 | "zvariant_utils", 2179 | ] 2180 | 2181 | [[package]] 2182 | name = "zbus_names" 2183 | version = "4.0.0" 2184 | source = "registry+https://github.com/rust-lang/crates.io-index" 2185 | checksum = "cdc27fbd3593ff015cef906527a2ec4115e2e3dbf6204a24d952ac4975c80614" 2186 | dependencies = [ 2187 | "serde", 2188 | "static_assertions", 2189 | "zvariant", 2190 | ] 2191 | 2192 | [[package]] 2193 | name = "zerocopy" 2194 | version = "0.7.35" 2195 | source = "registry+https://github.com/rust-lang/crates.io-index" 2196 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 2197 | dependencies = [ 2198 | "byteorder", 2199 | "zerocopy-derive", 2200 | ] 2201 | 2202 | [[package]] 2203 | name = "zerocopy-derive" 2204 | version = "0.7.35" 2205 | source = "registry+https://github.com/rust-lang/crates.io-index" 2206 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 2207 | dependencies = [ 2208 | "proc-macro2", 2209 | "quote", 2210 | "syn 2.0.82", 2211 | ] 2212 | 2213 | [[package]] 2214 | name = "zvariant" 2215 | version = "5.0.1" 2216 | source = "registry+https://github.com/rust-lang/crates.io-index" 2217 | checksum = "c690a1da8858fd4377b8cc3134a753b0bea1d8ebd78ad6e5897fab821c5e184e" 2218 | dependencies = [ 2219 | "endi", 2220 | "enumflags2", 2221 | "serde", 2222 | "static_assertions", 2223 | "zvariant_derive", 2224 | "zvariant_utils", 2225 | ] 2226 | 2227 | [[package]] 2228 | name = "zvariant_derive" 2229 | version = "5.0.1" 2230 | source = "registry+https://github.com/rust-lang/crates.io-index" 2231 | checksum = "83b6ddc1fed08493e4f2bd9350e7d00a3383467228735f3f169a9f8820fde755" 2232 | dependencies = [ 2233 | "proc-macro-crate", 2234 | "proc-macro2", 2235 | "quote", 2236 | "syn 2.0.82", 2237 | "zvariant_utils", 2238 | ] 2239 | 2240 | [[package]] 2241 | name = "zvariant_utils" 2242 | version = "3.0.1" 2243 | source = "registry+https://github.com/rust-lang/crates.io-index" 2244 | checksum = "6f8d85190ba70bc7b9540430df078bb529620b1464ed4a606010de584e27094d" 2245 | dependencies = [ 2246 | "proc-macro2", 2247 | "quote", 2248 | "serde", 2249 | "static_assertions", 2250 | "syn 2.0.82", 2251 | "winnow", 2252 | ] 2253 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mixxc" 3 | version = "0.2.4" 4 | authors = ["Elvyria "] 5 | description = "Minimalistic volume mixer." 6 | repository = "https://github.com/Elvyria/mixxc" 7 | license = "MIT" 8 | edition = "2021" 9 | 10 | [features] 11 | default = ["Wayland", "X11", "Sass"] 12 | Wayland = ["dep:gtk4-layer-shell"] 13 | Sass = ["dep:grass"] 14 | X11 = ["dep:x11rb", "dep:gdk-x11"] 15 | PipeWire = [] 16 | Accent = ["dep:zbus"] 17 | 18 | [dependencies] 19 | argh = "0.1" 20 | bitflags = "2.6" 21 | color-print = "0.3.6" 22 | derive_more = { version = "1", features = ["deref", "deref_mut", "from", "debug"] } 23 | enum_dispatch = "0.3" 24 | glib = "0.20" 25 | grass = { version = "0.13", package = "grass_compiler", optional = true } 26 | gtk = { version = "0.9", package = "gtk4" } 27 | gtk4-layer-shell = { version = "0.4", optional = true } 28 | num-traits = "0.2" 29 | parking_lot = "0.12.3" 30 | smallvec = { version = "1.13", features = ["union"] } 31 | thiserror = "1.0" 32 | tokio = { version = "1.40", features = ["time", "macros", "fs", "io-util", "signal", "process"] } 33 | tokio-util = "0.7.12" 34 | tracker = "0.2" 35 | 36 | [dependencies.x11rb] 37 | version = "0.13" 38 | optional = true 39 | features = ["xinerama"] 40 | 41 | [dependencies.gdk-x11] 42 | package = "gdk4-x11" 43 | version = "0.9" 44 | optional = true 45 | features = ["xlib"] 46 | 47 | [dependencies.relm4] 48 | version = "0.9.1" 49 | default-features = false 50 | features = ["macros"] 51 | 52 | [dependencies.libpulse-binding] 53 | version = "2.28" 54 | default-features = false 55 | features = ["pa_v8"] 56 | 57 | [dependencies.zbus] 58 | version = "5" 59 | default-features = false 60 | features = ["tokio"] 61 | optional = true 62 | 63 | [build-dependencies] 64 | anyhow = "1.0" 65 | regex = { version = "1.11.1", default-features = false } 66 | grass = { version = "0.13", package = "grass_compiler" } 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := release 2 | 3 | prefix ?= /usr/local 4 | bindir ?= ${prefix}/bin 5 | datarootdir ?= ${prefix}/share 6 | datadir ?= ${datarootdir} 7 | mandir ?= ${datarootdir}/man 8 | man1dir ?= ${mandir}/man1 9 | 10 | name = $(shell sed -nE 's/name *?= *?"(.+)"/\1/p' ./Cargo.toml) 11 | ifdef CARGO_TARGET_DIR 12 | target = ${CARGO_TARGET_DIR} 13 | else 14 | target = ./target 15 | endif 16 | 17 | release: 18 | $(MAKE) clean 19 | cargo build --locked --release 20 | 21 | debug: 22 | cargo build --locked 23 | 24 | clean: 25 | cargo clean --package ${name} 26 | 27 | install: 28 | test -d ${target}/release 29 | install -m 0755 -s ${target}/release/${name} ${bindir}/${name} 30 | install -m 0755 -d ${datadir}/${name} 31 | install -m 0644 $(wildcard ./style/*.scss) $(wildcard ${target}/release/build/${name}-*/out/*.css) ${datadir}/${name} 32 | install -m 0755 -d ${man1dir} 33 | gzip -9 -c ./doc/mixxc.1 > ${man1dir}/${name}.1.gz 34 | chmod 0644 ${man1dir}/${name}.1.gz 35 | mandb --no-purge --quiet 36 | 37 | uninstall: 38 | rm ${bindir}/${name} 39 | rm -f $(wildcard ${datadir}/${name}/*.css ${datadir}/${name}/*.scss) 40 | rmdir ${datadir}/${name} 41 | rm ${man1dir}/${name}.1.gz 42 | mandb --quiet 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mixxc 2 | [![Crates.io](https://img.shields.io/crates/v/mixxc?logo=rust)](https://crates.io/crates/mixxc) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow)](https://opensource.org/licenses/MIT) 4 | 5 | Mixxc is a minimalistic and customizable volume mixer, created to seamlessly complement desktop widgets. 6 | 7 | Currently, it supports only `pulseaudio` and `pipewire` (through the pulseaudio interface) by utilizing `libpulseaudio` to receive audio events. 8 | 9 | ![Preview](https://github.com/user-attachments/assets/b64dc1fe-71f7-4a8b-baef-495c7d3e4690) 10 | 11 | 12 | ## Installation 13 | Can be installed from [crates.io](https://crates.io/) with `cargo`: 14 | 15 | ```sh 16 | cargo install mixxc --locked --features Sass,Wayland... 17 | ``` 18 | 19 | or right after [building](#building) manually with `make`: 20 | ```sh 21 | make install 22 | ``` 23 | 24 | ## Dependencies 25 | * [GTK4](https://www.gtk.org/) (4.15.1+) 26 | * [gtk4-layer-shell](https://github.com/wmww/gtk4-layer-shell) (Feature: Wayland) 27 | * [libpulseaudio](https://www.freedesktop.org/wiki/Software/PulseAudio) 28 | * [libxcb](https://xcb.freedesktop.org/) (Feature: X11) 29 | 30 | ## Features 31 | Some features can be enabled at compile time. 32 | * [Accent](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html) - Inherits the accent color from the system's settings. 33 | * [Sass](https://sass-lang.com/) - Allows you to use SCSS instead of CSS. 34 | * [Wayland](https://wayland.freedesktop.org/) - Uses wlr-layer-shell to imitate window positioning. 35 | * [X11](https://www.x.org/) - Sets WM hints and properties, and repositions the window. 36 | 37 | ## Usage 38 | ``` 39 | Usage: mixxc [-w ] [-h ] [-s ] [-a ] [-A] [-m ] [-M] [-b ] [-u ] [-c ] [-i] [-x ] [-P] [-v] 40 | 41 | Minimalistic volume mixer. 42 | 43 | Options: 44 | -w, --width window width 45 | -h, --height window height 46 | -s, --spacing spacing between clients 47 | -a, --anchor screen anchor point: (t)op, (b)ottom, (l)eft, (r)ight 48 | -A, --active show only active sinks 49 | -m, --margin margin distance for each anchor point 50 | -M, --master enable master volume slider 51 | -b, --bar volume slider orientation: (h)orizontal, (v)ertical 52 | -u, --userstyle path to the userstyle 53 | -c, --close close the window after a specified amount of time (ms) when 54 | focus is lost (default: 0) 55 | -i, --icon enable client icons 56 | -x, --max-volume max volume level in percent (default: 100; 1-255) 57 | -P, --per-process use only one volume slider for each system process 58 | -v, --version print version 59 | --help display usage information 60 | ``` 61 | 62 | ## Customization 63 | Mixxc is built with GTK4 and uses CSS to define its appearance. 64 | You will find the style sheet in your config directory after the first launch. 65 | ```sh 66 | ${XDG_CONFIG_HOME:-$HOME/.config}/mixxc/style.css 67 | ``` 68 | If you have enabled the Sass feature, it will also look for *.scss and *.sass files. 69 | ```sh 70 | ${XDG_CONFIG_HOME:-$HOME/.config}/mixxc/style.sass 71 | ${XDG_CONFIG_HOME:-$HOME/.config}/mixxc/style.scss 72 | ``` 73 | 74 | ## Tips 75 | ### Anchoring 76 | It is often desirable to be able to position widgets relatively to a screen side. 77 | Two flags will help with this: `-a --anchor` and `-m --margin`. 78 | Each margin value provided will match every anchor point respectively.   79 | ```sh 80 | mixxc --anchor left --anchor bottom --margin 20 --margin 30 81 | ``` 82 | 83 | ### Startup Time 84 | If startup seems a bit slow or memory usage seems a bit too high try this: 85 | ```sh 86 | GSK_RENDERER=cairo GTK_USE_PORTAL=0 mixxc 87 | ``` 88 | 89 | ### Toggle Window 90 | If you want to toggle window with a click of a button, Unix way is the way: 91 | ```sh 92 | pkill mixxc | mixxc 93 | ``` 94 | 95 | ## Troubleshooting 96 | 97 | ### Environment 98 | Mixxc is developed and tested with: 99 | * Wayland (Hyprland): `0.45.2` 100 | * PipeWire: `1.2.6` 101 | 102 | If your setup is different and you experience issues, feel free to file a bug report. 103 | 104 | ### GTK 105 | To get GTK related messages a specific environment variable must be non empty. 106 | ```sh 107 | GTK_DEBUG=1 mixxc 108 | ``` 109 | 110 | ## Building 111 | To build this little thing, you'll need some [Rust](https://www.rust-lang.org/). 112 | 113 | ##### Makefile 114 | ```sh 115 | git clone --depth 1 https://github.com/Elvyria/mixxc 116 | cd mixxc 117 | make 118 | ``` 119 | 120 | ##### Cargo 121 | ```sh 122 | git clone --depth 1 https://github.com/Elvyria/mixxc 123 | cd mixxc 124 | cargo build --locked --release --features Sass,Wayland... 125 | ``` 126 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::fs::{self, File}; 3 | use std::process::Command; 4 | 5 | use anyhow::Result; 6 | 7 | fn main() { 8 | compile_style().unwrap(); 9 | git_hash().unwrap(); 10 | } 11 | 12 | fn git() -> Command { 13 | Command::new("git") 14 | } 15 | 16 | fn git_hash() -> Result<()> { 17 | if !git().args(["describe", "--exact-match", "--tags", "HEAD"]).status().is_ok_and(|s| !s.success()) { 18 | return Ok(()) 19 | } 20 | 21 | let output = git().args(["describe", "--tags", "HEAD"]).output()?; 22 | let output = String::from_utf8(output.stdout)?; 23 | 24 | println!("cargo:rustc-env=GIT_COMMIT={output}"); 25 | 26 | Ok(()) 27 | } 28 | 29 | fn compile_style() -> Result<()> { 30 | use grass::*; 31 | 32 | let source = "style/default.scss"; 33 | let destination = format!("{}/default.css", std::env::var("OUT_DIR").unwrap()); 34 | 35 | let source_mtime = fs::metadata(source)?.modified()?; 36 | 37 | if let Ok(destination_meta) = fs::metadata(&destination) { 38 | if Some(source_mtime) == destination_meta.modified().ok() { 39 | return Ok(()) 40 | } 41 | } 42 | 43 | let options = Options::default().style(OutputStyle::Expanded); 44 | let compiled = grass::from_path(source, &options)?; 45 | 46 | let mut f = File::create(destination)?; 47 | f.write_all(compiled.as_bytes())?; 48 | f.set_modified(source_mtime)?; 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /doc/mixxc.1: -------------------------------------------------------------------------------- 1 | .Dd March 25, 2024 2 | .Dt MIXXC 1 3 | .Os 4 | .Sh NAME 5 | .Nm mixxc 6 | .Nd minimalistic volume mixer 7 | .Sh DESCRIPTION 8 | Mixxc is a volume mixer for managing application volume levels, focused on providing a high level of customization for widget users and creators. 9 | .Sh OPTIONS 10 | .Bl -tag \-width Ds 11 | .It Fl w , Fl \-width Ar px 12 | In horizontal bar orientation it affects only the base width of the window and will not change. 13 | In vertical, it's used to specify width of each audio client. 14 | .It Fl h , Fl \-height Ar px 15 | In vertical bar orientation it only affects the base height of the window and will not change. 16 | In horizontal, it will be ignored if window requires more space. 17 | .It Fl s , Fl \-spacing Ar px 18 | Space gap between audio clients in pixels. 19 | .It Fl a , Fl \-anchor Ar side 20 | Snap window to the side of the screen. Can be specified multiple times. 21 | 22 | .Bl -bullet -compact 23 | .It 24 | t, top 25 | .It 26 | b, bottom 27 | .It 28 | l, left 29 | .It 30 | r, right 31 | .El 32 | .It Fl A , Fl \-active 33 | Sliders that are associated with paused media players will be hidden and reappear only when playback is resumed. 34 | .It Fl C , Fl \-accent 35 | Reads an accent-color property from the system settings 36 | .Xr xdg-settings 1 37 | and applies the color to the user style. 38 | .It Fl m , Fl \-margin Ar px 39 | Distance that window will keep from each anchor point respectively. 40 | .It Fl M , Fl \-master 41 | Show a volume slider for the default audio sink. 42 | .It Fl b , Fl \-bar Ar orientation 43 | Changes orientation of audio sliders. 44 | 45 | .Bl -bullet -compact 46 | .It 47 | h, horizontal 48 | .It 49 | v, vertical 50 | .El 51 | .It Fl u , Fl \-userstyle Ar file 52 | Specify path to userstyle. By default, 53 | .Nm 54 | will attempt to read style file from $XDG_CONFIG_HOME/mixxc, with priority for CSS supersets. 55 | 56 | .Bl -bullet -compact 57 | .It 58 | \&.css 59 | .It 60 | \&.scss (Feature: Sass) 61 | .It 62 | \&.sass (Feature: Sass) 63 | .El 64 | .It Fl k , Fl \-keep 65 | Prevent window from closing itself. By default, window will be closed if focus is lost and mouse is no longer over it. 66 | .It Fl i , Fl \-icon 67 | Show icons that applications provide or display a generic reactive volume icon. 68 | If not all application icons are displayed properly, you might need to update icon cache 69 | .Xr gtk4-update-icon-cache 1 70 | or update $XDG_DATA_DIRS. 71 | .It Fl x , Fl \-max\-volume Ar n 72 | Highest achievable volume level in percents. 73 | .br 74 | Minimum is 1. Default is 100. Maximum is 255. 75 | .It Fl P , Fl \-per\-process 76 | Create only a single slider per system process and control all related sinks through it, keeping all clients with the same volume state. 77 | .It Fl v , Fl \-version 78 | Print version information. 79 | .It Fl \-help 80 | Print help information. 81 | .El 82 | .Sh ENVIRONMENT 83 | .Bl -tag -width Ds 84 | .It Ev PULSE_PEAK_RATE 85 | Integer value that controls frequency at which audio server probes audio tracks for loudness in an allowed dynamic range. 86 | Setting this value to 0 significantly reduces number of window redraws and CPU usage. 87 | .It Ev GTK_DEBUG 88 | Every GTK related message will be ignored and not printed if GTK_DEBUG variable is not set. 89 | .El 90 | .Sh FILES 91 | .Bl -compact -tag -width Ds 92 | .It Pa $XDG_CONFIG_HOME/mixxc/style.css 93 | .It Pa $XDG_CONFIG_HOME/mixxc/style.scss 94 | .It Pa $XDG_CONFIG_HOME/mixxc/style.sass 95 | .El 96 | .Sh FEATURES 97 | Here's a list of features that can be included or excluded at compile time. 98 | Excluding some of them might lead to a smaller binary size and performance improvements. 99 | .Bl -ohang 100 | .It - Accent 101 | Support for system accent color. 102 | .It - Sass 103 | Support for CSS supersets. 104 | .It - X11 105 | Support for X Window System. 106 | .It - Wayland 107 | Support for Wayland. 108 | .El 109 | .Sh EXAMPLES 110 | Open mixxc in the bottom right corner with some spacing. 111 | .Bd -literal -offset indent 112 | $ mixxc --anchor bottom --margin 20 \\ 113 | --anchor right --margin 20 114 | .Ed 115 | 116 | Using short options to switch to vertical slider orientation with icons, set width and height for each one of them. 117 | .Bd -literal -offset indent 118 | $ mixxc -b v -i -w 75 -h 350 119 | .Ed 120 | 121 | If you don't need auto-closing - you can turn your custom made button into a switch. 122 | .Bd -literal -offset indent 123 | $ pkill mixxc | mixxc --keep 124 | .Ed 125 | 126 | GTK4 supports multiple rendering backends which might improve startup time, memory usage or fix graphical glitches. 127 | .Bd -literal -offset indent 128 | $ GSK_RENDERER=help mixxc 129 | .Ed 130 | .Sh AUTHORS 131 | Elvyria 132 | .Sh BUGS 133 | .Bl -ohang 134 | .It Firefox (123.0.1.1) 135 | .Bl -bullet 136 | .It 137 | Muted playback removes audio sink and discards playback description information when unmuted. 138 | .It 139 | Jumping to any part of a video playback results in `Remove <-> Create` request instead of `Modify`, while jumping to any part of an audio playback works as expected. 140 | .It 141 | Volume levels are never requested from audio server, this causes desynchronization between volume levels. 142 | .El 143 | 144 | These problems are exclusive to Firefox and might not appear under Chromium based browsers. 145 | .El 146 | -------------------------------------------------------------------------------- /src/accent.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, ZbusError}; 2 | 3 | use zbus::zvariant::{OwnedValue, Structure}; 4 | 5 | #[zbus::proxy( 6 | default_service = "org.freedesktop.portal.Desktop", 7 | default_path = "/org/freedesktop/portal/desktop", 8 | interface = "org.freedesktop.portal.Settings", 9 | async_name = "Settings" 10 | )] 11 | pub trait Settings { 12 | fn read(&self, namespace: &str, key: &str) -> zbus::Result; 13 | } 14 | 15 | pub enum Scheme { 16 | Default, 17 | Dark, 18 | Light, 19 | } 20 | 21 | impl Settings<'_> { 22 | async fn appearance(&self, key: &str) -> Result { 23 | self.read("org.freedesktop.appearance", key) 24 | .await 25 | .map_err(|e| ZbusError::Read { e, 26 | namespace: "org.freedesktop.appearance".to_string(), 27 | key: key.to_string() 28 | }) 29 | .map_err(Into::into) 30 | } 31 | 32 | #[allow(dead_code)] 33 | pub async fn scheme(&self) -> Result { 34 | let reply = self.appearance("color-scheme").await?; 35 | 36 | let v = reply.downcast_ref::() 37 | .map_err(|_| ZbusError::BadResult { v: format!("{reply:?}") })?; 38 | 39 | match v { 40 | 1 => Ok(Scheme::Dark), 41 | 2 => Ok(Scheme::Light), 42 | _ => Ok(Scheme::Default), 43 | } 44 | } 45 | 46 | pub async fn accent(&self) -> Result<(u8, u8, u8), Error> { 47 | let reply = self.appearance("accent-color").await?; 48 | 49 | let (r, g, b) = reply.downcast_ref::() 50 | .and_then(<(f64, f64, f64)>::try_from) 51 | .map_err(|_| ZbusError::BadResult { v: format!("{reply:?}") })?; 52 | 53 | let r = (255.0 * r) as u8; 54 | let g = (255.0 * g) as u8; 55 | let b = (255.0 * b) as u8; 56 | 57 | Ok((r, g, b)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/anchor.rs: -------------------------------------------------------------------------------- 1 | use crate::error::CLIError; 2 | 3 | bitflags::bitflags! { 4 | #[derive(Clone, Copy, PartialEq, Eq)] 5 | pub struct Anchor: u8 { 6 | const None = 0b0000; 7 | const Top = 0b0001; 8 | const Left = 0b0010; 9 | const Bottom = 0b0100; 10 | const Right = 0b1000; 11 | } 12 | } 13 | 14 | impl TryFrom<&String> for Anchor { 15 | type Error = CLIError; 16 | 17 | fn try_from(s: &String) -> Result { 18 | match s.as_bytes().first().map(u8::to_ascii_lowercase) { 19 | Some(b't') => Ok(Anchor::Top), 20 | Some(b'l') => Ok(Anchor::Left), 21 | Some(b'b') => Ok(Anchor::Bottom), 22 | Some(b'r') => Ok(Anchor::Right), 23 | _ => Err(CLIError::Anchor(s.to_owned())), 24 | } 25 | } 26 | } 27 | 28 | #[cfg(feature = "X11")] 29 | impl Anchor { 30 | pub fn position(&self, margins: &[i32], screen: (u32, u32), window: (u32, u32)) -> (i32, i32) { 31 | let (mut x, mut y) = (0i32, 0i32); 32 | 33 | for (i, anchor) in self.iter().enumerate() { 34 | let margin = margins.get(i).unwrap_or(&0); 35 | 36 | match anchor { 37 | Anchor::Top => y += margin, 38 | Anchor::Left => x += margin, 39 | Anchor::Bottom => y += screen.1 as i32 - window.1 as i32 - margin, 40 | Anchor::Right => x += screen.0 as i32 - window.0 as i32 - margin, 41 | _ => {}, 42 | } 43 | } 44 | 45 | (x, y) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::cell::Cell; 3 | use std::rc::Rc; 4 | use std::sync::Arc; 5 | use std::time::Duration; 6 | 7 | use relm4::component::{AsyncComponent, AsyncComponentSender, AsyncComponentParts}; 8 | use relm4::once_cell::sync::OnceCell; 9 | 10 | use gtk::glib::ControlFlow; 11 | use gtk::prelude::{ApplicationExt, GtkWindowExt, BoxExt, OrientableExt, WidgetExt, WidgetExtManual}; 12 | use gtk::Orientation; 13 | 14 | use smallvec::SmallVec; 15 | use tokio_util::sync::CancellationToken; 16 | 17 | use crate::anchor::Anchor; 18 | use crate::style::{self, StyleSettings}; 19 | use crate::widgets::sliderbox::{SliderBox, SliderMessage, Sliders}; 20 | use crate::server::{self, AudioServer, AudioServerEnum, Kind, MessageClient, MessageOutput, VolumeLevels}; 21 | use crate::widgets::switchbox::{SwitchBox, Switches}; 22 | 23 | pub static WM_CONFIG: OnceCell = const { OnceCell::new() }; 24 | 25 | pub struct App { 26 | server: Arc, 27 | 28 | max_volume: f64, 29 | master: bool, 30 | sliders: Sliders, 31 | switches: Switches, 32 | close_after: u32, 33 | 34 | ready: Rc>, 35 | shutdown: Option, 36 | } 37 | 38 | pub struct Config { 39 | pub width: u32, 40 | pub height: u32, 41 | pub spacing: i32, 42 | pub max_volume: f64, 43 | pub show_icons: bool, 44 | pub horizontal: bool, 45 | pub master: bool, 46 | pub show_corked: bool, 47 | pub per_process: bool, 48 | pub userstyle: Option, 49 | 50 | #[cfg(feature = "Accent")] 51 | pub accent: bool, 52 | 53 | pub server: AudioServerEnum, 54 | } 55 | 56 | pub struct WMConfig { 57 | pub anchors: Anchor, 58 | pub margins: Vec, 59 | pub close_after: u32, 60 | } 61 | 62 | #[derive(Debug)] 63 | pub enum ElementMessage { 64 | SetMute { ids: SmallVec<[u32; 3]>, kind: server::Kind, flag: bool }, 65 | SetVolume { ids: SmallVec<[u32; 3]>, kind: server::Kind, levels: VolumeLevels }, 66 | SetOutput { name: Arc, port: Arc }, 67 | Remove { id: u32 }, 68 | InterruptClose, 69 | Close 70 | } 71 | 72 | #[derive(Debug, derive_more::From)] 73 | pub enum CommandMessage { 74 | #[from] 75 | Server(server::Message), 76 | SetStyle(Cow<'static, str>), 77 | Success, 78 | #[allow(dead_code)] Connect, 79 | Show, 80 | Quit, 81 | } 82 | 83 | #[relm4::component(pub, async)] 84 | impl AsyncComponent for App { 85 | type Init = Config; 86 | type Input = ElementMessage; 87 | type Output = (); 88 | type CommandOutput = CommandMessage; 89 | 90 | view! { 91 | gtk::Window { 92 | set_resizable: false, 93 | set_title: Some(crate::APP_NAME), 94 | set_decorated: false, 95 | 96 | #[name(wrapper)] 97 | gtk::Box { 98 | set_orientation: if config.horizontal { 99 | Orientation::Horizontal 100 | } else { 101 | Orientation::Vertical 102 | }, 103 | 104 | #[local_ref] 105 | switch_box -> SwitchBox { 106 | add_css_class: "side", 107 | set_homogeneous: true, 108 | set_orientation: if config.horizontal { 109 | Orientation::Vertical 110 | } else { 111 | Orientation::Horizontal 112 | } 113 | }, 114 | 115 | #[local_ref] 116 | slider_box -> SliderBox { 117 | add_css_class: "main", 118 | set_has_icons: config.show_icons, 119 | set_show_corked: config.show_corked, 120 | set_spacing: config.spacing, 121 | set_max_value: config.max_volume, 122 | set_orientation: if config.horizontal { 123 | Orientation::Horizontal 124 | } else { 125 | Orientation::Vertical 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | fn init_loading_widgets(window: Self::Root) -> Option { 133 | let config = WM_CONFIG.get().unwrap(); 134 | 135 | #[cfg(feature = "Wayland")] 136 | if crate::xdg::is_wayland() { 137 | window.connect_realize(move |w| Self::init_wayland(w, config.anchors, &config.margins, config.close_after != 0)); 138 | } 139 | 140 | #[cfg(feature = "X11")] 141 | if crate::xdg::is_x11() { 142 | window.connect_realize(move |w| Self::realize_x11(w, config.anchors, config.margins.clone())); 143 | } 144 | 145 | None 146 | } 147 | 148 | async fn init(config: Self::Init, window: Self::Root, sender: AsyncComponentSender) -> AsyncComponentParts { 149 | if std::env::var("GTK_DEBUG").is_err() { 150 | glib::log_set_writer_func(|_, _| glib::LogWriterOutput::Handled); 151 | } 152 | 153 | let wm_config = WM_CONFIG.get().unwrap(); 154 | let server = Arc::new(config.server); 155 | 156 | sender.oneshot_command(async move { 157 | #[allow(unused_mut)] 158 | let mut settings = StyleSettings::default(); 159 | 160 | #[cfg(feature = "Accent")] 161 | { settings.accent = config.accent; } 162 | 163 | let style = match config.userstyle { 164 | Some(p) => style::read(p).await, 165 | None => { 166 | let config_dir = crate::config_dir().await.unwrap(); 167 | style::find(config_dir, settings).await 168 | }, 169 | }; 170 | 171 | let style = match style { 172 | Ok(s) => s, 173 | Err(e) => { 174 | eprintln!("{}", e); 175 | style::default(settings).await 176 | } 177 | }; 178 | 179 | CommandMessage::SetStyle(style) 180 | }); 181 | 182 | App::connect(server.clone(), &sender); 183 | 184 | sender.oneshot_command(async move { 185 | use tokio::signal::*; 186 | 187 | let mut stream = unix::signal(unix::SignalKind::interrupt()).unwrap(); 188 | stream.recv().await; 189 | 190 | CommandMessage::Quit 191 | }); 192 | 193 | let mut sliders = Sliders::new(sender.input_sender()); 194 | sliders.set_direction(wm_config.anchors, if config.horizontal { Orientation::Horizontal } else { Orientation::Vertical }); 195 | sliders.per_process = config.per_process; 196 | 197 | let model = App { 198 | server, 199 | max_volume: config.max_volume, 200 | master: config.master, 201 | sliders, 202 | switches: Switches::new(sender.input_sender()), 203 | ready: Rc::new(Cell::new(false)), 204 | shutdown: None, 205 | close_after: wm_config.close_after, 206 | }; 207 | 208 | let switch_box = model.switches.container.widget(); 209 | let slider_box = model.sliders.container.widget(); 210 | 211 | let widgets = view_output!(); 212 | 213 | if !config.horizontal && wm_config.anchors.contains(Anchor::Bottom) { 214 | switch_box.insert_after(&widgets.wrapper, Some(slider_box)); 215 | } 216 | 217 | window.set_default_height(config.height as i32); 218 | window.set_default_width(config.width as i32); 219 | 220 | if wm_config.close_after != 0 { 221 | let has_pointer = Rc::new(Cell::new(false)); 222 | 223 | let controller = gtk::EventControllerMotion::new(); 224 | controller.connect_motion({ 225 | let has_pointer = has_pointer.clone(); 226 | move |_, _, _| has_pointer.set(true) 227 | }); 228 | window.add_controller(controller); 229 | 230 | let sender = sender.clone(); 231 | 232 | window.connect_is_active_notify(move |window| { 233 | if window.is_active() { 234 | sender.input(ElementMessage::InterruptClose); 235 | } 236 | else if has_pointer.replace(false) { 237 | sender.input(ElementMessage::Close); 238 | } 239 | }); 240 | } 241 | 242 | window.add_tick_callback({ 243 | let ready = model.ready.clone(); 244 | 245 | move |window, _| { 246 | if !ready.get() { 247 | window.set_visible(false); 248 | } 249 | 250 | ControlFlow::Break 251 | } 252 | }); 253 | 254 | sender.oneshot_command(async move { 255 | tokio::time::sleep(Duration::from_millis(10)).await; 256 | CommandMessage::Show 257 | }); 258 | 259 | AsyncComponentParts { model, widgets } 260 | } 261 | 262 | async fn update_cmd(&mut self, message: Self::CommandOutput, sender: AsyncComponentSender, window: &Self::Root) { 263 | match message { 264 | CommandMessage::Server(msg) => self.handle_msg_cmd_server(msg, sender, window), 265 | CommandMessage::SetStyle(style) => relm4::set_global_css(&style), 266 | CommandMessage::Show => window.set_visible(true), 267 | CommandMessage::Success => {}, 268 | CommandMessage::Connect => App::connect(self.server.clone(), &sender), 269 | CommandMessage::Quit => { 270 | self.server.disconnect(); 271 | relm4::main_application().quit(); 272 | }, 273 | } 274 | } 275 | 276 | async fn update(&mut self, message: Self::Input, sender: AsyncComponentSender, _: &Self::Root) { 277 | use ElementMessage::*; 278 | 279 | match message { 280 | SetVolume { ids, kind, levels } => { 281 | self.server.set_volume(ids, kind, levels).await; 282 | }, 283 | Remove { id } => { 284 | self.sliders.remove(id); 285 | } 286 | SetMute { ids, kind, flag } => { 287 | self.server.set_mute(ids, kind, flag).await; 288 | } 289 | SetOutput { name, port } => { 290 | self.server.set_output_by_name(&name, Some(&port)).await; 291 | } 292 | InterruptClose => { 293 | if let Some(shutdown) = self.shutdown.take() { 294 | shutdown.cancel(); 295 | } 296 | }, 297 | Close => { 298 | if let Some(shutdown) = self.shutdown.take() { 299 | shutdown.cancel(); 300 | } 301 | 302 | self.shutdown = Some(CancellationToken::new()); 303 | let token = self.shutdown.as_ref().unwrap().clone(); 304 | 305 | let duration = Duration::from_millis(self.close_after as u64); 306 | 307 | sender.oneshot_command(async move { 308 | tokio::select! { 309 | _ = token.cancelled() => CommandMessage::Success, 310 | _ = tokio::time::sleep(duration) => { 311 | CommandMessage::Quit 312 | } 313 | } 314 | }) 315 | } 316 | } 317 | } 318 | } 319 | 320 | impl App where App: AsyncComponent { 321 | fn connect(server: Arc, sender: &AsyncComponentSender) { 322 | sender.spawn_command(move |sender| match server.connect(&sender) { 323 | Ok(_) | Err(server::error::Error::AlreadyConnected) => {}, 324 | Err(e) => panic!("{e}"), 325 | }) 326 | } 327 | 328 | fn handle_msg_cmd_server(&mut self, message: server::Message, sender: AsyncComponentSender, window: &::Root) { 329 | use server::Message::*; 330 | 331 | match message { 332 | OutputClient(msg) => self.handle_msg_output_client(msg, sender, window), 333 | Output(msg) => self.handle_msg_output(msg), 334 | Ready => if !self.ready.replace(true) { 335 | window.set_visible(true); 336 | 337 | let mut plan = Kind::Software 338 | .union(Kind::Out); 339 | 340 | sender.oneshot_command({ 341 | let sender = sender.command_sender().clone(); 342 | let server = self.server.clone(); 343 | let master = self.master; 344 | 345 | async move { 346 | if master { 347 | plan |= Kind::Hardware; 348 | 349 | server.request_outputs(&sender).await.unwrap(); 350 | server.request_master(&sender).await.unwrap(); 351 | } 352 | 353 | server.request_software(&sender).await.unwrap(); 354 | server.subscribe(plan, &sender).await.unwrap(); 355 | 356 | CommandMessage::Success 357 | } 358 | }); 359 | } 360 | Error(e) => eprintln!("{e}"), 361 | Disconnected(Some(e)) => { 362 | eprintln!("{e}"); 363 | 364 | self.server.disconnect(); 365 | 366 | self.ready.replace(false); 367 | 368 | self.sliders.clear(); 369 | self.switches.clear(); 370 | } 371 | Disconnected(None) => sender.command_sender().emit(CommandMessage::Quit), 372 | } 373 | } 374 | 375 | #[allow(unused_variables)] 376 | fn handle_msg_output_client(&mut self, message: MessageClient, sender: AsyncComponentSender, window: &::Root) { 377 | match message { 378 | MessageClient::Peak(id, peak) => { 379 | self.sliders.send(id, SliderMessage::ServerPeak(peak)); 380 | }, 381 | MessageClient::Changed(client) => { 382 | self.sliders.send(client.id, SliderMessage::ServerChange(client)); 383 | }, 384 | MessageClient::New(client) => { 385 | let mut client = *client; 386 | client.max_volume = f64::min(client.max_volume, self.max_volume); 387 | 388 | self.sliders.push_client(client); 389 | 390 | #[cfg(feature = "X11")] 391 | if crate::xdg::is_x11() { 392 | window.size_allocate(&window.allocation(), -1); 393 | } 394 | }, 395 | MessageClient::Removed(id) => { 396 | if !self.sliders.contains(id) { return } 397 | 398 | self.sliders.send(id, SliderMessage::Removed); 399 | 400 | sender.command({ 401 | let sender = sender.input_sender().clone(); 402 | 403 | move |_, shutdown| { 404 | shutdown.register(async move { 405 | tokio::time::sleep(Duration::from_millis(300)).await; 406 | sender.emit(ElementMessage::Remove { id }) 407 | }) 408 | .drop_on_shutdown() 409 | } 410 | }); 411 | }, 412 | } 413 | } 414 | 415 | fn handle_msg_output(&mut self, msg: MessageOutput) { 416 | match msg { 417 | MessageOutput::New(output) => { 418 | self.switches.push(output); 419 | }, 420 | MessageOutput::Master(output) => { 421 | self.switches.set_active(output); 422 | } 423 | } 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, io, fmt::Debug}; 2 | 3 | use crate::label; 4 | 5 | use thiserror::Error; 6 | 7 | #[macro_export] 8 | macro_rules! warnln { 9 | ($($arg:tt)*) => {{ 10 | println!("{}: {}", $crate::label::WARNING, format_args!($($arg)*)) 11 | }}; 12 | } 13 | 14 | #[derive(Error)] 15 | pub enum Error { 16 | #[error(transparent)] 17 | Cli(#[from] CLIError), 18 | 19 | #[error(transparent)] 20 | Config(#[from] ConfigError), 21 | 22 | #[error(transparent)] 23 | Style(#[from] StyleError), 24 | 25 | #[error(transparent)] 26 | Cache(#[from] CacheError), 27 | 28 | #[cfg(feature = "Accent")] 29 | #[error(transparent)] 30 | Accent(#[from] ZbusError), 31 | } 32 | 33 | impl Debug for Error { 34 | fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { 35 | write!(f, "\x1b[7D{}: {}", label::ERROR, self) 36 | } 37 | } 38 | 39 | #[derive(Error, Debug)] 40 | pub enum CLIError { 41 | #[error("'{0}' is not a valid anchor point")] 42 | Anchor(String), 43 | } 44 | 45 | #[derive(Error, Debug)] 46 | pub enum ConfigError { 47 | #[error("Unable to access a config directory {path}\n{e}")] 48 | Read { e: io::Error, path: PathBuf }, 49 | 50 | #[error("Unable to create a config directory {path}\n{e}")] 51 | Create { e: io::Error, path: PathBuf }, 52 | 53 | #[error("Unable to access a config directory\n{0} is not a directory")] 54 | NotDirectory(PathBuf), 55 | } 56 | 57 | #[derive(Error, Debug)] 58 | pub enum StyleError { 59 | #[error("Unable to create a style file ({path})\n{e}")] 60 | Create { e: io::Error, path: PathBuf }, 61 | 62 | #[error("Unknown style file extension (expected {expected})")] 63 | Extension { expected: &'static str }, 64 | 65 | #[error("Unable to read a style file ({path})\n{e}")] 66 | Read { e: io::Error, path: PathBuf }, 67 | 68 | #[error("Error while trying to get metadata ({path})\n{e}")] 69 | Meta { e: io::Error, path: PathBuf }, 70 | 71 | #[error("Unable to read mtime of a style ({path})\n{e}")] 72 | MTime { e: io::Error, path: PathBuf}, 73 | 74 | #[error("Unable to write a style to a file ({path})\n{e}")] 75 | Write { e: io::Error, path: PathBuf }, 76 | 77 | #[error(transparent)] 78 | NotFound(io::Error), 79 | 80 | #[cfg(not(feature = "Sass"))] 81 | #[error("Couldn't compile a style using the system `sass` binary ({path})\n{e:?}")] 82 | SystemCompiler { e: Option, path: PathBuf }, 83 | 84 | #[cfg(feature = "Sass")] 85 | #[error(transparent)] 86 | Sass(#[from] Box), 87 | } 88 | 89 | #[derive(Error, Debug)] 90 | pub enum CacheError { 91 | #[error("Unable to create a cache file ({path})\n{e}")] 92 | Create { e: io::Error, path: PathBuf }, 93 | 94 | #[error("Unable to read a cache file ({path})\n{e}")] 95 | Read { e: io::Error, path: PathBuf }, 96 | 97 | #[error("Unable to write a cache file ({path})\n{e}")] 98 | Write { e: io::Error, path: PathBuf }, 99 | 100 | #[error("Unable to update mtime for cache ({path})\n{e}")] 101 | MTime { e: io::Error, path: PathBuf }, 102 | } 103 | 104 | #[cfg(feature = "Accent")] 105 | #[derive(Error, Debug)] 106 | pub enum ZbusError { 107 | #[error("Couldn't establish a connection with the session bus\n{e}")] 108 | Connect { e: zbus::Error }, 109 | 110 | #[error("Couldn't create a proxy to access the bus interface\n{e}")] 111 | Proxy { e: zbus::Error }, 112 | 113 | #[error("Unable to read `{key}` from `{namespace}, make sure that your `xdg-desktop-portal` supports it and configured correctly`\n{e}")] 114 | Read { e: zbus::Error, namespace: String, key: String }, 115 | 116 | #[error("Unable to parse unexpected result from the portal\n{v}")] 117 | BadResult { v: String } 118 | } 119 | -------------------------------------------------------------------------------- /src/label.rs: -------------------------------------------------------------------------------- 1 | use color_print::cstr; 2 | 3 | pub const ERROR: &str = cstr!("Error"); 4 | 5 | #[allow(dead_code)] 6 | pub const WARNING: &str = cstr!("Warning"); 7 | 8 | #[cfg(not(feature = "Wayland"))] 9 | pub const WAYLAND: &str = cstr!("Wayland"); 10 | 11 | #[cfg(not(feature = "X11"))] 12 | pub const X11: &str = cstr!("X11"); 13 | 14 | #[cfg(not(feature = "Sass"))] 15 | pub const SASS: &str = cstr!("Sass"); 16 | 17 | #[cfg(feature = "Wayland")] 18 | pub const LAYER_SHELL_PROTOCOL: &str = cstr!("zwlr_layer_shell_v1"); 19 | 20 | pub const PULSE: &str = cstr!("Pulse Audio"); 21 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use error::{Error, ConfigError}; 4 | use anchor::Anchor; 5 | 6 | static APP_NAME: &str = "Mixxc"; 7 | static APP_ID: &str = "elvy.mixxc"; 8 | static APP_BINARY: &str = "mixxc"; 9 | 10 | #[derive(argh::FromArgs)] 11 | ///Minimalistic volume mixer. 12 | struct Args { 13 | /// window width 14 | #[argh(option, short = 'w')] 15 | width: Option, 16 | 17 | /// window height 18 | #[argh(option, short = 'h')] 19 | height: Option, 20 | 21 | /// spacing between clients 22 | #[argh(option, short = 's')] 23 | spacing: Option, 24 | 25 | /// screen anchor point: (t)op, (b)ottom, (l)eft, (r)ight 26 | #[argh(option, short = 'a', long = "anchor")] 27 | anchors: Vec, 28 | 29 | /// show only active sinks 30 | #[argh(switch, short = 'A', long = "active")] 31 | active_only: bool, 32 | 33 | #[cfg(feature = "Accent")] 34 | /// inherit accent color from the system's settings 35 | #[argh(switch, short = 'C', long = "accent")] 36 | accent: bool, 37 | 38 | /// margin distance for each anchor point 39 | #[argh(option, short = 'm', long = "margin")] 40 | margins: Vec, 41 | 42 | /// enable master volume slider 43 | #[argh(switch, short = 'M', long = "master")] 44 | master: bool, 45 | 46 | /// volume slider orientation: (h)orizontal, (v)ertical 47 | #[argh(option, short = 'b')] 48 | bar: Option, 49 | 50 | /// path to the userstyle 51 | #[argh(option, short = 'u')] 52 | userstyle: Option, 53 | 54 | /// close the window after a specified amount of time (ms) when focus is lost (default: 0) 55 | #[argh(option, short = 'c', long = "close")] 56 | close_after: Option, 57 | 58 | /// enable client icons 59 | #[argh(switch, short = 'i', long = "icon")] 60 | icon: bool, 61 | 62 | /// max volume level in percent (default: 100; 1-255) 63 | #[argh(option, short = 'x', long = "max-volume")] 64 | max_volume: Option, 65 | 66 | /// use only one volume slider for each system process 67 | #[argh(switch, short = 'P', long = "per-process")] 68 | per_process: bool, 69 | 70 | /// print version 71 | #[argh(switch, short = 'v')] 72 | version: bool, 73 | } 74 | 75 | fn main() -> Result<(), Error> { 76 | let args: Args = argh::from_env(); 77 | 78 | if args.version { 79 | print!("{}", env!("CARGO_PKG_NAME")); 80 | 81 | match option_env!("GIT_COMMIT") { 82 | Some(s) => println!(" {s}"), 83 | None => println!(" {}", env!("CARGO_PKG_VERSION")), 84 | }; 85 | 86 | return Ok(()) 87 | } 88 | 89 | let mut anchors = Anchor::None; 90 | 91 | for a in args.anchors.iter().map(Anchor::try_from) { 92 | anchors |= a?; 93 | } 94 | 95 | warning(&args); 96 | 97 | let app = relm4::RelmApp::new(crate::APP_ID).with_args(vec![]); 98 | 99 | // Vertically oriented bars imply that we are stacking clients horizontally 100 | let horizontal = args.bar.unwrap_or_default().starts_with('v'); 101 | 102 | app::WM_CONFIG.get_or_init(|| app::WMConfig { 103 | anchors, 104 | close_after: args.close_after.unwrap_or(0), 105 | margins: args.margins, 106 | }); 107 | 108 | app.run_async::(app::Config { 109 | width: args.width.unwrap_or(if horizontal { 65 } else { 350 }), 110 | height: args.height.unwrap_or(if horizontal { 350 } else { 30 }), 111 | spacing: args.spacing.unwrap_or(20) as i32, 112 | max_volume: args.max_volume.unwrap_or(100).max(1) as f64 / 100.0, 113 | show_icons: args.icon, 114 | horizontal, 115 | master: args.master, 116 | show_corked: !args.active_only, 117 | per_process: args.per_process, 118 | userstyle: args.userstyle, 119 | 120 | #[cfg(feature = "Accent")] 121 | accent: args.accent, 122 | 123 | server: server::pulse::Pulse::new().into(), 124 | }); 125 | 126 | Ok(()) 127 | } 128 | 129 | #[allow(unused_variables)] 130 | fn warning(args: &Args) { 131 | #[cfg(not(feature = "Wayland"))] 132 | if xdg::is_wayland() { 133 | warnln!("You are trying to use {APP_NAME} on Wayland, but '{}' feature wasn't included at compile time!", label::WAYLAND); 134 | } 135 | 136 | #[cfg(not(feature = "X11"))] 137 | if xdg::is_x11() { 138 | warnln!("You are trying to use {APP_NAME} on X Window System, but '{}' feature wasn't included at compile time!", label::X11); 139 | } 140 | 141 | #[cfg(not(feature = "Sass"))] 142 | if let Some(p) = &args.userstyle { 143 | let extension = p.extension().and_then(std::ffi::OsStr::to_str); 144 | if let Some("sass"|"scss") = extension { 145 | warnln!("You have specified *.{} file as userstyle, but '{}' feature wasn't included at compile time!", extension.unwrap(), label::SASS) 146 | } 147 | } 148 | } 149 | 150 | pub async fn config_dir() -> Result { 151 | use tokio::fs; 152 | 153 | let mut dir = xdg::config_dir(); 154 | dir.push(crate::APP_BINARY); 155 | 156 | let metadata = fs::metadata(&dir).await; 157 | 158 | match metadata { 159 | Err(_) => { 160 | fs::create_dir(&dir) 161 | .await 162 | .map_err(|e| ConfigError::Create { e, path: std::mem::take(&mut dir) })?; 163 | }, 164 | Ok(metadata) => if !metadata.is_dir() { 165 | return Err(ConfigError::NotDirectory(dir)) 166 | } 167 | } 168 | 169 | Ok(dir) 170 | } 171 | 172 | mod xdg; 173 | mod server; 174 | mod app; 175 | mod anchor; 176 | mod label; 177 | mod proto; 178 | mod error; 179 | mod style; 180 | mod widgets; 181 | 182 | #[cfg(feature = "Accent")] 183 | mod accent; 184 | -------------------------------------------------------------------------------- /src/proto/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "X11")] 2 | pub mod x; 3 | 4 | #[cfg(feature = "Wayland")] 5 | pub mod wayland; 6 | -------------------------------------------------------------------------------- /src/proto/wayland.rs: -------------------------------------------------------------------------------- 1 | use relm4::component::AsyncComponent; 2 | 3 | use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; 4 | 5 | use crate::{anchor::Anchor, app::App, label, warnln}; 6 | 7 | impl App where Self: AsyncComponent { 8 | pub fn init_wayland(window: &::Root, anchors: Anchor, margins: &[i32], focusable: bool) { 9 | if !gtk4_layer_shell::is_supported() { 10 | warnln!("You're using Wayland, but your compositor doesn't support {} protocol.", label::LAYER_SHELL_PROTOCOL); 11 | return 12 | } 13 | 14 | window.init_layer_shell(); 15 | window.set_layer(Layer::Top); 16 | window.set_namespace("volume-mixer"); 17 | 18 | if focusable { 19 | window.set_keyboard_mode(KeyboardMode::OnDemand); 20 | } 21 | 22 | for (i, anchor) in anchors.iter().enumerate() { 23 | let edge = anchor.try_into().unwrap(); 24 | 25 | window.set_anchor(edge, true); 26 | window.set_margin(edge, *margins.get(i).unwrap_or(&0)); 27 | } 28 | } 29 | } 30 | 31 | #[cfg(feature = "Wayland")] 32 | impl TryFrom for Edge { 33 | type Error = (); 34 | 35 | fn try_from(anchor: Anchor) -> Result { 36 | match anchor { 37 | Anchor::Top => Ok(Edge::Top), 38 | Anchor::Left => Ok(Edge::Left), 39 | Anchor::Bottom => Ok(Edge::Bottom), 40 | Anchor::Right => Ok(Edge::Right), 41 | _ => Err(()) 42 | } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/proto/x.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use relm4::component::AsyncComponent; 4 | 5 | use gtk::prelude::{Cast, NativeExt, WidgetExt, SurfaceExt}; 6 | 7 | use gdk_x11::{X11Surface, X11Display}; 8 | 9 | use x11rb::connection::Connection; 10 | use x11rb::errors::ReplyError; 11 | use x11rb::protocol::xinerama::get_screen_size; 12 | use x11rb::protocol::xproto::{PropMode, AtomEnum, ClientMessageEvent, CLIENT_MESSAGE_EVENT, EventMask, ConnectionExt, ConfigureWindowAux}; 13 | use x11rb::x11_utils::Serialize; 14 | 15 | use crate::anchor::Anchor; 16 | use crate::app::App; 17 | 18 | impl App where Self: AsyncComponent { 19 | pub fn realize_x11(window: &::Root, anchors: Anchor, margins: Vec) { 20 | let surface = window.surface().unwrap(); 21 | 22 | let Ok(xsurface) = surface.downcast::() else { 23 | return 24 | }; 25 | 26 | let Ok(xdisplay) = window.display().downcast::() else { 27 | return 28 | }; 29 | 30 | let (conn, _) = x11rb::connect(None).expect("connecting to X11"); 31 | let atoms = AtomCollection::new(&conn).unwrap().reply().expect("baking atomic cookie"); 32 | 33 | let conn = Rc::new(conn); 34 | 35 | let xid = xsurface.xid() as u32; 36 | 37 | set_wm_properties(conn.as_ref(), atoms, xid).expect("setting WM properties"); 38 | 39 | let screen_num = xdisplay.screen().screen_number() as u32; 40 | let screen = get_screen_size(conn.as_ref(), xid, screen_num).unwrap().reply().expect("collecting screen info"); 41 | 42 | window.connect_map({ 43 | let conn = conn.clone(); 44 | 45 | move |_| { // Place window off-screen while initializing 46 | let config = ConfigureWindowAux::new().x(screen.width as i32).y(screen.height as i32); 47 | conn.configure_window(xid, &config).unwrap().check().expect("hiding window offscreen"); 48 | 49 | add_wm_states(conn.as_ref(), atoms, xid).expect("updating _NET_WM_STATE"); 50 | } 51 | }); 52 | 53 | xsurface.connect_layout({ 54 | move |_, width, height| { 55 | let (x, y) = anchors.position(&margins, 56 | (screen.width, screen.height), 57 | (width as u32, height as u32)); 58 | 59 | let config = ConfigureWindowAux::new().x(x).y(y); 60 | conn.configure_window(xid, &config).unwrap().check().expect("moving window with `xcb_configure_window`"); 61 | } 62 | }); 63 | } 64 | } 65 | 66 | // Specification: 67 | // https://specifications.freedesktop.org/wm-spec/1.5/ar01s04.html 68 | fn set_wm_properties(conn: &impl Connection, atoms: AtomCollection, xid: u32) -> Result<(), ReplyError> { 69 | use x11rb::wrapper::ConnectionExt; 70 | 71 | conn.change_property32(PropMode::REPLACE, 72 | xid, 73 | atoms._NET_WM_WINDOW_TYPE, 74 | AtomEnum::ATOM, 75 | &[atoms._NET_WM_WINDOW_TYPE_UTILITY])?.check()?; 76 | 77 | conn.change_property32(PropMode::REPLACE, 78 | xid, 79 | atoms._NET_WM_ALLOWED_ACTIONS, 80 | AtomEnum::ATOM, 81 | &[atoms._NET_WM_ACTION_CLOSE, atoms._NET_WM_ACTION_ABOVE])?.check()?; 82 | 83 | conn.change_property32(PropMode::REPLACE, 84 | xid, 85 | atoms._NET_WM_BYPASS_COMPOSITOR, 86 | AtomEnum::CARDINAL, 87 | &[2])?.check()?; 88 | 89 | Ok(()) 90 | } 91 | 92 | fn add_wm_states(conn: &impl Connection, atoms: AtomCollection, xid: u32) -> Result<(), ReplyError> { 93 | add_wm_state(conn, xid, atoms, atoms._NET_WM_STATE_ABOVE, atoms._NET_WM_STATE_STICKY)?; 94 | add_wm_state(conn, xid, atoms, atoms._NET_WM_STATE_SKIP_TASKBAR, atoms._NET_WM_STATE_SKIP_PAGER)?; 95 | 96 | Ok(()) 97 | } 98 | 99 | fn send_message(conn: &impl Connection, xid: u32, event: ClientMessageEvent) -> Result<(), ReplyError> { 100 | conn.send_event(false, xid, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::STRUCTURE_NOTIFY, event.serialize())?.check() 101 | } 102 | 103 | fn add_wm_state(conn: &impl Connection, xid: u32, atoms: AtomCollection, s1: u32, s2: u32) -> Result<(), ReplyError> { 104 | const _NET_WM_STATE_ADD: u32 = 1; 105 | const _NET_WM_STATE_APP: u32 = 1; 106 | 107 | let message = ClientMessageEvent { 108 | response_type: CLIENT_MESSAGE_EVENT, 109 | format: 32, 110 | sequence: 0, 111 | window: xid, 112 | type_: atoms._NET_WM_STATE, 113 | data: [_NET_WM_STATE_ADD, s1, s2, _NET_WM_STATE_APP, 0].into(), 114 | }; 115 | 116 | send_message(conn, xid, message) 117 | } 118 | 119 | x11rb::atom_manager! { 120 | pub AtomCollection: AtomCollectionCookie { 121 | _NET_WM_STATE, 122 | _NET_WM_STATE_ABOVE, 123 | _NET_WM_STATE_SKIP_PAGER, 124 | _NET_WM_STATE_SKIP_TASKBAR, 125 | _NET_WM_STATE_STICKY, 126 | 127 | _NET_WM_WINDOW_TYPE, 128 | _NET_WM_WINDOW_TYPE_UTILITY, 129 | 130 | _NET_WM_BYPASS_COMPOSITOR, 131 | 132 | _NET_WM_ALLOWED_ACTIONS, 133 | _NET_WM_ACTION_CLOSE, 134 | _NET_WM_ACTION_ABOVE, 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/server/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::label; 4 | 5 | use libpulse_binding::error::{Code, PAErr}; 6 | use thiserror::Error; 7 | 8 | #[derive(Error)] 9 | pub enum Error { 10 | #[error("Connection to the audio server is already established")] 11 | AlreadyConnected, 12 | 13 | #[error(transparent)] 14 | Pulse(#[from] PulseError), 15 | } 16 | 17 | impl Debug for Error { 18 | fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { 19 | write!(f, "\x1b[7D{}: {} :{}", label::ERROR, label::PULSE, self) 20 | } 21 | } 22 | 23 | #[derive(Error, Debug)] 24 | pub enum PulseError { 25 | #[error("Couldn't establish connection with the PulseAudio server\n{0}")] 26 | Connection(Code), 27 | 28 | #[error("No connection to the pulse server")] 29 | NotConnected, 30 | 31 | #[error("Connection to the pulse audio server was terminated")] 32 | Disconnected, 33 | 34 | #[error("Quit the mainloop")] 35 | MainloopQuit, 36 | 37 | #[error("Audio sink without a name will be ignored. ID: {0}")] 38 | NamelessSink(u32), 39 | 40 | #[error("Audio sink has a port without a name and will be ignored. ID: {0}")] 41 | NamelessPort(u32), 42 | 43 | #[error("{0}")] 44 | Other(Code), 45 | } 46 | 47 | impl From for PulseError { 48 | fn from(e: PAErr) -> Self { 49 | use num_traits::FromPrimitive; 50 | use libpulse_binding::error::Code::*; 51 | 52 | if e.0 == -2 { 53 | return PulseError::MainloopQuit 54 | } 55 | 56 | let code = Code::from_i32(e.0).unwrap_or(Code::Unknown); 57 | match code { 58 | ConnectionTerminated => PulseError::Disconnected, 59 | ConnectionRefused | InvalidServer => { 60 | PulseError::Connection(code) 61 | }, 62 | _ => PulseError::Other(code), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "PipeWire")] 2 | pub mod pipewire; 3 | pub mod pulse; 4 | pub mod error; 5 | 6 | use derive_more::derive::{Debug, Deref, DerefMut}; 7 | 8 | use enum_dispatch::enum_dispatch; 9 | 10 | use error::Error; 11 | 12 | #[cfg(feature = "PipeWire")] 13 | use self::pipewire::Pipewire; 14 | use self::pulse::Pulse; 15 | 16 | pub struct InnerSender { 17 | sender: relm4::Sender, 18 | _message: std::marker::PhantomData, 19 | } 20 | 21 | impl InnerSender { 22 | #[inline] 23 | pub fn emit(&self, message: impl Into) { 24 | self.sender.emit(message.into()) 25 | } 26 | 27 | pub fn clone(&self) -> Self { 28 | InnerSender { 29 | sender: self.sender.clone(), 30 | _message: self._message 31 | } 32 | } 33 | } 34 | 35 | impl From<&relm4::Sender> for InnerSender { 36 | fn from(sender: &relm4::Sender) -> Self { 37 | Self { 38 | sender: sender.clone(), 39 | _message: std::marker::PhantomData, 40 | } 41 | } 42 | } 43 | 44 | pub type Sender = InnerSender; 45 | 46 | #[derive(Debug, Clone, Deref, DerefMut)] 47 | pub struct VolumeLevels(smallvec::SmallVec<[u32; 2]>); 48 | 49 | #[derive(Debug, Clone)] 50 | pub struct Volume { 51 | pub levels: VolumeLevels, 52 | 53 | #[debug(skip)] 54 | percent: &'static (dyn Fn(&Self) -> f64 + Sync), 55 | 56 | #[debug(skip)] 57 | set_percent: &'static (dyn Fn(&mut Self, f64) + Sync), 58 | } 59 | 60 | impl PartialEq for Volume { 61 | fn eq(&self, other: &Self) -> bool { 62 | self.levels[0] == other.levels[0] 63 | } 64 | } 65 | 66 | impl Volume { 67 | pub fn percent(&self) -> f64 { 68 | (self.percent)(self) 69 | } 70 | 71 | pub fn set_percent(&mut self, p: f64) { 72 | (self.set_percent)(self, p) 73 | } 74 | } 75 | 76 | #[derive(Debug, Clone)] 77 | pub struct OutputClient { 78 | pub id: u32, 79 | pub process: Option, 80 | pub name: String, 81 | pub description: String, 82 | pub icon: Option, 83 | pub volume: Volume, 84 | pub max_volume: f64, 85 | pub muted: bool, 86 | pub corked: bool, 87 | pub kind: Kind, 88 | } 89 | 90 | #[derive(Debug, Clone)] 91 | pub struct Output { 92 | pub name: String, 93 | pub port: String, 94 | pub master: bool, 95 | } 96 | 97 | #[derive(Debug)] 98 | pub enum Message { 99 | Output(MessageOutput), 100 | OutputClient(MessageClient), 101 | Disconnected(Option), 102 | Error(Error), 103 | Ready, 104 | } 105 | 106 | #[derive(Debug)] 107 | pub enum MessageClient { 108 | New(Box), 109 | Changed(Box), 110 | Removed(u32), 111 | Peak(u32, f32), 112 | } 113 | 114 | impl From for Message { 115 | fn from(msg: MessageClient) -> Self { 116 | Message::OutputClient(msg) 117 | } 118 | } 119 | 120 | #[derive(Debug)] 121 | pub enum MessageOutput { 122 | New(Output), 123 | Master(Output), 124 | } 125 | 126 | impl From for Message { 127 | fn from(msg: MessageOutput) -> Self { 128 | Message::Output(msg) 129 | } 130 | } 131 | 132 | #[enum_dispatch] 133 | pub enum AudioServerEnum { 134 | Pulse, 135 | #[cfg(feature = "PipeWire")] 136 | Pipewire, 137 | } 138 | 139 | bitflags::bitflags! { 140 | #[derive(Debug, Clone, Copy)] 141 | pub struct Kind: u8 { 142 | const Software = 0b0001; 143 | const Hardware = 0b0010; 144 | const Out = 0b0100; 145 | const In = 0b1000; 146 | } 147 | } 148 | 149 | #[enum_dispatch(AudioServerEnum)] 150 | pub trait AudioServer { 151 | fn connect(&self, sender: impl Into>) -> Result<(), Error>; 152 | fn disconnect(&self); 153 | async fn request_software(&self, sender: impl Into>) -> Result<(), Error>; 154 | async fn request_master(&self, sender: impl Into>) -> Result<(), Error>; 155 | async fn request_outputs(&self, sender: impl Into>) -> Result<(), Error>; 156 | async fn subscribe(&self, plan: Kind, sender: impl Into>) -> Result<(), Error>; 157 | async fn set_volume(&self, ids: impl IntoIterator, kind: Kind, levels: VolumeLevels); 158 | async fn set_mute(&self, ids: impl IntoIterator, kind: Kind, flag: bool); 159 | async fn set_output_by_name(&self, name: &str, port: Option<&str>); 160 | } 161 | -------------------------------------------------------------------------------- /src/server/pipewire.rs: -------------------------------------------------------------------------------- 1 | use relm4::Sender; 2 | 3 | use super::{AudioServer, Message, Volume}; 4 | 5 | #[derive(Clone, Copy)] 6 | pub struct Pipewire; 7 | 8 | impl AudioServer for Pipewire { 9 | fn connect(&self, sender: Sender) { 10 | unimplemented!() 11 | } 12 | 13 | fn disconnect(&self) { 14 | unimplemented!() 15 | } 16 | 17 | fn set_volume(&self, id: u32, volume: Volume) { 18 | unimplemented!() 19 | } 20 | 21 | fn set_mute(&self, id: u32, flag: bool) { 22 | unimplemented!() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/server/pulse.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::cell::RefCell; 3 | use std::pin::Pin; 4 | use std::sync::{Arc, atomic::{AtomicU8, Ordering}, OnceLock, Weak}; 5 | use std::thread::Thread; 6 | 7 | use libpulse_binding::callbacks::ListResult; 8 | use libpulse_binding::context::{self, introspect::{Introspector, SinkInfo, SinkInputInfo}, subscribe::{Facility, InterestMaskSet, Operation}, Context, State}; 9 | use libpulse_binding::def::{BufferAttr, PortAvailable, Retval}; 10 | use libpulse_binding::mainloop::standard::Mainloop; 11 | use libpulse_binding::proplist::{properties::APPLICATION_NAME, Proplist}; 12 | use libpulse_binding::sample::{Format, Spec}; 13 | use libpulse_binding::stream::{Stream, self, PeekResult}; 14 | use libpulse_binding::volume::ChannelVolumes; 15 | 16 | use derive_more::derive::{Deref, From}; 17 | use parking_lot::{Mutex, MutexGuard}; 18 | use smallvec::SmallVec; 19 | 20 | use tokio::sync::watch; 21 | 22 | use super::error::{Error, PulseError}; 23 | use super::{AudioServer, Kind, Message, MessageClient, MessageOutput, Output, OutputClient, Sender, Volume, VolumeLevels}; 24 | 25 | const DEFAULT_PEAK_RATE: u32 = 30; 26 | 27 | type Pb = Pin>; 28 | type Peakers = Vec>; 29 | 30 | type WeakContext = Weak>>; 31 | type WeakPeakers = Weak>>; 32 | 33 | pub struct Pulse { 34 | context: Arc>>, 35 | peakers: Arc>>, 36 | state: Arc, 37 | lock: watch::Sender, 38 | thread: Mutex, 39 | running: Mutex<()>, 40 | } 41 | 42 | #[repr(u8)] 43 | #[derive(PartialEq)] 44 | enum Lock { 45 | Unlocked = 0, 46 | Locked = 1, 47 | Aquire = 2, 48 | } 49 | 50 | impl Pulse { 51 | thread_local! { 52 | static MAINLOOP: RefCell = RefCell::new(Mainloop::new().unwrap()); 53 | } 54 | 55 | pub fn new() -> Self { 56 | let context = Pulse::MAINLOOP.with_borrow(|mainloop| { 57 | Context::new(mainloop, "Mixxc Context").unwrap() 58 | }); 59 | 60 | Self { 61 | context: Arc::new(Mutex::new(RefCell::new(context))), 62 | peakers: Arc::new(Mutex::new(RefCell::new(Vec::with_capacity(8)))), 63 | state: Arc::new(AtomicU8::new(0)), 64 | lock: watch::channel(Lock::Unlocked).0, 65 | thread: Mutex::new(std::thread::current()), 66 | running: Mutex::new(()), 67 | } 68 | } 69 | 70 | #[inline] 71 | fn set_state(&self, state: context::State) { 72 | self.state.store(state as u8, Ordering::Release); 73 | } 74 | 75 | #[inline] 76 | fn is_connected(&self) -> bool { 77 | self.state.load(Ordering::Acquire) == State::Ready as u8 78 | } 79 | 80 | #[inline] 81 | fn is_terminated(&self) -> bool { 82 | self.state.load(Ordering::Acquire) == State::Terminated as u8 83 | } 84 | 85 | async fn lock(&self) -> ContextRef { 86 | self.lock.send_replace(Lock::Aquire); 87 | self.lock.subscribe().wait_for(|lock| *lock == Lock::Locked).await.unwrap(); 88 | 89 | let context = self.context.try_lock().unwrap(); 90 | let thread = self.thread.try_lock().unwrap(); 91 | 92 | ContextRef { 93 | context, 94 | lock: &self.lock, 95 | thread, 96 | } 97 | } 98 | 99 | fn lock_blocking(&self) -> ContextRef { 100 | self.lock.send_replace(Lock::Aquire); 101 | while *self.lock.borrow() != Lock::Locked { std::hint::spin_loop(); } 102 | 103 | let context = self.context.try_lock().unwrap(); 104 | let thread = self.thread.try_lock().unwrap(); 105 | 106 | ContextRef { 107 | context, 108 | lock: &self.lock, 109 | thread, 110 | } 111 | } 112 | 113 | fn iterate(timeout: &Duration) -> Result { 114 | Self::MAINLOOP.with_borrow_mut(|mainloop| { 115 | mainloop.prepare(timeout.into()).map_err(PulseError::from)?; 116 | mainloop.poll().map_err(PulseError::from)?; 117 | mainloop.dispatch().map_err(PulseError::from) 118 | }) 119 | } 120 | 121 | fn is_locked(&self) -> bool { 122 | self.lock.send_if_modified(|lock| { 123 | match *lock == Lock::Aquire { 124 | true => { 125 | *lock = Lock::Locked; 126 | true 127 | } 128 | false => false, 129 | } 130 | }) 131 | } 132 | 133 | fn quit() { 134 | Self::MAINLOOP.with_borrow_mut(|mainloop| mainloop.quit(Retval(0))); 135 | } 136 | } 137 | 138 | impl AudioServer for Pulse { 139 | fn connect(&self, sender: impl Into>) -> Result<(), Error> { 140 | if self.is_connected() { 141 | return Err(Error::AlreadyConnected) 142 | } 143 | 144 | let mut proplist = Proplist::new().unwrap(); 145 | proplist.set_str(APPLICATION_NAME, crate::APP_NAME).unwrap(); 146 | 147 | self.context.lock().replace(Pulse::MAINLOOP.with_borrow(|mainloop| { 148 | Context::new_with_proplist(mainloop, "Mixxc Context", &proplist).unwrap() 149 | })); 150 | 151 | let sender: Sender = sender.into(); 152 | 153 | let state_callback = Box::new({ 154 | let context = Arc::downgrade(&self.context); 155 | let state = Arc::downgrade(&self.state); 156 | let sender = sender.clone(); 157 | 158 | move || state_callback(&context, &state, &sender) 159 | }); 160 | 161 | { 162 | let guard = self.context.lock(); 163 | let mut context = guard.borrow_mut(); 164 | 165 | // Manually calls state_callback and sets state to Connecting on success 166 | context.connect(None, context::FlagSet::NOAUTOSPAWN, None) 167 | .map_err(PulseError::from)?; 168 | 169 | self.set_state(State::Connecting); 170 | 171 | context.set_state_callback(Some(state_callback)); 172 | } 173 | 174 | *self.thread.lock() = std::thread::current(); 175 | 176 | let timeout = std::time::Duration::from_millis(1).into(); 177 | let _running = self.running.lock(); 178 | 179 | loop { 180 | match Pulse::iterate(&timeout) { 181 | Ok(_) => {}, 182 | Err(PulseError::MainloopQuit) => break, 183 | Err(e) => sender.emit(Message::Error(e.into())), 184 | }; 185 | 186 | if self.is_locked() { 187 | std::thread::park(); 188 | 189 | if self.is_terminated() { 190 | self.peakers.lock().borrow_mut().clear(); 191 | Pulse::quit() 192 | } 193 | } 194 | } 195 | 196 | Ok(()) 197 | } 198 | 199 | fn disconnect(&self) { 200 | { 201 | let guard = self.lock_blocking(); 202 | let mut context = guard.borrow_mut(); 203 | 204 | context.set_state_callback(None); 205 | context.disconnect(); 206 | 207 | // Context::disconnect manually calls state_callback and sets state to Terminated 208 | self.set_state(State::Terminated); 209 | } 210 | 211 | let _running = self.running.lock(); 212 | } 213 | 214 | async fn request_software(&self, sender: impl Into>) -> Result<(), Error> { 215 | if !self.is_connected() { 216 | return Err(PulseError::NotConnected.into()) 217 | } 218 | 219 | let sender = sender.into(); 220 | 221 | let input_callback = { 222 | let context = Arc::downgrade(&self.context); 223 | let peakers = Arc::downgrade(&self.peakers); 224 | 225 | move |info: ListResult<&SinkInputInfo>| { 226 | add_sink_input(info, &context, &sender, &peakers); 227 | } 228 | }; 229 | 230 | let context = self.lock().await; 231 | context.introspect().get_sink_input_info_list(input_callback); 232 | 233 | Ok(()) 234 | } 235 | 236 | async fn request_outputs(&self, sender: impl Into>) -> Result<(), Error> { 237 | if !self.is_connected() { 238 | return Err(PulseError::NotConnected.into()) 239 | } 240 | 241 | let sender = sender.into(); 242 | 243 | let sink_info_callback = move |info: ListResult<&SinkInfo>| { 244 | let ListResult::Item(info) = info else { 245 | return 246 | }; 247 | 248 | let Some(output_name) = &info.name else { 249 | let e = PulseError::NamelessSink(info.index).into(); 250 | sender.emit(Message::Error(e)); 251 | 252 | return; 253 | }; 254 | 255 | let ports = info.ports.iter() 256 | .filter(|p| p.available != PortAvailable::No); 257 | 258 | for port in ports { 259 | let Some(port_name) = &port.name else { 260 | let e = PulseError::NamelessPort(info.index).into(); 261 | sender.emit(Message::Error(e)); 262 | 263 | continue; 264 | }; 265 | 266 | let output = Output { 267 | name: output_name.to_string(), 268 | port: port_name.to_string(), 269 | master: false, 270 | }; 271 | 272 | let msg: Message = MessageOutput::New(output).into(); 273 | sender.emit(msg); 274 | } 275 | }; 276 | 277 | let guard = self.lock().await; 278 | let introspect = guard.introspect(); 279 | 280 | introspect.get_sink_info_list(sink_info_callback); 281 | 282 | Ok(()) 283 | } 284 | 285 | async fn request_master(&self, sender: impl Into>) -> Result<(), Error> { 286 | if !self.is_connected() { 287 | return Err(PulseError::NotConnected.into()) 288 | } 289 | 290 | let sender = sender.into(); 291 | 292 | let sink_callback = move |info: ListResult<&SinkInfo>| { 293 | if let ListResult::Item(info) = info { 294 | let client: Box = Box::new(info.into()); 295 | let msg: Message = MessageClient::New(client).into(); 296 | 297 | sender.emit(msg); 298 | 299 | let Some(output_name) = info.name.as_ref() else { 300 | let e = PulseError::NamelessSink(info.index).into(); 301 | sender.emit(Message::Error(e)); 302 | 303 | return 304 | }; 305 | 306 | let Some(port_name) = info.active_port.as_ref().and_then(|p| p.name.as_ref()) else { 307 | return 308 | }; 309 | 310 | let output = Output { 311 | name: output_name.to_string(), 312 | port: port_name.to_string(), 313 | master: true, 314 | }; 315 | 316 | let msg: Message = MessageOutput::Master(output).into(); 317 | sender.emit(msg) 318 | } 319 | }; 320 | 321 | let context = self.lock().await; 322 | context.introspect().get_sink_info_by_index(0, sink_callback); 323 | 324 | Ok(()) 325 | } 326 | 327 | async fn subscribe(&self, plan: Kind, sender: impl Into>) -> Result<(), Error> { 328 | if !self.is_connected() { 329 | return Err(PulseError::NotConnected.into()) 330 | } 331 | 332 | let sender = sender.into(); 333 | 334 | let subscribe_callback = Box::new({ 335 | let context = Arc::downgrade(&self.context); 336 | let peakers = Arc::downgrade(&self.peakers); 337 | 338 | move |facility, op, i| { 339 | subscribe_callback(&sender, &context, &peakers, facility, op, i) 340 | } 341 | }); 342 | 343 | let mut mask = InterestMaskSet::NULL; 344 | 345 | if plan.contains(Kind::Software) { 346 | mask |= InterestMaskSet::SINK_INPUT; 347 | } 348 | 349 | if plan.contains(Kind::Hardware) { 350 | mask |= InterestMaskSet::SINK; 351 | mask |= InterestMaskSet::SERVER; 352 | } 353 | 354 | let guard = self.lock().await; 355 | let mut context = guard.borrow_mut(); 356 | 357 | context.set_subscribe_callback(Some(subscribe_callback)); 358 | context.subscribe(mask, |_| ()); 359 | 360 | Ok(()) 361 | } 362 | 363 | async fn set_volume(&self, ids: impl IntoIterator, kind: Kind, levels: VolumeLevels) { 364 | if !self.is_connected() { 365 | return 366 | } 367 | 368 | let context = self.lock().await; 369 | let mut introspect = context.introspect(); 370 | 371 | let volume: ChannelVolumes = levels.into(); 372 | 373 | for id in ids.into_iter() { 374 | match kind { 375 | k if k.contains(Kind::Out | Kind::Software) => { 376 | introspect.set_sink_input_volume(id, &volume, None); 377 | }, 378 | k if k.contains(Kind::Out | Kind::Hardware) => { 379 | introspect.set_sink_volume_by_index(id, &volume, None); 380 | }, 381 | _ => {} 382 | }; 383 | } 384 | } 385 | 386 | async fn set_mute(&self, ids: impl IntoIterator, kind: Kind, flag: bool) { 387 | if !self.is_connected() { 388 | return 389 | } 390 | 391 | let context = self.lock().await; 392 | let mut introspect = context.introspect(); 393 | 394 | for id in ids.into_iter() { 395 | match kind { 396 | k if k.contains(Kind::Out | Kind::Software) => { 397 | introspect.set_sink_input_mute(id, flag, None); 398 | }, 399 | k if k.contains(Kind::Out | Kind::Hardware) => { 400 | introspect.set_sink_mute_by_index(id, flag, None); 401 | }, 402 | _ => {} 403 | }; 404 | } 405 | } 406 | 407 | async fn set_output_by_name(&self, name: &str, port: Option<&str>) { 408 | if !self.is_connected() || name.is_empty() { 409 | return 410 | } 411 | 412 | let context = self.lock().await; 413 | 414 | if let Some(port) = port { 415 | let mut introspect = context.introspect(); 416 | 417 | introspect.set_sink_port_by_name(name, port, None); 418 | } 419 | 420 | context.borrow_mut().set_default_sink(name, |_| {}); 421 | } 422 | } 423 | 424 | fn add_sink_input(info: ListResult<&SinkInputInfo>, context: &WeakContext, sender: &Sender, peakers: &WeakPeakers) 425 | { 426 | let Some(context) = context.upgrade() else { return }; 427 | let Some(peakers) = peakers.upgrade() else { return }; 428 | 429 | if let ListResult::Item(info) = info { 430 | if !info.has_volume { return } 431 | 432 | let client: Box = Box::new(info.into()); 433 | let id = client.id; 434 | 435 | let msg: Message = MessageClient::New(client).into(); 436 | sender.emit(msg); 437 | 438 | let guard = context.lock(); 439 | let mut context = guard.borrow_mut(); 440 | 441 | if let State::Ready = context.get_state() { 442 | if let Some(p) = create_peeker(&mut context, sender, id) { 443 | let guard = peakers.lock(); 444 | let mut peakers = guard.borrow_mut(); 445 | 446 | peakers.push(p) 447 | } 448 | } 449 | } 450 | } 451 | 452 | fn peak_callback(stream: &mut Stream, sender: &Sender, i: u32) { 453 | match stream.peek() { 454 | Ok(PeekResult::Data(b)) => { 455 | let bytes: [u8; 4] = unsafe { b.try_into().unwrap_unchecked() }; 456 | let peak: f32 = f32::from_ne_bytes(bytes); 457 | let msg: Message = MessageClient::Peak(i, peak).into(); 458 | 459 | if peak != 0.0 { sender.emit(msg); } 460 | } 461 | Ok(PeekResult::Hole(_)) => {}, 462 | _ => return, 463 | } 464 | 465 | let _ = stream.discard(); 466 | } 467 | 468 | fn create_peeker(context: &mut Context, sender: &Sender, i: u32) -> Option> { 469 | use stream::FlagSet; 470 | 471 | const PEAK_BUF_ATTR: &BufferAttr = &BufferAttr { 472 | maxlength: std::mem::size_of::() as u32, 473 | fragsize: std::mem::size_of::() as u32, 474 | 475 | prebuf: 0, minreq: 0, tlength: 0, 476 | }; 477 | 478 | static PEAK_SPEC: OnceLock = OnceLock::new(); 479 | 480 | let spec = PEAK_SPEC.get_or_init(|| { 481 | Spec { 482 | channels: 1, 483 | format: Format::FLOAT32NE, 484 | rate: std::env::var("PULSE_PEAK_RATE").ok() 485 | .and_then(|s| s.parse::().ok()) 486 | .unwrap_or(DEFAULT_PEAK_RATE) 487 | } 488 | }); 489 | 490 | const FLAGS: FlagSet = FlagSet::PEAK_DETECT 491 | .union(FlagSet::DONT_INHIBIT_AUTO_SUSPEND) 492 | .union(FlagSet::PASSTHROUGH) 493 | .union(FlagSet::START_UNMUTED); 494 | 495 | let mut stream = Stream::new(context, "Mixxc Peaker", spec, None)?; 496 | stream.set_monitor_stream(i).ok()?; 497 | stream.connect_record(None, Some(PEAK_BUF_ATTR), FLAGS).ok()?; 498 | 499 | let mut stream = Box::pin(stream); 500 | 501 | let peak_callback = Box::new({ 502 | let sender = sender.clone(); 503 | let stream: &mut Stream = unsafe { &mut *(stream.as_mut().get_mut() as *mut Stream) }; 504 | 505 | move |_| peak_callback(stream, &sender, i) 506 | }); 507 | 508 | stream.set_read_callback(Some(peak_callback)); 509 | 510 | Some(stream) 511 | } 512 | 513 | fn handle_server_change(sender: &Sender, context: &WeakContext) { 514 | let Some(introspect) = try_introspect(context) else { return }; 515 | 516 | let context = context.clone(); 517 | let sender = sender.clone(); 518 | 519 | introspect.get_server_info(move |info| { 520 | let Some(introspect) = try_introspect(&context) else { return }; 521 | let Some(name) = &info.default_sink_name else { return }; 522 | 523 | let sender = sender.clone(); 524 | 525 | let output_name = name.to_string(); 526 | 527 | introspect.get_sink_info_by_name(name, move |info| { 528 | let ListResult::Item(info) = info else { 529 | return 530 | }; 531 | 532 | let Some(port_name) = info.active_port.as_ref().and_then(|p| p.name.as_ref()) else { 533 | return 534 | }; 535 | 536 | let output = Output { 537 | name: output_name.to_string(), 538 | port: port_name.to_string(), 539 | master: true, 540 | }; 541 | 542 | let msg: Message = MessageOutput::Master(output).into(); 543 | sender.emit(msg); 544 | 545 | let client = Box::new(info.into()); 546 | let msg: Message = MessageClient::Changed(client).into(); 547 | sender.emit(msg); 548 | }); 549 | }); 550 | } 551 | 552 | fn handle_sink_change(sender: &Sender, context: &WeakContext) { 553 | let Some(introspect) = try_introspect(context) else { return }; 554 | 555 | introspect.get_sink_info_by_index(0, { 556 | let sender = sender.clone(); 557 | 558 | move |info| if let ListResult::Item(info) = info { 559 | let client = Box::new(info.into()); 560 | let msg: Message = MessageClient::Changed(client).into(); 561 | 562 | sender.emit(msg); 563 | } 564 | }); 565 | } 566 | 567 | fn handle_sink_input_change(sender: &Sender, context: &WeakContext, peakers: &WeakPeakers, op: Operation, i: u32) { 568 | let Some(introspect) = try_introspect(context) else { return }; 569 | 570 | match op { 571 | Operation::New => { 572 | introspect.get_sink_input_info(i, { 573 | let sender = sender.clone(); 574 | let context = context.clone(); 575 | let peakers = peakers.clone(); 576 | 577 | move |info| add_sink_input(info, &context, &sender, &peakers) 578 | }); 579 | }, 580 | Operation::Removed => { 581 | if let Some(peakers) = peakers.upgrade() { 582 | let guard = peakers.lock(); 583 | let mut peakers = guard.borrow_mut(); 584 | 585 | if let Some(pos) = peakers.iter().position(|stream| stream.get_index() == Some(i)) { 586 | peakers.remove(pos); 587 | } 588 | } 589 | 590 | let msg: Message = MessageClient::Removed(i).into(); 591 | sender.emit(msg); 592 | }, 593 | Operation::Changed => { 594 | introspect.get_sink_input_info(i, { 595 | let sender = sender.clone(); 596 | 597 | move |info| { 598 | if let ListResult::Item(info) = info { 599 | let client = Box::new(info.into()); 600 | let msg: Message = MessageClient::Changed(client).into(); 601 | 602 | sender.emit(msg); 603 | }; 604 | } 605 | }); 606 | }, 607 | } 608 | } 609 | 610 | fn subscribe_callback(sender: &Sender, context: &WeakContext, peakers: &WeakPeakers, facility: Option, op: Option, i: u32) { 611 | let Some(op) = op else { return }; 612 | 613 | match facility { 614 | Some(Facility::SinkInput) => { 615 | handle_sink_input_change(sender, context, peakers, op, i); 616 | }, 617 | Some(Facility::Sink) => { 618 | handle_sink_change(sender, context); 619 | } 620 | Some(Facility::Server) => { 621 | handle_server_change(sender, context); 622 | }, 623 | _ => {}, 624 | } 625 | 626 | } 627 | 628 | fn state_callback(context: &WeakContext, state: &Weak, sender: &Sender) { 629 | let Some(context) = context.upgrade() else { return }; 630 | 631 | let guard = context.lock(); 632 | let new_state = guard.borrow().get_state(); 633 | 634 | if let Some(state) = state.upgrade() { 635 | state.store(new_state as u8, Ordering::Release); 636 | } 637 | 638 | match new_state { 639 | State::Ready => sender.emit(Message::Ready), 640 | State::Failed => { 641 | let e = PulseError::from(guard.borrow().errno()); 642 | sender.emit(Message::Disconnected(Some(e.into()))); 643 | }, 644 | State::Terminated => sender.emit(Message::Disconnected(None)), 645 | _ => {}, 646 | } 647 | } 648 | 649 | fn try_introspect(context: &WeakContext) -> Option { 650 | let context = context.upgrade()?; 651 | 652 | let guard = context.lock(); 653 | let context = guard.borrow_mut(); 654 | 655 | Some(context.introspect()) 656 | } 657 | 658 | #[derive(Deref)] 659 | struct ContextRef<'a> { 660 | #[deref] 661 | context: MutexGuard<'a, RefCell>, 662 | lock: &'a watch::Sender, 663 | thread: MutexGuard<'a, Thread>, 664 | } 665 | 666 | impl<'a> ContextRef<'a> { 667 | fn introspect(&self) -> Introspector { 668 | let context = self.borrow(); 669 | context.introspect() 670 | } 671 | } 672 | 673 | impl Drop for ContextRef<'_> { 674 | #[inline] 675 | fn drop(&mut self) { 676 | self.lock.send_replace(Lock::Unlocked); 677 | self.thread.unpark(); 678 | } 679 | } 680 | 681 | #[derive(From)] 682 | struct Duration(std::time::Duration); 683 | 684 | impl From<&Duration> for Option { 685 | #[inline] 686 | fn from(d: &Duration) -> Self { 687 | match d.0.is_zero() { 688 | false => Some(libpulse_binding::time::MicroSeconds(d.0.as_micros() as u64)), 689 | true => None, 690 | } 691 | } 692 | } 693 | 694 | impl From for ChannelVolumes { 695 | fn from(levels: VolumeLevels) -> Self { 696 | let mut cv = ChannelVolumes::default(); 697 | cv.set_len(levels.len() as u8); 698 | cv.get_mut().copy_from_slice(unsafe { 699 | std::mem::transmute::<&[u32], &[libpulse_binding::volume::Volume]>(&levels) 700 | }); 701 | cv 702 | } 703 | } 704 | 705 | impl Volume { 706 | fn pulse_linear(&self) -> f64 { 707 | use libpulse_binding::volume::{Volume, VolumeLinear}; 708 | 709 | let max = *self.levels.iter().max().unwrap_or(&0); 710 | VolumeLinear::from(Volume(max)).0 711 | } 712 | 713 | fn set_pulse_linear(&mut self, v: f64) { 714 | use libpulse_binding::volume::{Volume, VolumeLinear}; 715 | 716 | let v = Volume::from(VolumeLinear(v)).0; 717 | let max = *self.levels.iter().max().unwrap(); 718 | 719 | if max > Volume::MUTED.0 { 720 | self.levels.iter_mut() 721 | .for_each(|i| *i = ((*i as u64 * v as u64 / max as u64) as u32).clamp(Volume::MUTED.0, Volume::MAX.0)); 722 | } 723 | else { self.levels.fill(v); } 724 | } 725 | } 726 | 727 | impl <'a> From<&SinkInputInfo<'a>> for OutputClient { 728 | fn from(sink_input: &SinkInputInfo<'a>) -> Self { 729 | let name = sink_input.proplist.get_str("application.name").unwrap_or_default(); 730 | let description = sink_input.name.as_ref().map(Cow::to_string).unwrap_or_default(); 731 | let icon = sink_input.proplist.get_str("application.icon_name"); 732 | let process = sink_input.proplist.get_str("application.process.id") 733 | .and_then(|b| b.parse::().ok()); 734 | 735 | // This would be the correct approach, but things get weird after 255% 736 | // static VOLUME_MAX: OnceLock = OnceLock::new(); 737 | // let max = *VOLUME_MAX.get_or_init(|| VolumeLinear::from(libpulse_binding::volume::Volume::ui_max()).0); 738 | 739 | let volume = Volume { 740 | levels: { 741 | let levels: &[u32] = unsafe { 742 | use libpulse_binding::volume::Volume; 743 | 744 | std::mem::transmute::<&[Volume], &[u32]>(sink_input.volume.get()) 745 | }; 746 | 747 | VolumeLevels(SmallVec::from_slice(&levels[..sink_input.volume.len() as usize])) 748 | }, 749 | percent: &Volume::pulse_linear, 750 | set_percent: &Volume::set_pulse_linear, 751 | }; 752 | 753 | OutputClient { 754 | id: sink_input.index, 755 | process, 756 | name, 757 | description, 758 | icon, 759 | volume, 760 | max_volume: 2.55, 761 | muted: sink_input.mute, 762 | corked: sink_input.corked, 763 | kind: Kind::Out | Kind::Software, 764 | } 765 | } 766 | } 767 | 768 | impl <'a> From<&SinkInfo<'a>> for OutputClient { 769 | fn from(sink: &SinkInfo<'a>) -> Self { 770 | let description = sink.active_port 771 | .as_ref() 772 | .and_then(|port| port.description.to_owned()) 773 | .unwrap_or_default() 774 | .to_string(); 775 | 776 | let volume = Volume { 777 | levels: { 778 | let levels: &[u32] = unsafe { 779 | use libpulse_binding::volume::Volume; 780 | 781 | std::mem::transmute::<&[Volume], &[u32]>(sink.volume.get()) 782 | }; 783 | VolumeLevels(SmallVec::from_slice(&levels[..sink.volume.len() as usize])) 784 | }, 785 | percent: &|v: &Volume| { 786 | use libpulse_binding::volume::Volume; 787 | 788 | *v.levels.iter().max().unwrap() as f64 / Volume::NORMAL.0 as f64 789 | }, 790 | set_percent: &|v: &mut Volume, p: f64| { 791 | v.levels.fill((libpulse_binding::volume::Volume::NORMAL.0 as f64 * p) as u32); 792 | }, 793 | }; 794 | 795 | OutputClient { 796 | id: 0, 797 | process: None, 798 | name: "Master".to_owned(), 799 | description, 800 | icon: None, 801 | volume, 802 | max_volume: 2.55, 803 | muted: sink.mute, 804 | corked: false, 805 | kind: Kind::Out | Kind::Hardware, 806 | } 807 | } 808 | } 809 | -------------------------------------------------------------------------------- /src/style.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::ffi::OsStr; 3 | use std::path::{PathBuf, Path}; 4 | 5 | use tokio::fs::{self, File}; 6 | use tokio::io::AsyncWriteExt; 7 | 8 | use crate::error::{CacheError, Error, StyleError}; 9 | 10 | #[derive(Default, Copy, Clone)] 11 | pub struct StyleSettings { 12 | #[cfg(feature = "Accent")] 13 | pub accent: bool, 14 | } 15 | 16 | #[allow(unused_variables)] 17 | pub async fn find(path: impl Into, settings: StyleSettings) -> Result, Error> { 18 | let mut path = path.into(); 19 | 20 | path.push("style"); 21 | 22 | for ext in ["scss", "sass"] { 23 | path.set_extension(ext); 24 | 25 | match read(&path).await { 26 | Ok(style) => { 27 | #[cfg(feature = "Accent")] 28 | if settings.accent { 29 | return apply_accent(style).await; 30 | } 31 | 32 | return Ok(style) 33 | }, 34 | Err(Error::Style(StyleError::NotFound(_))) => continue, 35 | Err(e) => return Err(e), 36 | } 37 | } 38 | 39 | path.set_extension("css"); 40 | 41 | let s = match path.exists() { 42 | true => read(path).await, 43 | false => write_default(path, StyleSettings::default()).await, 44 | }; 45 | 46 | #[cfg(feature = "Accent")] 47 | if settings.accent { 48 | if let Ok(s) = s { 49 | return apply_accent(s).await; 50 | } 51 | } 52 | 53 | s 54 | } 55 | 56 | #[allow(unused_variables)] 57 | pub async fn default(settings: StyleSettings) -> Cow<'static, str> { 58 | static DEFAULT_STYLE: &str = include_str!(concat!(env!("OUT_DIR"), "/default.css")); 59 | 60 | #[cfg(feature = "Accent")] 61 | if settings.accent { 62 | let s = Cow::Borrowed(DEFAULT_STYLE); 63 | 64 | if let Ok(s) = apply_accent(s).await { 65 | return s; 66 | } 67 | } 68 | 69 | Cow::Borrowed(DEFAULT_STYLE) 70 | } 71 | 72 | async fn write_default(path: impl AsRef, settings: StyleSettings) -> Result, Error> { 73 | let path = path.as_ref(); 74 | let style = default(settings).await; 75 | 76 | let mut fd = File::create(path) 77 | .await.map_err(|e| StyleError::Create { e, path: path.to_owned() })?; 78 | 79 | fd.write_all(style.as_bytes()) 80 | .await.map_err(|e| StyleError::Write { e, path: path.to_owned() })?; 81 | 82 | Ok(style) 83 | } 84 | 85 | pub async fn read(path: impl AsRef) -> Result, Error> { 86 | let path = path.as_ref(); 87 | 88 | match path.extension().and_then(OsStr::to_str) { 89 | Some("sass" | "scss") => compile_sass(path).await.map(Cow::Owned), 90 | Some("css") => { 91 | fs::read_to_string(path).await 92 | .map(Cow::Owned) 93 | .map_err(|e| StyleError::Read { e, path: path.to_owned() }) 94 | .map_err(Into::into) 95 | }, 96 | None | Some(_) => { 97 | #[allow(unused_variables)] 98 | let expected = "css"; 99 | 100 | #[cfg(feature = "Sass")] 101 | let expected = "css, sass, scss"; 102 | 103 | Err(StyleError::Extension { expected }.into()) 104 | }, 105 | } 106 | } 107 | 108 | async fn compile_sass(style_path: impl AsRef) -> Result { 109 | use crate::{xdg, error}; 110 | 111 | let style_path = style_path.as_ref(); 112 | 113 | let style_meta = match fs::metadata(style_path).await { 114 | Ok(m) => m, 115 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Err(StyleError::NotFound(e).into()), 116 | Err(e) => { 117 | return Err(StyleError::Meta { e, path: style_path.to_owned() }.into()) 118 | } 119 | }; 120 | 121 | let style_mtime = style_meta.modified().map_err(|e| StyleError::MTime { e, path: style_path.to_owned() })?; 122 | 123 | let mut cache_path = xdg::cache_dir(); 124 | cache_path.push(crate::APP_BINARY); 125 | cache_path.set_extension("css"); 126 | 127 | if let Ok(cache_meta) = fs::metadata(&cache_path).await { 128 | if Some(style_mtime) == cache_meta.modified().ok() { 129 | return fs::read_to_string(&cache_path).await 130 | .map_err(|e| error::CacheError::Read { e, path: cache_path }) 131 | .map_err(Into::into); 132 | } 133 | } 134 | 135 | #[cfg(feature = "Sass")] 136 | let compiled = { 137 | let style = fs::read_to_string(style_path).await 138 | .map_err(|e| StyleError::Read { e, path: style_path.to_owned() })?; 139 | 140 | grass::from_string(style, &grass::Options::default()).map_err(StyleError::Sass)? 141 | }; 142 | 143 | #[cfg(not(feature = "Sass"))] 144 | let compiled = { 145 | let output = tokio::process::Command::new("sass") 146 | .args(["--no-source-map", "-s", "expanded", &style_path.to_string_lossy()]) 147 | .output().await 148 | .map_err(|e| StyleError::SystemCompiler { e: Some(e), path: style_path.to_owned() })?; 149 | 150 | if !output.stderr.is_empty() { 151 | let _ = std::io::Write::write_all(&mut std::io::stderr(), &output.stderr); 152 | } 153 | 154 | if !output.status.success() { 155 | let e = StyleError::SystemCompiler { e: None, path: style_path.to_owned() }; 156 | return Err(e.into()) 157 | } 158 | 159 | unsafe { String::from_utf8_unchecked(output.stdout) } 160 | }; 161 | 162 | if let Err(e) = cache(cache_path, &compiled, style_mtime).await { 163 | eprintln!("{e}"); 164 | } 165 | 166 | Ok(compiled) 167 | } 168 | 169 | async fn cache(path: impl AsRef, style: &str, time: std::time::SystemTime) -> Result<(), CacheError> { 170 | use crate::error::CacheError; 171 | 172 | let path = path.as_ref(); 173 | 174 | let mut f = File::create(path).await 175 | .map_err(|e| CacheError::Create { e, path: path.to_owned() })?; 176 | 177 | f.write_all(style.as_bytes()).await 178 | .map_err(|e| CacheError::Write { e, path: path.to_owned() })?; 179 | 180 | f.into_std().await.set_modified(time) 181 | .map_err(|e| CacheError::MTime { e, path: path.to_owned() }) 182 | } 183 | 184 | #[cfg(feature = "Accent")] 185 | async fn apply_accent(s: Cow<'static, str>) -> Result, Error> { 186 | use crate::accent; 187 | use crate::error::ZbusError; 188 | 189 | let conn = zbus::Connection::session().await 190 | .map_err(|e| ZbusError::Connect { e })?; 191 | 192 | let settings = accent::Settings::new(&conn).await 193 | .map_err(|e| ZbusError::Proxy { e })?; 194 | 195 | let (r, g, b) = settings.accent().await?; 196 | 197 | match set_color(s.as_ref(), "accent", r, g, b).map(Cow::Owned) { 198 | Some(s) => Ok(s), 199 | None => Ok(s), 200 | } 201 | } 202 | 203 | #[cfg(feature = "Accent")] 204 | fn find_var(s: &str, name: &str) -> Option> { 205 | let start = s.find(&format!("--{name}:"))?; 206 | let end = s[start..].find(';')?; 207 | 208 | Some(start..start + end) 209 | } 210 | 211 | #[cfg(feature = "Accent")] 212 | fn set_var(s: impl Into, name: &str, value: &str) -> Option { 213 | let mut s = s.into(); 214 | s.replace_range( 215 | find_var(&s, name)?, 216 | &format!("--{name}: {value}")); 217 | 218 | Some(s) 219 | } 220 | 221 | #[cfg(feature = "Accent")] 222 | fn set_color(s: impl Into, name: &str, r: u8, g: u8, b: u8) -> Option { 223 | set_var(s, name, &format!("#{r:02X}{g:02X}{b:02X}")) 224 | } 225 | -------------------------------------------------------------------------------- /src/widgets/fill.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | use gtk::glib; 4 | 5 | use glib::Properties; 6 | use glib::subclass::types::ObjectSubclass; 7 | use glib::subclass::object::ObjectImpl; 8 | 9 | use gtk::prelude::ObjectExt; 10 | use gtk::subclass::box_::BoxImpl; 11 | use gtk::subclass::widget::WidgetImpl; 12 | use gtk::subclass::orientable::OrientableImpl; 13 | use gtk::subclass::prelude::DerivedObjectProperties; 14 | 15 | #[derive(Properties, Default)] 16 | #[properties(wrapper_type = super::Fill)] 17 | pub struct Fill { 18 | #[property(get, set)] 19 | value: Cell, 20 | } 21 | 22 | impl WidgetImpl for Fill {} 23 | impl OrientableImpl for Fill {} 24 | impl BoxImpl for Fill {} 25 | 26 | #[glib::object_subclass] 27 | impl ObjectSubclass for Fill { 28 | const NAME: &'static str = "CustomFill"; 29 | type Type = super::Fill; 30 | type ParentType = gtk::Widget; 31 | } 32 | 33 | #[glib::derived_properties] 34 | impl ObjectImpl for Fill {} 35 | -------------------------------------------------------------------------------- /src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod sliderbox; 2 | pub mod switchbox; 3 | 4 | pub enum GrowthDirection { 5 | TopLeft, 6 | BottomRight, 7 | } 8 | -------------------------------------------------------------------------------- /src/widgets/sliderbox.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::time::{Duration, Instant}; 3 | 4 | use relm4::Sender; 5 | use relm4::{RelmWidgetExt, FactorySender}; 6 | use relm4::factory::{FactoryView, FactoryVecDeque}; 7 | use relm4::once_cell::sync::OnceCell; 8 | use relm4::prelude::{DynamicIndex, FactoryComponent}; 9 | 10 | use gtk::{Orientation, Align, Justification}; 11 | use gtk::pango::EllipsizeMode; 12 | use gtk::glib::{self, Object, object::Cast, ControlFlow}; 13 | use gtk::prelude::{BoxExt, GtkWindowExt, GestureSingleExt, OrientableExt, RangeExt, WidgetExt, WidgetExtManual}; 14 | 15 | use smallvec::SmallVec; 16 | 17 | use crate::anchor::Anchor; 18 | use crate::app::ElementMessage; 19 | use crate::server::{self, OutputClient, Volume}; 20 | 21 | use super::GrowthDirection; 22 | 23 | #[derive(Debug)] 24 | pub enum SliderMessage { 25 | Mute, 26 | ValueChange(f64), 27 | Removed, 28 | ServerChange(Box), 29 | ServerPeak(f32), 30 | Refresh, 31 | } 32 | 33 | #[derive(Debug)] 34 | pub enum SliderCommand { 35 | Peak, 36 | Cork, 37 | } 38 | 39 | pub struct Sliders { 40 | pub container: FactoryVecDeque, 41 | pub direction: GrowthDirection, 42 | pub per_process: bool, 43 | } 44 | 45 | impl Sliders { 46 | pub fn new(sender: &Sender) -> Self { 47 | let container = FactoryVecDeque::builder() 48 | .launch(SliderBox::default()) 49 | .forward(sender, std::convert::identity); 50 | 51 | Self { 52 | container, 53 | direction: GrowthDirection::BottomRight, 54 | per_process: false, 55 | } 56 | } 57 | 58 | pub fn set_direction(&mut self, anchor: Anchor, orientation: Orientation) { 59 | self.direction = match orientation { 60 | Orientation::Horizontal if anchor.contains(Anchor::Right) => GrowthDirection::TopLeft, 61 | Orientation::Vertical if anchor.contains(Anchor::Bottom) => GrowthDirection::TopLeft, 62 | _ => GrowthDirection::BottomRight, 63 | }; 64 | } 65 | 66 | pub fn push_client(&mut self, client: OutputClient) { 67 | let mut sliders = self.container.guard(); 68 | 69 | if self.per_process && client.process.is_some() { 70 | let pos = sliders.iter_mut().position(|slider| slider.process == client.process); 71 | 72 | if let Some(i) = pos { 73 | sliders.get_mut(i).unwrap().clients.push(SmallClient::from(&client)); 74 | sliders.drop(); 75 | 76 | self.container.send(i, SliderMessage::Refresh); 77 | 78 | return 79 | } 80 | } 81 | 82 | match self.direction { 83 | GrowthDirection::TopLeft => sliders.push_front(client), 84 | GrowthDirection::BottomRight => sliders.push_back(client), 85 | }; 86 | 87 | sliders.drop(); 88 | } 89 | 90 | pub fn remove(&mut self, id: u32) { 91 | let mut sliders = self.container.guard(); 92 | 93 | let i = sliders.iter_mut().position(|slider| { 94 | match slider.clients.iter().position(|client| client.id == id) { 95 | Some(pos) => { 96 | slider.clients.remove(pos); 97 | true 98 | } 99 | None => false, 100 | } 101 | }); 102 | 103 | match i { 104 | Some(i) if sliders.get(i).unwrap().clients.is_empty() => { 105 | sliders.remove(i); 106 | sliders.drop(); 107 | } 108 | Some(i) => { 109 | sliders.drop(); 110 | self.container.send(i, SliderMessage::Refresh); 111 | } 112 | _ => {} 113 | } 114 | } 115 | 116 | pub fn clear(&mut self) { 117 | self.container.guard().clear(); 118 | } 119 | 120 | pub fn contains(&self, id: u32) -> bool { 121 | self.container 122 | .iter() 123 | .any(|e| e.clients.iter().any(|c| c.id == id)) 124 | } 125 | 126 | fn position(&self, id: u32) -> Option { 127 | self.container.iter().position(|slider| slider.clients.iter().any(|c| c.id == id)) 128 | } 129 | 130 | pub fn send(&self, id: u32, message: SliderMessage) { 131 | if let Some(index) = self.position(id) { 132 | self.container.send(index, message) 133 | } 134 | } 135 | } 136 | 137 | #[tracker::track] 138 | pub struct Slider { 139 | #[do_not_track] clients: SmallVec<[SmallClient; 3]>, 140 | #[do_not_track] process: Option, 141 | volume: Volume, 142 | volume_percent: u8, 143 | muted: bool, 144 | corked: bool, 145 | name: String, 146 | icon: Cow<'static, str>, 147 | #[no_eq] peak: f64, 148 | removed: bool, 149 | #[no_eq] updated: bool, 150 | #[do_not_track] kind: server::Kind, 151 | #[do_not_track] corking: bool, 152 | } 153 | 154 | impl Slider { 155 | fn is_corked(&self) -> bool { 156 | self.clients.iter().all(|id| id.corked) 157 | } 158 | 159 | fn is_muted(&self) -> bool { 160 | self.clients.iter().all(|id| id.muted) 161 | } 162 | 163 | fn description(&self) -> &str { 164 | if self.clients.len() == 1 { 165 | return self.clients[0].description.as_str() 166 | } 167 | 168 | self.clients.iter().reduce(|best, candidate| { 169 | let a = best.score(); 170 | let b = candidate.score(); 171 | 172 | match (b > a) || ((a == b) && candidate.id > best.id) { 173 | true => candidate, 174 | false => best 175 | } 176 | }) 177 | .map(|client| client.description.as_str()) 178 | .unwrap_or_default() 179 | } 180 | } 181 | 182 | #[relm4::factory(pub)] 183 | impl FactoryComponent for Slider { 184 | type Init = server::OutputClient; 185 | type Input = SliderMessage; 186 | type Output = ElementMessage; 187 | type ParentWidget = SliderBox; 188 | type CommandOutput = SliderCommand; 189 | 190 | view! { 191 | root = gtk::Box { 192 | add_css_class: "client", 193 | 194 | #[track = "self.changed(Self::corked())"] 195 | set_visible: { 196 | let parent = root.parent().expect("Slider has a parent") 197 | .downcast::().expect("Slider parent is a SliderBox"); 198 | 199 | !self.corked || parent.show_corked() 200 | }, 201 | 202 | #[track = "self.changed(Self::removed())"] 203 | set_class_active: ("new", !self.removed), 204 | 205 | #[track = "self.changed(Self::removed())"] 206 | set_class_active: ("removed", self.removed), 207 | 208 | #[track = "self.changed(Self::muted())"] 209 | set_class_active: ("muted", self.is_muted()), 210 | 211 | gtk::Image { 212 | add_css_class: "icon", 213 | set_use_fallback: false, 214 | #[track = "self.changed(Slider::icon())"] 215 | set_icon_name: Some(&self.icon), 216 | set_visible: parent.has_icons(), 217 | }, 218 | 219 | gtk::Box { 220 | set_orientation: Orientation::Vertical, 221 | 222 | #[name(name)] 223 | gtk::Label { 224 | #[track = "self.changed(Self::name())"] 225 | set_label: &self.name, 226 | add_css_class: "name", 227 | set_ellipsize: EllipsizeMode::End, 228 | }, 229 | 230 | #[name(description)] 231 | gtk::Label { 232 | #[track = "self.changed(Self::updated())"] 233 | set_label: self.description(), 234 | add_css_class: "description", 235 | set_ellipsize: EllipsizeMode::End, 236 | }, 237 | 238 | #[name(scale_wrapper)] 239 | gtk::Box { 240 | #[name(scale)] // 0.00004 is a rounding error 241 | gtk::Scale::with_range(Orientation::Horizontal, 0.0, parent.max_value() + 0.00004, 0.005) { 242 | #[track = "self.changed(Self::volume())"] 243 | set_value: self.volume.percent(), 244 | set_slider_size_fixed: true, 245 | set_show_fill_level: true, 246 | set_restrict_to_fill_level: false, 247 | #[track = "self.changed(Self::peak())"] 248 | set_fill_level: self.peak, 249 | connect_value_changed[sender] => move |scale| { 250 | sender.input(SliderMessage::ValueChange(scale.value())); 251 | }, 252 | }, 253 | 254 | gtk::Label { 255 | #[track = "self.changed(Self::volume_percent())"] 256 | set_label: &{ let mut s = self.volume_percent.to_string(); s.push('%'); s }, 257 | add_css_class: "volume", 258 | set_width_chars: 5, 259 | set_max_width_chars: 5, 260 | set_justify: Justification::Center, 261 | add_controller = gtk::GestureClick { 262 | set_button: gtk::gdk::BUTTON_PRIMARY, 263 | connect_released[sender] => move |_, _, _, _| { 264 | sender.input(SliderMessage::Mute); 265 | } 266 | } 267 | } 268 | } 269 | } 270 | } 271 | } 272 | 273 | fn init_widgets(&mut self, _: &Self::Index, root: Self::Root, _: &::ReturnedWidget, sender: FactorySender) -> Self::Widgets { 274 | let parent = root.parent().expect("Slider has a parent") 275 | .downcast::().expect("Slider parent is a SliderBox"); 276 | 277 | let widgets = view_output!(); 278 | 279 | match parent.orientation() { 280 | Orientation::Horizontal => { 281 | widgets.root.set_orientation(Orientation::Vertical); 282 | widgets.root.set_halign(Align::Center); 283 | 284 | let window = parent.toplevel_window().unwrap(); 285 | widgets.root.set_width_request(window.default_width()); 286 | 287 | widgets.name.set_halign(Align::Center); 288 | widgets.description.set_halign(Align::Center); 289 | 290 | widgets.scale_wrapper.set_orientation(Orientation::Vertical); 291 | 292 | widgets.scale.set_orientation(Orientation::Vertical); 293 | widgets.scale.set_vexpand(true); 294 | widgets.scale.set_inverted(true); 295 | } 296 | Orientation::Vertical => { 297 | widgets.name.set_halign(Align::Start); 298 | widgets.description.set_halign(Align::Start); 299 | 300 | widgets.scale.set_hexpand(true); 301 | } 302 | _ => unreachable!("Slider recieved an unknown orientation from parent"), 303 | } 304 | 305 | widgets.scale.connect_fill_level_notify({ 306 | let trough = widgets.scale.first_child().expect("getting GtkRange from GtkScale"); 307 | let fill = trough.first_child().expect("getting fill from GtkRange"); 308 | 309 | move |_| fill.queue_resize() 310 | }); 311 | 312 | widgets.root.add_tick_callback({ 313 | const DELAY: Duration = Duration::from_millis(500); 314 | let before: OnceCell = OnceCell::new(); 315 | 316 | move |root, _| { 317 | if Instant::now() - *before.get_or_init(Instant::now) < DELAY { 318 | return ControlFlow::Continue 319 | } 320 | 321 | root.remove_css_class("new"); 322 | ControlFlow::Break 323 | } 324 | }); 325 | 326 | widgets 327 | } 328 | 329 | fn init_model(init: Self::Init, _: &DynamicIndex, sender: FactorySender) -> Self { 330 | sender.command(|sender, shutdown| { 331 | shutdown.register(async move { 332 | let mut interval = tokio::time::interval(Duration::from_millis(10)); 333 | 334 | loop { 335 | interval.tick().await; 336 | sender.emit(SliderCommand::Peak); 337 | } 338 | }) 339 | .drop_on_shutdown() 340 | }); 341 | 342 | let volume_percent = (init.volume.percent() * 100.0) as u8; 343 | let clients = [ 344 | SmallClient::from(&init), 345 | SmallClient::default(), 346 | SmallClient::default() 347 | ]; 348 | 349 | Self { 350 | clients: SmallVec::from_buf_and_len(clients, 1), 351 | process: init.process, 352 | name: init.name, 353 | icon: client_icon(init.icon, volume_percent, init.muted), 354 | volume: init.volume, 355 | volume_percent, 356 | muted: init.muted, 357 | corked: init.corked, 358 | peak: 0.0, 359 | removed: false, 360 | kind: init.kind, 361 | updated: false, 362 | 363 | corking: false, 364 | 365 | tracker: 0, 366 | } 367 | } 368 | 369 | fn update_cmd(&mut self, cmd: Self::CommandOutput, _: FactorySender) { 370 | self.reset(); 371 | 372 | match cmd { 373 | SliderCommand::Peak => if self.peak > 0.0 { 374 | self.set_peak((self.peak - 0.01).max(0.0)); 375 | }, 376 | SliderCommand::Cork => if self.corking { 377 | self.corking = false; 378 | self.set_corked(!self.corked); 379 | } 380 | } 381 | } 382 | 383 | fn update(&mut self, message: Self::Input, sender: FactorySender) { 384 | self.reset(); 385 | 386 | match message { 387 | SliderMessage::ServerPeak(peak) => { 388 | let peak = (peak * 0.9) as f64; 389 | 390 | if peak > self.peak + 0.035 { 391 | self.set_peak(peak + 0.015); 392 | } 393 | }, 394 | SliderMessage::ValueChange(v) => { 395 | if self.volume_percent != 0 { 396 | self.set_peak(100.0 * self.peak * v / self.volume_percent as f64); 397 | } 398 | 399 | self.volume.set_percent(v); 400 | 401 | let _ = sender.output(ElementMessage::SetVolume { 402 | ids: self.clients.iter().map(|client| client.id).collect(), 403 | kind: self.kind, 404 | levels: self.volume.levels.clone() 405 | }); 406 | }, 407 | SliderMessage::Mute => { 408 | let _ = sender.output(ElementMessage::SetMute { 409 | ids: self.clients.iter().map(|client| client.id).collect(), 410 | kind: self.kind, 411 | flag: !self.is_muted() 412 | }); 413 | }, 414 | SliderMessage::Removed => { 415 | self.set_removed(true); 416 | } 417 | SliderMessage::ServerChange(client) => { 418 | if let Some(existing) = self.clients.iter_mut().find(|c| c.id == client.id) { 419 | let new: SmallClient = client.as_ref().into(); 420 | 421 | // TODO: This is really wasteful, please do something about it T_T 422 | if *existing != new { 423 | *existing = new; 424 | self.set_updated(true); 425 | } 426 | } 427 | 428 | self.set_volume_percent((client.volume.percent() * 100.0) as u8); 429 | self.set_volume(client.volume); 430 | self.set_name(client.name); 431 | self.set_muted(self.is_muted()); 432 | self.set_icon(client_icon(client.icon, self.volume_percent, self.muted)); 433 | 434 | if !self.corking && (self.corked != self.is_corked()) { 435 | sender.oneshot_command(async move { 436 | tokio::time::sleep(Duration::from_millis(45)).await; 437 | SliderCommand::Cork 438 | }) 439 | } 440 | 441 | self.corking = self.corked != self.is_corked(); 442 | }, 443 | SliderMessage::Refresh => { 444 | self.set_muted(self.is_muted()); 445 | self.set_corked(self.is_corked()); 446 | self.set_updated(true); 447 | } 448 | } 449 | } 450 | } 451 | 452 | #[derive(Default, PartialEq, Clone)] 453 | pub struct SmallClient { 454 | id: u32, 455 | description: String, 456 | corked: bool, 457 | muted: bool, 458 | } 459 | 460 | impl SmallClient { 461 | fn score(&self) -> u8 { 462 | (!self.corked as u8) << 3 | 463 | (!self.muted as u8) << 2 | 464 | (!self.description.is_empty() as u8) 465 | } 466 | } 467 | 468 | impl From<&OutputClient> for SmallClient { 469 | fn from(c: &OutputClient) -> Self { 470 | Self { 471 | id: c.id, 472 | description: c.description.clone(), 473 | corked: c.corked, 474 | muted: c.muted, 475 | } 476 | } 477 | } 478 | 479 | fn client_icon(icon: Option, volume_percent: u8, muted: bool) -> Cow<'static, str> { 480 | match icon { 481 | Some(name) => Cow::Owned(name), 482 | None => { 483 | let s = match volume_percent { 484 | _ if muted => "audio-volume-muted", 485 | v if v <= 25 => "audio-volume-low", 486 | v if v <= 75 => "audio-volume-medium", 487 | _ => "audio-volume-high", 488 | }; 489 | 490 | Cow::Borrowed(s) 491 | }, 492 | } 493 | } 494 | 495 | mod imp { 496 | use std::cell::Cell; 497 | 498 | use gtk::glib; 499 | 500 | use glib::Properties; 501 | use glib::subclass::types::ObjectSubclass; 502 | use glib::subclass::object::ObjectImpl; 503 | 504 | use gtk::prelude::ObjectExt; 505 | use gtk::subclass::box_::BoxImpl; 506 | use gtk::subclass::widget::WidgetImpl; 507 | use gtk::subclass::orientable::OrientableImpl; 508 | use gtk::subclass::prelude::DerivedObjectProperties; 509 | 510 | #[derive(Properties, Default)] 511 | #[properties(wrapper_type = super::SliderBox)] 512 | pub struct SliderBox { 513 | #[property(get, set)] 514 | has_icons: Cell, 515 | 516 | #[property(get, set)] 517 | show_corked: Cell, 518 | 519 | #[property(get, set)] 520 | max_value: Cell, 521 | } 522 | 523 | impl WidgetImpl for SliderBox {} 524 | impl OrientableImpl for SliderBox {} 525 | impl BoxImpl for SliderBox {} 526 | 527 | #[glib::object_subclass] 528 | impl ObjectSubclass for SliderBox { 529 | const NAME: &'static str = "SliderBox"; 530 | type Type = super::SliderBox; 531 | type ParentType = gtk::Box; 532 | } 533 | 534 | #[glib::derived_properties] 535 | impl ObjectImpl for SliderBox {} 536 | } 537 | 538 | // This is a 99% GTK boilerplate to get a custom Widget. 539 | // https://gtk-rs.org/gtk4-rs/git/book/g_object_subclassing.html 540 | glib::wrapper! { 541 | pub struct SliderBox(ObjectSubclass) 542 | @extends gtk::Box, gtk::Widget, 543 | @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; 544 | } 545 | 546 | impl SliderBox { 547 | pub fn default() -> Self { 548 | Object::builder().build() 549 | } 550 | } 551 | 552 | // FactoryComponent::ParentWidget must implement FactoryView. 553 | // This is the same implementation that is used for gtk::Box. 554 | // https://docs.rs/relm4/0.7.0-rc.1/src/relm4/factory/widgets/gtk.rs.html#5 555 | impl FactoryView for SliderBox { 556 | type Children = gtk::Widget; 557 | type ReturnedWidget = gtk::Widget; 558 | type Position = (); 559 | 560 | fn factory_remove(&self, widget: &Self::ReturnedWidget) { 561 | self.remove(widget); 562 | } 563 | 564 | fn factory_append(&self, widget: impl AsRef, _: &Self::Position) -> Self::ReturnedWidget { 565 | self.append(widget.as_ref()); 566 | widget.as_ref().clone() 567 | } 568 | 569 | fn factory_prepend(&self, widget: impl AsRef, _: &Self::Position) -> Self::ReturnedWidget { 570 | self.prepend(widget.as_ref()); 571 | widget.as_ref().clone() 572 | } 573 | 574 | fn factory_insert_after(&self, widget: impl AsRef, _: &Self::Position, other: &Self::ReturnedWidget) -> Self::ReturnedWidget { 575 | self.insert_child_after(widget.as_ref(), Some(other)); 576 | widget.as_ref().clone() 577 | } 578 | 579 | fn returned_widget_to_child(returned_widget: &Self::ReturnedWidget) -> Self::Children { 580 | returned_widget.clone() 581 | } 582 | 583 | fn factory_move_after(&self, widget: &Self::ReturnedWidget, other: &Self::ReturnedWidget) { 584 | self.reorder_child_after(widget, Some(other)); 585 | } 586 | 587 | fn factory_move_start(&self, widget: &Self::ReturnedWidget) { 588 | self.reorder_child_after(widget, None::<>k::Widget>); 589 | } 590 | } 591 | -------------------------------------------------------------------------------- /src/widgets/switchbox.rs: -------------------------------------------------------------------------------- 1 | use relm4::{FactorySender, RelmWidgetExt, Sender}; 2 | use relm4::prelude::{DynamicIndex, FactoryComponent}; 3 | use relm4::factory::{FactoryVecDeque, FactoryView}; 4 | 5 | use gtk::{Align, Orientation}; 6 | use gtk::glib::{self, Object}; 7 | use gtk::prelude::{BoxExt, GestureSingleExt, OrientableExt, WidgetExt}; 8 | 9 | use crate::app::ElementMessage; 10 | use crate::server::Output; 11 | 12 | #[derive(Clone, Debug)] 13 | pub enum SwitchMessage { 14 | Activate, 15 | Deactivate, 16 | Click, 17 | } 18 | 19 | pub struct Switches { 20 | pub container: FactoryVecDeque, 21 | } 22 | 23 | impl Switches { 24 | pub fn new(sender: &Sender) -> Self { 25 | let container = FactoryVecDeque::builder() 26 | .launch(SwitchBox::default()) 27 | .forward(sender, std::convert::identity); 28 | 29 | Self { container } 30 | } 31 | 32 | pub fn push(&mut self, output: Output) { 33 | let mut switches = self.container.guard(); 34 | 35 | switches.push_front(output); 36 | switches.drop(); 37 | } 38 | 39 | fn position(&self, output: Output) -> Option { 40 | self.container.iter().position(|switch| switch.name == output.name && switch.port == output.port) 41 | } 42 | 43 | pub fn set_active(&self, output: Output) { 44 | self.container.broadcast(SwitchMessage::Deactivate); 45 | 46 | if let Some(pos) = self.position(output) { 47 | self.container.send(pos, SwitchMessage::Activate); 48 | } 49 | 50 | } 51 | 52 | pub fn clear(&mut self) { 53 | self.container.guard().clear(); 54 | } 55 | } 56 | 57 | #[tracker::track] 58 | pub struct Switch { 59 | name: String, 60 | port: String, 61 | active: bool, 62 | } 63 | 64 | #[relm4::factory(pub)] 65 | impl FactoryComponent for Switch { 66 | type Init = Output; 67 | type Input = SwitchMessage; 68 | type Output = ElementMessage; 69 | type ParentWidget = SwitchBox; 70 | type CommandOutput = (); 71 | 72 | view! { 73 | root = gtk::Box { 74 | add_css_class: "output", 75 | set_orientation: Orientation::Horizontal, 76 | 77 | #[track = "self.changed(Self::active())"] 78 | set_class_active: ("master", self.active), 79 | 80 | add_controller = gtk::GestureClick { 81 | set_button: gtk::gdk::BUTTON_PRIMARY, 82 | connect_pressed[sender] => move |_, _, _, _| { 83 | sender.input_sender().emit(SwitchMessage::Click) 84 | } 85 | }, 86 | 87 | gtk::Image { 88 | set_expand: true, 89 | set_align: Align::Center, 90 | add_css_class: "icon", 91 | set_use_fallback: false, 92 | #[track = "self.changed(Self::port())"] 93 | set_icon_name: Some(icon(&self.port)), 94 | }, 95 | } 96 | } 97 | 98 | fn init_widgets(&mut self, _: &Self::Index, root: Self::Root, _: &::ReturnedWidget, sender: FactorySender) -> Self::Widgets { 99 | let widgets = view_output!(); 100 | 101 | widgets 102 | } 103 | 104 | fn init_model(init: Self::Init, _: &DynamicIndex, _: FactorySender) -> Self { 105 | Self { 106 | name: init.name, 107 | port: init.port, 108 | active: init.master, 109 | 110 | tracker: 0, 111 | } 112 | } 113 | 114 | fn update_cmd(&mut self, _: Self::CommandOutput, _: FactorySender) { 115 | self.reset(); 116 | } 117 | 118 | fn update(&mut self, message: Self::Input, sender: FactorySender) { 119 | self.reset(); 120 | 121 | match message { 122 | SwitchMessage::Activate => self.set_active(true), 123 | SwitchMessage::Deactivate => self.set_active(false), 124 | SwitchMessage::Click => sender.output_sender().emit(ElementMessage::SetOutput { 125 | name: self.name.as_str().into(), 126 | port: self.port.as_str().into(), 127 | }) 128 | } 129 | } 130 | } 131 | 132 | fn icon(s: &str) -> &'static str { 133 | let s = s.to_ascii_lowercase(); 134 | 135 | if s.contains("headphones") { 136 | "audio-headphones-symbolic" 137 | } 138 | else if s.contains("hdmi") { 139 | "computer-symbolic" 140 | } 141 | else { 142 | "multimedia-player-symbolic" 143 | } 144 | } 145 | 146 | mod imp { 147 | use std::cell::Cell; 148 | 149 | use glib::Properties; 150 | use glib::subclass::types::ObjectSubclass; 151 | use glib::subclass::object::ObjectImpl; 152 | 153 | use gtk::prelude::ObjectExt; 154 | use gtk::subclass::box_::BoxImpl; 155 | use gtk::subclass::widget::WidgetImpl; 156 | use gtk::subclass::orientable::OrientableImpl; 157 | use gtk::subclass::prelude::DerivedObjectProperties; 158 | 159 | #[derive(Properties, Default)] 160 | #[properties(wrapper_type = super::SwitchBox)] 161 | pub struct SwitchBox { 162 | #[property(get, set)] 163 | active: Cell, 164 | } 165 | 166 | impl WidgetImpl for SwitchBox {} 167 | impl OrientableImpl for SwitchBox {} 168 | impl BoxImpl for SwitchBox {} 169 | 170 | #[glib::object_subclass] 171 | impl ObjectSubclass for SwitchBox { 172 | const NAME: &'static str = "SwitchBox"; 173 | type Type = super::SwitchBox; 174 | type ParentType = gtk::Box; 175 | } 176 | 177 | #[glib::derived_properties] 178 | impl ObjectImpl for SwitchBox {} 179 | } 180 | 181 | glib::wrapper! { 182 | pub struct SwitchBox(ObjectSubclass) 183 | @extends gtk::Box, gtk::Widget, 184 | @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; 185 | } 186 | 187 | impl SwitchBox { 188 | pub fn default() -> Self { 189 | Object::builder().build() 190 | } 191 | } 192 | 193 | // FactoryComponent::ParentWidget must implement FactoryView. 194 | // This is the same implementation that is used for gtk::Box. 195 | // https://docs.rs/relm4/0.7.0-rc.1/src/relm4/factory/widgets/gtk.rs.html#5 196 | impl FactoryView for SwitchBox { 197 | type Children = gtk::Widget; 198 | type ReturnedWidget = gtk::Widget; 199 | type Position = (); 200 | 201 | fn factory_remove(&self, widget: &Self::ReturnedWidget) { 202 | self.remove(widget); 203 | } 204 | 205 | fn factory_append(&self, widget: impl AsRef, _: &Self::Position) -> Self::ReturnedWidget { 206 | self.append(widget.as_ref()); 207 | widget.as_ref().clone() 208 | } 209 | 210 | fn factory_prepend(&self, widget: impl AsRef, _: &Self::Position) -> Self::ReturnedWidget { 211 | self.prepend(widget.as_ref()); 212 | widget.as_ref().clone() 213 | } 214 | 215 | fn factory_insert_after(&self, widget: impl AsRef, _: &Self::Position, other: &Self::ReturnedWidget) -> Self::ReturnedWidget { 216 | self.insert_child_after(widget.as_ref(), Some(other)); 217 | widget.as_ref().clone() 218 | } 219 | 220 | fn returned_widget_to_child(returned_widget: &Self::ReturnedWidget) -> Self::Children { 221 | returned_widget.clone() 222 | } 223 | 224 | fn factory_move_after(&self, widget: &Self::ReturnedWidget, other: &Self::ReturnedWidget) { 225 | self.reorder_child_after(widget, Some(other)); 226 | } 227 | 228 | fn factory_move_start(&self, widget: &Self::ReturnedWidget) { 229 | self.reorder_child_after(widget, None::<>k::Widget>); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/xdg.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf, sync::OnceLock}; 2 | 3 | fn env_or_default(env: &str, fallback: &str) -> PathBuf { 4 | env::var_os(env) 5 | .map(PathBuf::from) 6 | .or_else(|| { 7 | env::var_os("HOME") 8 | .map(PathBuf::from) 9 | .map(|mut p| { p.push(fallback); p }) 10 | }) 11 | .unwrap_or_else(|| panic!("couldn't find the {env} directory")) 12 | } 13 | 14 | pub fn config_dir() -> PathBuf { 15 | env_or_default("XDG_CONFIG_HOME", ".config") 16 | } 17 | 18 | pub fn cache_dir() -> PathBuf { 19 | env_or_default("XDG_CACHE_HOME", ".cache") 20 | } 21 | 22 | enum Platform { 23 | Wayland, 24 | X11, 25 | Unknown, 26 | } 27 | 28 | fn platform() -> &'static Platform { 29 | static PLATFORM: OnceLock = OnceLock::new(); 30 | 31 | PLATFORM.get_or_init(|| { 32 | match env::var("XDG_SESSION_TYPE").map(|s| s.to_lowercase()).as_deref() { 33 | Ok("wayland") => return Platform::Wayland, 34 | Ok("x11") => return Platform::X11, 35 | _ => {}, 36 | } 37 | 38 | if env::var("WAYLAND_DISPLAY").is_ok() { 39 | return Platform::Wayland 40 | } 41 | 42 | if env::var("DISPLAY").is_ok() { 43 | return Platform::X11 44 | } 45 | 46 | Platform::Unknown 47 | }) 48 | } 49 | 50 | pub fn is_wayland() -> bool { 51 | matches!(platform(), Platform::Wayland) 52 | } 53 | 54 | pub fn is_x11() -> bool { 55 | matches!(platform(), Platform::X11) 56 | } 57 | -------------------------------------------------------------------------------- /style/default.scss: -------------------------------------------------------------------------------- 1 | /* Please refer to GTK4 CSS specification */ 2 | 3 | /* https://docs.gtk.org/gtk4/css-properties.html */ 4 | 5 | * { 6 | all: unset; 7 | 8 | --bg: #161616; 9 | --fg: #DDDDDD; 10 | --accent: #684EFF; 11 | } 12 | 13 | window { 14 | border: 1px #{'alpha(var(--accent), 0.4)'} solid; 15 | border-radius: 10px; 16 | 17 | background-color: var(--bg); 18 | } 19 | 20 | .side { 21 | $hide: false; 22 | 23 | background-color: var(--bg); 24 | 25 | .output { 26 | color: var(--fg); 27 | transition: background 750ms; 28 | padding: 5px; 29 | 30 | &.master { 31 | background: shade(var(--accent), 0.4); 32 | transition: background 0ms; 33 | } 34 | 35 | &.master:hover { 36 | background: shade(var(--accent), 0.4); 37 | } 38 | 39 | .icon { 40 | -gtk-icon-style: symbolic; 41 | -gtk-icon-size: 16px; 42 | } 43 | } 44 | 45 | .output:hover { 46 | background: var(--accent); 47 | } 48 | 49 | .output:active { 50 | background: shade(var(--accent), 1.1); 51 | } 52 | 53 | @if $hide { 54 | min-height: 0; 55 | min-width: 0; 56 | 57 | .output { 58 | padding: 0; 59 | 60 | .icon { 61 | -gtk-icon-style: symbolic; 62 | -gtk-icon-size: 0; 63 | } 64 | } 65 | } 66 | } 67 | 68 | .main { 69 | margin: 20px; 70 | } 71 | 72 | .client { 73 | $hide-name: false; 74 | $hide-description: false; 75 | 76 | color: var(--fg); 77 | font-family: 'Noto Sans'; 78 | font-size: 1.0em; 79 | 80 | .icon { 81 | color: var(--fg); 82 | 83 | -gtk-icon-style: symbolic; 84 | } 85 | 86 | @if $hide-name { 87 | .name { 88 | font-size: 0; 89 | } 90 | } 91 | 92 | @if $hide-description { 93 | .description { 94 | font-size: 0; 95 | } 96 | } 97 | 98 | .volume { 99 | /* Numeric Volume Level */ 100 | color: var(--fg); 101 | } 102 | 103 | scale { 104 | trough { 105 | /* Slider Bar */ 106 | background-color: lighter(var(--bg)); 107 | border-radius: 10px; 108 | 109 | slider { 110 | /* Slider Knob */ 111 | padding: 0; 112 | 113 | border: none; 114 | border-radius: 2px; 115 | 116 | background-color: var(--accent); 117 | transition-duration: 400ms; 118 | } 119 | 120 | slider:hover { 121 | /* Slider Knob */ 122 | background-color: shade(var(--accent), 1.1); 123 | } 124 | 125 | highlight { 126 | /* Slider Bar Filled */ 127 | border: none; 128 | border-radius: 10px; 129 | 130 | margin: 1px; 131 | 132 | transition: background-image 300ms; 133 | } 134 | 135 | fill { 136 | /* Slider Peak */ 137 | background: none; 138 | 139 | border-radius: 10px; 140 | 141 | margin: 0px; 142 | } 143 | } 144 | } 145 | 146 | scale:active { 147 | trough slider { 148 | /* Slider Knob */ 149 | background-color: shade(var(--accent), 1.1); 150 | transform: scale(1.1); 151 | } 152 | } 153 | 154 | &.muted { 155 | .volume { 156 | /* Numeric Volume Level */ 157 | text-decoration: line-through; 158 | } 159 | 160 | scale { 161 | trough { 162 | slider { 163 | /* Slider Knob */ 164 | background: shade(var(--accent), 0.5); 165 | } 166 | 167 | highlight { 168 | /* Slider Bar Filled */ 169 | background: shade(var(--accent), 0.5); 170 | } 171 | } 172 | } 173 | } 174 | } 175 | 176 | .client.horizontal { 177 | &.new { 178 | animation: client-add-horizontal 300ms ease; 179 | } 180 | 181 | &.removed { 182 | animation: client-remove-horizontal 300ms ease; 183 | } 184 | 185 | .icon { 186 | padding-right: 13px; 187 | 188 | -gtk-icon-size: 16px; 189 | } 190 | 191 | .volume { 192 | /* Numeric Volume Level */ 193 | padding-left: 15px; 194 | padding-bottom: 2px; 195 | } 196 | 197 | scale { 198 | trough { 199 | /* Slider Bar */ 200 | min-height: 4px; 201 | 202 | slider { 203 | /* Slider Knob */ 204 | min-height: 14px; 205 | min-width: 6px; 206 | 207 | margin-top: -7px; 208 | margin-bottom: -7px; 209 | } 210 | 211 | highlight { 212 | /* Slider Bar Filled */ 213 | background-image: linear-gradient(to left, shade(var(--accent), 0.6), var(--accent)); 214 | } 215 | 216 | fill { 217 | /* Slider Peak */ 218 | border-top: 1px solid #{'alpha(var(--accent), 0.8)'}; 219 | border-bottom: 1px solid #{'alpha(var(--accent), 0.8)'}; 220 | } 221 | } 222 | } 223 | 224 | &.muted { 225 | scale { 226 | trough { 227 | highlight { 228 | /* Slider Bar Filled */ 229 | background-image: linear-gradient(to left, shade(var(--accent), 0.3), shade(var(--accent), 0.7)); 230 | } 231 | 232 | fill { 233 | /* Slider Peak */ 234 | border-top: 1px solid #{'alpha(var(--accent), 0.5)'}; 235 | border-bottom: 1px solid #{'alpha(var(--accent), 0.5)'}; 236 | } 237 | } 238 | } 239 | } 240 | 241 | } 242 | 243 | @keyframes client-add-horizontal { 244 | from { 245 | transform: translateX(-200px); 246 | opacity: 0; 247 | } 248 | to { 249 | opacity: 1; 250 | } 251 | } 252 | 253 | @keyframes client-remove-horizontal { 254 | from { 255 | opacity: 1; 256 | } 257 | to { 258 | transform: translateX(-200px); 259 | opacity: 0; 260 | } 261 | } 262 | 263 | .client.vertical { 264 | &.new { 265 | animation: client-add-vertical 300ms ease; 266 | } 267 | 268 | &.removed { 269 | animation: client-remove-vertical 300ms ease; 270 | } 271 | 272 | .icon { 273 | padding-bottom: 5px; 274 | 275 | -gtk-icon-size: 20px; 276 | } 277 | 278 | .volume { 279 | /* Numeric Volume Level */ 280 | padding-top: 10px; 281 | } 282 | 283 | scale { 284 | trough { 285 | /* Slider Bar */ 286 | min-width: 4px; 287 | 288 | margin-top: 10px; 289 | 290 | slider { 291 | /* Slider Knob */ 292 | margin-left: -7px; 293 | margin-right: -7px; 294 | 295 | min-height: 6px; 296 | min-width: 14px; 297 | } 298 | 299 | highlight { 300 | /* Slider Bar Filled */ 301 | background-image: linear-gradient(to bottom, shade(var(--accent), 0.6), var(--accent)); 302 | } 303 | 304 | fill { 305 | /* Slider Peak */ 306 | border-left: 1px solid #{'alpha(var(--accent), 0.8)'}; 307 | border-right: 1px solid #{'alpha(var(--accent), 0.8)'}; 308 | } 309 | } 310 | } 311 | 312 | &.muted { 313 | scale { 314 | trough { 315 | highlight { 316 | /* Slider Bar Filled */ 317 | background-image: linear-gradient(to bottom, shade(var(--accent), 0.3), shade(var(--accent), 0.7)); 318 | } 319 | 320 | fill { 321 | /* Slider Peak */ 322 | border-left: 1px solid shade(var(--accent), 0.5); 323 | border-right: 1px solid shade(var(--accent), 0.5); 324 | } 325 | } 326 | } 327 | } 328 | } 329 | 330 | @keyframes client-add-vertical { 331 | from { 332 | transform: translateY(200px); 333 | opacity: 0; 334 | } 335 | to { 336 | opacity: 1; 337 | } 338 | } 339 | 340 | @keyframes client-remove-vertical { 341 | from { 342 | opacity: 1; 343 | } 344 | to { 345 | transform: translateY(200px); 346 | opacity: 0; 347 | } 348 | } 349 | --------------------------------------------------------------------------------