diff --git a/CHANGELOG.md b/CHANGELOG.md index 6869829f..451cb6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Special color for unavailable items - changelog with all the relevant user-facing changes to the project +- Media control keys support for Windows and macOS ### Changed diff --git a/Cargo.lock b/Cargo.lock index 7873563f..76be496d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1061f3ff92c2f65800df1f12fc7b4ff44ee14783104187dd04dfee6f11b0fd2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + [[package]] name = "addr2line" version = "0.21.0" @@ -103,6 +119,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "android-activity" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0" +dependencies = [ + "android-properties", + "bitflags 1.3.2", + "cc", + "jni-sys", + "libc", + "log", + "ndk 0.7.0", + "ndk-context", + "ndk-sys 0.4.1+23.1.7779620", + "num_enum 0.6.1", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -153,7 +193,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -163,9 +203,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.48.0", ] +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "async-broadcast" version = "0.5.1" @@ -257,7 +309,7 @@ dependencies = [ "futures-lite", "rustix 0.37.26", "signal-hook", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -383,6 +435,25 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-sys" +version = "0.1.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "block2" +version = "0.2.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" +dependencies = [ + "block-sys", + "objc2-encode", +] + [[package]] name = "blocking" version = "1.4.0" @@ -411,6 +482,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.4.3" @@ -423,6 +500,20 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "calloop" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8" +dependencies = [ + "bitflags 1.3.2", + "log", + "nix 0.25.1", + "slotmap", + "thiserror", + "vec_map", +] + [[package]] name = "cc" version = "1.0.83" @@ -454,6 +545,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.31" @@ -466,7 +563,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -557,6 +654,36 @@ dependencies = [ "winapi", ] +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -626,6 +753,30 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + [[package]] name = "coreaudio-rs" version = "0.10.0" @@ -659,7 +810,7 @@ dependencies = [ "lazy_static", "libc", "mach", - "ndk", + "ndk 0.6.0", "ndk-glue", "nix 0.23.2", "oboe", @@ -679,6 +830,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -879,6 +1039,26 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-crossroads" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4c83437187544ba5142427746835061b330446ca8902eabd70e4afb8f76de0" +dependencies = [ + "dbus", +] + [[package]] name = "deranged" version = "0.3.8" @@ -957,6 +1137,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "dlib" version = "0.5.2" @@ -1069,7 +1255,7 @@ checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1103,6 +1289,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fern" version = "0.6.2" @@ -1118,6 +1313,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1536,6 +1741,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -1546,7 +1754,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1637,6 +1845,15 @@ version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -1895,6 +2112,15 @@ version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -1932,6 +2158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -1943,7 +2170,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1994,6 +2221,7 @@ dependencies = [ "serde_cbor", "serde_json", "signal-hook", + "souvlaki", "strum", "strum_macros", "tokio", @@ -2002,6 +2230,8 @@ dependencies = [ "toml", "unicode-width", "url", + "windows 0.44.0", + "winit", "wl-clipboard-rs", "zbus", ] @@ -2025,8 +2255,22 @@ checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" dependencies = [ "bitflags 1.3.2", "jni-sys", - "ndk-sys", - "num_enum", + "ndk-sys 0.3.0", + "num_enum 0.5.11", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys 0.4.1+23.1.7779620", + "num_enum 0.5.11", + "raw-window-handle", "thiserror", ] @@ -2045,10 +2289,10 @@ dependencies = [ "lazy_static", "libc", "log", - "ndk", + "ndk 0.6.0", "ndk-context", "ndk-macro", - "ndk-sys", + "ndk-sys 0.3.0", ] [[package]] @@ -2073,6 +2317,15 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + [[package]] name = "nix" version = "0.23.2" @@ -2086,6 +2339,31 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + [[package]] name = "nix" version = "0.26.4" @@ -2272,7 +2550,16 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ - "num_enum_derive", + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", ] [[package]] @@ -2287,6 +2574,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "num_threads" version = "0.1.6" @@ -2322,6 +2621,32 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc-sys" +version = "0.2.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" + +[[package]] +name = "objc2" +version = "0.3.0-beta.3.patch-leaks.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468" +dependencies = [ + "block2", + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "2.0.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" +dependencies = [ + "objc-sys", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -2347,7 +2672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" dependencies = [ "jni", - "ndk", + "ndk 0.6.0", "ndk-context", "num-derive", "num-traits", @@ -2428,6 +2753,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "orbclient" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8378ac0dfbd4e7895f2d2c1f1345cab3836910baf3a300b000d04250f0c8428f" +dependencies = [ + "redox_syscall 0.3.5", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -2445,7 +2779,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" +dependencies = [ + "ttf-parser", ] [[package]] @@ -2524,7 +2867,7 @@ dependencies = [ "redox_syscall 0.3.5", "smallvec", "thread-id", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -2618,6 +2961,19 @@ dependencies = [ "dirs-next 1.0.2", ] +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "2.8.0" @@ -2631,7 +2987,7 @@ dependencies = [ "libc", "log", "pin-project-lite", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2782,6 +3138,12 @@ dependencies = [ "rand", ] +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -3003,7 +3365,7 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys 0.3.8", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3016,7 +3378,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.8", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3068,7 +3430,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3093,6 +3455,19 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sctk-adwaita" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda4e97be1fd174ccc2aae81c8b694e803fa99b34e8fd0f057a9d70698e3ed09" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -3281,6 +3656,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.9" @@ -3290,12 +3671,40 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +[[package]] +name = "smithay-client-toolkit" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870427e30b8f2cbe64bf43ec4b86e88fe39b0a84b3f15efd9c9c2d020bc86eb9" +dependencies = [ + "bitflags 1.3.2", + "calloop", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix 0.24.3", + "pkg-config", + "wayland-client 0.29.5", + "wayland-cursor", + "wayland-protocols 0.29.5", +] + [[package]] name = "socket2" version = "0.4.9" @@ -3313,7 +3722,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "souvlaki" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951a075f224d8c87bb62a08c9c27a373fd6d453407e89cae00a25e2eac74ef51" +dependencies = [ + "block", + "cocoa", + "core-graphics", + "dbus", + "dbus-crossroads", + "dispatch", + "objc", + "windows 0.44.0", ] [[package]] @@ -3340,6 +3765,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "strsim" version = "0.10.0" @@ -3437,7 +3868,7 @@ dependencies = [ "fastrand 2.0.1", "redox_syscall 0.3.5", "rustix 0.38.15", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3523,6 +3954,31 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -3555,7 +4011,7 @@ dependencies = [ "socket2 0.5.4", "tokio-macros", "tracing", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3708,6 +4164,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "ttf-parser" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" + [[package]] name = "typenum" version = "1.17.0" @@ -3814,6 +4276,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "vergen" version = "3.2.0" @@ -3939,7 +4407,23 @@ dependencies = [ "nix 0.26.4", "scoped-tls", "smallvec", - "wayland-sys", + "wayland-sys 0.31.1", +] + +[[package]] +name = "wayland-client" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +dependencies = [ + "bitflags 1.3.2", + "downcast-rs", + "libc", + "nix 0.24.3", + "scoped-tls", + "wayland-commons", + "wayland-scanner 0.29.5", + "wayland-sys 0.29.5", ] [[package]] @@ -3951,7 +4435,42 @@ dependencies = [ "bitflags 2.4.0", "nix 0.26.4", "wayland-backend", - "wayland-scanner", + "wayland-scanner 0.31.0", +] + +[[package]] +name = "wayland-commons" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" +dependencies = [ + "nix 0.24.3", + "once_cell", + "smallvec", + "wayland-sys 0.29.5", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +dependencies = [ + "nix 0.24.3", + "wayland-client 0.29.5", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +dependencies = [ + "bitflags 1.3.2", + "wayland-client 0.29.5", + "wayland-commons", + "wayland-scanner 0.29.5", ] [[package]] @@ -3962,8 +4481,8 @@ checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" dependencies = [ "bitflags 2.4.0", "wayland-backend", - "wayland-client", - "wayland-scanner", + "wayland-client 0.31.1", + "wayland-scanner 0.31.0", ] [[package]] @@ -3974,9 +4493,20 @@ checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ "bitflags 2.4.0", "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", + "wayland-client 0.31.1", + "wayland-protocols 0.31.0", + "wayland-scanner 0.31.0", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", ] [[package]] @@ -3990,6 +4520,17 @@ dependencies = [ "quote", ] +[[package]] +name = "wayland-sys" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + [[package]] name = "wayland-sys" version = "0.31.1" @@ -4061,13 +4602,31 @@ dependencies = [ "windows_x86_64_msvc 0.39.0", ] +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -4076,7 +4635,22 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -4085,15 +4659,21 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4106,6 +4686,12 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4118,6 +4704,12 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4130,6 +4722,12 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4142,12 +4740,24 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4160,12 +4770,53 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "winit" +version = "0.28.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9596d90b45384f5281384ab204224876e8e8bf7d58366d9b795ad99aa9894b94" +dependencies = [ + "android-activity", + "bitflags 1.3.2", + "cfg_aliases", + "core-foundation", + "core-graphics", + "dispatch", + "instant", + "libc", + "log", + "mio", + "ndk 0.7.0", + "objc2", + "once_cell", + "orbclient", + "percent-encoding", + "raw-window-handle", + "redox_syscall 0.3.5", + "sctk-adwaita", + "smithay-client-toolkit", + "wasm-bindgen", + "wayland-client 0.29.5", + "wayland-commons", + "wayland-protocols 0.29.5", + "wayland-scanner 0.29.5", + "web-sys", + "windows-sys 0.45.0", + "x11-dl", +] + [[package]] name = "winnow" version = "0.5.15" @@ -4191,7 +4842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -4209,8 +4860,8 @@ dependencies = [ "thiserror", "tree_magic_mini", "wayland-backend", - "wayland-client", - "wayland-protocols", + "wayland-client 0.31.1", + "wayland-protocols 0.31.0", "wayland-protocols-wlr", ] @@ -4223,6 +4874,17 @@ dependencies = [ "xcb", ] +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + [[package]] name = "xcb" version = "0.8.2" @@ -4233,6 +4895,15 @@ dependencies = [ "log", ] +[[package]] +name = "xcursor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" +dependencies = [ + "nom", +] + [[package]] name = "xdg-home" version = "1.0.0" @@ -4249,6 +4920,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" +[[package]] +name = "xml-rs" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" + [[package]] name = "xtask" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 1793689b..7c0b87f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ chrono = "0.4" clap = "4.4.7" clipboard = {version = "0.5", optional = true} crossbeam-channel = "0.5" -zbus = {version = "3.11.1", default-features = false, features = ["tokio"], optional = true} +cursive_buffered_backend = "0.6.1" fern = "0.6" futures = "0.3" ioctl-rs = {version = "0.2", optional = true} @@ -53,13 +53,15 @@ serde_cbor = "0.11.2" serde_json = "1.0" strum = "0.25" strum_macros = "0.25" +souvlaki = { version = "0.6.1", optional = true } tokio = {version = "1", features = ["rt-multi-thread", "sync", "time", "net"]} tokio-util = {version = "0.7.10", features = ["codec"]} tokio-stream = {version = "0.1.14", features = ["sync"]} toml = "0.8" unicode-width = "0.1.9" url = "2.2" -cursive_buffered_backend = "0.6.1" +winit = { version = "0.28.6", optional = true } +zbus = {version = "3.11.1", default-features = false, features = ["tokio"], optional = true} [target.'cfg(target_os = "linux")'.dependencies] wl-clipboard-rs = {version = "0.8", optional = true} @@ -83,11 +85,21 @@ version = "4" features = ["z"] optional = true +[target.'cfg(target_os = "windows")'.dependencies.windows] +version = "0.44" +features = [ + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_System_LibraryLoader", + "Win32_UI_WindowsAndMessaging" +] +optional = true + [features] alsa_backend = ["librespot-playback/alsa-backend"] cover = ["ioctl-rs"] # Support displaying the album cover -default = ["share_clipboard", "pulseaudio_backend", "mpris", "notify", "crossterm_backend"] -mpris = ["zbus"] # Allow ncspot to be controlled via MPRIS API +default = ["share_clipboard", "pulseaudio_backend", "media_control", "notify", "crossterm_backend"] +media_control = ["zbus", "souvlaki", "winit", "windows"] ncurses_backend = ["cursive/ncurses-backend"] notify = ["notify-rust"] # Show what's playing via a notification crossterm_backend = ["cursive/crossterm-backend"] diff --git a/doc/developers.md b/doc/developers.md index 5ae09771..37707020 100644 --- a/doc/developers.md +++ b/doc/developers.md @@ -66,18 +66,18 @@ cargo build --no-default-features --features portaudio_backend,pancurses_backend Rodio for Windows ```sh -cargo build --no-default-features --features rodio_backend,pancurses_backend +cargo build --no-default-features --features rodio_backend,pancurses_backend,media_control ``` ## Other Features Here are some auxiliary features you may wish to enable: -| Feature | Default | Description | -|-------------------|---------|--------------------------------------------------------------------------------------------| -| `cover` | off | Add a screen to show the album art. | -| `mpris` | on | Control `ncspot` via dbus. See [Arch Wiki: MPRIS](https://wiki.archlinux.org/title/MPRIS). | -| `notify` | on | Send a notification to show what's playing. | -| `share_clipboard` | on | Ability to copy the URL of a song/playlist/etc. to system clipboard. | +| Feature | Default | Description | +|-------------------|---------|----------------------------------------------------------------------| +| `cover` | off | Add a screen to show the album art. | +| `media_control` | on | Control `ncspot` via media keys. | +| `notify` | on | Send a notification to show what's playing. | +| `share_clipboard` | on | Ability to copy the URL of a song/playlist/etc. to system clipboard. | Consult [Cargo.toml](/Cargo.toml) for the full list of supported features. diff --git a/doc/users.md b/doc/users.md index 2d2d5e64..a42258e7 100644 --- a/doc/users.md +++ b/doc/users.md @@ -55,7 +55,7 @@ cargo install --locked ncspot ## Key Bindings The keybindings listed below are configured by default. Additionally, if you -built `ncspot` with MPRIS support, you may be able to use media keys to control +built `ncspot` with `media_control` feature, you may be able to use media keys to control playback depending on your desktop environment settings. Have a look at the [configuration section](#configuration) if you want to set custom bindings. @@ -180,7 +180,7 @@ Note: \ - mandatory arg; [BAR] - optional arg | `save [current]` | Save selected item, if `current` is passed the currently playing item will be saved | ## Remote control (IPC) -Apart from MPRIS, ncspot will also create a domain socket on UNIX platforms (Linux, macOS, *BSD). +Apart from `media_control`, ncspot will also create a domain socket on UNIX platforms (Linux, macOS, *BSD). The socket will be created in the platform's runtime directory. If XDG_RUNTIME_DIR is set, it will be created under `$XDG_RUNTIME_DIR/ncspot`. If XDG_RUNTIME_DIR isn't set, it will be created under `/run/user/` for Linux if it exists. In all other cases, it will be created under diff --git a/src/application.rs b/src/application.rs index 872d7f74..7c081235 100644 --- a/src/application.rs +++ b/src/application.rs @@ -20,7 +20,10 @@ use crate::ui::create_cursive; use crate::{authentication, ui, utils}; use crate::{command, queue, spotify}; -#[cfg(feature = "mpris")] +#[cfg(all(feature = "media_control", not(target_os = "linux")))] +use crate::media_control::{self, MediaControlManager}; + +#[cfg(all(feature = "media_control", target_os = "linux"))] use crate::mpris::{self, MprisManager}; #[cfg(unix)] @@ -67,8 +70,11 @@ pub struct Application { spotify: Spotify, /// Internally shared event_manager: EventManager, + /// Use media control keys using souvlaki. + #[cfg(all(feature = "media_control", not(target_os = "linux")))] + media_control_manager: MediaControlManager, /// An IPC implementation using the D-Bus MPRIS protocol, used to control and inspect ncspot. - #[cfg(feature = "mpris")] + #[cfg(all(feature = "media_control", target_os = "linux"))] mpris_manager: MprisManager, /// An IPC implementation using a Unix domain socket, used to control and inspect ncspot. #[cfg(unix)] @@ -129,7 +135,12 @@ impl Application { library.clone(), )); - #[cfg(feature = "mpris")] + #[cfg(all(feature = "media_control", not(target_os = "linux")))] + let media_control_manager = + media_control::MediaControlManager::new(spotify.clone(), queue.clone()) + .map_err(|err| -> String { format!("media_control error {err:?}") })?; + + #[cfg(all(feature = "media_control", target_os = "linux"))] let mpris_manager = mpris::MprisManager::new( event_manager.clone(), queue.clone(), @@ -200,7 +211,9 @@ impl Application { queue, spotify, event_manager, - #[cfg(feature = "mpris")] + #[cfg(all(feature = "media_control", not(target_os = "linux")))] + media_control_manager, + #[cfg(all(feature = "media_control", target_os = "linux"))] mpris_manager, #[cfg(unix)] ipc, @@ -232,7 +245,10 @@ impl Application { trace!("event received: {:?}", state); self.spotify.update_status(state.clone()); - #[cfg(feature = "mpris")] + #[cfg(all(feature = "media_control", not(target_os = "linux")))] + self.media_control_manager.update(); + + #[cfg(all(feature = "media_control", target_os = "linux"))] self.mpris_manager.update(); #[cfg(unix)] diff --git a/src/main.rs b/src/main.rs index 2aad71a8..7e6cea00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,10 @@ mod utils; #[cfg(unix)] mod ipc; -#[cfg(feature = "mpris")] +#[cfg(all(feature = "media_control", not(target_os = "linux")))] +mod media_control; + +#[cfg(all(feature = "media_control", target_os = "linux"))] mod mpris; fn main() -> Result<(), String> { diff --git a/src/media_control.rs b/src/media_control.rs new file mode 100644 index 00000000..38fae315 --- /dev/null +++ b/src/media_control.rs @@ -0,0 +1,356 @@ +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; +use tokio_stream::StreamExt; + +use souvlaki::{ + MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig, + SeekDirection, +}; + +use crate::application::ASYNC_RUNTIME; +use crate::model::playable::Playable; +use crate::queue::Queue; +use crate::spotify::{PlayerEvent, Spotify}; +use crate::traits::ListItem; + +struct MediaControlPlayer { + spotify: Spotify, + queue: Arc, + controls: MediaControls, + last_track: Option, +} + +enum MediaControlPlayerEvent { + SouvlakiEvent(MediaControlEvent), + UpdateEvent, +} + +impl MediaControlPlayer { + fn handle(&mut self, e: MediaControlPlayerEvent) { + match e { + MediaControlPlayerEvent::UpdateEvent => { + self.update(); + } + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::Play) => { + self.spotify.play(); + } + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::Pause) => { + self.spotify.pause(); + } + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::Toggle) => { + self.queue.toggleplayback(); + } + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::Next) => { + self.queue.next(true); + } + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::Previous) => { + if self.spotify.get_current_progress() < Duration::from_secs(5) { + self.queue.previous(); + } else { + self.spotify.seek(0); + } + } + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::Stop) => { + self.queue.stop(); + } + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::Seek( + SeekDirection::Forward, + )) => self.seek(5i32), + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::Seek( + SeekDirection::Backward, + )) => self.seek(-5i32), + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::SeekBy( + SeekDirection::Forward, + dur, + )) => self.seek(dur.as_secs() as i32), + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::SeekBy( + SeekDirection::Backward, + dur, + )) => { + self.seek(-(dur.as_secs() as i32)); + } + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::SetPosition( + MediaPosition(dur), + )) => { + if let Some(current_track) = self.queue.get_current() { + let position = dur.as_secs() as u32; + let duration = current_track.duration(); + + if position < duration { + self.spotify.seek(position); + } + } + } + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::OpenUri(uri)) => { + self.queue.open_uri(uri.as_str()); + } + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::Raise) => {} + MediaControlPlayerEvent::SouvlakiEvent(MediaControlEvent::Quit) => {} + } + } + + fn update(&mut self) { + let playable = self.queue.get_current(); + + if self.last_track != playable { + let playable = self.queue.get_current().and_then(|p| match p { + Playable::Track(track) => { + if track.cover_url.is_some() { + // We already have `cover_url`, no need to fetch the full track + Some(Playable::Track(track)) + } else { + self.spotify + .api + .track(&track.id.unwrap_or_default()) + .as_ref() + .map(|t| Playable::Track(t.into())) + } + } + Playable::Episode(episode) => Some(Playable::Episode(episode)), + }); + + let _ = self.controls.set_metadata(MediaMetadata { + title: playable.as_ref().map(|t| match t { + Playable::Track(t) => t.title.as_str(), + Playable::Episode(ep) => ep.name.as_str(), + }), + album: playable + .as_ref() + .and_then(|p| p.track()) + .and_then(|t| t.album) + .as_deref(), + artist: playable + .as_ref() + .and_then(|p| p.track()) + .and_then(|t| t.album_artists.into_iter().nth(0)) // just the first artist + .as_deref(), + duration: playable + .as_ref() + .map(|t| Duration::from_secs(t.duration() as u64)), + cover_url: playable.as_ref().and_then(|t| t.cover_url()).as_deref(), + }); + } + + let _ = self + .controls + .set_playback(match self.spotify.get_current_status() { + PlayerEvent::FinishedTrack => MediaPlayback::Playing { progress: None }, + PlayerEvent::Paused(dur) => MediaPlayback::Paused { + progress: Some(MediaPosition(dur)), + }, + PlayerEvent::Playing(_) => MediaPlayback::Playing { + progress: Some(MediaPosition(self.spotify.get_current_progress())), + }, + PlayerEvent::Stopped => MediaPlayback::Stopped, + }); + } + + fn seek(&self, secs_delta: i32) { + if let Some(current_track) = self.queue.get_current() { + let progress = self.spotify.get_current_progress(); + let new_position = progress.as_millis() as i32 + (secs_delta * 1000); + + if new_position <= 0 { + self.queue.previous(); + } else if new_position < current_track.duration() as i32 { + self.spotify.seek(new_position as u32); + } else { + self.queue.next(true); + } + } + } +} + +pub struct MediaControlManager { + tx: mpsc::UnboundedSender, + #[cfg(target_os = "windows")] + #[allow(dead_code)] + window: crate::media_control::windows::DummyWindow, // preventing window from getting dropped too early +} + +impl MediaControlManager { + pub fn new(spotify: Spotify, queue: Arc) -> Result { + #[cfg(not(target_os = "windows"))] + let hwnd = None; + + #[cfg(target_os = "windows")] + let (hwnd, window) = { + let window = windows::DummyWindow::new(&instance_bus_name()).unwrap(); + (Some(window.handle.0 as _), window) + }; + + let (tx, rx) = mpsc::unbounded_channel::(); + let txc = tx.clone(); + + let mut controls = MediaControls::new(PlatformConfig { + dbus_name: &instance_bus_name(), + display_name: "ncspot", + hwnd, + })?; + + controls.attach(move |e| { + if let Err(e) = txc.send(MediaControlPlayerEvent::SouvlakiEvent(e)) { + log::warn!("Could not process Media Control event: {e}"); + } + })?; + + let player = MediaControlPlayer { + controls, + last_track: None, + queue, + spotify, + }; + + ASYNC_RUNTIME.get().unwrap().spawn(async { + let result = Self::serve(UnboundedReceiverStream::new(rx), player).await; + if let Err(e) = result { + log::error!("Media Control error: {e}"); + } + }); + + Ok(Self { + tx, + #[cfg(target_os = "windows")] + window, + }) + } + + async fn serve( + mut rx: UnboundedReceiverStream, + mut player: MediaControlPlayer, + ) -> Result<(), Box> { + loop { + if let Some(e) = rx.next().await { + player.handle(e); + + #[cfg(target_os = "windows")] + windows::pump_event_queue(); + } + } + } + + pub fn update(&self) { + if let Err(e) = self.tx.send(MediaControlPlayerEvent::UpdateEvent) { + log::warn!("Could not update Media Control state: {e}"); + } + } +} + +/// Get the D-Bus bus name for this instance according to the MPRIS specification. +/// +/// https://specifications.freedesktop.org/mpris-spec/2.2/#Bus-Name-Policy +pub fn instance_bus_name() -> String { + format!( + "org.mpris.MediaPlayer2.ncspot.instance{}", + std::process::id() + ) +} + +// demonstrates how to make a minimal window to allow use of media keys on the command line +// ref: https://github.com/Sinono3/souvlaki/blob/master/examples/print_events.rs +#[cfg(target_os = "windows")] +mod windows { + use std::io::Error; + use std::mem; + + use windows::core::{HSTRING, PCWSTR}; + use windows::w; + use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; + use windows::Win32::System::LibraryLoader::GetModuleHandleW; + use windows::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetAncestor, + IsDialogMessageW, PeekMessageW, RegisterClassExW, TranslateMessage, GA_ROOT, MSG, + PM_REMOVE, WINDOW_EX_STYLE, WINDOW_STYLE, WM_QUIT, WNDCLASSEXW, + }; + + pub struct DummyWindow { + pub handle: HWND, + } + + impl DummyWindow { + pub fn new(class_name: &str) -> Result { + let class_name = PCWSTR(HSTRING::from(class_name).as_ptr()); + + let handle_result = unsafe { + let instance = GetModuleHandleW(None) + .map_err(|e| (format!("Getting module handle failed: {e}")))?; + + let wnd_class = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + hInstance: instance, + lpszClassName: class_name, + lpfnWndProc: Some(Self::wnd_proc), + ..Default::default() + }; + + if RegisterClassExW(&wnd_class) == 0 { + return Err(format!( + "Registering class failed: {}", + Error::last_os_error() + )); + } + + let handle = CreateWindowExW( + WINDOW_EX_STYLE::default(), + class_name, + w!(""), + WINDOW_STYLE::default(), + 0, + 0, + 0, + 0, + None, + None, + instance, + None, + ); + + if handle.0 == 0 { + Err(format!( + "Message only window creation failed: {}", + Error::last_os_error() + )) + } else { + Ok(handle) + } + }; + + handle_result.map(|handle| DummyWindow { handle }) + } + extern "system" fn wnd_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } + } + } + + impl Drop for DummyWindow { + fn drop(&mut self) { + unsafe { + DestroyWindow(self.handle); + } + } + } + + pub fn pump_event_queue() -> bool { + unsafe { + let mut msg: MSG = std::mem::zeroed(); + let mut has_message = PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool(); + while msg.message != WM_QUIT && has_message { + if !IsDialogMessageW(GetAncestor(msg.hwnd, GA_ROOT), &msg).as_bool() { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + has_message = PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool(); + } + + msg.message == WM_QUIT + } + } +} diff --git a/src/model/episode.rs b/src/model/episode.rs index 61980f6d..7858b842 100644 --- a/src/model/episode.rs +++ b/src/model/episode.rs @@ -118,3 +118,9 @@ impl ListItem for Episode { Box::new(self.clone()) } } + +impl PartialEq for Episode { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} diff --git a/src/model/playable.rs b/src/model/playable.rs index 9d2a42fc..f09b74eb 100644 --- a/src/model/playable.rs +++ b/src/model/playable.rs @@ -228,3 +228,13 @@ impl ListItem for Playable { self.as_listitem() } } + +impl PartialEq for Playable { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Track(l0), Self::Track(r0)) => l0 == r0, + (Self::Episode(l0), Self::Episode(r0)) => l0 == r0, + _ => false, + } + } +} diff --git a/src/model/track.rs b/src/model/track.rs index 95db9c60..05a0b32e 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -342,3 +342,9 @@ impl ListItem for Track { Box::new(self.clone()) } } + +impl PartialEq for Track { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} diff --git a/src/mpris.rs b/src/mpris.rs index a7fda8a9..f6e95415 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -10,15 +10,8 @@ use zbus::{dbus_interface, ConnectionBuilder}; use crate::application::ASYNC_RUNTIME; use crate::library::Library; -use crate::model::album::Album; -use crate::model::episode::Episode; use crate::model::playable::Playable; -use crate::model::playlist::Playlist; -use crate::model::show::Show; -use crate::model::track::Track; use crate::queue::RepeatSetting; -use crate::spotify::UriType; -use crate::spotify_url::SpotifyUrl; use crate::traits::ListItem; use crate::{ events::EventManager, @@ -370,96 +363,7 @@ impl MprisPlayer { } fn open_uri(&self, uri: &str) { - let spotify_url = if uri.contains("open.spotify.com") { - SpotifyUrl::from_url(uri) - } else if UriType::from_uri(uri).is_some() { - let id = &uri[uri.rfind(':').unwrap_or(0) + 1..uri.len()]; - let uri_type = UriType::from_uri(uri).unwrap(); - Some(SpotifyUrl::new(id, uri_type)) - } else { - None - }; - - let id = spotify_url - .as_ref() - .map(|s| s.id.clone()) - .unwrap_or("".to_string()); - let uri_type = spotify_url.map(|s| s.uri_type); - match uri_type { - Some(UriType::Album) => { - if let Some(a) = self.spotify.api.album(&id) { - if let Some(t) = &Album::from(&a).tracks { - let should_shuffle = self.queue.get_shuffle(); - self.queue.clear(); - let index = self.queue.append_next( - &t.iter() - .map(|track| Playable::Track(track.clone())) - .collect(), - ); - self.queue.play(index, should_shuffle, should_shuffle) - } - } - } - Some(UriType::Track) => { - if let Some(t) = self.spotify.api.track(&id) { - self.queue.clear(); - self.queue.append(Playable::Track(Track::from(&t))); - self.queue.play(0, false, false) - } - } - Some(UriType::Playlist) => { - if let Some(p) = self.spotify.api.playlist(&id) { - let mut playlist = Playlist::from(&p); - let spotify = self.spotify.clone(); - playlist.load_tracks(spotify); - if let Some(tracks) = &playlist.tracks { - let should_shuffle = self.queue.get_shuffle(); - self.queue.clear(); - let index = self.queue.append_next(tracks); - self.queue.play(index, should_shuffle, should_shuffle) - } - } - } - Some(UriType::Show) => { - if let Some(s) = self.spotify.api.get_show(&id) { - let mut show: Show = (&s).into(); - let spotify = self.spotify.clone(); - show.load_all_episodes(spotify); - if let Some(e) = &show.episodes { - let should_shuffle = self.queue.get_shuffle(); - self.queue.clear(); - let mut ep = e.clone(); - ep.reverse(); - let index = self.queue.append_next( - &ep.iter() - .map(|episode| Playable::Episode(episode.clone())) - .collect(), - ); - self.queue.play(index, should_shuffle, should_shuffle) - } - } - } - Some(UriType::Episode) => { - if let Some(e) = self.spotify.api.episode(&id) { - self.queue.clear(); - self.queue.append(Playable::Episode(Episode::from(&e))); - self.queue.play(0, false, false) - } - } - Some(UriType::Artist) => { - if let Some(a) = self.spotify.api.artist_top_tracks(&id) { - let should_shuffle = self.queue.get_shuffle(); - self.queue.clear(); - let index = self.queue.append_next( - &a.iter() - .map(|track| Playable::Track(track.clone())) - .collect(), - ); - self.queue.play(index, should_shuffle, should_shuffle) - } - } - None => {} - } + self.queue.open_uri(uri); } } diff --git a/src/queue.rs b/src/queue.rs index 9b030f3f..40148a14 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -10,9 +10,14 @@ use strum_macros::Display; use crate::config::{Config, PlaybackState}; use crate::library::Library; +use crate::model::album::Album; +use crate::model::episode::Episode; use crate::model::playable::Playable; -use crate::spotify::PlayerEvent; -use crate::spotify::Spotify; +use crate::model::playlist::Playlist; +use crate::model::show::Show; +use crate::model::track::Track; +use crate::spotify::{PlayerEvent, Spotify, UriType}; +use crate::spotify_url::SpotifyUrl; /// Repeat behavior for the [Queue]. #[derive(Display, Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -484,6 +489,99 @@ impl Queue { pub fn get_spotify(&self) -> Spotify { self.spotify.clone() } + + pub fn open_uri(&self, uri: &str) { + let spotify_url = if uri.contains("open.spotify.com") { + SpotifyUrl::from_url(uri) + } else if UriType::from_uri(uri).is_some() { + let id = &uri[uri.rfind(':').unwrap_or(0) + 1..uri.len()]; + let uri_type = UriType::from_uri(uri).unwrap(); + Some(SpotifyUrl::new(id, uri_type)) + } else { + None + }; + + let id = spotify_url + .as_ref() + .map(|s| s.id.clone()) + .unwrap_or("".to_string()); + let uri_type = spotify_url.map(|s| s.uri_type); + match uri_type { + Some(UriType::Album) => { + if let Some(a) = self.spotify.api.album(&id) { + if let Some(t) = &Album::from(&a).tracks { + let should_shuffle = self.get_shuffle(); + self.clear(); + let index = self.append_next( + &t.iter() + .map(|track| Playable::Track(track.clone())) + .collect(), + ); + self.play(index, should_shuffle, should_shuffle) + } + } + } + Some(UriType::Track) => { + if let Some(t) = self.spotify.api.track(&id) { + self.clear(); + self.append(Playable::Track(Track::from(&t))); + self.play(0, false, false) + } + } + Some(UriType::Playlist) => { + if let Some(p) = self.spotify.api.playlist(&id) { + let mut playlist = Playlist::from(&p); + let spotify = self.spotify.clone(); + playlist.load_tracks(spotify); + if let Some(tracks) = &playlist.tracks { + let should_shuffle = self.get_shuffle(); + self.clear(); + let index = self.append_next(tracks); + self.play(index, should_shuffle, should_shuffle) + } + } + } + Some(UriType::Show) => { + if let Some(s) = self.spotify.api.get_show(&id) { + let mut show: Show = (&s).into(); + let spotify = self.spotify.clone(); + show.load_all_episodes(spotify); + if let Some(e) = &show.episodes { + let should_shuffle = self.get_shuffle(); + self.clear(); + let mut ep = e.clone(); + ep.reverse(); + let index = self.append_next( + &ep.iter() + .map(|episode| Playable::Episode(episode.clone())) + .collect(), + ); + self.play(index, should_shuffle, should_shuffle) + } + } + } + Some(UriType::Episode) => { + if let Some(e) = self.spotify.api.episode(&id) { + self.clear(); + self.append(Playable::Episode(Episode::from(&e))); + self.play(0, false, false) + } + } + Some(UriType::Artist) => { + if let Some(a) = self.spotify.api.artist_top_tracks(&id) { + let should_shuffle = self.get_shuffle(); + self.clear(); + let index = self.append_next( + &a.iter() + .map(|track| Playable::Track(track.clone())) + .collect(), + ); + self.play(index, should_shuffle, should_shuffle) + } + } + None => {} + } + } } /// Send a notification using the desktops default notification method.