diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 0538b0a..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -patreon: Aurailus -github: Aurailus diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af30472..17d8831 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,8 @@ -name: release +name: ci on: push: - branches: [ master ] pull_request: - branches: [ master ] env: CARGO_TERM_COLOR: always @@ -15,20 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Install Dependencies - run: sudo apt install libpango1.0-dev libatk1.0-dev libgtk-3-dev libpulse-dev; cargo install cargo-deb - - name: Build Executable - run: cargo build --verbose --release - - name: Upload Executable Artifact - uses: actions/upload-artifact@v2.2.2 + - uses: actions/checkout@v2.3.4 + - uses: cachix/install-nix-action@v14.1 with: - name: myxer - path: target/release/myxer - - name: Build Debian - run: cargo deb --verbose - - name: Upload Debian Artifact - uses: actions/upload-artifact@v2.2.2 - with: - name: Myxer.deb - path: target/debian/Myxer* + nix_path: nixpkgs=channel:nixos-unstable + - run: nix-build diff --git a/.gitignore b/.gitignore index ea8c4bf..d787b70 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/result diff --git a/BUILDING.md b/BUILDING.md index 946b89f..4b81e34 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,11 +1,8 @@ -# Building +# Building with Cargo +Download this repository, Cargo, and `libpulse-dev` & `libgtk-3-dev` system libraries, and run `cargo build --release` in the root directory. -Building Myxer is trivial. Download this repository, Cargo, and `libpulse-dev` & `libgtk-3-dev` system libraries, and run `cargo build --release` in the root directory. - -## Prebuilt Binaries - -Major releases are available on the [Releases](https://github.com/Aurailus/Myxer/releases) page. If you want something more breaking edge, you can download an artifact of the lastest commit [here](https://nightly.link/Aurailus/myxer/workflows/release/master/Myxer.zip). These artifacts are untested, YMMV. - -## Development +# Building with Nix +Run `nix build` +## Development Call `cargo run` to build and run the application. If you have nodemon installed, you can call it on the root directory to automatically watch the source files for changes and recompile. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8171cc..2f2cb45 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,4 +14,4 @@ File names and variable names are `snake_case`'d, structs and traits are `Pascal ## License -Read through the [License](https://github.com/Aurailus/Myxer/blob/master/LICENSE.md) before contributing. All contributions must be under the same license. +Read through the [License](https://github.com/ErinvanderVeen/Myxer/blob/master/LICENSE.md) before contributing. All contributions must be under the same license. diff --git a/Cargo.lock b/Cargo.lock index 7a6dfd9..60c8759 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,30 +1,30 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "anyhow" -version = "1.0.38" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" +checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7" [[package]] name = "atk" -version = "0.9.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812b4911e210bd51b24596244523c856ca749e6223c50a7fbbba3f89ee37c426" +checksum = "a83b21d2aa75e464db56225e1bda2dd5993311ba1095acaa8fa03d1ae67026ba" dependencies = [ "atk-sys", "bitflags", "glib", - "glib-sys", - "gobject-sys", "libc", ] [[package]] name = "atk-sys" -version = "0.10.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f530e4af131d94cc4fa15c5c9d0348f0ef28bac64ba660b6b2a1cf2605dedfce" +checksum = "badcf670157c84bb8b1cf6b5f70b650fed78da2033c9eed84c4e49b11cbe83ea" dependencies = [ "glib-sys", "gobject-sys", @@ -40,30 +40,28 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cairo-rs" -version = "0.9.1" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5c0f2e047e8ca53d0ff249c54ae047931d7a6ebe05d00af73e0ffeb6e34bdb8" +checksum = "33b5725979db0c586d98abad2193cdb612dd40ef95cd26bd99851bf93b3cb482" dependencies = [ "bitflags", "cairo-sys-rs", "glib", - "glib-sys", - "gobject-sys", "libc", "thiserror", ] [[package]] name = "cairo-sys-rs" -version = "0.10.0" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ed2639b9ad5f1d6efa76de95558e11339e7318426d84ac4890b86c03e828ca7" +checksum = "b448b876970834fda82ba3aeaccadbd760206b75388fc5c1b02f1e343b697570" dependencies = [ "glib-sys", "libc", @@ -71,16 +69,19 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.0.67" +name = "cfg-expr" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +checksum = "b412e83326147c2bb881f8b40edfbf9905b9b8abaebd0e47ca190ba62fda8f0e" +dependencies = [ + "smallvec", +] [[package]] name = "colorsys" -version = "0.6.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f002e5b764d885ca647fc2ae36abfec4263d900470dc25c3320ca911b1b83e75" +checksum = "be475c891fad1522a7bfd1cb7204ba8e8fe48a93a0ad6992356aca07a4e0cbce" [[package]] name = "either" @@ -89,41 +90,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] -name = "futures" -version = "0.3.13" +name = "field-offset" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f55667319111d593ba876406af7c409c0ebb44dc4be6132a783ccf163ea14c1" +checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "memoffset", + "rustc_version", ] [[package]] name = "futures-channel" -version = "0.3.13" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939" +checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" dependencies = [ "futures-core", - "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.13" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" [[package]] name = "futures-executor" -version = "0.3.13" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891a4b7b96d84d5940084b2a37632dd65deeae662c114ceaa2c879629c9c0ad1" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" dependencies = [ "futures-core", "futures-task", @@ -132,94 +127,63 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.13" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" - -[[package]] -name = "futures-macro" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea405816a5139fb39af82c2beb921d52143f556038378d6db21183a5c37fbfb7" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" [[package]] name = "futures-task" -version = "0.3.13" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" [[package]] name = "futures-util" -version = "0.3.13" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" dependencies = [ - "futures-channel", + "autocfg", "futures-core", - "futures-io", - "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] [[package]] name = "gdk" -version = "0.13.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db00839b2a68a7a10af3fa28dfb3febaba3a20c3a9ac2425a33b7df1f84a6b7d" +checksum = "b9d749dcfc00d8de0d7c3a289e04a04293eb5ba3d8a4e64d64911d481fa9933b" dependencies = [ "bitflags", "cairo-rs", - "cairo-sys-rs", "gdk-pixbuf", "gdk-sys", "gio", - "gio-sys", "glib", - "glib-sys", - "gobject-sys", "libc", "pango", ] [[package]] name = "gdk-pixbuf" -version = "0.9.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6dae3cb99dd49b758b88f0132f8d401108e63ae8edd45f432d42cdff99998a" +checksum = "534192cb8f01daeb8fab2c8d4baa8f9aae5b7a39130525779f5c2608e235b10f" dependencies = [ "gdk-pixbuf-sys", "gio", - "gio-sys", "glib", - "glib-sys", - "gobject-sys", "libc", ] [[package]] name = "gdk-pixbuf-sys" -version = "0.10.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bfe468a7f43e97b8d193a762b6c5cf67a7d36cacbc0b9291dbcae24bfea1e8f" +checksum = "f097c0704201fbc8f69c1762dc58c6947c8bb188b8ed0bc7e65259f1894fe590" dependencies = [ "gio-sys", "glib-sys", @@ -230,9 +194,9 @@ dependencies = [ [[package]] name = "gdk-sys" -version = "0.10.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9653cfc500fd268015b1ac055ddbc3df7a5c9ea3f4ccef147b3957bd140d69" +checksum = "0e091b3d3d6696949ac3b3fb3c62090e5bfd7bd6850bef5c3c5ea701de1b1f1e" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -247,20 +211,16 @@ dependencies = [ [[package]] name = "gio" -version = "0.9.1" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb60242bfff700772dae5d9e3a1f7aa2e4ebccf18b89662a16acb2822568561" +checksum = "711c3632b3ebd095578a9c091418d10fed492da9443f58ebc8f45efbeb215cb0" dependencies = [ "bitflags", - "futures", "futures-channel", "futures-core", "futures-io", - "futures-util", "gio-sys", "glib", - "glib-sys", - "gobject-sys", "libc", "once_cell", "thiserror", @@ -268,9 +228,9 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.10.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e24fb752f8f5d2cf6bbc2c606fd2bc989c81c5e2fe321ab974d54f8b6344eac" +checksum = "c0a41df66e57fcc287c4bcf74fc26b884f31901ea9792ec75607289b456f48fa" dependencies = [ "glib-sys", "gobject-sys", @@ -281,32 +241,31 @@ dependencies = [ [[package]] name = "glib" -version = "0.10.3" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" +checksum = "7c515f1e62bf151ef6635f528d05b02c11506de986e43b34a5c920ef0b3796a4" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-executor", "futures-task", - "futures-util", "glib-macros", "glib-sys", "gobject-sys", "libc", "once_cell", + "smallvec", ] [[package]] name = "glib-macros" -version = "0.10.1" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" +checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" dependencies = [ "anyhow", "heck", - "itertools", "proc-macro-crate", "proc-macro-error", "proc-macro2", @@ -316,9 +275,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.10.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" +checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" dependencies = [ "libc", "system-deps", @@ -326,9 +285,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.10.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" +checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" dependencies = [ "glib-sys", "libc", @@ -337,37 +296,32 @@ dependencies = [ [[package]] name = "gtk" -version = "0.9.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f022f2054072b3af07666341984562c8e626a79daa8be27b955d12d06a5ad6a" +checksum = "2eb51122dd3317e9327ec1e4faa151d1fa0d95664cd8fb8dcfacf4d4d29ac70c" dependencies = [ "atk", "bitflags", "cairo-rs", - "cairo-sys-rs", - "cc", + "field-offset", + "futures-channel", "gdk", "gdk-pixbuf", - "gdk-pixbuf-sys", - "gdk-sys", "gio", - "gio-sys", "glib", - "glib-sys", - "gobject-sys", "gtk-sys", + "gtk3-macros", "libc", "once_cell", "pango", - "pango-sys", "pkg-config", ] [[package]] name = "gtk-sys" -version = "0.10.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89acda6f084863307d948ba64a4b1ef674e8527dddab147ee4cdcc194c880457" +checksum = "8c14c8d3da0545785a7c5a120345b3abb534010fb8ae0f2ef3f47c027fba303e" dependencies = [ "atk-sys", "cairo-sys-rs", @@ -381,35 +335,50 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gtk3-macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21de1da96dc117443fb03c2e270b2d34b7de98d0a79a19bbb689476173745b79" +dependencies = [ + "anyhow", + "heck", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "heck" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] [[package]] name = "itertools" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" dependencies = [ "either", ] [[package]] name = "libc" -version = "0.2.86" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" +checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673" [[package]] name = "libpulse-binding" -version = "2.23.0" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2405f806801527dfb3d2b6d48a282cdebe9a1b41b0652e0d7b5bad81dbc700e" +checksum = "86835d7763ded6bc16b6c0061ec60214da7550dfcd4ef93745f6f0096129676a" dependencies = [ "bitflags", "libc", @@ -421,9 +390,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.18.0" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf17e9832643c4f320c42b7d78b2c0510f45aa5e823af094413b94e45076ba82" +checksum = "f12950b69c1b66233a900414befde36c8d4ea49deec1e1f34e4cd2f586e00c7d" dependencies = [ "libc", "num-derive", @@ -433,10 +402,13 @@ dependencies = [ ] [[package]] -name = "memchr" -version = "2.3.4" +name = "memoffset" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] [[package]] name = "myxer" @@ -474,20 +446,18 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad167a2f54e832b82dbe003a046280dceffe5227b5f79e08e363a29638cfddd" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "pango" -version = "0.9.1" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9937068580bebd8ced19975938573803273ccbcbd598c58d4906efd4ac87c438" +checksum = "546fd59801e5ca735af82839007edd226fe7d3bb06433ec48072be4439c28581" dependencies = [ "bitflags", "glib", - "glib-sys", - "gobject-sys", "libc", "once_cell", "pango-sys", @@ -495,9 +465,9 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.10.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d2650c8b62d116c020abd0cea26a4ed96526afda89b1c4ea567131fdefc890" +checksum = "2367099ca5e761546ba1d501955079f097caa186bb53ce0f718dca99ac1942fe" dependencies = [ "glib-sys", "gobject-sys", @@ -505,11 +475,20 @@ dependencies = [ "system-deps", ] +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + [[package]] name = "pin-project-lite" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pin-utils" @@ -519,16 +498,17 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" [[package]] name = "proc-macro-crate" -version = "0.1.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" dependencies = [ + "thiserror", "toml", ] @@ -557,46 +537,61 @@ dependencies = [ ] [[package]] -name = "proc-macro-hack" -version = "0.5.19" +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] [[package]] -name = "proc-macro-nested" -version = "0.1.7" +name = "rustc_version" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver", +] [[package]] -name = "proc-macro2" -version = "1.0.24" +name = "semver" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" dependencies = [ - "unicode-xid", + "semver-parser", ] [[package]] -name = "quote" -version = "1.0.9" +name = "semver-parser" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" dependencies = [ - "proc-macro2", + "pest", ] [[package]] name = "serde" -version = "1.0.123" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" [[package]] name = "slab" -version = "0.4.2" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "slice_as_array" @@ -604,17 +599,23 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64c963ee59ddedb5ab95dc2cd97c48b4a292572a52c5636fbbabdb9985bfe4c3" +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + [[package]] name = "strum" -version = "0.18.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" [[package]] name = "strum_macros" -version = "0.18.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" dependencies = [ "heck", "proc-macro2", @@ -624,9 +625,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.60" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" dependencies = [ "proc-macro2", "quote", @@ -635,11 +636,14 @@ dependencies = [ [[package]] name = "system-deps" -version = "1.3.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" +checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" dependencies = [ + "anyhow", + "cfg-expr", "heck", + "itertools", "pkg-config", "strum", "strum_macros", @@ -650,18 +654,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.24" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.24" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -677,29 +681,35 @@ dependencies = [ "serde", ] +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + [[package]] name = "unicode-segmentation" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-xid" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "version-compare" -version = "0.0.10" +version = "0.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" [[package]] name = "winapi" diff --git a/Cargo.toml b/Cargo.toml index 0b1cb09..cc5b9de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,37 +4,26 @@ version = "1.2.1" description = "A modern Volume Mixer for PulseAudio" readme = "README.md" license = "GPL-3.0" -authors = [ "Auri " ] -homepage = "https://myxer.aurailus.com" -repository = "https://github.com/Aurailus/Myxer" +authors = [ "Auri ", "Erin van der Veen " ] +homepage = "https://github.com/ErinvanderVeen/Myxer" +repository = "https://github.com/ErinvanderVeen/Myxer" edition = "2018" -[package.metadata.deb] -name = "Myxer" -section = "sound" -copyright = "(c) Auri Collings, 2021" -extended-description-file = "DEBIAN.txt" -assets = [ - ["target/release/myxer", "usr/bin/", "755"], - ["README.md", "usr/share/doc/Myxer/README", "644"], - ["Myxer.desktop", "/usr/share/applications/", "644"] -] - [dependencies] -gdk = "0.13.2" -glib = "0.10.3" -pango = "0.9.1" -colorsys = "0.6.3" -slice_as_array = "1.1.0" +gdk = "0.14" +glib = "0.14" +pango = "0.14" +colorsys = "0.5" +slice_as_array = "1.1" [dependencies.libpulse] -version = "2.23.0" +version = "2.25" package = "libpulse-binding" [dependencies.gtk] -version = "0.9.0" -features = [ "v3_22" ] +version = "0.14" +features = [ "v3_24" ] [dependencies.gio] -version = "" -features = [ "v2_44" ] +version = "0.14" +features = [ "v2_66" ] diff --git a/DEBIAN.txt b/DEBIAN.txt deleted file mode 100644 index 48f07f2..0000000 --- a/DEBIAN.txt +++ /dev/null @@ -1 +0,0 @@ -Myxer is a lightweight, powerful Volume Mixer. Devices, Streams, and even Card profiles can be managed, providing a complete replacement for the stock Volume Mixer. diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index 1f4866a..0000000 --- a/PKGBUILD +++ /dev/null @@ -1,24 +0,0 @@ -pkgname=myxer -_pkgname=Myxer -pkgver=1.2.1 -pkgrel=1 -pkgdesc='A modern Volume Mixer for PulseAudio, built with you in mind.' -url='https://github.com/Aurailus/Myxer' -source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/$pkgver.tar.gz") -arch=('any') -license=('GPL3') -makedepends=('cargo') -depends=('pulseaudio' 'gtk3') -sha256sums=('4784746fd491d51397b3c47eb5ed5cf3f04ba54a116c192620bb532db2c2d550') - -build () { - cd "$srcdir/$_pkgname-$pkgver" - - cargo build --release -} - -package() { - cd "$srcdir/$_pkgname-$pkgver" - - install -Dm755 target/release/$pkgname "${pkgdir}/usr/bin/myxer" -} diff --git a/README.md b/README.md index ef99f3b..53e827a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,13 @@

Myxer

- +

A modern Volume Mixer for PulseAudio, built with you in mind.

- Releases - Commit Activity - Join Discord - Support on Patreon + Releases


@@ -20,7 +17,7 @@ Myxer is a lightweight, powerful Volume Mixer built with modern UI design for a

- + ### Adaptive @@ -33,7 +30,7 @@ Additionally, one can easily configure PulseAudio panel plugins to open Myxer wh

- + ### Advanced @@ -45,18 +42,18 @@ Behind the context menu, there are options to show individual audio channels and

Open Source

-Myxer is licensed under the [GNU General Public License v3](https://github.com/Aurailus/Myxer/blob/master/LICENSE.md). It's being actively developed, and all issues and pull requests will be responded to promptly. It's also super lightweight, and should only take an hour or two to read through the source code, if that's your sort of thing. See [Contributing](https://github.com/Aurailus/Myxer/blob/master/CONTRIBUTING.md) for more details. +Myxer is licensed under the [GNU General Public License v3](https://github.com/ErinvanderVeen/Myxer/blob/master/LICENSE.md). It's being actively developed, and all issues and pull requests will be responded to promptly. It's also super lightweight, and should only take an hour or two to read through the source code, if that's your sort of thing. See [Contributing](https://github.com/ErinvanderVeen/Myxer/blob/master/CONTRIBUTING.md) for more details.

-

Heard enough? Download the Latest Release now.

+

Heard enough? Download the Latest Release now.

-

Latest Artifact (Deb)   •   Building   •   Contributing

+

Building   •   Contributing




© [Auri Collings](https://twitter.com/Aurailus), 2021. Made with <3 - +© Erin van der Veen, 2021. diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..20add04 --- /dev/null +++ b/default.nix @@ -0,0 +1,37 @@ +{ pkgs ? import { }, isShell ? false }: + +pkgs.rustPlatform.buildRustPackage rec { + pname = "myxer"; + version = "1.2.1"; + + src = ./.; + + cargoLock = { lockFile = ./Cargo.lock; }; + + nativeBuildInputs = [ pkgs.pkg-config ]; + + buildInputs = with pkgs; + if isShell then [ + rust-analyzer + libpulseaudio + glib + pango + gtk3 + ] else [ + libpulseaudio + glib + pango + gtk3 + ]; + + # Currently no tests are implemented, so we avoid building the package twice + doCheck = false; + + meta = with pkgs.lib; { + description = "A modern Volume Mixer for PulseAudio"; + homepage = "https://github.com/Aurailus/Myxer"; + license = licenses.gpl3Only; + maintainers = with maintainers; [ erin ]; + platforms = platforms.linux; + }; +} diff --git a/nodemon.json b/nodemon.json deleted file mode 100755 index 633105b..0000000 --- a/nodemon.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "watch": ["src"], - "ext": ".rs", - "exec": "RUST_BACKTRACE=1 cargo run" -} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..bcad0ad --- /dev/null +++ b/shell.nix @@ -0,0 +1 @@ +import ./default.nix { isShell = true; } diff --git a/src/card.rs b/src/card.rs index 0b8563e..5598dff 100644 --- a/src/card.rs +++ b/src/card.rs @@ -3,41 +3,38 @@ * Has a dropdown that allows the current card profile to be changed. */ +use std::cell::RefCell; + use gtk::prelude::*; -use glib::translate::ToGlib; -use glib::translate::FromGlib; use crate::pulse::Pulse; use crate::shared::Shared; - /** * Holds a Card's data. */ #[derive(Debug, Clone, Default)] pub struct CardData { - pub index: u32, - - pub name: String, - pub icon: String, + pub index: u32, - pub profiles: Vec<(String, String)>, - pub active_profile: String -} + pub name: String, + pub icon: String, + pub profiles: Vec<(String, String)>, + pub active_profile: String, +} /** * Holds a Card's widgets. */ struct CardWidgets { - root: gtk::Box, - - label: gtk::Label, - combo: gtk::ComboBoxText, -} + root: gtk::Box, + label: gtk::Label, + combo: gtk::ComboBoxText, +} /** * A widget representing a pulseaudio sound card. @@ -45,122 +42,123 @@ struct CardWidgets { */ pub struct Card { - pub widget: gtk::Box, - widgets: CardWidgets, + pub widget: gtk::Box, + widgets: CardWidgets, - data: CardData, - pulse: Option>, - combo_connect_id: Option, + data: CardData, + pulse: Option>, + combo_connect_id: RefCell>, } impl Card { - - /** - * Constructs the GTK widgets required for the card widget. - */ - - fn build() -> CardWidgets { - let root = gtk::Box::new(gtk::Orientation::Horizontal, 0); - root.set_widget_name("card"); - - let inner = gtk::Box::new(gtk::Orientation::Vertical, 0); - inner.set_border_width(3); - root.pack_start(&inner, true, true, 3); - - let label_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); - label_box.set_border_width(0); - inner.pack_start(&label_box, false, false, 3); - - let icon = gtk::Image::from_icon_name(Some("audio-card"), gtk::IconSize::LargeToolbar); - let label = gtk::Label::new(Some("Unknown Card")); - - label_box.pack_start(&icon, false, false, 3); - label_box.pack_start(&label, false, true, 3); - - let combo = gtk::ComboBoxText::new(); - inner.pack_start(&combo, false, false, 6); - - CardWidgets { - root, - label, - combo - } - } - - - /** - * Creates a new card widget. - */ - - pub fn new(pulse: Option>) -> Self { - let widgets = Card::build(); - Self { - widget: widgets.root.clone(), widgets, - data: CardData::default(), - pulse, - combo_connect_id: None - } - } - - - /** - * Disconnect's a card widget from the Pulse instance, - * in the event that significant information has changed for the callbacks to be invalid. - */ - - fn disconnect(&mut self) { - if self.combo_connect_id.is_some() { - self.widgets.combo.disconnect(glib::signal::SignalHandlerId::from_glib(self.combo_connect_id.as_ref().unwrap().to_glib())); - } - } - - - /** - * Connects a callback to the widget's dropdown, - * so that changing the selected option changes the card's profile. - * If the Pulse instance is None, the callback is not bound. - */ - - fn connect(&mut self) { - if self.pulse.is_none() { return; } - - let index = self.data.index; - let pulse = self.pulse.as_ref().unwrap().clone(); - self.combo_connect_id = Some(self.widgets.combo.connect_changed(move |combo| { - pulse.borrow_mut().set_card_profile(index, &String::from(combo.get_active_id().unwrap())); - })); - } - - - /** - * Updates the Card's data, and visually refreshes the required components. - */ - - pub fn set_data(&mut self, data: &CardData) { - if data.index != self.data.index { - self.data.index = data.index; - self.disconnect(); - self.connect(); - } - - if data.name != self.data.name { - self.data.name = data.name.clone(); - self.widgets.label.set_label(&self.data.name); - } - - if data.profiles.len() != self.data.profiles.len() { - self.disconnect(); - self.data.profiles = data.profiles.clone(); - self.widgets.combo.remove_all(); - for (i, n) in &data.profiles { self.widgets.combo.append(Some(&i), &n); } - self.connect(); - } - - if data.active_profile != self.data.active_profile { - self.disconnect(); - self.data.active_profile = data.active_profile.clone(); - self.widgets.combo.set_active_id(Some(&self.data.active_profile)); - self.connect(); - } - } + /** + * Constructs the GTK widgets required for the card widget. + */ + + fn build() -> CardWidgets { + let root = gtk::Box::new(gtk::Orientation::Horizontal, 0); + root.set_widget_name("card"); + + let inner = gtk::Box::new(gtk::Orientation::Vertical, 0); + inner.set_border_width(3); + root.pack_start(&inner, true, true, 3); + + let label_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + label_box.set_border_width(0); + inner.pack_start(&label_box, false, false, 3); + + let icon = gtk::Image::from_icon_name(Some("audio-card"), gtk::IconSize::LargeToolbar); + let label = gtk::Label::new(Some("Unknown Card")); + + label_box.pack_start(&icon, false, false, 3); + label_box.pack_start(&label, false, true, 3); + + let combo = gtk::ComboBoxText::new(); + inner.pack_start(&combo, false, false, 6); + + CardWidgets { root, label, combo } + } + + /** + * Creates a new card widget. + */ + + pub fn new(pulse: Option>) -> Self { + let widgets = Card::build(); + Self { + widget: widgets.root.clone(), + widgets, + data: CardData::default(), + pulse, + combo_connect_id: RefCell::new(None), + } + } + + /** + * Disconnect's a card widget from the Pulse instance, + * in the event that significant information has changed for the callbacks to be invalid. + */ + + fn disconnect(&self) { + if let Some(id) = self.combo_connect_id.borrow_mut().take() { + self.widgets.combo.disconnect(id); + } + } + + /** + * Connects a callback to the widget's dropdown, + * so that changing the selected option changes the card's profile. + * If the Pulse instance is None, the callback is not bound. + */ + + fn connect(&mut self) { + if self.pulse.is_none() { + return; + } + + let index = self.data.index; + let pulse = self.pulse.as_ref().unwrap().clone(); + self.combo_connect_id + .replace(Some(self.widgets.combo.connect_changed(move |combo| { + pulse + .borrow_mut() + .set_card_profile(index, &String::from(combo.active_id().unwrap())); + }))); + } + + /** + * Updates the Card's data, and visually refreshes the required components. + */ + + pub fn set_data(&mut self, data: &CardData) { + if data.index != self.data.index { + self.data.index = data.index; + self.disconnect(); + self.connect(); + } + + if data.name != self.data.name { + self.data.name = data.name.clone(); + self.widgets.label.set_label(&self.data.name); + } + + if data.profiles.len() != self.data.profiles.len() { + self.disconnect(); + self.data.profiles = data.profiles.clone(); + self.widgets.combo.remove_all(); + for (i, n) in &data.profiles { + self.widgets.combo.append(Some(&i), &n); + } + self.connect(); + } + + if data.active_profile != self.data.active_profile { + self.disconnect(); + self.data.active_profile = data.active_profile.clone(); + self.widgets + .combo + .set_active_id(Some(&self.data.active_profile)); + self.connect(); + } + } } diff --git a/src/main.rs b/src/main.rs index ab5064e..f7b3bcc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,18 +5,17 @@ #![allow(clippy::tabs_in_doc_comments)] -use gio::prelude::*; - mod card; mod meter; mod pulse; -mod window; mod shared; +mod window; +use gdk::prelude::{ApplicationExt, ApplicationExtManual}; use pulse::Pulse; -use window::Myxer; use shared::Shared; - +use std::time::Duration; +use window::Myxer; /** * Attempts to start the application. @@ -28,19 +27,17 @@ use shared::Shared; */ fn main() { - let pulse = Shared::new(Pulse::new()); - - let app = gtk::Application::new(Some("com.aurailus.myxer"), Default::default()) - .expect("Failed to initialize GTK application."); + let pulse = Shared::new(Pulse::new()); - let pulse_clone = pulse.clone(); - app.connect_activate(|app| drop(app.register::(None))); - app.connect_startup(move |app| activate(app, &pulse_clone)); - app.run(&[]); + let app = gtk::Application::new(Some("com.aurailus.myxer"), Default::default()); - pulse.borrow_mut().cleanup(); -} + let pulse_clone = pulse.clone(); + app.connect_activate(|app| drop(app.register::(None))); + app.connect_startup(move |app| activate(app, &pulse_clone)); + app.run(); + pulse.borrow_mut().cleanup(); +} /** * Called by GTK when the application has initialized. Creates the main Myxer @@ -48,10 +45,10 @@ fn main() { */ fn activate(app: >k::Application, pulse: &Shared) { - let mut myxer = Myxer::new(app, pulse); + let mut myxer = Myxer::new(app, pulse); - glib::timeout_add_local(1000 / 30, move || { - myxer.update(); - glib::Continue(true) - }); + glib::timeout_add_local(Duration::from_millis(1000 / 30), move || { + myxer.update(); + glib::Continue(true) + }); } diff --git a/src/meter/base_meter.rs b/src/meter/base_meter.rs index b0210c5..fc0ad5b 100644 --- a/src/meter/base_meter.rs +++ b/src/meter/base_meter.rs @@ -3,10 +3,10 @@ */ use gtk::prelude::*; -use libpulse::volume::{ Volume, ChannelVolumes }; +use libpulse::volume::{ChannelVolumes, Volume}; +use crate::pulse::{Pulse, StreamType}; use crate::shared::Shared; -use crate::pulse::{ Pulse, StreamType }; /** The maximum natural volume, i.e. 100% */ pub const MAX_NATURAL_VOL: u32 = 65536; @@ -18,13 +18,20 @@ pub const MAX_SCALE_VOL: u32 = (MAX_NATURAL_VOL as f64 * 1.5) as u32; pub const SCALE_STEP: f64 = MAX_NATURAL_VOL as f64 / 20.0; /** The icon names for the input meter statuses. */ -pub const INPUT_ICONS: [&str; 4] = [ "microphone-sensitivity-muted-symbolic", "microphone-sensitivity-low-symbolic", - "microphone-sensitivity-medium-symbolic", "microphone-sensitivity-high-symbolic" ]; +pub const INPUT_ICONS: [&str; 4] = [ + "microphone-sensitivity-muted-symbolic", + "microphone-sensitivity-low-symbolic", + "microphone-sensitivity-medium-symbolic", + "microphone-sensitivity-high-symbolic", +]; /** The icon names for the output meter statuses. */ -pub const OUTPUT_ICONS: [&str; 4] = [ "audio-volume-muted-symbolic", "audio-volume-low-symbolic", - "audio-volume-medium-symbolic", "audio-volume-high-symbolic" ]; - +pub const OUTPUT_ICONS: [&str; 4] = [ + "audio-volume-muted-symbolic", + "audio-volume-low-symbolic", + "audio-volume-medium-symbolic", + "audio-volume-high-symbolic", +]; /** * Holds a Meter widget's display data. @@ -32,37 +39,35 @@ pub const OUTPUT_ICONS: [&str; 4] = [ "audio-volume-muted-symbolic", "audio-volu #[derive(Debug, Clone, Default)] pub struct MeterData { - pub t: StreamType, - pub index: u32, + pub t: StreamType, + pub index: u32, - pub name: String, - pub icon: String, - pub description: String, + pub name: String, + pub icon: String, + pub description: String, - pub volume: ChannelVolumes, - pub muted: bool, + pub volume: ChannelVolumes, + pub muted: bool, } - /** * Holds references to a Meter's widgets. */ pub struct MeterWidgets { - pub root: gtk::Box, - - pub icon: gtk::Image, - pub label: gtk::Label, - pub select: gtk::Button, - pub app_button: gtk::Button, - - pub status: gtk::Button, - pub status_icon: gtk::Image, - - pub scales_outer: gtk::Box, - pub scales_inner: gtk::Box, -} + pub root: gtk::Box, + + pub icon: gtk::Image, + pub label: gtk::Label, + pub select: gtk::Button, + pub app_button: gtk::Button, + + pub status: gtk::Button, + pub status_icon: gtk::Image, + pub scales_outer: gtk::Box, + pub scales_inner: gtk::Box, +} /** * The base trait for all Meter widgets. @@ -71,199 +76,203 @@ pub struct MeterWidgets { */ pub trait Meter { - /** - * Gets the meter's underlying stream index. - */ - - fn get_index(&self) -> u32; - + /** + * Gets the meter's underlying stream index. + */ - /** - * Sets whether or not to split channels into individual meters. - * - * * `split` - Whether or not channels should be separated. - */ + fn get_index(&self) -> u32; - fn split_channels(&mut self, split: bool); + /** + * Sets whether or not to split channels into individual meters. + * + * * `split` - Whether or not channels should be separated. + */ + fn split_channels(&mut self, split: bool); - /** - * Updates the meter's data, and visually refreshes the required widgets. - */ + /** + * Updates the meter's data, and visually refreshes the required widgets. + */ - fn set_data(&mut self, data: &MeterData); + fn set_data(&mut self, data: &MeterData); + /** + * Sets the meter's current peak. + * + * * `peak` - The meter's peak, or None if no peak indicator should be shown. + */ - /** - * Sets the meter's current peak. - * - * * `peak` - The meter's peak, or None if no peak indicator should be shown. - */ - - fn set_peak(&mut self, peak: Option); + fn set_peak(&mut self, peak: Option); } impl dyn Meter { - - /** - * Builds a scale. This may be for a single channel, or all channels. - */ - - fn build_scale() -> gtk::Scale { - let scale = gtk::Scale::with_range(gtk::Orientation::Vertical, 0.0, MAX_SCALE_VOL as f64, SCALE_STEP); - - scale.set_inverted(true); - scale.set_draw_value(false); - scale.set_increments(SCALE_STEP, SCALE_STEP); - scale.set_restrict_to_fill_level(false); - - scale.add_mark(0.0, gtk::PositionType::Right, Some("")); - scale.add_mark(MAX_SCALE_VOL as f64, gtk::PositionType::Right, Some("")); - scale.add_mark(MAX_NATURAL_VOL as f64, gtk::PositionType::Right, Some("")); - - scale - } - - - /** - * Builds the required scales for a Meter. - * This may be one or more, depending on the state of the `split` variable. - * - * * `pulse` - The pulse store to bind events to. - * * `data` - The meter data to base the scales off of. - * * `split` - Whether or not one merged bar should be created, or individual bars for each channel. - */ - - pub fn build_scales(pulse: &Shared, data: &MeterData, split: bool) -> gtk::Box { - let t = data.t; - let index = data.index; - - let pulse = pulse.clone(); - let scales_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); - - if split { - for _ in 0 .. data.volume.len() { - let scale = Meter::build_scale(); - let pulse = pulse.clone(); - - scale.connect_change_value(move |scale, _, val| { - let parent = scale.get_parent().unwrap().downcast::().unwrap(); - let children = parent.get_children(); - - let mut volumes = ChannelVolumes::default(); - - // So, if you're wondering why rev() is necessary or why I set the len after or why this is horrible in general, - // Check out libpulse_binding::volumes::ChannelVolumes::set, and you'll see ._. - for (i, w) in children.iter().enumerate().rev() { - let s = w.clone().downcast::().unwrap(); - let value = if *scale == s { val } else { s.get_value() }; - let volume = Volume(value as u32); - volumes.set(i as u8 + 1, volume); - } - - volumes.set_len(children.len() as u8); - - let pulse = pulse.borrow_mut(); - pulse.set_volume(t, index, volumes); - if volumes.max().0 > 0 { pulse.set_muted(t, index, false); } - gtk::Inhibit(false) - }); - - scales_box.pack_start(&scale, false, false, 0); - } - } - else { - let scale = Meter::build_scale(); - let channels = data.volume.len(); - let pulse = pulse.clone(); - scale.connect_change_value(move |_, _, value| { - let mut volumes = ChannelVolumes::default(); - volumes.set_len(channels); - volumes.set(channels, Volume(value as u32)); - let pulse = pulse.borrow_mut(); - pulse.set_volume(t, index, volumes); - if volumes.max().0 > 0 { pulse.set_muted(t, index, false); } - gtk::Inhibit(false) - }); - scales_box.pack_start(&scale, false, false, 0); - } - - scales_box.show_all(); - scales_box - } - - - /** - * Initializes all of the Widgets to make a meter, and returns them. - */ - - pub fn build_meter() -> MeterWidgets { - let root = gtk::Box::new(gtk::Orientation::Vertical, 0); - root.set_widget_name("meter"); - - root.set_orientation(gtk::Orientation::Vertical); - root.set_hexpand(false); - root.set_size_request(86, -1); - - let app_button = gtk::Button::new(); - app_button.set_widget_name("top"); - app_button.get_style_context().add_class("flat"); - let label_container = gtk::Box::new(gtk::Orientation::Vertical, 0); - app_button.add(&label_container); - - let icon = gtk::Image::from_icon_name(Some("audio-volume-muted-symbolic"), gtk::IconSize::Dnd); - - let label = gtk::Label::new(Some("Unknown")); - label.set_widget_name("app_label"); - - label.set_size_request(-1, 42); - label.set_justify(gtk::Justification::Center); - label.set_ellipsize(pango::EllipsizeMode::End); - label.set_line_wrap_mode(pango::WrapMode::WordChar); - label.set_max_width_chars(8); - label.set_line_wrap(true); - label.set_lines(2); - - label_container.pack_end(&label, false, true, 0); - label_container.pack_end(&icon, false, false, 3); - - let select = gtk::Button::new(); - select.set_widget_name("app_select"); - select.get_style_context().add_class("flat"); - - let scales_outer = gtk::Box::new(gtk::Orientation::Horizontal, 0); - let scales_inner = gtk::Box::new(gtk::Orientation::Horizontal, 0); - scales_outer.pack_start(&scales_inner, true, false, 0); - - let status_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); - let status_icon = gtk::Image::from_icon_name(Some("audio-volume-muted-symbolic"), gtk::IconSize::Button); - - let status = gtk::Button::new(); - status.set_widget_name("mute_toggle"); - status_box.pack_start(&status, true, false, 0); - - status.set_image(Some(&status_icon)); - status.set_always_show_image(true); - status.get_style_context().add_class("flat"); - status.get_style_context().add_class("muted"); - - root.pack_end(&status_box, false, false, 3); - root.pack_end(&scales_outer, true, true, 2); - root.pack_start(&app_button, false, false, 0); - - MeterWidgets { - root, - - icon, - label, - select, - app_button, - - status, - status_icon, - - scales_outer, - scales_inner - } - } + /** + * Builds a scale. This may be for a single channel, or all channels. + */ + + fn build_scale() -> gtk::Scale { + let scale = gtk::Scale::with_range( + gtk::Orientation::Vertical, + 0.0, + MAX_SCALE_VOL as f64, + SCALE_STEP, + ); + + scale.set_inverted(true); + scale.set_draw_value(false); + scale.set_increments(SCALE_STEP, SCALE_STEP); + scale.set_restrict_to_fill_level(false); + + scale.add_mark(0.0, gtk::PositionType::Right, Some("")); + scale.add_mark(MAX_SCALE_VOL as f64, gtk::PositionType::Right, Some("")); + scale.add_mark(MAX_NATURAL_VOL as f64, gtk::PositionType::Right, Some("")); + + scale + } + + /** + * Builds the required scales for a Meter. + * This may be one or more, depending on the state of the `split` variable. + * + * * `pulse` - The pulse store to bind events to. + * * `data` - The meter data to base the scales off of. + * * `split` - Whether or not one merged bar should be created, or individual bars for each channel. + */ + + pub fn build_scales(pulse: &Shared, data: &MeterData, split: bool) -> gtk::Box { + let t = data.t; + let index = data.index; + + let pulse = pulse.clone(); + let scales_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + + if split { + for _ in 0..data.volume.len() { + let scale = ::build_scale(); + let pulse = pulse.clone(); + + scale.connect_change_value(move |scale, _, val| { + let parent = scale.parent().unwrap().downcast::().unwrap(); + let children = parent.children(); + + let mut volumes = ChannelVolumes::default(); + + // So, if you're wondering why rev() is necessary or why I set the len after or why this is horrible in general, + // Check out libpulse_binding::volumes::ChannelVolumes::set, and you'll see ._. + for (i, w) in children.iter().enumerate().rev() { + let s = w.clone().downcast::().unwrap(); + let value = if *scale == s { val } else { s.value() }; + let volume = Volume(value as u32); + volumes.set(i as u8 + 1, volume); + } + + volumes.set_len(children.len() as u8); + + let pulse = pulse.borrow_mut(); + pulse.set_volume(t, index, volumes); + if volumes.max().0 > 0 { + pulse.set_muted(t, index, false); + } + gtk::Inhibit(false) + }); + + scales_box.pack_start(&scale, false, false, 0); + } + } else { + let scale = ::build_scale(); + let channels = data.volume.len(); + let pulse = pulse.clone(); + scale.connect_change_value(move |_, _, value| { + let mut volumes = ChannelVolumes::default(); + volumes.set_len(channels); + volumes.set(channels, Volume(value as u32)); + let pulse = pulse.borrow_mut(); + pulse.set_volume(t, index, volumes); + if volumes.max().0 > 0 { + pulse.set_muted(t, index, false); + } + gtk::Inhibit(false) + }); + scales_box.pack_start(&scale, false, false, 0); + } + + scales_box.show_all(); + scales_box + } + + /** + * Initializes all of the Widgets to make a meter, and returns them. + */ + + pub fn build_meter() -> MeterWidgets { + let root = gtk::Box::new(gtk::Orientation::Vertical, 0); + root.set_widget_name("meter"); + + root.set_orientation(gtk::Orientation::Vertical); + root.set_hexpand(false); + root.set_size_request(86, -1); + + let app_button = gtk::Button::new(); + app_button.set_widget_name("top"); + app_button.style_context().add_class("flat"); + let label_container = gtk::Box::new(gtk::Orientation::Vertical, 0); + app_button.add(&label_container); + + let icon = + gtk::Image::from_icon_name(Some("audio-volume-muted-symbolic"), gtk::IconSize::Dnd); + + let label = gtk::Label::new(Some("Unknown")); + label.set_widget_name("app_label"); + + label.set_size_request(-1, 42); + label.set_justify(gtk::Justification::Center); + label.set_ellipsize(pango::EllipsizeMode::End); + label.set_line_wrap_mode(pango::WrapMode::WordChar); + label.set_max_width_chars(8); + label.set_line_wrap(true); + label.set_lines(2); + + label_container.pack_end(&label, false, true, 0); + label_container.pack_end(&icon, false, false, 3); + + let select = gtk::Button::new(); + select.set_widget_name("app_select"); + select.style_context().add_class("flat"); + + let scales_outer = gtk::Box::new(gtk::Orientation::Horizontal, 0); + let scales_inner = gtk::Box::new(gtk::Orientation::Horizontal, 0); + scales_outer.pack_start(&scales_inner, true, false, 0); + + let status_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + let status_icon = + gtk::Image::from_icon_name(Some("audio-volume-muted-symbolic"), gtk::IconSize::Button); + + let status = gtk::Button::new(); + status.set_widget_name("mute_toggle"); + status_box.pack_start(&status, true, false, 0); + + status.set_image(Some(&status_icon)); + status.set_always_show_image(true); + status.style_context().add_class("flat"); + status.style_context().add_class("muted"); + + root.pack_end(&status_box, false, false, 3); + root.pack_end(&scales_outer, true, true, 2); + root.pack_start(&app_button, false, false, 0); + + MeterWidgets { + root, + + icon, + label, + select, + app_button, + + status, + status_icon, + + scales_outer, + scales_inner, + } + } } diff --git a/src/meter/sink_meter.rs b/src/meter/sink_meter.rs index f690f56..4177c87 100644 --- a/src/meter/sink_meter.rs +++ b/src/meter/sink_meter.rs @@ -3,13 +3,12 @@ */ use gtk::prelude::*; -use glib::translate::{ ToGlib, FromGlib }; +use std::cell::RefCell; +use super::base_meter::{Meter, MeterData, MeterWidgets}; +use super::base_meter::{MAX_NATURAL_VOL, MAX_SCALE_VOL, OUTPUT_ICONS}; use crate::pulse::Pulse; use crate::shared::Shared; -use super::base_meter::{ Meter, MeterWidgets, MeterData }; -use super::base_meter::{ MAX_NATURAL_VOL, MAX_SCALE_VOL, OUTPUT_ICONS }; - /** * A meter widget representing a sink. @@ -17,236 +16,291 @@ use super::base_meter::{ MAX_NATURAL_VOL, MAX_SCALE_VOL, OUTPUT_ICONS }; */ pub struct SinkMeter { - pub widget: gtk::Box, + pub widget: gtk::Box, - data: MeterData, - widgets: MeterWidgets, - pulse: Shared, + data: MeterData, + widgets: MeterWidgets, + pulse: Shared, - split: bool, - peak: Option, + split: bool, + peak: Option, - l_id: Option, - s_id: Option, + l_id: RefCell>, + s_id: RefCell>, } impl SinkMeter { - - /** - * Creates a new SinkMeter. - */ - - pub fn new(pulse: Shared) -> Self { - let widgets = Meter::build_meter(); - Self { - widget: widgets.root.clone(), - - pulse, - widgets, - data: MeterData::default(), - - split: false, peak: None, s_id: None, l_id: None - } - } - - - /** - * Rebuilds widgets that are dependent on the Pulse instance or the sink index. - * Reconnects the widgets to the Pulse instance, if one is provided. - */ - - fn rebuild_widgets(&mut self) { - let scales = Meter::build_scales(&self.pulse, &self.data, self.split); - self.widgets.scales_outer.remove(&self.widgets.scales_inner); - self.widgets.scales_outer.pack_start(&scales, true, false, 0); - self.widgets.scales_inner = scales; - self.update_widgets(); - - let t = self.data.t; - let index = self.data.index; - let pulse = self.pulse.clone(); - - if self.s_id.is_some() { self.widgets.status.disconnect(glib::signal::SignalHandlerId::from_glib(self.s_id.as_ref().unwrap().to_glib())) } - self.s_id = Some(self.widgets.status.connect_clicked(move |status| { - pulse.borrow_mut().set_muted(t, index, - !status.get_style_context().has_class("muted")); - })); - - let pulse = self.pulse.clone(); - let index = self.data.index; - - if self.l_id.is_some() { self.widgets.app_button.disconnect( - glib::signal::SignalHandlerId::from_glib(self.l_id.as_ref().unwrap().to_glib())) } - self.l_id = Some(self.widgets.app_button.connect_clicked(move |trigger| { - SinkMeter::show_popup(&trigger, &pulse, index); - })); - } - - - /** - * Updates each scale widget to reflect the current volume level. - */ - - fn update_widgets(&mut self) { - for (i, v) in self.data.volume.get().iter().enumerate() { - if let Some(scale) = self.widgets.scales_inner.get_children().get(i) { - let scale = scale.clone().downcast::().expect("Scales box has non-scale children."); - scale.set_sensitive(!self.data.muted); - scale.set_value(v.0 as f64); - } - } - } - - - /** - * Shows a popup menu on the top button, with items to set - * the Sink as default, and change the visible sink. - */ - - fn show_popup(trigger: >k::Button, pulse_shr: &Shared, index: u32) { - let pulse = pulse_shr.borrow_mut(); - let root = gtk::PopoverMenu::new(); - root.set_border_width(6); - - let menu = gtk::Box::new(gtk::Orientation::Vertical, 0); - menu.set_size_request(132, -1); - root.add(&menu); - - // let split_channels = gtk::ModelButton::new(); - // split_channels.set_property_text(Some("Split Channels")); - // split_channels.set_action_name(Some("app.split_channels")); - // menu.add(&split_channels); - - let set_default = gtk::ModelButton::new(); - set_default.set_property_role(gtk::ButtonRole::Check); - set_default.set_property_text(Some("Set as Default")); - set_default.set_property_active(pulse.default_sink == index); - set_default.set_sensitive(pulse.default_sink != index); - - let pulse_clone = pulse_shr.clone(); - set_default.connect_clicked(move |set_default| { - pulse_clone.borrow_mut().set_default_sink(index); - set_default.set_property_active(true); - set_default.set_sensitive(false); - }); - menu.add(&set_default); - - if pulse.sinks.len() >= 2 { - menu.pack_start(>k::SeparatorMenuItem::new(), false, false, 3); - - let label = gtk::Label::new(Some("Visible Output")); - label.set_sensitive(false); - menu.pack_start(&label, true, true, 3); - - for (i, v) in &pulse.sinks { - let button = gtk::ModelButton::new(); - button.set_property_role(gtk::ButtonRole::Radio); - button.set_property_active(v.data.index == index); - let button_label = gtk::Label::new(Some(&v.data.description)); - button_label.set_ellipsize(pango::EllipsizeMode::End); - button_label.set_max_width_chars(18); - button.get_child().unwrap().downcast::().unwrap().add(&button_label); - - let i = *i; - let root = root.clone(); - let pulse_clone = pulse_shr.clone(); - button.connect_clicked(move |_| { - pulse_clone.borrow_mut().set_active_sink(i); - root.popdown(); - }); - - menu.add(&button); - } - } - - for child in &root.get_children() { child.show_all(); } - root.set_relative_to(Some(trigger)); - root.popup(); - } + /** + * Creates a new SinkMeter. + */ + + pub fn new(pulse: Shared) -> Self { + let widgets = ::build_meter(); + Self { + widget: widgets.root.clone(), + + pulse, + widgets, + data: MeterData::default(), + + split: false, + peak: None, + s_id: RefCell::new(None), + l_id: RefCell::new(None), + } + } + + /** + * Rebuilds widgets that are dependent on the Pulse instance or the sink index. + * Reconnects the widgets to the Pulse instance, if one is provided. + */ + + fn rebuild_widgets(&mut self) { + let scales = ::build_scales(&self.pulse, &self.data, self.split); + self.widgets.scales_outer.remove(&self.widgets.scales_inner); + self.widgets + .scales_outer + .pack_start(&scales, true, false, 0); + self.widgets.scales_inner = scales; + self.update_widgets(); + + let t = self.data.t; + let index = self.data.index; + let pulse = self.pulse.clone(); + + if let Some(id) = self.s_id.borrow_mut().take() { + self.widgets.status.disconnect(id) + } + self.s_id + .replace(Some(self.widgets.status.connect_clicked(move |status| { + pulse + .borrow_mut() + .set_muted(t, index, !status.style_context().has_class("muted")); + }))); + + let pulse = self.pulse.clone(); + let index = self.data.index; + + if let Some(id) = self.l_id.borrow_mut().take() { + self.widgets.app_button.disconnect(id) + } + self.l_id + .replace(Some(self.widgets.app_button.connect_clicked( + move |trigger| { + SinkMeter::show_popup(&trigger, &pulse, index); + }, + ))); + } + + /** + * Updates each scale widget to reflect the current volume level. + */ + + fn update_widgets(&mut self) { + for (i, v) in self.data.volume.get().iter().enumerate() { + if let Some(scale) = self.widgets.scales_inner.children().get(i) { + let scale = scale + .clone() + .downcast::() + .expect("Scales box has non-scale children."); + scale.set_sensitive(!self.data.muted); + scale.set_value(v.0 as f64); + } + } + } + + /** + * Shows a popup menu on the top button, with items to set + * the Sink as default, and change the visible sink. + */ + + fn show_popup(trigger: >k::Button, pulse_shr: &Shared, index: u32) { + let pulse = pulse_shr.borrow_mut(); + let root = gtk::PopoverMenu::new(); + root.set_border_width(6); + + let menu = gtk::Box::new(gtk::Orientation::Vertical, 0); + menu.set_size_request(132, -1); + root.add(&menu); + + // let split_channels = gtk::ModelButton::new(); + // split_channels.set_property_text(Some("Split Channels")); + // split_channels.set_action_name(Some("app.split_channels")); + // menu.add(&split_channels); + + let set_default = gtk::ModelButton::new(); + set_default.set_property("role", gtk::ButtonRole::Check); + set_default.set_property("text", Some("Set as Default")); + set_default.set_property("active", pulse.default_sink == index); + set_default.set_sensitive(pulse.default_sink != index); + + let pulse_clone = pulse_shr.clone(); + set_default.connect_clicked(move |set_default| { + pulse_clone.borrow_mut().set_default_sink(index); + set_default.set_property("active", true); + set_default.set_sensitive(false); + }); + menu.add(&set_default); + + if pulse.sinks.len() >= 2 { + menu.pack_start(>k::SeparatorMenuItem::new(), false, false, 3); + + let label = gtk::Label::new(Some("Visible Output")); + label.set_sensitive(false); + menu.pack_start(&label, true, true, 3); + + for (i, v) in &pulse.sinks { + let button = gtk::ModelButton::new(); + button.set_property("role", gtk::ButtonRole::Radio); + button.set_property("active", v.data.index == index); + let button_label = gtk::Label::new(Some(&v.data.description)); + button_label.set_ellipsize(pango::EllipsizeMode::End); + button_label.set_max_width_chars(18); + button + .child() + .unwrap() + .downcast::() + .unwrap() + .add(&button_label); + + let i = *i; + let root = root.clone(); + let pulse_clone = pulse_shr.clone(); + button.connect_clicked(move |_| { + pulse_clone.borrow_mut().set_active_sink(i); + root.popdown(); + }); + + menu.add(&button); + } + } + + for child in &root.children() { + child.show_all(); + } + root.set_relative_to(Some(trigger)); + root.popup(); + } } impl Meter for SinkMeter { - fn get_index(&self) -> u32 { - self.data.index - } - - fn split_channels(&mut self, split: bool) { - if self.split == split { return } - self.split = split; - self.rebuild_widgets(); - } - - fn set_data(&mut self, data: &MeterData) { - let volume_old = self.data.volume; - let volume_changed = data.volume != volume_old; - - if data.t != self.data.t || data.index != self.data.index || data.volume.len() != self.data.volume.len() { - self.data.t = data.t; - self.data.volume = data.volume; - self.data.index = data.index; - self.rebuild_widgets(); - } - - if data.icon != self.data.icon { - self.data.icon = data.icon.clone(); - self.widgets.icon.set_from_icon_name(Some(&self.data.icon), gtk::IconSize::Dnd); - } - - if data.name != self.data.name { - self.data.name = data.name.clone(); - self.rebuild_widgets(); - } - - if data.description != self.data.description { - self.data.description = data.description.clone(); - self.widgets.label.set_label(&self.data.description); - self.widgets.app_button.set_tooltip_text(Some(&self.data.description)); - } - - if volume_changed || data.muted != self.data.muted { - self.data.volume = data.volume; - self.data.muted = data.muted; - self.update_widgets(); - - let status_vol = if self.data.muted { 0 } else { self.data.volume.max().0 }; - - self.widgets.status_icon.set_from_icon_name(Some(OUTPUT_ICONS[ - if status_vol == 0 { 0 } else if status_vol >= MAX_NATURAL_VOL { 3 } - else if status_vol >= MAX_NATURAL_VOL / 2 { 2 } else { 1 }]), gtk::IconSize::Button); - - let mut vol_scaled = ((status_vol as f64) / MAX_NATURAL_VOL as f64 * 100.0).round() as u8; - if vol_scaled > 150 { vol_scaled = 150 } - - let mut string = vol_scaled.to_string(); - string.push('%'); - - self.widgets.status.set_label(&string); - - if status_vol == 0 {self.widgets.status.get_style_context().add_class("muted") } - else { self.widgets.status.get_style_context().remove_class("muted") } - } - } - - fn set_peak(&mut self, peak: Option) { - if self.peak != peak { - self.peak = peak; - - if self.peak.is_some() { - for (i, s) in self.widgets.scales_inner.get_children().iter().enumerate() { - let s = s.clone().downcast::().expect("Scales box has non-scale children."); - let peak_scaled = self.peak.unwrap() as f64 * (self.data.volume.get()[i].0 as f64 / MAX_SCALE_VOL as f64); - s.set_fill_level(peak_scaled as f64); - s.set_show_fill_level(!self.data.muted && peak_scaled > 0.5); - s.get_style_context().add_class("visualizer"); - } - } - else { - for s in &self.widgets.scales_inner.get_children() { - let s = s.clone().downcast::().expect("Scales box has non-scale children."); - s.set_show_fill_level(false); - s.get_style_context().remove_class("visualizer"); - } - } - } - } + fn get_index(&self) -> u32 { + self.data.index + } + + fn split_channels(&mut self, split: bool) { + if self.split == split { + return; + } + self.split = split; + self.rebuild_widgets(); + } + + fn set_data(&mut self, data: &MeterData) { + let volume_old = self.data.volume; + let volume_changed = data.volume != volume_old; + + if data.t != self.data.t + || data.index != self.data.index + || data.volume.len() != self.data.volume.len() + { + self.data.t = data.t; + self.data.volume = data.volume; + self.data.index = data.index; + self.rebuild_widgets(); + } + + if data.icon != self.data.icon { + self.data.icon = data.icon.clone(); + self.widgets + .icon + .set_from_icon_name(Some(&self.data.icon), gtk::IconSize::Dnd); + } + + if data.name != self.data.name { + self.data.name = data.name.clone(); + self.rebuild_widgets(); + } + + if data.description != self.data.description { + self.data.description = data.description.clone(); + self.widgets.label.set_label(&self.data.description); + self.widgets + .app_button + .set_tooltip_text(Some(&self.data.description)); + } + + if volume_changed || data.muted != self.data.muted { + self.data.volume = data.volume; + self.data.muted = data.muted; + self.update_widgets(); + + let status_vol = if self.data.muted { + 0 + } else { + self.data.volume.max().0 + }; + + self.widgets.status_icon.set_from_icon_name( + Some( + OUTPUT_ICONS[if status_vol == 0 { + 0 + } else if status_vol >= MAX_NATURAL_VOL { + 3 + } else if status_vol >= MAX_NATURAL_VOL / 2 { + 2 + } else { + 1 + }], + ), + gtk::IconSize::Button, + ); + + let mut vol_scaled = + ((status_vol as f64) / MAX_NATURAL_VOL as f64 * 100.0).round() as u8; + if vol_scaled > 150 { + vol_scaled = 150 + } + + let mut string = vol_scaled.to_string(); + string.push('%'); + + self.widgets.status.set_label(&string); + + if status_vol == 0 { + self.widgets.status.style_context().add_class("muted") + } else { + self.widgets.status.style_context().remove_class("muted") + } + } + } + + fn set_peak(&mut self, peak: Option) { + if self.peak != peak { + self.peak = peak; + + if self.peak.is_some() { + for (i, s) in self.widgets.scales_inner.children().iter().enumerate() { + let s = s + .clone() + .downcast::() + .expect("Scales box has non-scale children."); + let peak_scaled = self.peak.unwrap() as f64 + * (self.data.volume.get()[i].0 as f64 / MAX_SCALE_VOL as f64); + s.set_fill_level(peak_scaled as f64); + s.set_show_fill_level(!self.data.muted && peak_scaled > 0.5); + s.style_context().add_class("visualizer"); + } + } else { + for s in &self.widgets.scales_inner.children() { + let s = s + .clone() + .downcast::() + .expect("Scales box has non-scale children."); + s.set_show_fill_level(false); + s.style_context().remove_class("visualizer"); + } + } + } + } } diff --git a/src/meter/source_meter.rs b/src/meter/source_meter.rs index e779cea..67e9885 100644 --- a/src/meter/source_meter.rs +++ b/src/meter/source_meter.rs @@ -2,14 +2,14 @@ * A specialized meter for representing sources. */ +use std::cell::RefCell; + use gtk::prelude::*; -use glib::translate::{ ToGlib, FromGlib }; +use super::base_meter::{Meter, MeterData, MeterWidgets}; +use super::base_meter::{INPUT_ICONS, MAX_NATURAL_VOL, MAX_SCALE_VOL}; use crate::pulse::Pulse; use crate::shared::Shared; -use super::base_meter::{ Meter, MeterWidgets, MeterData }; -use super::base_meter::{ MAX_NATURAL_VOL, MAX_SCALE_VOL, INPUT_ICONS }; - /** * A meter widget representing a source. @@ -17,238 +17,292 @@ use super::base_meter::{ MAX_NATURAL_VOL, MAX_SCALE_VOL, INPUT_ICONS }; */ pub struct SourceMeter { - pub widget: gtk::Box, + pub widget: gtk::Box, - data: MeterData, - widgets: MeterWidgets, - pulse: Shared, + data: MeterData, + widgets: MeterWidgets, + pulse: Shared, - split: bool, - peak: Option, + split: bool, + peak: Option, - l_id: Option, - s_id: Option, + l_id: RefCell>, + s_id: RefCell>, } impl SourceMeter { - - /** - * Creates a new SourceMeter. - */ - - pub fn new(pulse: Shared) -> Self { - let widgets = Meter::build_meter(); - - Self { - widget: widgets.root.clone(), - - pulse, - widgets, - data: MeterData::default(), - - split: false, peak: None, l_id: None, s_id: None - } - } - - - /** - * Rebuilds widgets that are dependent on the Pulse instance or the source index. - * Reconnects the widgets to the Pulse instance, if one is provided. - */ - - fn rebuild_widgets(&mut self) { - let scales = Meter::build_scales(&self.pulse, &self.data, self.split); - self.widgets.scales_outer.remove(&self.widgets.scales_inner); - self.widgets.scales_outer.pack_start(&scales, true, false, 0); - self.widgets.scales_inner = scales; - self.update_widgets(); - - let t = self.data.t; - let index = self.data.index; - let pulse = self.pulse.clone(); - - if self.s_id.is_some() { self.widgets.status.disconnect( - glib::signal::SignalHandlerId::from_glib(self.s_id.as_ref().unwrap().to_glib())) } - self.s_id = Some(self.widgets.status.connect_clicked(move |status| { - pulse.borrow_mut().set_muted(t, index, - !status.get_style_context().has_class("muted")); - })); - - let pulse = self.pulse.clone(); - let index = self.data.index; - - if self.l_id.is_some() { self.widgets.app_button.disconnect( - glib::signal::SignalHandlerId::from_glib(self.l_id.as_ref().unwrap().to_glib())) } - self.l_id = Some(self.widgets.app_button.connect_clicked(move |trigger| { - SourceMeter::show_popup(&trigger, &pulse, index); - })); - } - - - /** - * Updates each scale widget to reflect the current volume level. - */ - - fn update_widgets(&mut self) { - for (i, v) in self.data.volume.get().iter().enumerate() { - if let Some(scale) = self.widgets.scales_inner.get_children().get(i) { - let scale = scale.clone().downcast::().expect("Scales box has non-scale children."); - scale.set_sensitive(!self.data.muted); - scale.set_value(v.0 as f64); - } - } - } - - - /** - * Shows a popup menu on the top button, with items to set - * the Sink as default, and change the visible source. - */ - - fn show_popup(trigger: >k::Button, pulse_shr: &Shared, index: u32) { - let pulse = pulse_shr.borrow_mut(); - let root = gtk::PopoverMenu::new(); - root.set_border_width(6); - - let menu = gtk::Box::new(gtk::Orientation::Vertical, 0); - menu.set_size_request(132, -1); - root.add(&menu); - - // let split_channels = gtk::ModelButton::new(); - // split_channels.set_property_text(Some("Split Channels")); - // split_channels.set_action_name(Some("app.split_channels")); - // menu.add(&split_channels); - - let set_default = gtk::ModelButton::new(); - set_default.set_property_role(gtk::ButtonRole::Check); - set_default.set_property_text(Some("Set as Default")); - set_default.set_property_active(pulse.default_source == index); - set_default.set_sensitive(pulse.default_source != index); - - let pulse_clone = pulse_shr.clone(); - set_default.connect_clicked(move |set_default| { - pulse_clone.borrow_mut().set_default_source(index); - set_default.set_property_active(true); - set_default.set_sensitive(false); - }); - menu.add(&set_default); - - if pulse.sources.len() >= 2 { - menu.pack_start(>k::SeparatorMenuItem::new(), false, false, 3); - - let label = gtk::Label::new(Some("Visible Input")); - label.set_sensitive(false); - menu.pack_start(&label, true, true, 3); - - for (i, v) in &pulse.sources { - let button = gtk::ModelButton::new(); - button.set_property_role(gtk::ButtonRole::Radio); - button.set_property_active(v.data.index == index); - let button_label = gtk::Label::new(Some(&v.data.description)); - button_label.set_ellipsize(pango::EllipsizeMode::End); - button_label.set_max_width_chars(18); - button.get_child().unwrap().downcast::().unwrap().add(&button_label); - - let i = *i; - let root = root.clone(); - let pulse_clone = pulse_shr.clone(); - button.connect_clicked(move |_| { - pulse_clone.borrow_mut().set_active_source(i); - root.popdown(); - }); - - menu.add(&button); - } - } - - for child in &root.get_children() { child.show_all(); } - root.set_relative_to(Some(trigger)); - root.popup(); - } + /** + * Creates a new SourceMeter. + */ + + pub fn new(pulse: Shared) -> Self { + let widgets = ::build_meter(); + + Self { + widget: widgets.root.clone(), + + pulse, + widgets, + data: MeterData::default(), + + split: false, + peak: None, + l_id: RefCell::new(None), + s_id: RefCell::new(None), + } + } + + /** + * Rebuilds widgets that are dependent on the Pulse instance or the source index. + * Reconnects the widgets to the Pulse instance, if one is provided. + */ + + fn rebuild_widgets(&mut self) { + let scales = ::build_scales(&self.pulse, &self.data, self.split); + self.widgets.scales_outer.remove(&self.widgets.scales_inner); + self.widgets + .scales_outer + .pack_start(&scales, true, false, 0); + self.widgets.scales_inner = scales; + self.update_widgets(); + + let t = self.data.t; + let index = self.data.index; + let pulse = self.pulse.clone(); + + if let Some(id) = self.s_id.borrow_mut().take() { + self.widgets.status.disconnect(id) + } + self.s_id + .replace(Some(self.widgets.status.connect_clicked(move |status| { + pulse + .borrow_mut() + .set_muted(t, index, !status.style_context().has_class("muted")); + }))); + + let pulse = self.pulse.clone(); + let index = self.data.index; + + if let Some(id) = self.l_id.borrow_mut().take() { + self.widgets.app_button.disconnect(id) + } + self.l_id + .replace(Some(self.widgets.app_button.connect_clicked( + move |trigger| { + SourceMeter::show_popup(&trigger, &pulse, index); + }, + ))); + } + + /** + * Updates each scale widget to reflect the current volume level. + */ + + fn update_widgets(&mut self) { + for (i, v) in self.data.volume.get().iter().enumerate() { + if let Some(scale) = self.widgets.scales_inner.children().get(i) { + let scale = scale + .clone() + .downcast::() + .expect("Scales box has non-scale children."); + scale.set_sensitive(!self.data.muted); + scale.set_value(v.0 as f64); + } + } + } + + /** + * Shows a popup menu on the top button, with items to set + * the Sink as default, and change the visible source. + */ + + fn show_popup(trigger: >k::Button, pulse_shr: &Shared, index: u32) { + let pulse = pulse_shr.borrow_mut(); + let root = gtk::PopoverMenu::new(); + root.set_border_width(6); + + let menu = gtk::Box::new(gtk::Orientation::Vertical, 0); + menu.set_size_request(132, -1); + root.add(&menu); + + // let split_channels = gtk::ModelButton::new(); + // split_channels.set_property_text(Some("Split Channels")); + // split_channels.set_action_name(Some("app.split_channels")); + // menu.add(&split_channels); + + let set_default = gtk::ModelButton::new(); + set_default.set_property("role", gtk::ButtonRole::Check); + set_default.set_property("text", Some("Set as Default")); + set_default.set_property("active", pulse.default_source == index); + set_default.set_sensitive(pulse.default_source != index); + + let pulse_clone = pulse_shr.clone(); + set_default.connect_clicked(move |set_default| { + pulse_clone.borrow_mut().set_default_source(index); + set_default.set_property("active", true); + set_default.set_sensitive(false); + }); + menu.add(&set_default); + + if pulse.sources.len() >= 2 { + menu.pack_start(>k::SeparatorMenuItem::new(), false, false, 3); + + let label = gtk::Label::new(Some("Visible Input")); + label.set_sensitive(false); + menu.pack_start(&label, true, true, 3); + + for (i, v) in &pulse.sources { + let button = gtk::ModelButton::new(); + button.set_property("role", gtk::ButtonRole::Radio); + button.set_property("active", v.data.index == index); + let button_label = gtk::Label::new(Some(&v.data.description)); + button_label.set_ellipsize(pango::EllipsizeMode::End); + button_label.set_max_width_chars(18); + button + .child() + .unwrap() + .downcast::() + .unwrap() + .add(&button_label); + + let i = *i; + let root = root.clone(); + let pulse_clone = pulse_shr.clone(); + button.connect_clicked(move |_| { + pulse_clone.borrow_mut().set_active_source(i); + root.popdown(); + }); + + menu.add(&button); + } + } + + for child in &root.children() { + child.show_all(); + } + root.set_relative_to(Some(trigger)); + root.popup(); + } } impl Meter for SourceMeter { - fn get_index(&self) -> u32 { - self.data.index - } - - fn split_channels(&mut self, split: bool) { - if self.split == split { return } - self.split = split; - self.rebuild_widgets(); - } - - fn set_data(&mut self, data: &MeterData) { - let volume_old = self.data.volume; - let volume_changed = data.volume != volume_old; - - if data.t != self.data.t || data.index != self.data.index || data.volume.len() != self.data.volume.len() { - self.data.t = data.t; - self.data.volume = data.volume; - self.data.index = data.index; - self.rebuild_widgets(); - } - - if data.icon != self.data.icon { - self.data.icon = data.icon.clone(); - self.widgets.icon.set_from_icon_name(Some(&self.data.icon), gtk::IconSize::Dnd); - } - - if data.name != self.data.name { - self.data.name = data.name.clone(); - self.rebuild_widgets(); - } - - if data.description != self.data.description { - self.data.description = data.description.clone(); - self.widgets.label.set_label(&self.data.description); - self.widgets.app_button.set_tooltip_text(Some(&self.data.description)); - } - - if volume_changed || data.muted != self.data.muted { - self.data.volume = data.volume; - self.data.muted = data.muted; - self.update_widgets(); - - let status_vol = if self.data.muted { 0 } else { self.data.volume.max().0 }; - - self.widgets.status_icon.set_from_icon_name(Some(INPUT_ICONS[ - if status_vol == 0 { 0 } else if status_vol >= MAX_NATURAL_VOL { 3 } - else if status_vol >= MAX_NATURAL_VOL / 2 { 2 } else { 1 }]), gtk::IconSize::Button); - - let mut vol_scaled = ((status_vol as f64) / MAX_NATURAL_VOL as f64 * 100.0).round() as u8; - if vol_scaled > 150 { vol_scaled = 150 } - - let mut string = vol_scaled.to_string(); - string.push('%'); - - self.widgets.status.set_label(&string); - - if status_vol == 0 {self.widgets.status.get_style_context().add_class("muted") } - else { self.widgets.status.get_style_context().remove_class("muted") } - } - } - - fn set_peak(&mut self, peak: Option) { - if self.peak != peak { - self.peak = peak; - - if self.peak.is_some() { - for (i, s) in self.widgets.scales_inner.get_children().iter().enumerate() { - let s = s.clone().downcast::().expect("Scales box has non-scale children."); - let peak_scaled = self.peak.unwrap() as f64 * (self.data.volume.get()[i].0 as f64 / MAX_SCALE_VOL as f64); - s.set_fill_level(peak_scaled as f64); - s.set_show_fill_level(!self.data.muted && peak_scaled > 0.5); - s.get_style_context().add_class("visualizer"); - } - } - else { - for s in &self.widgets.scales_inner.get_children() { - let s = s.clone().downcast::().expect("Scales box has non-scale children."); - s.set_show_fill_level(false); - s.get_style_context().remove_class("visualizer"); - } - } - } - } + fn get_index(&self) -> u32 { + self.data.index + } + + fn split_channels(&mut self, split: bool) { + if self.split == split { + return; + } + self.split = split; + self.rebuild_widgets(); + } + + fn set_data(&mut self, data: &MeterData) { + let volume_old = self.data.volume; + let volume_changed = data.volume != volume_old; + + if data.t != self.data.t + || data.index != self.data.index + || data.volume.len() != self.data.volume.len() + { + self.data.t = data.t; + self.data.volume = data.volume; + self.data.index = data.index; + self.rebuild_widgets(); + } + + if data.icon != self.data.icon { + self.data.icon = data.icon.clone(); + self.widgets + .icon + .set_from_icon_name(Some(&self.data.icon), gtk::IconSize::Dnd); + } + + if data.name != self.data.name { + self.data.name = data.name.clone(); + self.rebuild_widgets(); + } + + if data.description != self.data.description { + self.data.description = data.description.clone(); + self.widgets.label.set_label(&self.data.description); + self.widgets + .app_button + .set_tooltip_text(Some(&self.data.description)); + } + + if volume_changed || data.muted != self.data.muted { + self.data.volume = data.volume; + self.data.muted = data.muted; + self.update_widgets(); + + let status_vol = if self.data.muted { + 0 + } else { + self.data.volume.max().0 + }; + + self.widgets.status_icon.set_from_icon_name( + Some( + INPUT_ICONS[if status_vol == 0 { + 0 + } else if status_vol >= MAX_NATURAL_VOL { + 3 + } else if status_vol >= MAX_NATURAL_VOL / 2 { + 2 + } else { + 1 + }], + ), + gtk::IconSize::Button, + ); + + let mut vol_scaled = + ((status_vol as f64) / MAX_NATURAL_VOL as f64 * 100.0).round() as u8; + if vol_scaled > 150 { + vol_scaled = 150 + } + + let mut string = vol_scaled.to_string(); + string.push('%'); + + self.widgets.status.set_label(&string); + + if status_vol == 0 { + self.widgets.status.style_context().add_class("muted") + } else { + self.widgets.status.style_context().remove_class("muted") + } + } + } + + fn set_peak(&mut self, peak: Option) { + if self.peak != peak { + self.peak = peak; + + if self.peak.is_some() { + for (i, s) in self.widgets.scales_inner.children().iter().enumerate() { + let s = s + .clone() + .downcast::() + .expect("Scales box has non-scale children."); + let peak_scaled = self.peak.unwrap() as f64 + * (self.data.volume.get()[i].0 as f64 / MAX_SCALE_VOL as f64); + s.set_fill_level(peak_scaled as f64); + s.set_show_fill_level(!self.data.muted && peak_scaled > 0.5); + s.style_context().add_class("visualizer"); + } + } else { + for s in &self.widgets.scales_inner.children() { + let s = s + .clone() + .downcast::() + .expect("Scales box has non-scale children."); + s.set_show_fill_level(false); + s.style_context().remove_class("visualizer"); + } + } + } + } } diff --git a/src/meter/stream_meter.rs b/src/meter/stream_meter.rs index 07b9785..5f9d26f 100644 --- a/src/meter/stream_meter.rs +++ b/src/meter/stream_meter.rs @@ -2,14 +2,14 @@ * A specialized meter for representing application streams. */ +use std::cell::RefCell; + use gtk::prelude::*; -use glib::translate::{ ToGlib, FromGlib }; +use super::base_meter::{Meter, MeterData, MeterWidgets}; +use super::base_meter::{INPUT_ICONS, MAX_NATURAL_VOL, MAX_SCALE_VOL, OUTPUT_ICONS}; +use crate::pulse::{Pulse, StreamType}; use crate::shared::Shared; -use crate::pulse::{ Pulse, StreamType }; -use super::base_meter::{ Meter, MeterWidgets, MeterData }; -use super::base_meter::{ MAX_NATURAL_VOL, MAX_SCALE_VOL, INPUT_ICONS, OUTPUT_ICONS }; - /** * A meter widget representing an application stream, @@ -17,159 +17,208 @@ use super::base_meter::{ MAX_NATURAL_VOL, MAX_SCALE_VOL, INPUT_ICONS, OUTPUT_ICO */ pub struct StreamMeter { - pub widget: gtk::Box, + pub widget: gtk::Box, - data: MeterData, - widgets: MeterWidgets, - pulse: Shared, + data: MeterData, + widgets: MeterWidgets, + pulse: Shared, - split: bool, - peak: Option, + split: bool, + peak: Option, - b_id: Option, + b_id: RefCell>, } impl StreamMeter { - - /** - * Creates a new StreamMeter. - */ - - pub fn new(pulse: Shared) -> Self { - let widgets = Meter::build_meter(); - Self { - widget: widgets.root.clone(), - - pulse, - widgets, - data: MeterData::default(), - - split: false, peak: None, b_id: None - } - } - - - /** - * Rebuilds widgets that are dependent on the Pulse instance or the stream index. - * Reconnects the widgets to the Pulse instance, if one is provided. - */ - - fn rebuild_widgets(&mut self) { - let scales = Meter::build_scales(&self.pulse, &self.data, self.split); - self.widgets.scales_outer.remove(&self.widgets.scales_inner); - self.widgets.scales_outer.pack_start(&scales, true, false, 0); - self.widgets.scales_inner = scales; - self.update_widgets(); - - let t = self.data.t; - let index = self.data.index; - let pulse = self.pulse.clone(); - - if self.b_id.is_some() { self.widgets.status.disconnect(glib::signal::SignalHandlerId::from_glib(self.b_id.as_ref().unwrap().to_glib())) } - self.b_id = Some(self.widgets.status.connect_clicked(move |status| { - pulse.borrow_mut().set_muted(t, index, !status.get_style_context().has_class("muted")); - })); - } - - - /** - * Updates each scale widget to reflect the current volume level. - */ - - fn update_widgets(&mut self) { - for (i, v) in self.data.volume.get().iter().enumerate() { - if let Some(scale) = self.widgets.scales_inner.get_children().get(i) { - let scale = scale.clone().downcast::().expect("Scales box has non-scale children."); - scale.set_value(if self.data.muted { 0.0 } else { v.0 as f64 }); - } - } - } + /** + * Creates a new StreamMeter. + */ + + pub fn new(pulse: Shared) -> Self { + let widgets = ::build_meter(); + Self { + widget: widgets.root.clone(), + + pulse, + widgets, + data: MeterData::default(), + + split: false, + peak: None, + b_id: RefCell::new(None), + } + } + + /** + * Rebuilds widgets that are dependent on the Pulse instance or the stream index. + * Reconnects the widgets to the Pulse instance, if one is provided. + */ + + fn rebuild_widgets(&mut self) { + let scales = ::build_scales(&self.pulse, &self.data, self.split); + self.widgets.scales_outer.remove(&self.widgets.scales_inner); + self.widgets + .scales_outer + .pack_start(&scales, true, false, 0); + self.widgets.scales_inner = scales; + self.update_widgets(); + + let t = self.data.t; + let index = self.data.index; + let pulse = self.pulse.clone(); + + if let Some(id) = self.b_id.borrow_mut().take() { + self.widgets.status.disconnect(id) + } + self.b_id + .replace(Some(self.widgets.status.connect_clicked(move |status| { + pulse + .borrow_mut() + .set_muted(t, index, !status.style_context().has_class("muted")); + }))); + } + + /** + * Updates each scale widget to reflect the current volume level. + */ + + fn update_widgets(&mut self) { + for (i, v) in self.data.volume.get().iter().enumerate() { + if let Some(scale) = self.widgets.scales_inner.children().get(i) { + let scale = scale + .clone() + .downcast::() + .expect("Scales box has non-scale children."); + scale.set_value(if self.data.muted { 0.0 } else { v.0 as f64 }); + } + } + } } impl Meter for StreamMeter { - fn get_index(&self) -> u32 { - self.data.index - } - - fn split_channels(&mut self, split: bool) { - if self.split == split { return } - self.split = split; - self.rebuild_widgets(); - } - - fn set_data(&mut self, data: &MeterData) { - let volume_old = self.data.volume; - let volume_changed = data.volume != volume_old; - - if data.t != self.data.t || data.index != self.data.index || data.volume.len() != self.data.volume.len() { - self.data.t = data.t; - self.data.volume = data.volume; - self.data.index = data.index; - self.rebuild_widgets(); - } - - if data.icon != self.data.icon { - self.data.icon = data.icon.clone(); - self.widgets.icon.set_from_icon_name(Some(&self.data.icon), gtk::IconSize::Dnd); - } - - if data.name != self.data.name { - self.data.name = data.name.clone(); - } - - if data.description != self.data.description { - self.data.description = data.description.clone(); - self.widgets.label.set_label(&self.data.description); - self.widgets.app_button.set_tooltip_text(Some(&self.data.description)); - } - - if volume_changed || data.muted != self.data.muted { - self.data.volume = data.volume; - self.data.muted = data.muted; - self.update_widgets(); - - let status_vol = if self.data.muted { 0 } else { self.data.volume.max().0 }; - - let &icons = if self.data.t == StreamType::Sink || self.data.t == StreamType::SinkInput - { &OUTPUT_ICONS } else { &INPUT_ICONS }; - - self.widgets.status_icon.set_from_icon_name(Some(icons[ - if status_vol == 0 { 0 } else if status_vol >= MAX_NATURAL_VOL { 3 } - else if status_vol >= MAX_NATURAL_VOL / 2 { 2 } else { 1 }]), gtk::IconSize::Button); - - let mut vol_scaled = ((status_vol as f64) / MAX_NATURAL_VOL as f64 * 100.0).round() as u8; - if vol_scaled > 150 { vol_scaled = 150 } - - let mut string = vol_scaled.to_string(); - string.push('%'); - - self.widgets.status.set_label(&string); - - if status_vol == 0 {self.widgets.status.get_style_context().add_class("muted") } - else { self.widgets.status.get_style_context().remove_class("muted") } - } - } - - fn set_peak(&mut self, peak: Option) { - if self.peak != peak { - self.peak = peak; - - if self.peak.is_some() { - for (i, s) in self.widgets.scales_inner.get_children().iter().enumerate() { - let s = s.clone().downcast::().expect("Scales box has non-scale children."); - let peak_scaled = self.peak.unwrap() as f64 * (self.data.volume.get()[i].0 as f64 / MAX_SCALE_VOL as f64); - s.set_fill_level(peak_scaled as f64); - s.set_show_fill_level(!self.data.muted && peak_scaled > 0.5); - s.get_style_context().add_class("visualizer"); - } - } - else { - for s in &self.widgets.scales_inner.get_children() { - let s = s.clone().downcast::().expect("Scales box has non-scale children."); - s.set_show_fill_level(false); - s.get_style_context().remove_class("visualizer"); - } - } - } - } + fn get_index(&self) -> u32 { + self.data.index + } + + fn split_channels(&mut self, split: bool) { + if self.split == split { + return; + } + self.split = split; + self.rebuild_widgets(); + } + + fn set_data(&mut self, data: &MeterData) { + let volume_old = self.data.volume; + let volume_changed = data.volume != volume_old; + + if data.t != self.data.t + || data.index != self.data.index + || data.volume.len() != self.data.volume.len() + { + self.data.t = data.t; + self.data.volume = data.volume; + self.data.index = data.index; + self.rebuild_widgets(); + } + + if data.icon != self.data.icon { + self.data.icon = data.icon.clone(); + self.widgets + .icon + .set_from_icon_name(Some(&self.data.icon), gtk::IconSize::Dnd); + } + + if data.name != self.data.name { + self.data.name = data.name.clone(); + } + + if data.description != self.data.description { + self.data.description = data.description.clone(); + self.widgets.label.set_label(&self.data.description); + self.widgets + .app_button + .set_tooltip_text(Some(&self.data.description)); + } + + if volume_changed || data.muted != self.data.muted { + self.data.volume = data.volume; + self.data.muted = data.muted; + self.update_widgets(); + + let status_vol = if self.data.muted { + 0 + } else { + self.data.volume.max().0 + }; + + let &icons = if self.data.t == StreamType::Sink || self.data.t == StreamType::SinkInput + { + &OUTPUT_ICONS + } else { + &INPUT_ICONS + }; + + self.widgets.status_icon.set_from_icon_name( + Some( + icons[if status_vol == 0 { + 0 + } else if status_vol >= MAX_NATURAL_VOL { + 3 + } else if status_vol >= MAX_NATURAL_VOL / 2 { + 2 + } else { + 1 + }], + ), + gtk::IconSize::Button, + ); + + let mut vol_scaled = + ((status_vol as f64) / MAX_NATURAL_VOL as f64 * 100.0).round() as u8; + if vol_scaled > 150 { + vol_scaled = 150 + } + + let mut string = vol_scaled.to_string(); + string.push('%'); + + self.widgets.status.set_label(&string); + + if status_vol == 0 { + self.widgets.status.style_context().add_class("muted") + } else { + self.widgets.status.style_context().remove_class("muted") + } + } + } + + fn set_peak(&mut self, peak: Option) { + if self.peak != peak { + self.peak = peak; + + if self.peak.is_some() { + for (i, s) in self.widgets.scales_inner.children().iter().enumerate() { + let s = s + .clone() + .downcast::() + .expect("Scales box has non-scale children."); + let peak_scaled = self.peak.unwrap() as f64 + * (self.data.volume.get()[i].0 as f64 / MAX_SCALE_VOL as f64); + s.set_fill_level(peak_scaled as f64); + s.set_show_fill_level(!self.data.muted && peak_scaled > 0.5); + s.style_context().add_class("visualizer"); + } + } else { + for s in &self.widgets.scales_inner.children() { + let s = s + .clone() + .downcast::() + .expect("Scales box has non-scale children."); + s.set_show_fill_level(false); + s.style_context().remove_class("visualizer"); + } + } + } + } } diff --git a/src/pulse.rs b/src/pulse.rs index b2320c8..3abc720 100644 --- a/src/pulse.rs +++ b/src/pulse.rs @@ -3,27 +3,28 @@ * Monitors the pulse server for updates, and also exposes methods to request changes. */ -use slice_as_array::{ slice_as_array, slice_as_array_transmute }; +use slice_as_array::{slice_as_array, slice_as_array_transmute}; -use libpulse::def::BufferAttr; use libpulse::callbacks::ListResult; -use libpulse::sample::{ Spec, Format }; +use libpulse::context::introspect::{ + CardInfo, ServerInfo, SinkInfo, SinkInputInfo, SourceInfo, SourceOutputInfo, +}; +use libpulse::context::subscribe::{Facility, InterestMaskSet, Operation}; +use libpulse::context::{Context, FlagSet as CtxFlagSet, State as ContextState}; +use libpulse::def::BufferAttr; use libpulse::mainloop::threaded::Mainloop; -use libpulse::proplist::{ Proplist, properties }; -use libpulse::volume::{ Volume, ChannelVolumes }; -use libpulse::stream::{ Stream, FlagSet as StreamFlagSet, PeekResult }; -use libpulse::context::subscribe::{ InterestMaskSet, Facility, Operation }; -use libpulse::context::{ Context, FlagSet as CtxFlagSet, State as ContextState }; -use libpulse::context::introspect::{ ServerInfo, SourceInfo, SinkInfo, SinkInputInfo, SourceOutputInfo, CardInfo }; +use libpulse::proplist::{properties, Proplist}; +use libpulse::sample::{Format, Spec}; +use libpulse::stream::{FlagSet as StreamFlagSet, PeekResult, Stream}; +use libpulse::volume::{ChannelVolumes, Volume}; use std::collections::HashMap; -use std::sync::mpsc::{ channel, Sender, Receiver }; +use std::sync::mpsc::{channel, Receiver, Sender}; -use super::shared::Shared; use super::card::CardData; use super::meter::MeterData; use super::meter::MAX_NATURAL_VOL; - +use super::shared::Shared; /** * Represents a stream's underlying type. @@ -31,14 +32,18 @@ use super::meter::MAX_NATURAL_VOL; #[derive(Copy, Clone, Debug, PartialEq)] pub enum StreamType { - Sink, SinkInput, Source, SourceOutput + Sink, + SinkInput, + Source, + SourceOutput, } impl Default for StreamType { - fn default() -> Self { StreamType::Sink } + fn default() -> Self { + StreamType::Sink + } } - /** * The different message types that can be passed from the pulse * thread to the data store. They contain data related to the @@ -46,26 +51,24 @@ impl Default for StreamType { */ enum TxMessage { - Default(String, String), - StreamUpdate(StreamType, TxStreamData), - StreamRemove(StreamType, u32), - CardUpdate(CardData), - CardRemove(u32), - Peak(StreamType, u32, u32) + Default(String, String), + StreamUpdate(StreamType, TxStreamData), + StreamRemove(StreamType, u32), + CardUpdate(CardData), + CardRemove(u32), + Peak(StreamType, u32, u32), } - /** * Transferrable information pretaining to a stream. */ #[derive(Debug)] pub struct TxStreamData { - pub data: MeterData, - pub monitor_index: u32, + pub data: MeterData, + pub monitor_index: u32, } - /** * Stored representation of a pulse stream. * The stream index is not in this struct, but @@ -73,17 +76,18 @@ pub struct TxStreamData { */ pub struct StreamData { - pub data: MeterData, + pub data: MeterData, - pub peak: u32, - pub monitor_index: u32, - pub monitor: Shared + pub peak: u32, + pub monitor_index: u32, + pub monitor: Shared, } - /** Container for mspc channel sender & receiver. */ -struct Channel { tx: Sender, rx: Receiver } - +struct Channel { + tx: Sender, + rx: Receiver, +} /** * The main controller for all pulse server interactions. @@ -92,660 +96,795 @@ struct Channel { tx: Sender, rx: Receiver } */ pub struct Pulse { - mainloop: Shared, - context: Shared, - channel: Channel, - - pub default_sink: u32, - pub default_source: u32, - pub active_sink: u32, - pub active_source: u32, - - pub sinks: HashMap, - pub sink_inputs: HashMap, - pub sources: HashMap, - pub source_outputs: HashMap, - pub cards: HashMap, + mainloop: Shared, + context: Shared, + channel: Channel, + + pub default_sink: u32, + pub default_source: u32, + pub active_sink: u32, + pub active_source: u32, + + pub sinks: HashMap, + pub sink_inputs: HashMap, + pub sources: HashMap, + pub source_outputs: HashMap, + pub cards: HashMap, } impl Pulse { - - /** - * Creates a new pulse controller, configuring (but not initializing) the pulse connection. - */ - - pub fn new() -> Self { - let mut proplist = Proplist::new().unwrap(); - proplist.set_str(properties::APPLICATION_NAME, "Myxer").unwrap(); - - let mainloop = Shared::new(Mainloop::new().expect("Failed to initialize pulse mainloop.")); - - let context = Shared::new( - Context::new_with_proplist(&*mainloop.borrow(), "Myxer Context", &proplist) - .expect("Failed to initialize pulse context.")); - - let ( tx, rx ) = channel::(); - - Pulse { - mainloop, context, - channel: Channel { tx, rx }, - - default_sink: u32::MAX, - default_source: u32::MAX, - active_sink: u32::MAX, - active_source: u32::MAX, - - sinks: HashMap::new(), - sink_inputs: HashMap::new(), - sources: HashMap::new(), - source_outputs: HashMap::new(), - cards: HashMap::new() - } - } - - - /** - * Initiates a connection to pulse. Blocks until success, panics on failure. - * TODO: Graceful error handling, with debug message. - * TODO: Try to see if there's a way to avoid using unsafe? It's in the docs... but...? - */ - - pub fn connect(&mut self) { - let mut mainloop = self.mainloop.borrow_mut(); - let mut ctx = self.context.borrow_mut(); - - let mainloop_shr_ref = self.mainloop.clone(); - let ctx_shr_ref = self.context.clone(); - - ctx.set_state_callback(Some(Box::new(move || { - match unsafe { (*ctx_shr_ref.as_ptr()).get_state() } { - ContextState::Ready | - ContextState::Failed | - ContextState::Terminated => - unsafe { (*mainloop_shr_ref.as_ptr()).signal(false); }, - _ => {}, - } - }))); - - ctx.connect(None, CtxFlagSet::NOFLAGS, None) - .expect("Failed to connect to the pulse server."); - - mainloop.lock(); - mainloop.start().expect("Failed to start pulse mainloop."); - - loop { - match ctx.get_state() { - ContextState::Ready => { - ctx.set_state_callback(None); - mainloop.unlock(); - break; - }, - ContextState::Failed | - ContextState::Terminated => { - eprintln!("Context state failed/terminated, quitting..."); - mainloop.unlock(); - mainloop.stop(); - panic!("Pulse session terminated."); - }, - _ => { mainloop.wait(); }, - } - } - - drop(ctx); - drop(mainloop); - self.subscribe(); - } - - - /** - * Asynchronously sets the default sink to the index provided. - * This is sometimes described as the fallback device. - * - * * `sink` - The sink index to set as the default. - */ - - pub fn set_default_sink(&self, sink: u32) { - if let Some(sink) = self.sinks.get(&sink) { - let mut mainloop = self.mainloop.borrow_mut(); - mainloop.lock(); - self.context.borrow_mut().set_default_sink(&sink.data.name, |_|()); - mainloop.unlock(); - } - } - - - /** - * Asynchronously sets the default source to the index provided. - * This is sometimes described as the fallback device. - * - * * `source` - The source index to set as the default. - */ - - pub fn set_default_source(&self, source: u32) { - if let Some(source) = self.sources.get(&source) { - let mut mainloop = self.mainloop.borrow_mut(); - mainloop.lock(); - self.context.borrow_mut().set_default_source(&source.data.name, |_|()); - mainloop.unlock(); - } - } - - - /** - * Sets the 'active' sink to the index provided. - * This is the sink that is currently displayed on the interface. - * - * * `sink` - The sink index to set as active. - */ - - pub fn set_active_sink(&mut self, sink: u32) { - self.active_sink = sink; - } - - - /** - * Sets the 'active' source to the index provided. - * This is the source that is currently displayed on the interface. - * - * * `source` - The source to set as active. - */ - - pub fn set_active_source(&mut self, source: u32) { - self.active_source = source; - } - - - /** - * Sets the volume of the stream to the volumes specified. - * This operation is asynchronous, so changes will not be reflected immediately. - * - * * `t` - The type of stream to set the volume of. - * * `index` - The index of the stream to set the volume of. - * * `volumes` - The desired volumes to set the channels of the stream to. - */ - - pub fn set_volume(&self, t: StreamType, index: u32, volumes: ChannelVolumes) { - let mut introspect = self.context.borrow().introspect(); - let mut mainloop = self.mainloop.borrow_mut(); - mainloop.lock(); - - match t { - StreamType::Sink => { introspect.set_sink_volume_by_index(index, &volumes, None); }, - StreamType::SinkInput => { introspect.set_sink_input_volume(index, &volumes, None); }, - StreamType::Source => { introspect.set_source_volume_by_index(index, &volumes, None); }, - StreamType::SourceOutput => { introspect.set_source_output_volume(index, &volumes, None); } - }; - - mainloop.unlock(); - } - - - /** - * Mutes or unmutes a stream. - * This operation is asynchronous, so changes will not be reflected immediately. - * - * * `t` - The type of stream to update. - * * `index` - The index of the stream to update. - * * `mute` - Whether the stream should be muted or not. - */ - - pub fn set_muted(&self, t: StreamType, index: u32, mute: bool) { - // If unmuting a stream that has been set to 0 volume, it should be reset to full. - if !mute { - let entry = match t { - StreamType::Sink => self.sinks.get(&index), - StreamType::SinkInput => self.sink_inputs.get(&index), - StreamType::Source => self.sources.get(&index), - StreamType::SourceOutput => self.source_outputs.get(&index), - }; - - if let Some(entry) = entry { - if entry.data.volume.max().0 == 0 { - let mut volumes = ChannelVolumes::default(); - volumes.set_len(entry.data.volume.len()); - volumes.set(entry.data.volume.len(), Volume(MAX_NATURAL_VOL)); - self.set_volume(t, index, volumes); - } - } - }; - - let mut introspect = self.context.borrow().introspect(); - let mut mainloop = self.mainloop.borrow_mut(); - mainloop.lock(); - - match t { - StreamType::Sink => { introspect.set_sink_mute_by_index(index, mute, None) }, - StreamType::SinkInput => { introspect.set_sink_input_mute(index, mute, None) }, - StreamType::Source => { introspect.set_source_mute_by_index(index, mute, None) }, - StreamType::SourceOutput => { introspect.set_source_output_mute(index, mute, None) } - }; - - mainloop.unlock(); - } - - - /** - * Set's a sound card's profile. - * This effects how the card behaves, and how the system can utilize it. - * - * * `index` - The card index to update. - * * `profile` - The profile name to update the card to. - */ - - pub fn set_card_profile(&self, index: u32, profile: &str) { - let mut introspect = self.context.borrow().introspect(); - let mut mainloop = self.mainloop.borrow_mut(); - mainloop.lock(); - introspect.set_card_profile_by_index(index, profile, None); - mainloop.unlock(); - } - - - /** - * Binds listeners to server events, and triggers an - * initial sweep to populate the internal stores. - * Called by connect(), separated for readability. - */ - - fn subscribe(&mut self) { - /** Updates the client when the server information changes. */ - fn tx_server(tx: &Sender, item: &ServerInfo<'_>) { - tx.send(TxMessage::Default(item.default_sink_name.clone().unwrap().into_owned(), - item.default_source_name.clone().unwrap().into_owned())).unwrap(); - }; - - /** Updates the client when a sink changes. */ - fn tx_sink(tx: &Sender, result: ListResult<&SinkInfo<'_>>) { - if let ListResult::Item(item) = result { - tx.send(TxMessage::StreamUpdate(StreamType::Sink, TxStreamData { - data: MeterData { - t: StreamType::Sink, - index: item.index, - icon: "multimedia-volume-control".to_owned(), - name: item.name.clone().unwrap().into_owned(), - description: item.description.clone().unwrap().into_owned(), - volume: item.volume, - muted: item.mute - }, - monitor_index: item.monitor_source - })).unwrap(); - }; - }; - - /** Updates the client when a sink input changes. */ - fn tx_sink_input(tx: &Sender, result: ListResult<&SinkInputInfo<'_>>) { - if let ListResult::Item(item) = result { - tx.send(TxMessage::StreamUpdate(StreamType::SinkInput, TxStreamData { - data: MeterData { - t: StreamType::SinkInput, - index: item.index, - icon: item.proplist.get_str("application.icon_name").unwrap_or_else(|| "audio-card".to_owned()), - name: item.name.clone().unwrap().into_owned(), - description: item.proplist.get_str("application.name").unwrap_or_else(|| "".to_owned()), - volume: item.volume, - muted: item.mute - }, - monitor_index: item.sink - })).unwrap(); - }; - }; - - /** Updates the client when a source changes. */ - fn tx_source(tx: &Sender, result: ListResult<&SourceInfo<'_>>) { - if let ListResult::Item(item) = result { - let name = item.name.clone().unwrap().into_owned(); - if name.ends_with(".monitor") { return; } - tx.send(TxMessage::StreamUpdate(StreamType::Source, TxStreamData { - data: MeterData { - t: StreamType::Source, - index: item.index, - icon: "audio-input-microphone".to_owned(), - name: item.name.clone().unwrap().into_owned(), - description: item.description.clone().unwrap().into_owned(), - volume: item.volume, - muted: item.mute - }, - monitor_index: item.index - })).unwrap(); - }; - }; - - /** Updates the client when a source output changes. */ - fn tx_source_output(tx: &Sender, result: ListResult<&SourceOutputInfo<'_>>) { - if let ListResult::Item(item) = result { - let app_id = item.proplist.get_str("application.process.binary").unwrap_or_else(|| "".to_owned()).to_lowercase(); - if app_id.contains("pavucontrol") || app_id.contains("myxer") { return; } - tx.send(TxMessage::StreamUpdate(StreamType::SourceOutput, TxStreamData { - data: MeterData { - t: StreamType::SourceOutput, - index: item.index, - icon: item.proplist.get_str("application.icon_name").unwrap_or_else(|| "audio-card".to_owned()), - name: item.name.clone().unwrap().into_owned(), - description: item.proplist.get_str("application.name").unwrap_or_else(|| "".to_owned()), - volume: item.volume, - muted: item.mute - }, - monitor_index: item.source - })).unwrap(); - }; - }; - - /** Updates the client when a sound card changes. */ - fn tx_card(tx: &Sender, result: ListResult<&CardInfo<'_>>) { - if let ListResult::Item(item) = result { - tx.send(TxMessage::CardUpdate(CardData { - index: item.index, - name: item.proplist.get_str("device.description") - .or_else(|| item.proplist.get_str("device.alias")) - .or_else(|| item.proplist.get_str("device.name")) - .unwrap_or_else(|| "".to_owned()), - icon: item.proplist.get_str("device.icon_name").unwrap_or_else(|| "audio-card-pci".to_owned()), - profiles: item.profiles.iter().map(|p| (p.name.as_ref().unwrap().clone().into_owned(), - p.description.as_ref().unwrap().clone().into_owned())).collect(), - active_profile: item.active_profile.as_ref().unwrap().name.as_ref().unwrap().clone().into_owned() - })).unwrap(); - } - } - - let mut mainloop = self.mainloop.borrow_mut(); - mainloop.lock(); - let mut context = self.context.borrow_mut(); - let introspect = context.introspect(); - - let tx = self.channel.tx.clone(); - introspect.get_sink_info_list(move |res| tx_sink(&tx, res)); - let tx = self.channel.tx.clone(); - introspect.get_sink_input_info_list(move |res| tx_sink_input(&tx, res)); - let tx = self.channel.tx.clone(); - introspect.get_source_info_list(move |res| tx_source(&tx, res)); - let tx = self.channel.tx.clone(); - introspect.get_source_output_info_list(move |res| tx_source_output(&tx, res)); - let tx = self.channel.tx.clone(); - introspect.get_card_info_list(move |res| tx_card(&tx, res)); - let tx = self.channel.tx.clone(); - introspect.get_server_info(move |res| tx_server(&tx, res)); - - let tx = self.channel.tx.clone(); - context.subscribe(InterestMaskSet::SERVER | InterestMaskSet::SINK | InterestMaskSet::SINK_INPUT | - InterestMaskSet::SOURCE | InterestMaskSet::SOURCE_OUTPUT | InterestMaskSet::CARD, |_|()); - context.set_subscribe_callback(Some(Box::new(move |fac, op, index| { - let tx = tx.clone(); - let facility = fac.unwrap(); - let operation = op.unwrap(); - - match facility { - Facility::Server => { introspect.get_server_info(move |res| tx_server(&tx, res)); }, - Facility::Sink => match operation { - Operation::Removed => tx.send(TxMessage::StreamRemove(StreamType::Sink, index)).unwrap(), - _ => { introspect.get_sink_info_by_index(index, move |res| tx_sink(&tx, res)); } - }, - Facility::SinkInput => match operation { - Operation::Removed => tx.send(TxMessage::StreamRemove(StreamType::SinkInput, index)).unwrap(), - _ => { introspect.get_sink_input_info(index, move |res| tx_sink_input(&tx, res)); } - }, - Facility::Source => match operation { - Operation::Removed => tx.send(TxMessage::StreamRemove(StreamType::Source, index)).unwrap(), - _ => { introspect.get_source_info_by_index(index, move |res| tx_source(&tx, res)); } - }, - Facility::SourceOutput => match operation { - Operation::Removed => tx.send(TxMessage::StreamRemove(StreamType::SourceOutput, index)).unwrap(), - _ => { introspect.get_source_output_info(index, move |res| tx_source_output(&tx, res)); } - }, - Facility::Card => match operation { - Operation::Removed => tx.send(TxMessage::CardRemove(index)).unwrap(), - _ => { introspect.get_card_info_by_index(index, move |res| tx_card(&tx, res)); } - }, - _ => () - }; - }))); - - mainloop.unlock(); - } - - - /** - * Handles queued messages from the pulse thread, updating the internal storage. - * Returns a boolean indicating that a layout refresh is required. - */ - - pub fn update(&mut self) -> bool { - let mut received = false; - - loop { - let res = self.channel.rx.try_recv(); - match res { - Ok(res) => { - received = true; - match res { - TxMessage::Default(sink, source) => self.update_default(sink, source), - TxMessage::StreamUpdate(t, data) => self.update_stream(t, &data), - TxMessage::StreamRemove(t, ind) => self.remove_stream(t, ind), - TxMessage::CardUpdate(data) => self.update_card(&data), - TxMessage::CardRemove(ind) => self.remove_card(ind), - TxMessage::Peak(t, ind, peak) => self.update_peak(t, ind, peak), - } - }, - _ => break - } - } - - received - } - - - /** - * Closes the connection to the pulse server, and cleans up any dangling monitors. - * After this operation, no other methods should be called, and the instance should be freed from memory. - */ - - pub fn cleanup(&mut self) { - while let Some((i, _)) = self.sinks.iter().next() { let i = *i; self.remove_stream(StreamType::Sink, i) } - while let Some((i, _)) = self.sink_inputs.iter().next() { let i = *i; self.remove_stream(StreamType::SinkInput, i) } - while let Some((i, _)) = self.sources.iter().next() { let i = *i; self.remove_stream(StreamType::Source, i) } - while let Some((i, _)) = self.source_outputs.iter().next() { let i = *i; self.remove_stream(StreamType::SourceOutput, i) } - - let mut mainloop = self.mainloop.borrow_mut(); - mainloop.stop(); - } - - - /** - * Updates the stored default sink and source to the ones identified. - * This method is called by the update method, the names are provided by the pulse server. - * - * * `sink` - The default sink. - * * `source` - The default source. - */ - - fn update_default(&mut self, sink: String, source: String) { - for (i, v) in &self.sinks { - if v.data.name == sink { - self.default_sink = *i; - self.active_sink = *i; - break; - } - } - - for (i, v) in &self.sources { - if v.data.name == source { - self.default_source = *i; - self.active_source = *i; - break; - } - } - } - - - /** - * Updates a stream in the store, or creates a new one and begins monitoring the peaks. - * This method is called by the update method, the data is provided by the pulse server. - * - * * `t` - The type of stream to update. - * * `stream` - The new stream's data. - */ - - fn update_stream(&mut self, t: StreamType, stream: &TxStreamData) { - let data = stream.data.clone(); - let index = data.index; - - let entry = match t { - StreamType::Sink => self.sinks.get_mut(&index), - StreamType::SinkInput => self.sink_inputs.get_mut(&index), - StreamType::Source => self.sources.get_mut(&index), - StreamType::SourceOutput => self.source_outputs.get_mut(&index), - }; - - if let Some(stream) = entry { stream.data = data; } - else { - let source_str = stream.monitor_index.to_string(); - let monitor = self.create_monitor_stream(t, if t == StreamType::SinkInput { None } else { Some(&source_str) }, index); - let data = StreamData { data, peak: 0, monitor, monitor_index: stream.monitor_index }; - match t { - StreamType::Sink => self.sinks.insert(index, data), - StreamType::SinkInput => self.sink_inputs.insert(index, data), - StreamType::Source => self.sources.insert(index, data), - StreamType::SourceOutput => self.source_outputs.insert(index, data) - }; - } - } - - - /** - * Removes a stream from the store, stopping the monitor, if there is one. - * This method is called by the update method, the data is provided by the pulse server. - * - * * `t` - The type of stream to remove. - * * `index` - The index of the stream to remove. - */ - - fn remove_stream(&mut self, t: StreamType, index: u32) { - let stream_opt = match t { - StreamType::Sink => self.sinks.get_mut(&index), - StreamType::SinkInput => self.sink_inputs.get_mut(&index), - StreamType::Source => self.sources.get_mut(&index), - StreamType::SourceOutput => self.source_outputs.get_mut(&index), - }; - - if let Some(stream) = stream_opt { - let mut monitor = stream.monitor.borrow_mut(); - let mut mainloop = self.mainloop.borrow_mut(); - mainloop.lock(); - if monitor.get_state().is_good() { - monitor.set_read_callback(None); - let _ = monitor.disconnect(); - } - mainloop.unlock(); - } - - match t { - StreamType::Sink => self.sinks.remove(&index), - StreamType::SinkInput => self.sink_inputs.remove(&index), - StreamType::Source => self.sources.remove(&index), - StreamType::SourceOutput => self.source_outputs.remove(&index), - }; - } - - - /** - * Updates a stored stream's peak. - * This method is called by the update method, the data is provided by a monitor stream. - * - * * `t` - The type of stream to update. - * * `index` - The index of the stream to update. - * * `peak` - The peak value to store. - */ - - fn update_peak(&mut self, t: StreamType, index: u32, peak: u32) { - match t { - StreamType::Sink => self.sinks.entry(index).and_modify(|e| e.peak = peak), - StreamType::SinkInput => self.sink_inputs.entry(index).and_modify(|e| e.peak = peak), - StreamType::Source => self.sources.entry(index).and_modify(|e| e.peak = peak), - StreamType::SourceOutput => self.source_outputs.entry(index).and_modify(|e| e.peak = peak) - }; - } - - - /** - * Creates a monitor stream for the stream specified, and returns it. - * Panics if there's an error. - * TODO: Don't panic. - * - * * `t` - The type of stream to monitor. - * * `source` - The source string of the stream, if one is needed. - * * `stream_index` - The index of the stream to monitor. - */ - - fn create_monitor_stream(&mut self, t: StreamType, source: Option<&str>, stream_index: u32) -> Shared { - fn read_callback(stream: &mut Stream, t: StreamType, index: u32, tx: &Sender) { - let mut raw_peak = 0.0; - while stream.readable_size().is_some() { - match stream.peek().unwrap() { - PeekResult::Hole(_) => stream.discard().unwrap(), - PeekResult::Data(b) => { - #[allow(clippy::transmute_ptr_to_ref)] - let buf = slice_as_array!(b, [u8; 4]).expect("Bad length."); - raw_peak = f32::from_le_bytes(*buf).max(raw_peak); - stream.discard().unwrap(); - }, - _ => break - } - } - let peak = (raw_peak.sqrt() * 65535.0 * 1.5).round() as u32; - tx.send(TxMessage::Peak(t, index, peak)).unwrap(); - } - - let attr = BufferAttr { - fragsize: 4, - maxlength: u32::MAX, - ..Default::default() - }; - - let spec = Spec { channels: 1, format: Format::F32le, rate: 30 }; - assert!(spec.is_valid()); - - let stream = Shared::new(Stream::new(&mut self.context.borrow_mut(), "Peak Detect", &spec, None).unwrap()); - { - let mut stream_mut = stream.borrow_mut(); - if t == StreamType::SinkInput { - stream_mut.set_monitor_stream(stream_index).unwrap(); - } - - let mut mainloop = self.mainloop.borrow_mut(); - mainloop.lock(); - stream_mut.connect_record(source, Some(&attr), - StreamFlagSet::DONT_MOVE | StreamFlagSet::ADJUST_LATENCY | StreamFlagSet::PEAK_DETECT).unwrap(); - mainloop.unlock(); - - let stream_clone = stream.clone(); - let txc = self.channel.tx.clone(); - stream_mut.set_read_callback(Some(Box::new(move |_| read_callback(&mut stream_clone.borrow_mut(), t, stream_index, &txc)))); - } - - stream - } - - - /** - * Updates a card in the store, or creates a new one. - * This method is called by the update method, the data is provided by the pulse server. - * - * * `data` - The card's data. - */ - - fn update_card(&mut self, data: &CardData) { - let index = data.index; - self.cards.insert(index, data.clone()); - } - - - /** - * Removes a card from the store. - * This method is called by the update method, the data is provided by the pulse server. - * - * * `index` - The index of the stream to remove. - */ - - fn remove_card(&mut self, index: u32) { - self.cards.remove(&index); - } + /** + * Creates a new pulse controller, configuring (but not initializing) the pulse connection. + */ + + pub fn new() -> Self { + let mut proplist = Proplist::new().unwrap(); + proplist + .set_str(properties::APPLICATION_NAME, "Myxer") + .unwrap(); + + let mainloop = Shared::new(Mainloop::new().expect("Failed to initialize pulse mainloop.")); + + let context = Shared::new( + Context::new_with_proplist(&*mainloop.borrow(), "Myxer Context", &proplist) + .expect("Failed to initialize pulse context."), + ); + + let (tx, rx) = channel::(); + + Pulse { + mainloop, + context, + channel: Channel { tx, rx }, + + default_sink: u32::MAX, + default_source: u32::MAX, + active_sink: u32::MAX, + active_source: u32::MAX, + + sinks: HashMap::new(), + sink_inputs: HashMap::new(), + sources: HashMap::new(), + source_outputs: HashMap::new(), + cards: HashMap::new(), + } + } + + /** + * Initiates a connection to pulse. Blocks until success, panics on failure. + * TODO: Graceful error handling, with debug message. + * TODO: Try to see if there's a way to avoid using unsafe? It's in the docs... but...? + */ + + pub fn connect(&mut self) { + let mut mainloop = self.mainloop.borrow_mut(); + let mut ctx = self.context.borrow_mut(); + + let mainloop_shr_ref = self.mainloop.clone(); + let ctx_shr_ref = self.context.clone(); + + ctx.set_state_callback(Some(Box::new(move || { + match unsafe { (*ctx_shr_ref.as_ptr()).get_state() } { + ContextState::Ready | ContextState::Failed | ContextState::Terminated => unsafe { + (*mainloop_shr_ref.as_ptr()).signal(false); + }, + _ => {} + } + }))); + + ctx.connect(None, CtxFlagSet::NOFLAGS, None) + .expect("Failed to connect to the pulse server."); + + mainloop.lock(); + mainloop.start().expect("Failed to start pulse mainloop."); + + loop { + match ctx.get_state() { + ContextState::Ready => { + ctx.set_state_callback(None); + mainloop.unlock(); + break; + } + ContextState::Failed | ContextState::Terminated => { + eprintln!("Context state failed/terminated, quitting..."); + mainloop.unlock(); + mainloop.stop(); + panic!("Pulse session terminated."); + } + _ => { + mainloop.wait(); + } + } + } + + drop(ctx); + drop(mainloop); + self.subscribe(); + } + + /** + * Asynchronously sets the default sink to the index provided. + * This is sometimes described as the fallback device. + * + * * `sink` - The sink index to set as the default. + */ + + pub fn set_default_sink(&self, sink: u32) { + if let Some(sink) = self.sinks.get(&sink) { + let mut mainloop = self.mainloop.borrow_mut(); + mainloop.lock(); + self.context + .borrow_mut() + .set_default_sink(&sink.data.name, |_| ()); + mainloop.unlock(); + } + } + + /** + * Asynchronously sets the default source to the index provided. + * This is sometimes described as the fallback device. + * + * * `source` - The source index to set as the default. + */ + + pub fn set_default_source(&self, source: u32) { + if let Some(source) = self.sources.get(&source) { + let mut mainloop = self.mainloop.borrow_mut(); + mainloop.lock(); + self.context + .borrow_mut() + .set_default_source(&source.data.name, |_| ()); + mainloop.unlock(); + } + } + + /** + * Sets the 'active' sink to the index provided. + * This is the sink that is currently displayed on the interface. + * + * * `sink` - The sink index to set as active. + */ + + pub fn set_active_sink(&mut self, sink: u32) { + self.active_sink = sink; + } + + /** + * Sets the 'active' source to the index provided. + * This is the source that is currently displayed on the interface. + * + * * `source` - The source to set as active. + */ + + pub fn set_active_source(&mut self, source: u32) { + self.active_source = source; + } + + /** + * Sets the volume of the stream to the volumes specified. + * This operation is asynchronous, so changes will not be reflected immediately. + * + * * `t` - The type of stream to set the volume of. + * * `index` - The index of the stream to set the volume of. + * * `volumes` - The desired volumes to set the channels of the stream to. + */ + + pub fn set_volume(&self, t: StreamType, index: u32, volumes: ChannelVolumes) { + let mut introspect = self.context.borrow().introspect(); + let mut mainloop = self.mainloop.borrow_mut(); + mainloop.lock(); + + match t { + StreamType::Sink => { + introspect.set_sink_volume_by_index(index, &volumes, None); + } + StreamType::SinkInput => { + introspect.set_sink_input_volume(index, &volumes, None); + } + StreamType::Source => { + introspect.set_source_volume_by_index(index, &volumes, None); + } + StreamType::SourceOutput => { + introspect.set_source_output_volume(index, &volumes, None); + } + }; + + mainloop.unlock(); + } + + /** + * Mutes or unmutes a stream. + * This operation is asynchronous, so changes will not be reflected immediately. + * + * * `t` - The type of stream to update. + * * `index` - The index of the stream to update. + * * `mute` - Whether the stream should be muted or not. + */ + + pub fn set_muted(&self, t: StreamType, index: u32, mute: bool) { + // If unmuting a stream that has been set to 0 volume, it should be reset to full. + if !mute { + let entry = match t { + StreamType::Sink => self.sinks.get(&index), + StreamType::SinkInput => self.sink_inputs.get(&index), + StreamType::Source => self.sources.get(&index), + StreamType::SourceOutput => self.source_outputs.get(&index), + }; + + if let Some(entry) = entry { + if entry.data.volume.max().0 == 0 { + let mut volumes = ChannelVolumes::default(); + volumes.set_len(entry.data.volume.len()); + volumes.set(entry.data.volume.len(), Volume(MAX_NATURAL_VOL)); + self.set_volume(t, index, volumes); + } + } + }; + + let mut introspect = self.context.borrow().introspect(); + let mut mainloop = self.mainloop.borrow_mut(); + mainloop.lock(); + + match t { + StreamType::Sink => introspect.set_sink_mute_by_index(index, mute, None), + StreamType::SinkInput => introspect.set_sink_input_mute(index, mute, None), + StreamType::Source => introspect.set_source_mute_by_index(index, mute, None), + StreamType::SourceOutput => introspect.set_source_output_mute(index, mute, None), + }; + + mainloop.unlock(); + } + + /** + * Set's a sound card's profile. + * This effects how the card behaves, and how the system can utilize it. + * + * * `index` - The card index to update. + * * `profile` - The profile name to update the card to. + */ + + pub fn set_card_profile(&self, index: u32, profile: &str) { + let mut introspect = self.context.borrow().introspect(); + let mut mainloop = self.mainloop.borrow_mut(); + mainloop.lock(); + introspect.set_card_profile_by_index(index, profile, None); + mainloop.unlock(); + } + + /** + * Binds listeners to server events, and triggers an + * initial sweep to populate the internal stores. + * Called by connect(), separated for readability. + */ + + fn subscribe(&mut self) { + /** Updates the client when the server information changes. */ + fn tx_server(tx: &Sender, item: &ServerInfo<'_>) { + tx.send(TxMessage::Default( + item.default_sink_name.clone().unwrap().into_owned(), + item.default_source_name.clone().unwrap().into_owned(), + )) + .unwrap(); + } + + /** Updates the client when a sink changes. */ + fn tx_sink(tx: &Sender, result: ListResult<&SinkInfo<'_>>) { + if let ListResult::Item(item) = result { + tx.send(TxMessage::StreamUpdate( + StreamType::Sink, + TxStreamData { + data: MeterData { + t: StreamType::Sink, + index: item.index, + icon: "multimedia-volume-control".to_owned(), + name: item.name.clone().unwrap().into_owned(), + description: item.description.clone().unwrap().into_owned(), + volume: item.volume, + muted: item.mute, + }, + monitor_index: item.monitor_source, + }, + )) + .unwrap(); + }; + } + + /** Updates the client when a sink input changes. */ + fn tx_sink_input(tx: &Sender, result: ListResult<&SinkInputInfo<'_>>) { + if let ListResult::Item(item) = result { + tx.send(TxMessage::StreamUpdate( + StreamType::SinkInput, + TxStreamData { + data: MeterData { + t: StreamType::SinkInput, + index: item.index, + icon: item + .proplist + .get_str("application.icon_name") + .unwrap_or_else(|| "audio-card".to_owned()), + name: item.name.clone().unwrap().into_owned(), + description: item + .proplist + .get_str("application.name") + .unwrap_or_else(|| "".to_owned()), + volume: item.volume, + muted: item.mute, + }, + monitor_index: item.sink, + }, + )) + .unwrap(); + }; + } + + /** Updates the client when a source changes. */ + fn tx_source(tx: &Sender, result: ListResult<&SourceInfo<'_>>) { + if let ListResult::Item(item) = result { + let name = item.name.clone().unwrap().into_owned(); + if name.ends_with(".monitor") { + return; + } + tx.send(TxMessage::StreamUpdate( + StreamType::Source, + TxStreamData { + data: MeterData { + t: StreamType::Source, + index: item.index, + icon: "audio-input-microphone".to_owned(), + name: item.name.clone().unwrap().into_owned(), + description: item.description.clone().unwrap().into_owned(), + volume: item.volume, + muted: item.mute, + }, + monitor_index: item.index, + }, + )) + .unwrap(); + }; + } + + /** Updates the client when a source output changes. */ + fn tx_source_output(tx: &Sender, result: ListResult<&SourceOutputInfo<'_>>) { + if let ListResult::Item(item) = result { + let app_id = item + .proplist + .get_str("application.process.binary") + .unwrap_or_else(|| "".to_owned()) + .to_lowercase(); + if app_id.contains("pavucontrol") || app_id.contains("myxer") { + return; + } + tx.send(TxMessage::StreamUpdate( + StreamType::SourceOutput, + TxStreamData { + data: MeterData { + t: StreamType::SourceOutput, + index: item.index, + icon: item + .proplist + .get_str("application.icon_name") + .unwrap_or_else(|| "audio-card".to_owned()), + name: item.name.clone().unwrap().into_owned(), + description: item + .proplist + .get_str("application.name") + .unwrap_or_else(|| "".to_owned()), + volume: item.volume, + muted: item.mute, + }, + monitor_index: item.source, + }, + )) + .unwrap(); + }; + } + + /** Updates the client when a sound card changes. */ + fn tx_card(tx: &Sender, result: ListResult<&CardInfo<'_>>) { + if let ListResult::Item(item) = result { + tx.send(TxMessage::CardUpdate(CardData { + index: item.index, + name: item + .proplist + .get_str("device.description") + .or_else(|| item.proplist.get_str("device.alias")) + .or_else(|| item.proplist.get_str("device.name")) + .unwrap_or_else(|| "".to_owned()), + icon: item + .proplist + .get_str("device.icon_name") + .unwrap_or_else(|| "audio-card-pci".to_owned()), + profiles: item + .profiles + .iter() + .map(|p| { + ( + p.name.as_ref().unwrap().clone().into_owned(), + p.description.as_ref().unwrap().clone().into_owned(), + ) + }) + .collect(), + active_profile: item + .active_profile + .as_ref() + .unwrap() + .name + .as_ref() + .unwrap() + .clone() + .into_owned(), + })) + .unwrap(); + } + } + + let mut mainloop = self.mainloop.borrow_mut(); + mainloop.lock(); + let mut context = self.context.borrow_mut(); + let introspect = context.introspect(); + + let tx = self.channel.tx.clone(); + introspect.get_sink_info_list(move |res| tx_sink(&tx, res)); + let tx = self.channel.tx.clone(); + introspect.get_sink_input_info_list(move |res| tx_sink_input(&tx, res)); + let tx = self.channel.tx.clone(); + introspect.get_source_info_list(move |res| tx_source(&tx, res)); + let tx = self.channel.tx.clone(); + introspect.get_source_output_info_list(move |res| tx_source_output(&tx, res)); + let tx = self.channel.tx.clone(); + introspect.get_card_info_list(move |res| tx_card(&tx, res)); + let tx = self.channel.tx.clone(); + introspect.get_server_info(move |res| tx_server(&tx, res)); + + let tx = self.channel.tx.clone(); + context.subscribe( + InterestMaskSet::SERVER + | InterestMaskSet::SINK + | InterestMaskSet::SINK_INPUT + | InterestMaskSet::SOURCE + | InterestMaskSet::SOURCE_OUTPUT + | InterestMaskSet::CARD, + |_| (), + ); + context.set_subscribe_callback(Some(Box::new(move |fac, op, index| { + let tx = tx.clone(); + let facility = fac.unwrap(); + let operation = op.unwrap(); + + match facility { + Facility::Server => { + introspect.get_server_info(move |res| tx_server(&tx, res)); + } + Facility::Sink => match operation { + Operation::Removed => tx + .send(TxMessage::StreamRemove(StreamType::Sink, index)) + .unwrap(), + _ => { + introspect.get_sink_info_by_index(index, move |res| tx_sink(&tx, res)); + } + }, + Facility::SinkInput => match operation { + Operation::Removed => tx + .send(TxMessage::StreamRemove(StreamType::SinkInput, index)) + .unwrap(), + _ => { + introspect.get_sink_input_info(index, move |res| tx_sink_input(&tx, res)); + } + }, + Facility::Source => match operation { + Operation::Removed => tx + .send(TxMessage::StreamRemove(StreamType::Source, index)) + .unwrap(), + _ => { + introspect.get_source_info_by_index(index, move |res| tx_source(&tx, res)); + } + }, + Facility::SourceOutput => match operation { + Operation::Removed => tx + .send(TxMessage::StreamRemove(StreamType::SourceOutput, index)) + .unwrap(), + _ => { + introspect + .get_source_output_info(index, move |res| tx_source_output(&tx, res)); + } + }, + Facility::Card => match operation { + Operation::Removed => tx.send(TxMessage::CardRemove(index)).unwrap(), + _ => { + introspect.get_card_info_by_index(index, move |res| tx_card(&tx, res)); + } + }, + _ => (), + }; + }))); + + mainloop.unlock(); + } + + /** + * Handles queued messages from the pulse thread, updating the internal storage. + * Returns a boolean indicating that a layout refresh is required. + */ + + pub fn update(&mut self) -> bool { + let mut received = false; + + loop { + let res = self.channel.rx.try_recv(); + match res { + Ok(res) => { + received = true; + match res { + TxMessage::Default(sink, source) => self.update_default(sink, source), + TxMessage::StreamUpdate(t, data) => self.update_stream(t, &data), + TxMessage::StreamRemove(t, ind) => self.remove_stream(t, ind), + TxMessage::CardUpdate(data) => self.update_card(&data), + TxMessage::CardRemove(ind) => self.remove_card(ind), + TxMessage::Peak(t, ind, peak) => self.update_peak(t, ind, peak), + } + } + _ => break, + } + } + + received + } + + /** + * Closes the connection to the pulse server, and cleans up any dangling monitors. + * After this operation, no other methods should be called, and the instance should be freed from memory. + */ + + pub fn cleanup(&mut self) { + while let Some((i, _)) = self.sinks.iter().next() { + let i = *i; + self.remove_stream(StreamType::Sink, i) + } + while let Some((i, _)) = self.sink_inputs.iter().next() { + let i = *i; + self.remove_stream(StreamType::SinkInput, i) + } + while let Some((i, _)) = self.sources.iter().next() { + let i = *i; + self.remove_stream(StreamType::Source, i) + } + while let Some((i, _)) = self.source_outputs.iter().next() { + let i = *i; + self.remove_stream(StreamType::SourceOutput, i) + } + + let mut mainloop = self.mainloop.borrow_mut(); + mainloop.stop(); + } + + /** + * Updates the stored default sink and source to the ones identified. + * This method is called by the update method, the names are provided by the pulse server. + * + * * `sink` - The default sink. + * * `source` - The default source. + */ + + fn update_default(&mut self, sink: String, source: String) { + for (i, v) in &self.sinks { + if v.data.name == sink { + self.default_sink = *i; + self.active_sink = *i; + break; + } + } + + for (i, v) in &self.sources { + if v.data.name == source { + self.default_source = *i; + self.active_source = *i; + break; + } + } + } + + /** + * Updates a stream in the store, or creates a new one and begins monitoring the peaks. + * This method is called by the update method, the data is provided by the pulse server. + * + * * `t` - The type of stream to update. + * * `stream` - The new stream's data. + */ + + fn update_stream(&mut self, t: StreamType, stream: &TxStreamData) { + let data = stream.data.clone(); + let index = data.index; + + let entry = match t { + StreamType::Sink => self.sinks.get_mut(&index), + StreamType::SinkInput => self.sink_inputs.get_mut(&index), + StreamType::Source => self.sources.get_mut(&index), + StreamType::SourceOutput => self.source_outputs.get_mut(&index), + }; + + if let Some(stream) = entry { + stream.data = data; + } else { + let source_str = stream.monitor_index.to_string(); + let monitor = self.create_monitor_stream( + t, + if t == StreamType::SinkInput { + None + } else { + Some(&source_str) + }, + index, + ); + let data = StreamData { + data, + peak: 0, + monitor, + monitor_index: stream.monitor_index, + }; + match t { + StreamType::Sink => self.sinks.insert(index, data), + StreamType::SinkInput => self.sink_inputs.insert(index, data), + StreamType::Source => self.sources.insert(index, data), + StreamType::SourceOutput => self.source_outputs.insert(index, data), + }; + } + } + + /** + * Removes a stream from the store, stopping the monitor, if there is one. + * This method is called by the update method, the data is provided by the pulse server. + * + * * `t` - The type of stream to remove. + * * `index` - The index of the stream to remove. + */ + + fn remove_stream(&mut self, t: StreamType, index: u32) { + let stream_opt = match t { + StreamType::Sink => self.sinks.get_mut(&index), + StreamType::SinkInput => self.sink_inputs.get_mut(&index), + StreamType::Source => self.sources.get_mut(&index), + StreamType::SourceOutput => self.source_outputs.get_mut(&index), + }; + + if let Some(stream) = stream_opt { + let mut monitor = stream.monitor.borrow_mut(); + let mut mainloop = self.mainloop.borrow_mut(); + mainloop.lock(); + if monitor.get_state().is_good() { + monitor.set_read_callback(None); + let _ = monitor.disconnect(); + } + mainloop.unlock(); + } + + match t { + StreamType::Sink => self.sinks.remove(&index), + StreamType::SinkInput => self.sink_inputs.remove(&index), + StreamType::Source => self.sources.remove(&index), + StreamType::SourceOutput => self.source_outputs.remove(&index), + }; + } + + /** + * Updates a stored stream's peak. + * This method is called by the update method, the data is provided by a monitor stream. + * + * * `t` - The type of stream to update. + * * `index` - The index of the stream to update. + * * `peak` - The peak value to store. + */ + + fn update_peak(&mut self, t: StreamType, index: u32, peak: u32) { + match t { + StreamType::Sink => self.sinks.entry(index).and_modify(|e| e.peak = peak), + StreamType::SinkInput => self.sink_inputs.entry(index).and_modify(|e| e.peak = peak), + StreamType::Source => self.sources.entry(index).and_modify(|e| e.peak = peak), + StreamType::SourceOutput => self + .source_outputs + .entry(index) + .and_modify(|e| e.peak = peak), + }; + } + + /** + * Creates a monitor stream for the stream specified, and returns it. + * Panics if there's an error. + * TODO: Don't panic. + * + * * `t` - The type of stream to monitor. + * * `source` - The source string of the stream, if one is needed. + * * `stream_index` - The index of the stream to monitor. + */ + + fn create_monitor_stream( + &mut self, + t: StreamType, + source: Option<&str>, + stream_index: u32, + ) -> Shared { + fn read_callback(stream: &mut Stream, t: StreamType, index: u32, tx: &Sender) { + let mut raw_peak = 0.0; + while stream.readable_size().is_some() { + match stream.peek().unwrap() { + PeekResult::Hole(_) => stream.discard().unwrap(), + PeekResult::Data(b) => { + #[allow(clippy::transmute_ptr_to_ref)] + let buf = slice_as_array!(b, [u8; 4]).expect("Bad length."); + raw_peak = f32::from_le_bytes(*buf).max(raw_peak); + stream.discard().unwrap(); + } + _ => break, + } + } + let peak = (raw_peak.sqrt() * 65535.0 * 1.5).round() as u32; + tx.send(TxMessage::Peak(t, index, peak)).unwrap(); + } + + let attr = BufferAttr { + fragsize: 4, + maxlength: u32::MAX, + ..Default::default() + }; + + let spec = Spec { + channels: 1, + format: Format::F32le, + rate: 30, + }; + assert!(spec.is_valid()); + + let stream = Shared::new( + Stream::new(&mut self.context.borrow_mut(), "Peak Detect", &spec, None).unwrap(), + ); + { + let mut stream_mut = stream.borrow_mut(); + if t == StreamType::SinkInput { + stream_mut.set_monitor_stream(stream_index).unwrap(); + } + + let mut mainloop = self.mainloop.borrow_mut(); + mainloop.lock(); + stream_mut + .connect_record( + source, + Some(&attr), + StreamFlagSet::DONT_MOVE + | StreamFlagSet::ADJUST_LATENCY + | StreamFlagSet::PEAK_DETECT, + ) + .unwrap(); + mainloop.unlock(); + + let stream_clone = stream.clone(); + let txc = self.channel.tx.clone(); + stream_mut.set_read_callback(Some(Box::new(move |_| { + read_callback(&mut stream_clone.borrow_mut(), t, stream_index, &txc) + }))); + } + + stream + } + + /** + * Updates a card in the store, or creates a new one. + * This method is called by the update method, the data is provided by the pulse server. + * + * * `data` - The card's data. + */ + + fn update_card(&mut self, data: &CardData) { + let index = data.index; + self.cards.insert(index, data.clone()); + } + + /** + * Removes a card from the store. + * This method is called by the update method, the data is provided by the pulse server. + * + * * `index` - The index of the stream to remove. + */ + + fn remove_card(&mut self, index: u32) { + self.cards.remove(&index); + } } diff --git a/src/window/about.rs b/src/window/about.rs index fa5a8a0..ef661b7 100644 --- a/src/window/about.rs +++ b/src/window/about.rs @@ -4,24 +4,23 @@ use gtk::prelude::*; - /** * Creates and runs the About popup window, * which contains information about the app, license, and Auri. */ pub fn about() { - let about = gtk::AboutDialog::new(); - about.set_logo_icon_name(Some("multimedia-volume-control")); - about.set_program_name("Myxer"); - about.set_version(Some("1.2.1")); - about.set_comments(Some("A modern Volume Mixer for PulseAudio.")); - about.set_website(Some("https://myxer.aurailus.com")); - about.set_copyright(Some("© 2021 Auri Collings")); - about.set_license_type(gtk::License::Gpl30); - about.add_credit_section("Created by", &[ "Auri Collings" ]); - about.add_credit_section("libpulse-binding by", &[ "Lyndon Brown" ]); + let about = gtk::AboutDialog::new(); + about.set_logo_icon_name(Some("multimedia-volume-control")); + about.set_program_name("Myxer"); + about.set_version(Some("1.2.1")); + about.set_comments(Some("A modern Volume Mixer for PulseAudio.")); + about.set_website(Some("https://myxer.aurailus.com")); + about.set_copyright(Some("© 2021 Auri Collings, © 2021 Erin van der Veen")); + about.set_license_type(gtk::License::Gpl30); + about.add_credit_section("Created by", &["Auri Collings"]); + about.add_credit_section("libpulse-binding by", &["Lyndon Brown"]); - about.connect_response(|about, _| about.close()); - about.run(); + about.connect_response(|about, _| about.close()); + about.run(); } diff --git a/src/window/myxer.rs b/src/window/myxer.rs index b9462fb..ed39306 100644 --- a/src/window/myxer.rs +++ b/src/window/myxer.rs @@ -5,347 +5,422 @@ use std::collections::HashMap; use gtk::prelude::*; -use gio::prelude::*; use super::style; +use super::{about, Profiles}; +use crate::meter::{Meter, SinkMeter, SourceMeter, StreamMeter}; use crate::pulse::Pulse; use crate::shared::Shared; -use super::{ about, Profiles }; -use crate::meter::{ Meter, SinkMeter, SourceMeter, StreamMeter }; - /** * Stores meter widgets and options. */ pub struct Meters { - pub sink: SinkMeter, - pub sink_box: gtk::Box, - pub sink_inputs: HashMap, - pub sink_inputs_box: gtk::Box, - - pub source: SourceMeter, - pub source_box: gtk::Box, - pub source_outputs: HashMap, - pub source_outputs_box: gtk::Box, - - pub show_visualizers: bool, - pub separate_channels: bool + pub sink: SinkMeter, + pub sink_box: gtk::Box, + pub sink_inputs: HashMap, + pub sink_inputs_box: gtk::Box, + + pub source: SourceMeter, + pub source_box: gtk::Box, + pub source_outputs: HashMap, + pub source_outputs_box: gtk::Box, + + pub show_visualizers: bool, + pub separate_channels: bool, } impl Meters { - - /** - * Creates the Struct, and some base widgets, - * including the Sink and Source meters. - * - * * `pulse` - The Pulse instance used by the app. - */ - - pub fn new(pulse: &Shared) -> Self { - let sink = SinkMeter::new(pulse.clone()); - - let sink_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); - sink_box.get_style_context().add_class("pad_side"); - sink_box.add(&sink.widget); - - let source = SourceMeter::new(pulse.clone()); - - let source_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); - source_box.get_style_context().add_class("pad_side"); - source_box.add(&source.widget); - - let sink_inputs_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); - sink_inputs_box.get_style_context().add_class("pad_side"); - - let source_outputs_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); - source_outputs_box.get_style_context().add_class("pad_side"); - - Meters { - sink, source, - sink_box, source_box, - sink_inputs_box, source_outputs_box, - sink_inputs: HashMap::new(), - source_outputs: HashMap::new(), - show_visualizers: true, - separate_channels: false, - } - } - - - /** - * Toggles the show visualizers setting, and returns its current state. - */ - - fn toggle_visualizers(&mut self) -> bool { - self.show_visualizers = !self.show_visualizers; - self.show_visualizers - } - - - /** - * Toggles the separate channels setting, and returns its current state. - */ - - fn toggle_separate_channels(&mut self) -> bool { - self.separate_channels = !self.separate_channels; - self.separate_channels - } + /** + * Creates the Struct, and some base widgets, + * including the Sink and Source meters. + * + * * `pulse` - The Pulse instance used by the app. + */ + + pub fn new(pulse: &Shared) -> Self { + let sink = SinkMeter::new(pulse.clone()); + + let sink_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + sink_box.style_context().add_class("pad_side"); + sink_box.add(&sink.widget); + + let source = SourceMeter::new(pulse.clone()); + + let source_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + source_box.style_context().add_class("pad_side"); + source_box.add(&source.widget); + + let sink_inputs_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + sink_inputs_box.style_context().add_class("pad_side"); + + let source_outputs_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); + source_outputs_box.style_context().add_class("pad_side"); + + Meters { + sink, + source, + sink_box, + source_box, + sink_inputs_box, + source_outputs_box, + sink_inputs: HashMap::new(), + source_outputs: HashMap::new(), + show_visualizers: true, + separate_channels: false, + } + } + + /** + * Toggles the show visualizers setting, and returns its current state. + */ + + fn toggle_visualizers(&mut self) -> bool { + self.show_visualizers = !self.show_visualizers; + self.show_visualizers + } + + /** + * Toggles the separate channels setting, and returns its current state. + */ + + fn toggle_separate_channels(&mut self) -> bool { + self.separate_channels = !self.separate_channels; + self.separate_channels + } } - /** * The main Myxer application window, * Displays meters for each sink, source, sink input, and source output. */ pub struct Myxer { - pulse: Shared, - meters: Shared, + pulse: Shared, + meters: Shared, - profiles: Shared> + profiles: Shared>, } impl Myxer { - - /** - * Initializes the main window. - * - * * `app` - The GTK application. - * * `pulse` - The Pulse store instance. - */ - - pub fn new(app: >k::Application, pulse: &Shared) -> Self { - let window = gtk::ApplicationWindow::new(app); - let header = gtk::HeaderBar::new(); - let stack = gtk::Stack::new(); - - { - window.set_title("Volume Mixer"); - window.set_icon_name(Some("multimedia-volume-control")); - - let geom = gdk::Geometry { - min_width: 580, min_height: 400, - max_width: 10000, max_height: 400, - base_width: -1, base_height: -1, - width_inc: -1, height_inc: -1, - min_aspect: 0.0, max_aspect: 0.0, - win_gravity: gdk::Gravity::Center - }; - - window.set_type_hint(gdk::WindowTypeHint::Dialog); - window.set_geometry_hints::(None, Some(&geom), gdk::WindowHints::MIN_SIZE | gdk::WindowHints::MAX_SIZE); - window.get_style_context().add_class("Myxer"); - style::style(&window); - - let stack_switcher = gtk::StackSwitcher::new(); - stack_switcher.set_stack(Some(&stack)); - - header.set_show_close_button(true); - header.set_custom_title(Some(&stack_switcher)); - - let title_vert = gtk::Box::new(gtk::Orientation::Vertical, 0); - header.pack_start(&title_vert); - - let title_hor = gtk::Box::new(gtk::Orientation::Horizontal, 0); - title_vert.pack_start(&title_hor, true, true, 0); - - let icon = gtk::Image::from_icon_name(Some("multimedia-volume-control"), gtk::IconSize::Button); - title_hor.pack_start(&icon, true, true, 3); - let title = gtk::Label::new(Some("Volume Mixer")); - title.get_style_context().add_class("title"); - title_hor.pack_start(&title, true, true, 0); - - window.set_titlebar(Some(&header)); - } - - { - let prefs_button = gtk::Button::from_icon_name(Some("open-menu-symbolic"), gtk::IconSize::SmallToolbar); - prefs_button.get_style_context().add_class("flat"); - prefs_button.set_widget_name("preferences"); - prefs_button.set_can_focus(false); - header.pack_end(&prefs_button); - - let prefs = gtk::PopoverMenu::new(); - prefs.set_pointing_to(>k::Rectangle { x: 12, y: 32, width: 2, height: 2 }); - prefs.set_relative_to(Some(&prefs_button)); - prefs.set_border_width(6); - - let prefs_box = gtk::Box::new(gtk::Orientation::Vertical, 0); - prefs.add(&prefs_box); - - let show_visualizers = gtk::ModelButton::new(); - show_visualizers.set_property_text(Some("Visualize Peaks")); - show_visualizers.set_action_name(Some("app.show_visualizers")); - prefs_box.add(&show_visualizers); - - let split_channels = gtk::ModelButton::new(); - split_channels.set_property_text(Some("Split Channels")); - split_channels.set_action_name(Some("app.split_channels")); - prefs_box.add(&split_channels); - - let card_profiles = gtk::ModelButton::new(); - card_profiles.set_property_text(Some("Card Profiles...")); - card_profiles.set_action_name(Some("app.card_profiles")); - prefs_box.add(&card_profiles); - - prefs_box.pack_start(>k::Separator::new(gtk::Orientation::Horizontal), false, false, 4); - - let about = gtk::ModelButton::new(); - about.set_property_text(Some("About Myxer")); - about.set_action_name(Some("app.about")); - prefs_box.add(&about); - - prefs_box.show_all(); - prefs_button.connect_clicked(move |_| prefs.popup()); - } - - pulse.borrow_mut().connect(); - let meters = Shared::new(Meters::new(pulse)); - - { - let output = gtk::Box::new(gtk::Orientation::Horizontal, 0); - output.pack_start(&meters.borrow_mut().sink_box, false, false, 0); - - output.pack_start(>k::Separator::new(gtk::Orientation::Vertical), false, true, 0); - - let output_scroller = gtk::ScrolledWindow::new::(None, None); - output_scroller.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never); - output_scroller.get_style_context().add_class("bordered"); - output.pack_start(&output_scroller, true, true, 0); - output_scroller.add(&meters.borrow().sink_inputs_box); - - let input = gtk::Box::new(gtk::Orientation::Horizontal, 0); - input.pack_start(&meters.borrow_mut().source_box, false, false, 0); - - input.pack_start(>k::Separator::new(gtk::Orientation::Vertical), false, true, 0); - - let input_scroller = gtk::ScrolledWindow::new::(None, None); - input_scroller.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never); - input_scroller.get_style_context().add_class("bordered"); - input.pack_start(&input_scroller, true, true, 0); - input_scroller.add(&meters.borrow().source_outputs_box); - - stack.add_titled(&output, "output", "Output"); - stack.add_titled(&input, "input", "Input"); - - window.add(&stack); - window.show_all(); - } - - let profiles = Shared::new(None); - - { - let actions = gio::SimpleActionGroup::new(); - window.insert_action_group("app", Some(&actions)); - - let about = gio::SimpleAction::new("about", None); - about.connect_activate(|_, _| about::about()); - actions.add_action(&about); - - let card_profiles = gio::SimpleAction::new("card_profiles", None); - let pulse = pulse.clone(); - let profiles = profiles.clone(); - card_profiles.connect_activate(move |_, _| { - profiles.replace(Some(Profiles::new(&window, &pulse))); - }); - actions.add_action(&card_profiles); - - let meters_clone = meters.clone(); - let split_channels = gio::SimpleAction::new_stateful("split_channels", glib::VariantTy::new("bool").ok(), &false.to_variant()); - split_channels.connect_activate(move |s, _| s.set_state(&meters_clone.borrow_mut().toggle_separate_channels().to_variant())); - actions.add_action(&split_channels); - - let meters_clone = meters.clone(); - let show_visualizers = gio::SimpleAction::new_stateful("show_visualizers", glib::VariantTy::new("bool").ok(), &true.to_variant()); - show_visualizers.connect_activate(move |s, _| s.set_state(&meters_clone.borrow_mut().toggle_visualizers().to_variant())); - actions.add_action(&show_visualizers); - } - - Self { - pulse: pulse.clone(), - meters, - profiles - } - } - - - /** - * Updates the app's widgets based on information stored in the Pulse instance. - * Kills the Card Profiles window if it has been requested. - */ - - pub fn update(&mut self) { - let mut kill = false; - if let Some(profiles) = self.profiles.borrow_mut().as_mut() { kill = !profiles.update(); } - if kill { self.profiles.replace(None); } - - if self.pulse.borrow_mut().update() { - let pulse = self.pulse.borrow(); - - let mut meters = self.meters.borrow_mut(); - - let offset = meters.sink.widget.get_allocation().height + - meters.sink.widget.get_margin_bottom() - meters.sink_inputs_box.get_allocation().height; - if offset != meters.sink.widget.get_margin_bottom() { meters.sink.widget.set_margin_bottom(offset) } - - let offset = meters.source.widget.get_allocation().height + - meters.source.widget.get_margin_bottom() - meters.source_outputs_box.get_allocation().height; - if offset != meters.source.widget.get_margin_bottom() { meters.source.widget.set_margin_bottom(offset) } - - - let show = meters.show_visualizers; - let separate = meters.separate_channels; - - - if let Some(sink) = pulse.sinks.get(&pulse.active_sink) { - meters.sink.set_data(&sink.data); - meters.sink.split_channels(separate); - meters.sink.set_peak(if show { Some(sink.peak) } else { None }); - } - - for (index, input) in &pulse.sink_inputs { - let sink_inputs_box = meters.sink_inputs_box.clone(); - - let meter = meters.sink_inputs.entry(*index).or_insert_with(|| StreamMeter::new(self.pulse.clone())); - if meter.widget.get_parent().is_none() { sink_inputs_box.pack_start(&meter.widget, false, false, 0); } - meter.set_data(&input.data); - meter.split_channels(separate); - meter.set_peak(if show { Some(input.peak) } else { None }); - } - - let sink_inputs_box = meters.sink_inputs_box.clone(); - meters.sink_inputs.retain(|index, meter| { - let keep = pulse.sink_inputs.contains_key(index); - if !keep { sink_inputs_box.remove(&meter.widget); } - keep - }); - - if let Some(source) = pulse.sources.get(&pulse.active_source) { - meters.source.set_data(&source.data); - meters.source.split_channels(separate); - meters.source.set_peak(if show { Some(source.peak) } else { None }); - } - - for (index, output) in &pulse.source_outputs { - let source_outputs_box = meters.source_outputs_box.clone(); - - let meter = meters.source_outputs.entry(*index).or_insert_with(|| StreamMeter::new(self.pulse.clone())); - if meter.widget.get_parent().is_none() { source_outputs_box.pack_start(&meter.widget, false, false, 0); } - meter.set_data(&output.data); - meter.split_channels(separate); - meter.set_peak(if show { Some(output.peak) } else { None }); - } - - let source_outputs_box = meters.source_outputs_box.clone(); - meters.source_outputs.retain(|index, meter| { - let keep = pulse.source_outputs.contains_key(index); - if !keep { source_outputs_box.remove(&meter.widget); } - keep - }); - - meters.sink_inputs_box.show_all(); - meters.source_outputs_box.show_all(); - } - } + /** + * Initializes the main window. + * + * * `app` - The GTK application. + * * `pulse` - The Pulse store instance. + */ + + pub fn new(app: >k::Application, pulse: &Shared) -> Self { + let window = gtk::ApplicationWindow::new(app); + let header = gtk::HeaderBar::new(); + let stack = gtk::Stack::new(); + + { + window.set_title("Volume Mixer"); + window.set_icon_name(Some("multimedia-volume-control")); + + let geom = gdk::Geometry { + min_width: 580, + min_height: 400, + max_width: 10000, + max_height: 400, + base_width: -1, + base_height: -1, + width_inc: -1, + height_inc: -1, + min_aspect: 0.0, + max_aspect: 0.0, + win_gravity: gdk::Gravity::Center, + }; + + window.set_type_hint(gdk::WindowTypeHint::Dialog); + window.set_geometry_hints::( + None, + Some(&geom), + gdk::WindowHints::MIN_SIZE | gdk::WindowHints::MAX_SIZE, + ); + window.style_context().add_class("Myxer"); + style::style(&window); + + let stack_switcher = gtk::StackSwitcher::new(); + stack_switcher.set_stack(Some(&stack)); + + header.set_show_close_button(true); + header.set_custom_title(Some(&stack_switcher)); + + let title_vert = gtk::Box::new(gtk::Orientation::Vertical, 0); + header.pack_start(&title_vert); + + let title_hor = gtk::Box::new(gtk::Orientation::Horizontal, 0); + title_vert.pack_start(&title_hor, true, true, 0); + + let icon = gtk::Image::from_icon_name( + Some("multimedia-volume-control"), + gtk::IconSize::Button, + ); + title_hor.pack_start(&icon, true, true, 3); + let title = gtk::Label::new(Some("Volume Mixer")); + title.style_context().add_class("title"); + title_hor.pack_start(&title, true, true, 0); + + window.set_titlebar(Some(&header)); + } + + { + let prefs_button = gtk::Button::from_icon_name( + Some("open-menu-symbolic"), + gtk::IconSize::SmallToolbar, + ); + prefs_button.style_context().add_class("flat"); + prefs_button.set_widget_name("preferences"); + prefs_button.set_can_focus(false); + header.pack_end(&prefs_button); + + let prefs = gtk::PopoverMenu::new(); + prefs.set_pointing_to(>k::Rectangle { + x: 12, + y: 32, + width: 2, + height: 2, + }); + prefs.set_relative_to(Some(&prefs_button)); + prefs.set_border_width(6); + + let prefs_box = gtk::Box::new(gtk::Orientation::Vertical, 0); + prefs.add(&prefs_box); + + let show_visualizers = gtk::ModelButton::new(); + show_visualizers.set_property("text", Some("Visualize Peaks")); + show_visualizers.set_action_name(Some("app.show_visualizers")); + prefs_box.add(&show_visualizers); + + let split_channels = gtk::ModelButton::new(); + split_channels.set_property("text", Some("Split Channels")); + split_channels.set_action_name(Some("app.split_channels")); + prefs_box.add(&split_channels); + + let card_profiles = gtk::ModelButton::new(); + card_profiles.set_property("text", Some("Card Profiles...")); + card_profiles.set_action_name(Some("app.card_profiles")); + prefs_box.add(&card_profiles); + + prefs_box.pack_start( + >k::Separator::new(gtk::Orientation::Horizontal), + false, + false, + 4, + ); + + let about = gtk::ModelButton::new(); + about.set_property("text", Some("About Myxer")); + about.set_action_name(Some("app.about")); + prefs_box.add(&about); + + prefs_box.show_all(); + prefs_button.connect_clicked(move |_| prefs.popup()); + } + + pulse.borrow_mut().connect(); + let meters = Shared::new(Meters::new(pulse)); + + { + let output = gtk::Box::new(gtk::Orientation::Horizontal, 0); + output.pack_start(&meters.borrow_mut().sink_box, false, false, 0); + + output.pack_start( + >k::Separator::new(gtk::Orientation::Vertical), + false, + true, + 0, + ); + + let output_scroller = + gtk::ScrolledWindow::new::(None, None); + output_scroller.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never); + output_scroller.style_context().add_class("bordered"); + output.pack_start(&output_scroller, true, true, 0); + output_scroller.add(&meters.borrow().sink_inputs_box); + + let input = gtk::Box::new(gtk::Orientation::Horizontal, 0); + input.pack_start(&meters.borrow_mut().source_box, false, false, 0); + + input.pack_start( + >k::Separator::new(gtk::Orientation::Vertical), + false, + true, + 0, + ); + + let input_scroller = + gtk::ScrolledWindow::new::(None, None); + input_scroller.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never); + input_scroller.style_context().add_class("bordered"); + input.pack_start(&input_scroller, true, true, 0); + input_scroller.add(&meters.borrow().source_outputs_box); + + stack.add_titled(&output, "output", "Output"); + stack.add_titled(&input, "input", "Input"); + + window.add(&stack); + window.show_all(); + } + + let profiles = Shared::new(None); + + { + let actions = gio::SimpleActionGroup::new(); + window.insert_action_group("app", Some(&actions)); + + let about = gio::SimpleAction::new("about", None); + about.connect_activate(|_, _| about::about()); + actions.add_action(&about); + + let card_profiles = gio::SimpleAction::new("card_profiles", None); + let pulse = pulse.clone(); + let profiles = profiles.clone(); + card_profiles.connect_activate(move |_, _| { + profiles.replace(Some(Profiles::new(&window, &pulse))); + }); + actions.add_action(&card_profiles); + + let meters_clone = meters.clone(); + let split_channels = gio::SimpleAction::new_stateful( + "split_channels", + glib::VariantTy::new("bool").ok(), + &false.to_variant(), + ); + split_channels.connect_activate(move |s, _| { + s.set_state( + &meters_clone + .borrow_mut() + .toggle_separate_channels() + .to_variant(), + ) + }); + actions.add_action(&split_channels); + + let meters_clone = meters.clone(); + let show_visualizers = gio::SimpleAction::new_stateful( + "show_visualizers", + glib::VariantTy::new("bool").ok(), + &true.to_variant(), + ); + show_visualizers.connect_activate(move |s, _| { + s.set_state(&meters_clone.borrow_mut().toggle_visualizers().to_variant()) + }); + actions.add_action(&show_visualizers); + } + + Self { + pulse: pulse.clone(), + meters, + profiles, + } + } + + /** + * Updates the app's widgets based on information stored in the Pulse instance. + * Kills the Card Profiles window if it has been requested. + */ + + pub fn update(&mut self) { + let mut kill = false; + if let Some(profiles) = self.profiles.borrow_mut().as_mut() { + kill = !profiles.update(); + } + if kill { + self.profiles.replace(None); + } + + if self.pulse.borrow_mut().update() { + let pulse = self.pulse.borrow(); + + let mut meters = self.meters.borrow_mut(); + + let offset = meters.sink.widget.allocation().height + + meters.sink.widget.margin_bottom() + - meters.sink_inputs_box.allocation().height; + if offset != meters.sink.widget.margin_bottom() { + meters.sink.widget.set_margin_bottom(offset) + } + + let offset = meters.source.widget.allocation().height + + meters.source.widget.margin_bottom() + - meters.source_outputs_box.allocation().height; + if offset != meters.source.widget.margin_bottom() { + meters.source.widget.set_margin_bottom(offset) + } + + let show = meters.show_visualizers; + let separate = meters.separate_channels; + + if let Some(sink) = pulse.sinks.get(&pulse.active_sink) { + meters.sink.set_data(&sink.data); + meters.sink.split_channels(separate); + meters + .sink + .set_peak(if show { Some(sink.peak) } else { None }); + } + + for (index, input) in &pulse.sink_inputs { + let sink_inputs_box = meters.sink_inputs_box.clone(); + + let meter = meters + .sink_inputs + .entry(*index) + .or_insert_with(|| StreamMeter::new(self.pulse.clone())); + if meter.widget.parent().is_none() { + sink_inputs_box.pack_start(&meter.widget, false, false, 0); + } + meter.set_data(&input.data); + meter.split_channels(separate); + meter.set_peak(if show { Some(input.peak) } else { None }); + } + + let sink_inputs_box = meters.sink_inputs_box.clone(); + meters.sink_inputs.retain(|index, meter| { + let keep = pulse.sink_inputs.contains_key(index); + if !keep { + sink_inputs_box.remove(&meter.widget); + } + keep + }); + + if let Some(source) = pulse.sources.get(&pulse.active_source) { + meters.source.set_data(&source.data); + meters.source.split_channels(separate); + meters + .source + .set_peak(if show { Some(source.peak) } else { None }); + } + + for (index, output) in &pulse.source_outputs { + let source_outputs_box = meters.source_outputs_box.clone(); + + let meter = meters + .source_outputs + .entry(*index) + .or_insert_with(|| StreamMeter::new(self.pulse.clone())); + if meter.widget.parent().is_none() { + source_outputs_box.pack_start(&meter.widget, false, false, 0); + } + meter.set_data(&output.data); + meter.split_channels(separate); + meter.set_peak(if show { Some(output.peak) } else { None }); + } + + let source_outputs_box = meters.source_outputs_box.clone(); + meters.source_outputs.retain(|index, meter| { + let keep = pulse.source_outputs.contains_key(index); + if !keep { + source_outputs_box.remove(&meter.widget); + } + keep + }); + + meters.sink_inputs_box.show_all(); + meters.source_outputs_box.show_all(); + } + } } diff --git a/src/window/profiles.rs b/src/window/profiles.rs index 7fe32c5..27ac91e 100644 --- a/src/window/profiles.rs +++ b/src/window/profiles.rs @@ -10,104 +10,126 @@ use crate::card::Card; use crate::pulse::Pulse; use crate::shared::Shared; - /** * Stores the created Card widgets. */ struct Cards { - cards: HashMap, - cards_box: gtk::Box, + cards: HashMap, + cards_box: gtk::Box, } impl Cards { - - /** - * Initializes the structure. - */ - - pub fn new() -> Self { - Cards { cards: HashMap::new(), cards_box: gtk::Box::new(gtk::Orientation::Vertical, 8) } - } + /** + * Initializes the structure. + */ + + pub fn new() -> Self { + Cards { + cards: HashMap::new(), + cards_box: gtk::Box::new(gtk::Orientation::Vertical, 8), + } + } } - /** * The Card Profiles popup window. * Allows listing and changing pulseaudio sound Card profiles. */ pub struct Profiles { - cards: Shared, - pulse: Shared, + cards: Shared, + pulse: Shared, - /** Indicates if the popup should remain open. */ - live: Shared + /** Indicates if the popup should remain open. */ + live: Shared, } impl Profiles { - - /** - * Creates the Card Profiles window, and its contents. - */ - - pub fn new(parent: >k::ApplicationWindow, pulse: &Shared) -> Self { - let dialog = gtk::Dialog::with_buttons(Some("Card Profiles"), Some(parent), gtk::DialogFlags::all(), &[]); - dialog.set_border_width(0); - - let live = Shared::new(true); - dialog.connect_response(|s, _| s.emit_close()); - let live_clone = live.clone(); - dialog.connect_close(move |_| { live_clone.replace(false); }); - - let geom = gdk::Geometry { - min_width: 450, min_height: 550, - max_width: 450, max_height: 10000, - base_width: -1, base_height: -1, - width_inc: -1, height_inc: -1, - min_aspect: 0.0, max_aspect: 0.0, - win_gravity: gdk::Gravity::Center - }; - - dialog.set_geometry_hints::(None, Some(&geom), gdk::WindowHints::MIN_SIZE | gdk::WindowHints::MAX_SIZE); - let cards = Shared::new(Cards::new()); - - let scroller = gtk::ScrolledWindow::new::(None, None); - scroller.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic); - dialog.get_content_area().pack_start(&scroller, true, true, 0); - dialog.get_content_area().set_border_width(0); - scroller.add(&cards.borrow().cards_box); - - dialog.show_all(); - - Self { - live, - cards, - pulse: pulse.clone() - } - } - - - /** - * Updates the card widgets to the latest information, - * returns a boolean indicating if the window should continue to be open or not. - */ - - pub fn update(&mut self) -> bool { - let pulse = self.pulse.borrow_mut(); - let mut cards = self.cards.borrow_mut(); - for (index, data) in &pulse.cards { - let cards_box = cards.cards_box.clone(); - - let card = cards.cards.entry(*index).or_insert_with(|| Card::new(Some(self.pulse.clone()))); - if card.widget.get_parent().is_none() { - cards_box.pack_start(&card.widget, false, false, 0); - cards_box.pack_start(>k::Separator::new(gtk::Orientation::Horizontal), false, false, 0); - } - card.set_data(&data); - } - - cards.cards_box.show_all(); - *self.live.borrow() - } + /** + * Creates the Card Profiles window, and its contents. + */ + + pub fn new(parent: >k::ApplicationWindow, pulse: &Shared) -> Self { + let dialog = gtk::Dialog::with_buttons( + Some("Card Profiles"), + Some(parent), + gtk::DialogFlags::all(), + &[], + ); + dialog.set_border_width(0); + + let live = Shared::new(true); + dialog.connect_response(|s, _| s.emit_close()); + let live_clone = live.clone(); + dialog.connect_close(move |_| { + live_clone.replace(false); + }); + + let geom = gdk::Geometry { + min_width: 450, + min_height: 550, + max_width: 450, + max_height: 10000, + base_width: -1, + base_height: -1, + width_inc: -1, + height_inc: -1, + min_aspect: 0.0, + max_aspect: 0.0, + win_gravity: gdk::Gravity::Center, + }; + + dialog.set_geometry_hints::( + None, + Some(&geom), + gdk::WindowHints::MIN_SIZE | gdk::WindowHints::MAX_SIZE, + ); + let cards = Shared::new(Cards::new()); + + let scroller = gtk::ScrolledWindow::new::(None, None); + scroller.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic); + dialog.content_area().pack_start(&scroller, true, true, 0); + dialog.content_area().set_border_width(0); + scroller.add(&cards.borrow().cards_box); + + dialog.show_all(); + + Self { + live, + cards, + pulse: pulse.clone(), + } + } + + /** + * Updates the card widgets to the latest information, + * returns a boolean indicating if the window should continue to be open or not. + */ + + pub fn update(&mut self) -> bool { + let pulse = self.pulse.borrow_mut(); + let mut cards = self.cards.borrow_mut(); + for (index, data) in &pulse.cards { + let cards_box = cards.cards_box.clone(); + + let card = cards + .cards + .entry(*index) + .or_insert_with(|| Card::new(Some(self.pulse.clone()))); + if card.widget.parent().is_none() { + cards_box.pack_start(&card.widget, false, false, 0); + cards_box.pack_start( + >k::Separator::new(gtk::Orientation::Horizontal), + false, + false, + 0, + ); + } + card.set_data(&data); + } + + cards.cards_box.show_all(); + *self.live.borrow() + } } diff --git a/src/window/style.rs b/src/window/style.rs index 041fad3..24ca242 100644 --- a/src/window/style.rs +++ b/src/window/style.rs @@ -6,7 +6,6 @@ use gtk::prelude::*; - /** * Applies application-specific styles to the specified window. * Generates a few GTK widgets to steal their theme colors using deprecated methods. @@ -17,35 +16,61 @@ use gtk::prelude::*; */ pub fn style(window: >k::ApplicationWindow) { - let provider = gtk::CssProvider::new(); - - let mut s = String::new(); - - let mut add_color = |identifier: &str, color: &gdk::RGBA| { - s.push_str("@define-color "); - s.push_str(identifier); - s.push(' '); - s.push_str(&colorsys::Rgb::new(color.red * 255.0, color.green * 255.0, color.blue * 255.0, None).to_css_string()); - s.push_str(";\n"); - }; - - let row = gtk::ListBoxRow::new(); - let button = gtk::Button::new(); - add_color("scale_color", &row.get_style_context().get_background_color(gtk::StateFlags::SELECTED)); - add_color("background_color", &window.get_style_context().get_background_color(gtk::StateFlags::NORMAL)); - add_color("foreground_color", &button.get_style_context().get_color(gtk::StateFlags::NORMAL)); - - s.push_str(STYLE); - - provider.load_from_data(s.as_bytes()).expect("Failed to load CSS."); - gtk::StyleContext::add_provider_for_screen(&gdk::Screen::get_default().expect("Error initializing GTK CSS provider."), - &provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION); + let provider = gtk::CssProvider::new(); + + let mut s = String::new(); + + let mut add_color = |identifier: &str, color: &gdk::RGBA| { + s.push_str("@define-color "); + s.push_str(identifier); + s.push(' '); + s.push_str( + &colorsys::Rgb::new( + color.red * 255.0, + color.green * 255.0, + color.blue * 255.0, + None, + ) + .to_css_string(), + ); + s.push_str(";\n"); + }; + + let row = gtk::ListBoxRow::new(); + let button = gtk::Button::new(); + // TODO: find a better way to get theme colors + // add_color( + // "scale_color", + // &row.style_context() + // .get_background_color(gtk::StateFlags::SELECTED), + // ); + // add_color( + // "background_color", + // &window + // .style_context() + // .get_background_color(gtk::StateFlags::NORMAL), + // ); + // add_color( + // "foreground_color", + // &button.style_context().get_color(gtk::StateFlags::NORMAL), + // ); + + s.push_str(STYLE); + + provider + .load_from_data(s.as_bytes()) + .expect("Failed to load CSS."); + gtk::StyleContext::add_provider_for_screen( + &gdk::Screen::default().expect("Error initializing GTK CSS provider."), + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); } /** * The custom stylesheet used by Myxer. */ - + const STYLE: &str = r#" .title { margin-left: -6px;