diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 212fce3..fa3c702 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,7 @@ name: Release Build on: + workflow_dispatch: release: types: [created] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..bc2d22a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,27 @@ +name: Rust Unit Tests + +on: + workflow_dispatch: + push: + branches: + - '**' + + pull_request: + branches: + - '**' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Install dependencies + run: sudo apt install libasound2-dev + + - name: Setup rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Run tests + run: cargo test --all --verbose diff --git a/Cargo.lock b/Cargo.lock index 3e9f7ae..0184fa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,43 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "version_check", - "zerocopy 0.8.26", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "alsa" version = "0.9.1" @@ -46,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.3", + "bitflags 2.10.0", "cfg-if", "libc", ] @@ -63,9 +26,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -78,36 +41,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] @@ -136,9 +100,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -148,52 +112,41 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", + "pin-project-lite", "slab", ] -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - [[package]] name = "async-io" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", - "rustix 0.38.42", + "rustix", "slab", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "async-lock" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ "event-listener", "event-listener-strategy", @@ -202,9 +155,9 @@ dependencies = [ [[package]] name = "async-process" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel", "async-io", @@ -215,8 +168,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 0.38.42", - "tracing", + "rustix", ] [[package]] @@ -232,9 +184,9 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", "async-lock", @@ -242,10 +194,10 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.42", + "rustix", "signal-hook-registry", "slab", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -256,9 +208,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -271,32 +223,11 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atomic_float" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" - [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -312,24 +243,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.3" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", @@ -340,15 +262,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.21.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -358,16 +280,17 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.7" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -379,9 +302,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -391,9 +314,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.24" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -401,9 +324,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.24" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -413,9 +336,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.24" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -425,28 +348,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" - -[[package]] -name = "color-eyre" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" -dependencies = [ - "backtrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", -] +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" @@ -469,33 +379,15 @@ dependencies = [ [[package]] name = "console" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" +checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" dependencies = [ "encode_unicode", "libc", "once_cell", "unicode-width", - "windows-sys 0.60.2", -] - -[[package]] -name = "convert_case" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", + "windows-sys 0.61.2", ] [[package]] @@ -554,15 +446,6 @@ dependencies = [ "windows", ] -[[package]] -name = "cpufeatures" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" -dependencies = [ - "libc", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -575,43 +458,21 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.3", - "crossterm_winapi", - "derive_more 2.0.1", + "bitflags 2.10.0", "document-features", "futures-core", "mio", "parking_lot", - "rustix 1.0.8", + "rustix", "signal-hook", "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", ] [[package]] name = "cssparser" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" +checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" dependencies = [ "cssparser-macros", "dtoa-short", @@ -638,46 +499,25 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "derive_more" -version = "0.99.20" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ - "convert_case 0.7.1", "proc-macro2", "quote", + "rustc_version", "syn", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "dirs" version = "6.0.0" @@ -696,7 +536,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -705,7 +545,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "objc2", ] @@ -722,9 +562,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -746,9 +586,9 @@ dependencies = [ [[package]] name = "ego-tree" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c6ba7d4eec39eaa9ab24d44a0e73a7949a1095a8b3f3abb11eddf27dbb56a53" +checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" [[package]] name = "encode_unicode" @@ -767,15 +607,15 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", @@ -783,9 +623,9 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", @@ -794,25 +634,25 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -821,9 +661,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", @@ -851,6 +691,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "fnv" version = "1.0.7" @@ -874,9 +720,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -941,9 +787,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", @@ -1002,59 +848,43 @@ dependencies = [ "byteorder", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getopts" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "h2" -version = "0.4.7" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -1071,9 +901,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -1083,9 +913,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1104,24 +934,22 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.29.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" dependencies = [ "log", - "mac", "markup5ever", "match_token", ] [[package]] name = "http" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1137,12 +965,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -1150,25 +978,27 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.5.2" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1176,11 +1006,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -1209,40 +1038,48 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1251,104 +1088,66 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1357,9 +1156,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1367,15 +1166,15 @@ dependencies = [ [[package]] name = "indenter" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.7.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", @@ -1383,9 +1182,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.0" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ "console", "portable-atomic", @@ -1396,21 +1195,31 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jni" @@ -1436,9 +1245,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -1452,70 +1261,60 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "libc", ] [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" - -[[package]] -name = "linux-raw-sys" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.4" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.22" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lowfi" -version = "1.7.2" +version = "2.0.0-dev" dependencies = [ "arc-swap", - "atomic_float", "bytes", "clap", - "color-eyre", - "convert_case 0.8.0", "crossterm", "dirs", "eyre", @@ -1523,16 +1322,14 @@ dependencies = [ "futures", "html-escape", "indicatif", - "lazy_static", "libc", "mpris-server", - "regex", "reqwest", "rodio", "scraper", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "unicode-segmentation", "url", @@ -1555,23 +1352,20 @@ dependencies = [ [[package]] name = "markup5ever" -version = "0.14.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" dependencies = [ "log", - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", "tendril", + "web_atoms", ] [[package]] name = "match_token" -version = "0.1.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" dependencies = [ "proc-macro2", "quote", @@ -1580,9 +1374,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -1599,32 +1393,23 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "miniz_oxide" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" -dependencies = [ - "adler2", -] - [[package]] name = "mio" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "mpris-server" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058bc2227727af394f34aa51da3e36aeecf2c808f39315d35f754872660750ae" +checksum = "392ce2be7224867869df37e473f28871ab0ff725c0014f1b196ba56a38aea9a8" dependencies = [ "async-channel", "futures-channel", @@ -1635,9 +1420,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -1656,7 +1441,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys", @@ -1687,11 +1472,11 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -1750,9 +1535,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -1760,9 +1545,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1772,20 +1557,20 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] [[package]] name = "objc2-audio-toolbox" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "libc", "objc2", "objc2-core-audio", @@ -1796,9 +1581,9 @@ dependencies = [ [[package]] name = "objc2-core-audio" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ "dispatch2", "objc2", @@ -1808,21 +1593,21 @@ dependencies = [ [[package]] name = "objc2-core-audio-types" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "objc2", ] [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "dispatch2", "objc2", ] @@ -1835,35 +1620,32 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "objc2", ] [[package]] -name = "object" -version = "0.36.7" +name = "once_cell" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "once_cell" -version = "1.20.2" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -1885,15 +1667,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -1917,12 +1699,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "owo-colors" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" - [[package]] name = "parking" version = "2.2.1" @@ -1931,9 +1707,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1941,22 +1717,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" @@ -2035,23 +1811,22 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polling" -version = "3.7.4" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 0.38.42", - "tracing", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -2061,12 +1836,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] -name = "ppv-lite86" -version = "0.2.20" +name = "potential_utf" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ - "zerocopy 0.7.35", + "zerovec", ] [[package]] @@ -2077,27 +1852,27 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -2114,18 +1889,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", "rand_core", ] @@ -2134,17 +1897,14 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", ] [[package]] @@ -2153,45 +1913,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.17", ] -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", @@ -2206,44 +1937,40 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tokio-util", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "windows-registry", ] [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -2261,42 +1988,32 @@ dependencies = [ ] [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustix" -version = "0.38.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags 2.9.3", - "errno", - "libc", - "linux-raw-sys 0.4.14", - "windows-sys 0.59.0", + "semver", ] [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "rustls-pki-types", @@ -2306,25 +2023,19 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls-pki-types" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ - "rustls-pki-types", + "zeroize", ] -[[package]] -name = "rustls-pki-types" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" - [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -2333,15 +2044,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -2354,11 +2065,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2369,11 +2080,10 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scraper" -version = "0.21.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0e749d29b2064585327af5038a5a8eb73aeebad4a3472e83531a436563f7208" +checksum = "e5f3a24d916e78954af99281a455168d4a9515d65eca99a18da1b813689c4ad9" dependencies = [ - "ahash", "cssparser", "ego-tree", "getopts", @@ -2389,7 +2099,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -2398,9 +2108,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -2408,13 +2118,13 @@ dependencies = [ [[package]] name = "selectors" -version = "0.26.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" +checksum = "5685b6ae43bfcf7d2e7dfcfb5d8e8f61b46442c902531e41a32a9a8bf0ee0fb6" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "cssparser", - "derive_more 0.99.20", + "derive_more", "fxhash", "log", "new_debug_unreachable", @@ -2426,19 +2136,35 @@ dependencies = [ ] [[package]] -name = "serde" -version = "1.0.219" +name = "semver" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2447,21 +2173,22 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", @@ -2482,24 +2209,13 @@ dependencies = [ [[package]] name = "servo_arc" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "204ea332803bd95a0b60388590d59cf6468ec9becf626e2451f1d26a1d972de4" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ "stable_deref_trait", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -2508,9 +2224,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -2518,9 +2234,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -2529,9 +2245,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -2544,40 +2260,31 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -2624,9 +2331,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symphonia" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" dependencies = [ "lazy_static", "symphonia-bundle-flac", @@ -2643,9 +2350,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-flac" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" dependencies = [ "log", "symphonia-core", @@ -2655,9 +2362,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" dependencies = [ "lazy_static", "log", @@ -2667,9 +2374,9 @@ dependencies = [ [[package]] name = "symphonia-codec-aac" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" dependencies = [ "lazy_static", "log", @@ -2678,9 +2385,9 @@ dependencies = [ [[package]] name = "symphonia-codec-pcm" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" dependencies = [ "log", "symphonia-core", @@ -2688,9 +2395,9 @@ dependencies = [ [[package]] name = "symphonia-codec-vorbis" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" dependencies = [ "log", "symphonia-core", @@ -2699,9 +2406,9 @@ dependencies = [ [[package]] name = "symphonia-core" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" dependencies = [ "arrayvec", "bitflags 1.3.2", @@ -2712,9 +2419,9 @@ dependencies = [ [[package]] name = "symphonia-format-isomp4" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" dependencies = [ "encoding_rs", "log", @@ -2725,9 +2432,9 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" dependencies = [ "log", "symphonia-core", @@ -2737,9 +2444,9 @@ dependencies = [ [[package]] name = "symphonia-format-riff" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" dependencies = [ "extended", "log", @@ -2749,9 +2456,9 @@ dependencies = [ [[package]] name = "symphonia-metadata" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" dependencies = [ "encoding_rs", "lazy_static", @@ -2761,9 +2468,9 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" dependencies = [ "symphonia-core", "symphonia-metadata", @@ -2771,9 +2478,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.95" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -2791,9 +2498,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -2806,7 +2513,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.10.0", "core-foundation", "system-configuration-sys", ] @@ -2823,16 +2530,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.2.15", + "getrandom 0.3.4", "once_cell", - "rustix 0.38.42", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -2857,11 +2563,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -2877,9 +2583,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -2888,9 +2594,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2898,25 +2604,24 @@ dependencies = [ [[package]] name = "tokio" -version = "1.42.0" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -2935,9 +2640,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2945,9 +2650,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -2958,18 +2663,31 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ "winnow", ] @@ -2988,6 +2706,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3002,9 +2738,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3013,9 +2749,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -3024,9 +2760,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", ] @@ -3048,12 +2784,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - [[package]] name = "uds_windows" version = "1.1.0" @@ -3067,9 +2797,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -3079,15 +2809,15 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unit-prefix" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[package]] name = "untrusted" @@ -3097,13 +2827,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -3112,17 +2843,11 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8-width" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" [[package]] name = "utf8_iter" @@ -3136,18 +2861,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" @@ -3169,49 +2899,37 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -3222,9 +2940,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3232,22 +2950,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" @@ -3264,9 +2985,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -3282,6 +3003,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3300,11 +3033,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3335,19 +3068,19 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.2.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-result 0.2.0", + "windows-link", + "windows-result 0.4.1", "windows-strings", - "windows-targets 0.52.6", ] [[package]] @@ -3361,21 +3094,20 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -3396,22 +3128,22 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -3447,19 +3179,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3476,9 +3208,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3494,9 +3226,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3512,9 +3244,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -3524,9 +3256,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3542,9 +3274,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3560,9 +3292,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3578,9 +3310,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3596,57 +3328,37 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.6.22" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.3", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3654,9 +3366,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -3666,13 +3378,12 @@ dependencies = [ [[package]] name = "zbus" -version = "4.4.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" dependencies = [ "async-broadcast", "async-executor", - "async-fs", "async-io", "async-lock", "async-process", @@ -3683,20 +3394,17 @@ dependencies = [ "enumflags2", "event-listener", "futures-core", - "futures-sink", - "futures-util", + "futures-lite", "hex", "nix", "ordered-stream", - "rand", "serde", "serde_repr", - "sha1", - "static_assertions", "tracing", "uds_windows", - "windows-sys 0.52.0", - "xdg-home", + "uuid", + "windows-sys 0.61.2", + "winnow", "zbus_macros", "zbus_names", "zvariant", @@ -3704,83 +3412,45 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.4.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", + "zbus_names", + "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "3.0.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", + "winnow", "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" -dependencies = [ - "zerocopy-derive 0.8.26", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", @@ -3790,15 +3460,26 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3807,9 +3488,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -3818,22 +3499,23 @@ dependencies = [ [[package]] name = "zvariant" -version = "4.2.0" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" dependencies = [ "endi", "enumflags2", "serde", - "static_assertions", + "winnow", "zvariant_derive", + "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "4.2.0" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3844,11 +3526,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "2.1.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" dependencies = [ "proc-macro2", "quote", + "serde", "syn", + "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 7cc13c7..1068446 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "lowfi" -version = "1.7.2" +version = "2.0.0-dev" +rust-version = "1.83.0" edition = "2021" description = "An extremely simple lofi player." license = "MIT" @@ -27,38 +28,47 @@ clap = { version = "4.5.21", features = ["derive", "cargo"] } eyre = "0.6.12" fastrand = "2.3.0" thiserror = "2.0.12" -color-eyre = { version = "0.6.5", default-features = false } # Async -tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "fs"], default-features = false } -futures = "0.3.31" +tokio = { version = "1.41.1", features = ["macros", "rt", "fs"], default-features = false } arc-swap = "1.7.1" +futures = "0.3.31" # Data reqwest = { version = "0.12.9", features = ["stream"] } bytes = "1.9.0" # I/O -crossterm = { version = "0.29.0", features = ["event-stream"] } +crossterm = { version = "0.29.0", features = ["event-stream"], default-features = false } rodio = { version = "0.21.1", features = ["symphonia-mp3", "playback"], default-features = false } -mpris-server = { version = "0.8.1", optional = true } +mpris-server = { version = "0.9.0", optional = true } dirs = "6.0.0" # Misc -convert_case = "0.8.0" -lazy_static = "1.5.0" -url = "2.5.4" unicode-segmentation = "1.12.0" +url = "2.5.4" # Scraper serde = { version = "1.0.219", features = ["derive"], optional = true } serde_json = { version = "1.0.142", optional = true } -scraper = { version = "0.21.0", optional = true } +scraper = { version = "0.24.0", optional = true } html-escape = { version = "0.2.13", optional = true } indicatif = { version = "0.18.0", optional = true } -regex = "1.11.1" -atomic_float = "1.1.0" [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2.167" +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } + +unwrap_in_result = "warn" +missing_docs_in_private_items = "warn" + +missing_errors_doc = "allow" +missing_panics_doc = "allow" +must_use_candidate = "allow" +cast_precision_loss = "allow" +cast_sign_loss = "allow" +cast_possible_truncation = "allow" diff --git a/README.md b/README.md index 025e91e..05db5c9 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,24 @@ It'll do this as simply as it can: no albums, no ads, just lofi. ![example image](media/example1.png) +## The Rewrite + +This branch serves as a rewrite for lowfi. The main focus is to make the code more +maintainable. This includes such things as: + +- Replacing `Mutex` & `Arc` with channels, massively improving readability and flow. +- More clearly handling tracks in different phases of loading, instead of having +a mess of different structs. +- Making the UI code cleaner and easier to follow. +- Rethinking input & control of the player, especially with MPRIS in mind. +- Making track loading simpler and more consistent. + +This is an *internal rewrite*, and the goal is to retain every single feature. +If there is a feature present in the original version of lowfi that is not present +in the rewrite, then it is a bug and must be implemented. + +Currently, it is in an extremely early and non-functional state. + ## Disclaimer As of the 1.7.0 version of lowfi, **all** of the audio files embedded @@ -28,7 +46,7 @@ and as such it buffers 5 whole songs at a time instead of parts of the same song ### Dependencies -You'll need Rust 1.74.0+. +You'll need Rust 1.83.0+. On MacOS & Windows, no extra dependencies are needed. @@ -222,7 +240,7 @@ Each track will be first appended to the header, and then use the combination to the track. > [!NOTE] -> lowfi _will not_ put a `/` between the base & track for added flexibility, +> lowfi *will not* put a `/` between the base & track for added flexibility, > so for most cases you should have a trailing `/` in your header. The exception to this is if the track name begins with a protocol like `https://`, diff --git a/src/player/audio.rs b/src/audio.rs similarity index 76% rename from src/player/audio.rs rename to src/audio.rs index 2fc9e6d..114aea4 100644 --- a/src/player/audio.rs +++ b/src/audio.rs @@ -1,7 +1,9 @@ +pub mod waiter; + /// This gets the output stream while also shutting up alsa with [libc]. /// Uses raw libc calls, and therefore is functional only on Linux. #[cfg(target_os = "linux")] -pub fn silent_get_output_stream() -> eyre::Result { +fn silent_get_output_stream() -> crate::Result { use libc::freopen; use rodio::OutputStreamBuilder; use std::ffi::CString; @@ -23,7 +25,7 @@ pub fn silent_get_output_stream() -> eyre::Result eyre::Result crate::Result { + #[cfg(target_os = "linux")] + let mut stream = silent_get_output_stream()?; + #[cfg(not(target_os = "linux"))] + let mut stream = rodio::OutputStreamBuilder::open_default_stream()?; + stream.log_on_drop(false); Ok(stream) } diff --git a/src/audio/waiter.rs b/src/audio/waiter.rs new file mode 100644 index 0000000..ed13ada --- /dev/null +++ b/src/audio/waiter.rs @@ -0,0 +1,59 @@ +use std::{sync::Arc, time::Duration}; + +use rodio::Sink; +use tokio::{ + sync::{mpsc, Notify}, + task::{self, JoinHandle}, + time, +}; + +/// Lightweight helper that waits for the current sink to drain and then +/// notifies the player to advance to the next track. +pub struct Handle { + /// Background task monitoring the sink. + task: JoinHandle<()>, + + /// Notification primitive used to wake the waiter. + notify: Arc, +} + +impl Drop for Handle { + fn drop(&mut self) { + self.task.abort(); + } +} + +impl Handle { + /// Create a new `Handle` which watches the provided `sink` and sends + /// `Message::Next` down `tx` when the sink becomes empty. + pub fn new(sink: Arc, tx: mpsc::Sender) -> Self { + let notify = Arc::new(Notify::new()); + + Self { + task: task::spawn(Self::waiter(sink, tx, Arc::clone(¬ify))), + notify, + } + } + + /// Notify the waiter that playback state may have changed and it should + /// re-check the sink emptiness condition. + pub fn notify(&self) { + self.notify.notify_one(); + } + + /// Background loop that waits for the sink to drain and then attempts + /// to send a `Message::Next` to the provided channel. + async fn waiter(sink: Arc, tx: mpsc::Sender, notify: Arc) { + loop { + notify.notified().await; + + while !sink.empty() { + time::sleep(Duration::from_millis(8)).await; + } + + if tx.try_send(crate::Message::Next).is_err() { + break; + } + } + } +} diff --git a/src/bookmark.rs b/src/bookmark.rs new file mode 100644 index 0000000..e64452a --- /dev/null +++ b/src/bookmark.rs @@ -0,0 +1,89 @@ +//! Bookmark persistence and helpers. +//! +//! Bookmarks are persisted to `bookmarks.txt` inside the application data +//! directory and follow the same track-list entry format (see `tracks::Info::to_entry`). + +use std::path::PathBuf; +use tokio::{fs, io}; + +use crate::{data_dir, tracks}; + +/// Result alias for bookmark operations. +type Result = std::result::Result; + +/// Errors that might occur while managing bookmarks. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("data directory not found")] + Directory, + + #[error("io failure")] + Io(#[from] io::Error), +} + +/// Manages the bookmarks in the current player. +pub struct Bookmarks { + /// The different entries in the bookmarks file. + pub(crate) entries: Vec, +} + +impl Bookmarks { + /// Returns the path to `bookmarks.txt`, creating the parent directory + /// if necessary. + pub async fn path() -> Result { + let data_dir = data_dir().map_err(|_| Error::Directory)?; + fs::create_dir_all(data_dir.clone()).await?; + + Ok(data_dir.join("bookmarks.txt")) + } + + /// Loads bookmarks from disk. If no file exists an empty list is returned. + pub async fn load() -> Result { + let text = fs::read_to_string(Self::path().await?) + .await + .unwrap_or_default(); + + let entries: Vec = text + .trim_start_matches("noheader") + .trim() + .lines() + .filter_map(|x| { + if x.is_empty() { + None + } else { + Some(x.to_owned()) + } + }) + .collect(); + + Ok(Self { entries }) + } + + /// Saves bookmarks to disk in `bookmarks.txt`. + pub async fn save(&self) -> Result<()> { + let text = format!("noheader\n{}", self.entries.join("\n")); + fs::write(Self::path().await?, text).await?; + Ok(()) + } + + /// Toggles bookmarking for `track` and returns whether it is now bookmarked. + /// + /// If the track exists it is removed; otherwise it is appended to the list. + pub fn bookmark(&mut self, track: &tracks::Info) -> Result { + let entry = track.to_entry(); + let idx = self.entries.iter().position(|x| **x == entry); + + if let Some(idx) = idx { + self.entries.remove(idx); + } else { + self.entries.push(entry); + } + + Ok(idx.is_none()) + } + + /// Returns true if `track` is currently bookmarked. + pub fn bookmarked(&mut self, track: &tracks::Info) -> bool { + self.entries.contains(&track.to_entry()) + } +} diff --git a/src/download.rs b/src/download.rs new file mode 100644 index 0000000..e222b94 --- /dev/null +++ b/src/download.rs @@ -0,0 +1,152 @@ +use std::{ + sync::atomic::{self, AtomicBool, AtomicU8}, + time::Duration, +}; + +use reqwest::Client; +use tokio::{ + sync::mpsc::{self, Receiver, Sender}, + task::JoinHandle, +}; + +use crate::tracks; + +/// Flag indicating whether the downloader is actively fetching a track. +/// +/// This is used internally to prevent concurrent downloader starts and to +/// indicate to the UI that a download is in progress. +static LOADING: AtomicBool = AtomicBool::new(false); + +/// Global download progress in the range 0..=100 updated atomically. +/// +/// The UI can read this `AtomicU8` to render a global progress indicator +/// when there isn't an immediately queued track available. +pub(crate) static PROGRESS: AtomicU8 = AtomicU8::new(0); + +/// A convenient alias for the progress `AtomicU8` pointer type. +pub type Progress = &'static AtomicU8; + +/// The downloader, which has all of the state necessary +/// to download tracks and add them to the queue. +pub struct Downloader { + /// The track queue itself, which in this case is actually + /// just an asynchronous sender. + /// + /// It is a [`Sender`] because the tracks will have to be + /// received by a completely different thread, so this avoids + /// the need to use an explicit [`tokio::sync::Mutex`]. + queue: Sender, + + /// The [`Sender`] which is used to inform the + /// [`crate::Player`] with [`crate::Message::Loaded`]. + tx: Sender, + + /// The list of tracks to download from. + tracks: tracks::List, + + /// The [`reqwest`] client to use for downloads. + client: Client, +} + +impl Downloader { + /// Initializes the downloader with a track list. + /// + /// `tx` specifies the [`Sender`] to be notified with [`crate::Message::Loaded`]. + pub fn init( + size: usize, + timeout: u64, + tracks: tracks::List, + tx: Sender, + ) -> crate::Result { + let client = Client::builder() + .user_agent(concat!( + env!("CARGO_PKG_NAME"), + "/", + env!("CARGO_PKG_VERSION") + )) + .timeout(Duration::from_secs(timeout)) + .build()?; + + let (qtx, qrx) = mpsc::channel(size - 1); + let downloader = Self { + queue: qtx, + tx, + tracks, + client, + }; + + Ok(Handle { + queue: qrx, + task: tokio::spawn(downloader.run()), + }) + } + + /// Actually runs the downloader, consuming it and beginning + /// the cycle of downloading tracks and reporting to the + /// rest of the program. + async fn run(self) -> crate::Result<()> { + const ERROR_TIMEOUT: Duration = Duration::from_secs(1); + + loop { + let result = self.tracks.random(&self.client, &PROGRESS).await; + match result { + Ok(track) => { + self.queue.send(track).await?; + + if LOADING.load(atomic::Ordering::Relaxed) { + self.tx.send(crate::Message::Loaded).await?; + LOADING.store(false, atomic::Ordering::Relaxed); + } + } + Err(error) => { + PROGRESS.store(0, atomic::Ordering::Relaxed); + if !error.timeout() { + tokio::time::sleep(ERROR_TIMEOUT).await; + } + } + } + } + } +} + +/// Downloader handle, responsible for managing +/// the downloader task and internal buffer. +pub struct Handle { + /// The queue receiver, which can be used to actually + /// fetch a track from the queue. + queue: Receiver, + + /// The downloader task, which can be aborted. + task: JoinHandle>, +} + +/// The output when a track is requested from the downloader. +pub enum Output { + /// No track was immediately available from the downloader. When present, + /// the `Option` provides a reference to the global download + /// progress so callers can show a loading indicator. + Loading(Option), + + /// A successfully downloaded (but not yet decoded) track ready to be + /// enqueued for decoding/playback. + Queued(tracks::Queued), +} + +impl Handle { + /// Gets either a queued track, or a progress report, + /// depending on the state of the internal download buffer. + #[rustfmt::skip] + pub fn track(&mut self) -> Output { + self.queue.try_recv().map_or_else(|_| { + LOADING.store(true, atomic::Ordering::Relaxed); + Output::Loading(Some(&PROGRESS)) + }, Output::Queued, + ) + } +} + +impl Drop for Handle { + fn drop(&mut self) { + self.task.abort(); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..7f20842 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,76 @@ +//! Application-wide error type. +//! +//! This module exposes a single `Error` enum that aggregates the common +//! error kinds used across the application (IO, networking, UI, audio, +//! persistence). Higher-level functions should generally return +//! `crate::error::Result` to make error handling consistent. + +use crate::{bookmark, tracks, ui, volume}; +use tokio::sync::{broadcast, mpsc}; + +/// Result alias using the crate-wide `Error` type. +pub type Result = std::result::Result; + +/// Central application error. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Errors while loading or saving the persistent volume settings. + #[error("unable to load/save the persistent volume: {0}")] + PersistentVolume(#[from] volume::Error), + + /// Errors while loading or saving bookmarks. + #[error("unable to load/save bookmarks: {0}")] + Bookmarks(#[from] bookmark::Error), + + /// Network request failures from `reqwest`. + #[error("unable to fetch data: {0}")] + Request(#[from] reqwest::Error), + + /// Failure converting to/from a C string (FFI helpers). + #[error("C string null error: {0}")] + FfiNull(#[from] std::ffi::NulError), + + /// Errors coming from the audio backend / stream handling. + #[error("audio playing error: {0}")] + Rodio(#[from] rodio::StreamError), + + /// Failure to send an internal `Message` over the mpsc channel. + #[error("couldn't send internal message: {0}")] + Send(#[from] mpsc::error::SendError), + + /// Failure to enqueue a track into the queue channel. + #[error("couldn't add track to the queue: {0}")] + Queue(#[from] mpsc::error::SendError), + + /// Failure to broadcast UI updates. + #[error("couldn't update UI state: {0}")] + Broadcast(#[from] broadcast::error::SendError), + + /// Generic IO error. + #[error("io error: {0}")] + Io(#[from] std::io::Error), + + /// Data directory was not found or could not be determined. + #[error("directory not found")] + Directory, + + /// Downloader failed to provide the requested track. + #[error("couldn't fetch track from downloader")] + Download, + + /// Integer parsing errors. + #[error("couldn't parse integer: {0}")] + Parse(#[from] std::num::ParseIntError), + + /// Track subsystem error. + #[error("track failure")] + Track(#[from] tracks::Error), + + /// UI subsystem error. + #[error("ui failure")] + UI(#[from] ui::Error), + + /// Error returned when a spawned task join failed. + #[error("join error")] + JoinError(#[from] tokio::task::JoinError), +} diff --git a/src/main.rs b/src/main.rs index 870774a..c38599f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,22 @@ //! An extremely simple lofi player. - -#![warn(clippy::all, clippy::pedantic, clippy::nursery)] - -use clap::{Parser, Subcommand}; +pub mod error; use std::path::PathBuf; -mod messages; -mod play; -mod player; -mod tracks; +use clap::{Parser, Subcommand}; +mod tests; +pub use error::{Error, Result}; +pub mod message; +pub mod ui; +pub use message::Message; + +use crate::player::Player; +pub mod audio; +pub mod bookmark; +pub mod download; +pub mod player; +pub mod tracks; +pub mod volume; -#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::restriction)] #[cfg(feature = "scrape")] mod scrapers; @@ -21,7 +27,7 @@ use crate::scrapers::Source; #[derive(Parser, Clone)] #[command(about, version)] #[allow(clippy::struct_excessive_bools)] -struct Args { +pub struct Args { /// Use an alternate terminal screen. #[clap(long, short)] alternate: bool, @@ -43,7 +49,7 @@ struct Args { fps: u8, /// Timeout in seconds for music downloads. - #[clap(long, default_value_t = 3)] + #[clap(long, default_value_t = 16)] timeout: u64, /// Include ALSA & other logs. @@ -54,13 +60,13 @@ struct Args { #[clap(long, short, default_value_t = 3)] width: usize, - /// Use a custom track list - #[clap(long, short, alias = "list", alias = "tracks", short_alias = 'l')] - track_list: Option, + /// Track list to play music from + #[clap(long, short, alias = "list", alias = "tracks", short_alias = 'l', default_value_t = String::from("chillhop"))] + track_list: String, /// Internal song buffer size. - #[clap(long, short = 's', alias = "buffer", default_value_t = 5)] - buffer_size: usize, + #[clap(long, short = 's', alias = "buffer", default_value_t = 5, value_parser = clap::value_parser!(u32).range(2..))] + buffer_size: u32, /// The command that was ran. /// This is [None] if no command was specified. @@ -79,33 +85,41 @@ enum Commands { }, } -/// Gets lowfi's data directory. -pub fn data_dir() -> eyre::Result { - let dir = dirs::data_dir() - .ok_or(player::Error::DataDir)? - .join("lowfi"); +/// Returns the application data directory used for persistency. +/// +/// The function returns the platform-specific user data directory with +/// a `lowfi` subfolder. Callers may use this path to store config, +/// bookmarks, and other persistent files. +pub fn data_dir() -> crate::Result { + let dir = dirs::data_dir().unwrap().join("lowfi"); Ok(dir) } -#[tokio::main] +/// Program entry point. +/// +/// Parses CLI arguments, initializes the audio stream and player, then +/// runs the main event loop. On exit it performs cleanup of the UI and +/// returns the inner result. +#[tokio::main(flavor = "current_thread")] async fn main() -> eyre::Result<()> { - color_eyre::install()?; + let args = Args::parse(); - let cli = Args::parse(); - - if let Some(command) = cli.command { + #[cfg(feature = "scrape")] + if let Some(command) = &args.command { match command { - #[cfg(feature = "scrape")] Commands::Scrape { source } => match source { Source::Archive => scrapers::archive::scrape().await?, Source::Lofigirl => scrapers::lofigirl::scrape().await?, Source::Chillhop => scrapers::chillhop::scrape().await?, }, } - } else { - play::play(cli).await?; - }; + } - Ok(()) + let stream = audio::stream()?; + let mut player = Player::init(args, stream.mixer()).await?; + let result = player.run().await; + + player.environment().cleanup(result.is_ok())?; + Ok(result?) } diff --git a/src/message.rs b/src/message.rs new file mode 100644 index 0000000..d40c96d --- /dev/null +++ b/src/message.rs @@ -0,0 +1,37 @@ +/// Handles communication between different parts of the program. +#[allow(dead_code, reason = "this code may not be dead depending on features")] +#[derive(PartialEq, Debug, Clone)] +pub enum Message { + /// Deliberate user request to go to the next song, also sent when the + /// song is over by the waiter. + Next, + + /// When a track is loaded after the caller previously being told to wait. + /// If a track is taken from the queue, then there is no waiting, so this + /// is never actually sent. + Loaded, + + /// Similar to Next, but specific to the first track. + Init, + + /// Unpause the [Sink]. + Play, + + /// Pauses the [Sink]. + Pause, + + /// Pauses the [Sink]. This will also unpause it if it is paused. + PlayPause, + + /// Change the volume of playback. + ChangeVolume(f32), + + /// Set the volume of playback, rather than changing it. + SetVolume(f32), + + /// Bookmark the current track. + Bookmark, + + /// Quits gracefully. + Quit, +} diff --git a/src/messages.rs b/src/messages.rs deleted file mode 100644 index b33e0c6..0000000 --- a/src/messages.rs +++ /dev/null @@ -1,37 +0,0 @@ -/// Handles communication between the frontend & audio player. -#[derive(PartialEq, Debug, Clone, Copy)] -pub enum Message { - /// Notifies the audio server that it should update the track. - Next, - - /// Special in that this isn't sent in a "client to server" sort of way, - /// but rather is sent by a child of the server when a song has not only - /// been requested but also downloaded aswell. - NewSong, - - /// This signal is only sent if a track timed out. In that case, - /// lowfi will try again and again to retrieve the track. - TryAgain, - - /// Similar to Next, but specific to the first track. - Init, - - /// Unpause the [Sink]. - #[allow(dead_code, reason = "this code may not be dead depending on features")] - Play, - - /// Pauses the [Sink]. - Pause, - - /// Pauses the [Sink]. This will also unpause it if it is paused. - PlayPause, - - /// Change the volume of playback. - ChangeVolume(f32), - - /// Bookmark the current track. - Bookmark, - - /// Quits gracefully. - Quit, -} diff --git a/src/play.rs b/src/play.rs deleted file mode 100644 index 0c70229..0000000 --- a/src/play.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Responsible for the basic initialization & shutdown of the audio server & frontend. - -use crossterm::cursor::Show; -use crossterm::event::PopKeyboardEnhancementFlags; -use crossterm::terminal::{self, Clear, ClearType}; -use std::io::{stdout, IsTerminal}; -use std::process::exit; -use std::sync::Arc; -use std::{env, panic}; -use tokio::{sync::mpsc, task}; - -use crate::messages::Message; -use crate::player::persistent_volume::PersistentVolume; -use crate::player::Player; -use crate::player::{self, ui}; -use crate::Args; - -/// Initializes the audio server, and then safely stops -/// it when the frontend quits. -pub async fn play(args: Args) -> eyre::Result<(), player::Error> { - // TODO: This isn't a great way of doing things, - // but it's better than vanilla behaviour at least. - let eyre_hook = panic::take_hook(); - - panic::set_hook(Box::new(move |x| { - let mut lock = stdout().lock(); - crossterm::execute!( - lock, - Clear(ClearType::FromCursorDown), - Show, - PopKeyboardEnhancementFlags - ) - .unwrap(); - terminal::disable_raw_mode().unwrap(); - - eyre_hook(x); - exit(1) - })); - - // Actually initializes the player. - // Stream kept here in the master thread to keep it alive. - let (player, stream) = Player::new(&args).await?; - let player = Arc::new(player); - - // Initialize the UI, as well as the internal communication channel. - let (tx, rx) = mpsc::channel(8); - let ui = if stdout().is_terminal() && !(env::var("LOWFI_DISABLE_UI") == Ok("1".to_owned())) { - Some(task::spawn(ui::start( - Arc::clone(&player), - tx.clone(), - args.clone(), - ))) - } else { - None - }; - - // Sends the player an "init" signal telling it to start playing a song straight away. - tx.send(Message::Init).await?; - - // Actually starts the player. - Player::play(Arc::clone(&player), tx.clone(), rx, args.debug).await?; - - // Save the volume.txt file for the next session. - PersistentVolume::save(player.sink.volume()) - .await - .map_err(player::Error::PersistentVolumeSave)?; - - // Save the bookmarks for the next session. - player.bookmarks.save().await?; - - drop(stream); - player.sink.stop(); - if let Some(x) = ui { - x.abort(); - } - - Ok(()) -} diff --git a/src/player.rs b/src/player.rs index 118a159..c1d8d1c 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,314 +1,242 @@ -//! Responsible for playing & queueing audio. -//! This also has the code for the underlying -//! audio server which adds new tracks. +use std::sync::Arc; -use std::{collections::VecDeque, sync::Arc, time::Duration}; - -use arc_swap::ArcSwapOption; -use atomic_float::AtomicF32; -use downloader::Downloader; -use reqwest::Client; -use rodio::{OutputStream, OutputStreamBuilder, Sink}; -use tokio::{ - select, - sync::{ - mpsc::{Receiver, Sender}, - RwLock, - }, - task, +use tokio::sync::{ + broadcast, + mpsc::{self, Receiver}, }; -#[cfg(feature = "mpris")] -use mpris_server::{PlaybackStatus, PlayerInterface, Property}; - use crate::{ - messages::Message, - player::{self, bookmark::Bookmarks, persistent_volume::PersistentVolume}, - tracks::{self, list::List}, - Args, + audio::waiter, + bookmark::Bookmarks, + download::{self, Downloader}, + tracks::{self, List}, + ui, + volume::PersistentVolume, + Message, }; -pub mod audio; -pub mod bookmark; -pub mod downloader; -pub mod error; -pub mod persistent_volume; -pub mod queue; -pub mod ui; +#[derive(Clone, Debug)] +/// Represents the currently known playback state. +/// +/// * [`Current::Loading`] indicates the player is waiting for data. +/// * [`Current::Track`] indicates the player has a decoded track available. +pub enum Current { + /// Waiting for a track to arrive. The optional `Progress` is used to + /// indicate global download progress when present. + Loading(Option), -pub use error::Error; + /// A decoded track that can be played; contains the track `Info`. + Track(tracks::Info), +} -#[cfg(feature = "mpris")] -pub mod mpris; +impl Default for Current { + fn default() -> Self { + // By default the player starts in a loading state with no progress. + Self::Loading(None) + } +} -/// Main struct responsible for queuing up & playing tracks. -// TODO: Consider refactoring [Player] from being stored in an [Arc], into containing many smaller [Arc]s. -// TODO: In other words, this would change the type from `Arc` to just `Player`. -// TODO: -// TODO: This is conflicting, since then it'd clone ~10 smaller [Arc]s -// TODO: every single time, which could be even worse than having an -// TODO: [Arc] of an [Arc] in some cases (Like with [Sink] & [Client]). +impl Current { + /// Returns `true` if this `Current` value represents a loading state. + pub const fn loading(&self) -> bool { + matches!(self, Self::Loading(_)) + } +} + +/// The high-level application player. +/// +/// `Player` composes the downloader, UI, audio sink and bookkeeping state. +/// It owns background `Handle`s and drives the main message loop in `run`. pub struct Player { - /// [rodio]'s [`Sink`] which can control playback. - pub sink: Sink, + /// Background downloader that fills the internal queue. + downloader: download::Handle, - /// The internal buffer size. - pub buffer_size: usize, + /// Persistent bookmark storage used by the player. + bookmarks: Bookmarks, - /// The [`TrackInfo`] of the current track. - /// This is [`None`] when lowfi is buffering/loading. - current: ArcSwapOption, + /// Shared audio sink used for playback. + sink: Arc, - /// The current progress for downloading tracks, if - /// `current` is None. - progress: AtomicF32, + /// Receiver for incoming `Message` commands. + rx: Receiver, - /// The tracks, which is a [`VecDeque`] that holds - /// *undecoded* [Track]s. - /// - /// This is populated specifically by the [Downloader]. - tracks: RwLock>, + /// Broadcast channel used to send UI updates. + broadcast: broadcast::Sender, - /// The bookmarks, which are saved on quit. - pub bookmarks: Bookmarks, + /// Current playback state (loading or track). + current: Current, - /// The timeout for track downloads, as a [Duration]. - timeout: Duration, + /// UI handle for rendering and input. + ui: ui::Handle, - /// The actual list of tracks to be played. - list: List, + /// Notifies when a play head has been appended. + waiter: waiter::Handle, +} - /// The initial volume level. - volume: PersistentVolume, - - /// The web client, which can contain a `UserAgent` & some - /// settings that help lowfi work more effectively. - client: Client, +impl Drop for Player { + fn drop(&mut self) { + // Ensure playback is stopped when the player is dropped. + self.sink.stop(); + } } impl Player { - /// Just a shorthand for setting `current`. - fn set_current(&self, info: tracks::Info) { - self.current.store(Some(Arc::new(info))); + /// Returns the `Environment` currently used by the UI. + pub const fn environment(&self) -> ui::Environment { + self.ui.environment } - /// A shorthand for checking if `self.current` is [Some]. - pub fn current_exists(&self) -> bool { - self.current.load().is_some() - } - - /// Sets the volume of the sink, and also clamps the value to avoid negative/over 100% values. - pub fn set_volume(&self, volume: f32) { - self.sink.set_volume(volume.clamp(0.0, 1.0)); - } - - /// Initializes the entire player, including audio devices & sink. + /// Sets the in-memory current state and notifies the UI about the change. /// - /// This also will load the track list & persistent volume. - pub async fn new(args: &Args) -> eyre::Result<(Self, OutputStream), player::Error> { - // Load the bookmarks. - let bookmarks = Bookmarks::load().await?; + /// If the new state is a `Track`, this will also update the bookmarked flag + /// based on persistent bookmarks. + pub fn set_current(&mut self, current: Current) -> crate::Result<()> { + self.current = current.clone(); + self.update(ui::Update::Track(current))?; - // Load the volume file. - let volume = PersistentVolume::load() - .await - .map_err(player::Error::PersistentVolumeLoad)?; - - // Load the track list. - let list = List::load(args.track_list.as_ref()) - .await - .map_err(player::Error::TrackListLoad)?; - - // We should only shut up alsa forcefully on Linux if we really have to. - #[cfg(target_os = "linux")] - let mut stream = if !args.alternate && !args.debug { - audio::silent_get_output_stream()? - } else { - OutputStreamBuilder::open_default_stream()? + let Current::Track(track) = &self.current else { + return Ok(()); }; - #[cfg(not(target_os = "linux"))] - let mut stream = OutputStreamBuilder::open_default_stream()?; - - stream.log_on_drop(false); // Frankly, this is a stupid feature. Stop shoving your crap into my beloved stderr!!! - let sink = Sink::connect_new(stream.mixer()); - - if args.paused { - sink.pause(); - } - - let client = Client::builder() - .user_agent(concat!( - env!("CARGO_PKG_NAME"), - "/", - env!("CARGO_PKG_VERSION") - )) - .timeout(Duration::from_secs(args.timeout * 5)) - .build()?; - - let player = Self { - tracks: RwLock::new(VecDeque::with_capacity(args.buffer_size)), - buffer_size: args.buffer_size, - current: ArcSwapOption::new(None), - progress: AtomicF32::new(0.0), - timeout: Duration::from_secs(args.timeout), - bookmarks, - client, - sink, - volume, - list, - }; - - Ok((player, stream)) - } - - /// This is the main "audio server". - /// - /// `rx` & `tx` are used to communicate with it, for example when to - /// skip tracks or pause. - /// - /// This will also initialize a [Downloader] as well as an MPRIS server if enabled. - /// The [Downloader]s internal buffer size is determined by `buf_size`. - pub async fn play( - player: Arc, - tx: Sender, - mut rx: Receiver, - debug: bool, - ) -> eyre::Result<(), player::Error> { - // Initialize the mpris player. - // - // We're initializing here, despite MPRIS being a "user interface", - // since we need to be able to *actively* write new information to MPRIS - // specifically when it occurs, unlike the UI which passively reads the - // information each frame. Blame MPRIS, not me. - #[cfg(feature = "mpris")] - let mpris = mpris::Server::new(Arc::clone(&player), tx.clone()) - .await - .inspect_err(|x| { - dbg!(x); - })?; - - // `itx` is used to notify the `Downloader` when it needs to download new tracks. - let downloader = Downloader::new(Arc::clone(&player)); - let (itx, downloader) = downloader.start(debug); - - // Start buffering tracks immediately. - Downloader::notify(&itx).await?; - - // Set the initial sink volume to the one specified. - player.set_volume(player.volume.float()); - - // Whether the last signal was a `NewSong`. This is helpful, since we - // only want to autoplay if there hasn't been any manual intervention. - // - // In other words, this will be `true` after a new track has been fully - // loaded and it'll be `false` if a track is still currently loading. - let mut new = false; - - loop { - let clone = Arc::clone(&player); - - let msg = select! { - biased; - - Some(x) = rx.recv() => x, - // This future will finish only at the end of the current track. - // The condition is a kind-of hack which gets around the quirks - // of `sleep_until_end`. - // - // That's because `sleep_until_end` will return instantly if the sink - // is uninitialized. That's why we put a check to make sure that the last - // signal we got was `NewSong`, since we shouldn't start waiting for the - // song to be over until it has actually started. - // - // It's also important to note that the condition is only checked at the - // beginning of the loop, not throughout. - Ok(()) = task::spawn_blocking(move || clone.sink.sleep_until_end()), - if new => Message::Next, - }; - - match msg { - Message::Next | Message::Init | Message::TryAgain => { - // We manually skipped, so we shouldn't actually wait for the song - // to be over until we recieve the `NewSong` signal. - new = false; - - // This basically just prevents `Next` while a song is still currently loading. - if msg == Message::Next && !player.current_exists() { - continue; - } - - // Handle the rest of the signal in the background, - // as to not block the main audio server thread. - task::spawn(Self::next( - Arc::clone(&player), - itx.clone(), - tx.clone(), - debug, - )); - } - Message::Play => { - player.sink.play(); - - #[cfg(feature = "mpris")] - mpris.playback(PlaybackStatus::Playing).await?; - } - Message::Pause => { - player.sink.pause(); - - #[cfg(feature = "mpris")] - mpris.playback(PlaybackStatus::Paused).await?; - } - Message::PlayPause => { - if player.sink.is_paused() { - player.sink.play(); - } else { - player.sink.pause(); - } - - #[cfg(feature = "mpris")] - mpris - .playback(mpris.player().playback_status().await?) - .await?; - } - Message::ChangeVolume(change) => { - player.set_volume(player.sink.volume() + change); - - #[cfg(feature = "mpris")] - mpris - .changed(vec![Property::Volume(player.sink.volume().into())]) - .await?; - } - // This basically just continues, but more importantly, it'll re-evaluate - // the select macro at the beginning of the loop. - // See the top section to find out why this matters. - Message::NewSong => { - // We've recieved `NewSong`, so on the next loop iteration we'll - // begin waiting for the song to be over in order to autoplay. - new = true; - - #[cfg(feature = "mpris")] - mpris - .changed(vec![ - Property::Metadata(mpris.player().metadata().await?), - Property::PlaybackStatus(mpris.player().playback_status().await?), - ]) - .await?; - - continue; - } - Message::Bookmark => { - let current = player.current.load(); - let current = current.as_ref().unwrap(); - - player.bookmarks.bookmark(current).await?; - } - Message::Quit => break, - } - } - - downloader.abort(); + let bookmarked = self.bookmarks.bookmarked(track); + self.update(ui::Update::Bookmarked(bookmarked))?; Ok(()) } + + /// Sends a `ui::Update` to the broadcast channel. + pub fn update(&mut self, update: ui::Update) -> crate::Result<()> { + self.broadcast.send(update)?; + Ok(()) + } + + /// Initialize a `Player` with the provided CLI `args` and audio `mixer`. + /// + /// This sets up the audio sink, UI, downloader, bookmarks and persistent + /// volume state. The function returns a fully constructed `Player` ready + /// to be driven via `run`. + pub async fn init(args: crate::Args, mixer: &rodio::mixer::Mixer) -> crate::Result { + let (tx, rx) = mpsc::channel(8); + if args.paused { + tx.send(Message::Pause).await?; + } + + tx.send(Message::Init).await?; + + let (utx, urx) = broadcast::channel(8); + let list = List::load(args.track_list.as_ref()).await?; + + let sink = Arc::new(rodio::Sink::connect_new(mixer)); + let state = ui::State::initial(Arc::clone(&sink), args.width, list.name.clone()); + + let volume = PersistentVolume::load().await?; + sink.set_volume(volume.float()); + + Ok(Self { + ui: ui::Handle::init(tx.clone(), urx, state, &args).await?, + downloader: Downloader::init( + args.buffer_size as usize, + args.timeout, + list, + tx.clone(), + )?, + waiter: waiter::Handle::new(Arc::clone(&sink), tx), + bookmarks: Bookmarks::load().await?, + current: Current::default(), + broadcast: utx, + rx, + sink, + }) + } + + /// Persist state that should survive a run (bookmarks and volume). + pub async fn close(&self) -> crate::Result<()> { + self.bookmarks.save().await?; + PersistentVolume::save(self.sink.volume()).await?; + + Ok(()) + } + + /// Play a queued track by decoding, appending to the sink and notifying + /// other subsystems that playback has changed. + pub fn play(&mut self, queued: tracks::Queued) -> crate::Result<()> { + let decoded = queued.decode()?; + self.sink.append(decoded.data); + self.set_current(Current::Track(decoded.info))?; + self.waiter.notify(); + + Ok(()) + } + + /// Drives the main message loop of the player. + /// + /// This will return when a `Message::Quit` is received. It handles commands + /// coming from the frontend and updates playback/UI state accordingly. + pub async fn run(&mut self) -> crate::Result<()> { + while let Some(message) = self.rx.recv().await { + match message { + Message::Next | Message::Init | Message::Loaded => { + if message == Message::Next && self.current.loading() { + continue; + } + + self.sink.stop(); + match self.downloader.track() { + download::Output::Loading(progress) => { + self.set_current(Current::Loading(progress))?; + } + download::Output::Queued(queued) => self.play(queued)?, + } + } + Message::Play => { + self.sink.play(); + } + Message::Pause => { + self.sink.pause(); + } + Message::PlayPause => { + if self.sink.is_paused() { + self.sink.play(); + } else { + self.sink.pause(); + } + } + Message::ChangeVolume(change) => { + self.sink + .set_volume((self.sink.volume() + change).clamp(0.0, 1.0)); + self.update(ui::Update::Volume)?; + } + Message::SetVolume(set) => { + self.sink.set_volume(set.clamp(0.0, 1.0)); + self.update(ui::Update::Volume)?; + } + Message::Bookmark => { + let Current::Track(current) = &self.current else { + continue; + }; + + let bookmarked = self.bookmarks.bookmark(current)?; + self.update(ui::Update::Bookmarked(bookmarked))?; + } + Message::Quit => break, + } + + #[cfg(feature = "mpris")] + match message { + Message::ChangeVolume(_) | Message::SetVolume(_) => { + self.ui.mpris.update_volume().await? + } + Message::Play | Message::Pause | Message::PlayPause => { + self.ui.mpris.update_playback().await? + } + Message::Init | Message::Loaded | Message::Next => { + self.ui.mpris.update_metadata().await? + } + _ => (), + } + } + + self.close().await?; + Ok(()) + } } diff --git a/src/player/bookmark.rs b/src/player/bookmark.rs deleted file mode 100644 index 0ff6761..0000000 --- a/src/player/bookmark.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! Module for handling saving, loading, and adding -//! bookmarks. - -use std::path::PathBuf; -use std::sync::atomic::AtomicBool; - -use tokio::sync::RwLock; -use tokio::{fs, io}; - -use crate::{data_dir, tracks}; - -/// Errors that might occur while managing bookmarks. -#[derive(Debug, thiserror::Error)] -pub enum BookmarkError { - #[error("data directory not found")] - DataDir, - - #[error("io failure")] - Io(#[from] io::Error), -} - -/// Manages the bookmarks in the current player. -pub struct Bookmarks { - /// The different entries in the bookmarks file. - entries: RwLock>, - - /// The internal bookmarked register, which keeps track - /// of whether a track is bookmarked or not. - /// - /// This is much more efficient than checking every single frame. - bookmarked: AtomicBool, -} - -impl Bookmarks { - /// Gets the path of the bookmarks file. - pub async fn path() -> eyre::Result { - let data_dir = data_dir().map_err(|_| BookmarkError::DataDir)?; - fs::create_dir_all(data_dir.clone()).await?; - - Ok(data_dir.join("bookmarks.txt")) - } - - /// Loads bookmarks from the `bookmarks.txt` file. - pub async fn load() -> eyre::Result { - let text = fs::read_to_string(Self::path().await?) - .await - .unwrap_or_default(); - - let lines: Vec = text - .trim_start_matches("noheader") - .trim() - .lines() - .filter_map(|x| { - if x.is_empty() { - None - } else { - Some(x.to_string()) - } - }) - .collect(); - - Ok(Self { - entries: RwLock::new(lines), - bookmarked: AtomicBool::new(false), - }) - } - - // Saves the bookmarks to the `bookmarks.txt` file. - pub async fn save(&self) -> eyre::Result<(), BookmarkError> { - let text = format!("noheader\n{}", self.entries.read().await.join("\n")); - fs::write(Self::path().await?, text).await?; - Ok(()) - } - - /// Bookmarks a given track with a full path and optional custom name. - /// - /// Returns whether the track is now bookmarked, or not. - pub async fn bookmark(&self, track: &tracks::Info) -> eyre::Result<(), BookmarkError> { - let entry = track.to_entry(); - let idx = self.entries.read().await.iter().position(|x| **x == entry); - - if let Some(idx) = idx { - self.entries.write().await.remove(idx); - } else { - self.entries.write().await.push(entry); - }; - - self.bookmarked - .swap(idx.is_none(), std::sync::atomic::Ordering::Relaxed); - - Ok(()) - } - - /// Returns whether a track is bookmarked or not by using the internal - /// bookmarked register. - pub fn bookmarked(&self) -> bool { - self.bookmarked.load(std::sync::atomic::Ordering::Relaxed) - } - - /// Sets the internal bookmarked register by checking against - /// the current track's info. - pub async fn set_bookmarked(&self, track: &tracks::Info) { - let val = self.entries.read().await.contains(&track.to_entry()); - self.bookmarked - .swap(val, std::sync::atomic::Ordering::Relaxed); - } -} diff --git a/src/player/downloader.rs b/src/player/downloader.rs deleted file mode 100644 index 4963139..0000000 --- a/src/player/downloader.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Contains the [`Downloader`] struct. - -use std::{error::Error, sync::Arc}; - -use tokio::{ - sync::mpsc::{self, Receiver, Sender}, - task::{self, JoinHandle}, - time::sleep, -}; - -use super::Player; - -/// This struct is responsible for downloading tracks in the background. -/// -/// This is not used for the first track or a track when the buffer is currently empty. -pub struct Downloader { - /// The player for the downloader to download to & with. - player: Arc, - - /// The internal reciever, which is used by the downloader to know - /// when to begin downloading more tracks. - rx: Receiver<()>, - - /// A copy of the internal sender, which can be useful for keeping - /// track of it. - tx: Sender<()>, -} - -impl Downloader { - /// Uses a sender recieved from [Sender] to notify the - /// download thread that it should resume downloading. - pub async fn notify(sender: &Sender<()>) -> Result<(), mpsc::error::SendError<()>> { - sender.send(()).await - } - - /// Initializes the [Downloader]. - /// - /// This also sends a [`Sender`] which can be used to notify - /// when the downloader needs to begin downloading more tracks. - pub fn new(player: Arc) -> Self { - let (tx, rx) = mpsc::channel(8); - Self { player, rx, tx } - } - - /// Push a new, random track onto the internal buffer. - pub async fn push_buffer(&self, debug: bool) { - let data = self.player.list.random(&self.player.client, None).await; - match data { - Ok(track) => self.player.tracks.write().await.push_back(track), - Err(error) => { - if debug { - panic!("{error} - {:?}", error.source()) - } - - if !error.is_timeout() { - sleep(self.player.timeout).await; - } - } - } - } - - /// Actually starts & consumes the [Downloader]. - pub fn start(mut self, debug: bool) -> (Sender<()>, JoinHandle<()>) { - let tx = self.tx.clone(); - - let handle = task::spawn(async move { - // Loop through each update notification. - while self.rx.recv().await == Some(()) { - // For each update notification, we'll push tracks until the buffer is completely full. - while self.player.tracks.read().await.len() < self.player.buffer_size { - self.push_buffer(debug).await; - } - } - }); - - (tx, handle) - } -} diff --git a/src/player/error.rs b/src/player/error.rs deleted file mode 100644 index e417e2e..0000000 --- a/src/player/error.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::ffi::NulError; - -use crate::{messages::Message, player::bookmark::BookmarkError}; -use tokio::sync::mpsc::error::SendError; - -#[cfg(feature = "mpris")] -use mpris_server::zbus::{self, fdo}; - -/// Any errors which might occur when running or initializing the lowfi player. -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("unable to load the persistent volume")] - PersistentVolumeLoad(eyre::Error), - - #[error("unable to save the persistent volume")] - PersistentVolumeSave(eyre::Error), - - #[error("sending internal message failed")] - Communication(#[from] SendError), - - #[error("unable to load track list")] - TrackListLoad(eyre::Error), - - #[error("interfacing with audio failed")] - Stream(#[from] rodio::StreamError), - - #[error("NUL error, if you see this, something has gone VERY wrong")] - Nul(#[from] NulError), - - #[error("unable to send or prepare network request")] - Reqwest(#[from] reqwest::Error), - - #[cfg(feature = "mpris")] - #[error("mpris bus error")] - ZBus(#[from] zbus::Error), - - // TODO: This has a terrible error message, mainly because I barely understand - // what this error even represents. What does fdo mean?!?!? Why, MPRIS!?!? - #[cfg(feature = "mpris")] - #[error("mpris fdo (zbus interface) error")] - Fdo(#[from] fdo::Error), - - #[error("unable to notify downloader")] - DownloaderNotify(#[from] SendError<()>), - - #[error("unable to find data directory")] - DataDir, - - #[error("bookmarking load/unload failed")] - Bookmark(#[from] BookmarkError), -} diff --git a/src/player/persistent_volume.rs b/src/player/persistent_volume.rs deleted file mode 100644 index 6609f8c..0000000 --- a/src/player/persistent_volume.rs +++ /dev/null @@ -1,70 +0,0 @@ -use eyre::eyre; -use std::path::PathBuf; -use tokio::fs; - -/// This is the representation of the persistent volume, -/// which is loaded at startup and saved on shutdown. -#[derive(Clone, Copy)] -pub struct PersistentVolume { - /// The volume, as a percentage. - inner: u16, -} - -impl PersistentVolume { - /// Retrieves the config directory. - async fn config() -> eyre::Result { - let config = dirs::config_dir() - .ok_or_else(|| eyre!("Couldn't find config directory"))? - .join(PathBuf::from("lowfi")); - - if !config.exists() { - fs::create_dir_all(&config).await?; - } - - Ok(config) - } - - /// Returns the volume as a float from 0 to 1. - pub fn float(self) -> f32 { - f32::from(self.inner) / 100.0 - } - - /// Loads the [`PersistentVolume`] from [`dirs::config_dir()`]. - pub async fn load() -> eyre::Result { - let config = Self::config().await?; - let volume = config.join(PathBuf::from("volume.txt")); - - // Basically just read from the volume file if it exists, otherwise return 100. - let volume = if volume.exists() { - let contents = fs::read_to_string(volume).await?; - let trimmed = contents.trim(); - let stripped = trimmed.strip_suffix("%").unwrap_or(trimmed); - stripped - .parse() - .map_err(|_error| eyre!("volume.txt file is invalid"))? - } else { - fs::write(&volume, "100").await?; - 100u16 - }; - - Ok(Self { inner: volume }) - } - - /// Saves `volume` to `volume.txt`. - pub async fn save(volume: f32) -> eyre::Result<()> { - let config = Self::config().await?; - let path = config.join(PathBuf::from("volume.txt")); - - // Already rounded & absolute, therefore this should be safe. - #[expect( - clippy::as_conversions, - clippy::cast_sign_loss, - clippy::cast_possible_truncation - )] - let percentage = (volume * 100.0).abs().round() as u16; - - fs::write(path, percentage.to_string()).await?; - - Ok(()) - } -} diff --git a/src/player/queue.rs b/src/player/queue.rs deleted file mode 100644 index 1fd665f..0000000 --- a/src/player/queue.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::{ - error::Error, - sync::{atomic::Ordering, Arc}, -}; -use tokio::{sync::mpsc::Sender, time::sleep}; - -use crate::{ - messages::Message, - player::{downloader::Downloader, Player}, - tracks, -}; - -impl Player { - /// Fetches the next track from the queue, or a random track if the queue is empty. - /// This will also set the current track to the fetched track's info. - async fn fetch(&self) -> Result { - // TODO: Consider replacing this with `unwrap_or_else` when async closures are stablized. - let track = self.tracks.write().await.pop_front(); - let track = if let Some(track) = track { - track - } else { - // If the queue is completely empty, then fallback to simply getting a new track. - // This is relevant particularly at the first song. - - // Serves as an indicator that the queue is "loading". - // We're doing it here so that we don't get the "loading" display - // for only a frame in the other case that the buffer is not empty. - self.current.store(None); - self.progress.store(0.0, Ordering::Relaxed); - self.list.random(&self.client, Some(&self.progress)).await? - }; - - let decoded = track.decode()?; - - // Set the current track. - self.set_current(decoded.info.clone()); - - Ok(decoded) - } - - /// Gets, decodes, and plays the next track in the queue while also handling the downloader. - /// - /// This functions purpose is to be called in the background, so that when the audio server recieves a - /// `Next` signal it will still be able to respond to other signals while it's loading. - /// - /// This also sends the either a `NewSong` or `TryAgain` signal to `tx`. - pub async fn next( - player: Arc, - itx: Sender<()>, - tx: Sender, - debug: bool, - ) -> eyre::Result<()> { - // Stop the sink. - player.sink.stop(); - - let track = player.fetch().await; - - match track { - Ok(track) => { - // Start playing the new track. - player.sink.append(track.data); - - // Set whether it's bookmarked. - player.bookmarks.set_bookmarked(&track.info).await; - - // Notify the background downloader that there's an empty spot - // in the buffer. - Downloader::notify(&itx).await?; - - // Notify the audio server that the next song has actually been downloaded. - tx.send(Message::NewSong).await?; - } - Err(error) => { - if debug { - panic!("{error} - {:?}", error.source()) - } - - if !error.is_timeout() { - sleep(player.timeout).await; - } - - tx.send(Message::TryAgain).await?; - } - }; - - Ok(()) - } -} diff --git a/src/player/ui.rs b/src/player/ui.rs deleted file mode 100644 index 259365c..0000000 --- a/src/player/ui.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! The module which manages all user interface, including inputs. - -#![allow( - clippy::as_conversions, - clippy::cast_sign_loss, - clippy::cast_precision_loss, - clippy::cast_possible_truncation, - reason = "the ui is full of these because of various layout & positioning aspects, and for a simple music player making all casts safe is not worth the effort" -)] - -use std::{ - fmt::Write as _, - io::{stdout, Stdout}, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, - }, - time::Duration, -}; - -use crate::Args; - -use crossterm::{ - cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show}, - event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, - style::{Print, Stylize as _}, - terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, -}; - -use lazy_static::lazy_static; -use thiserror::Error; -use tokio::{sync::mpsc::Sender, task, time::sleep}; -use unicode_segmentation::UnicodeSegmentation; - -use super::Player; -use crate::messages::Message; - -mod components; -mod input; - -/// The error type for the UI, which is used to handle errors that occur -/// while drawing the UI or handling input. -#[derive(Debug, Error)] -pub enum UIError { - #[error("unable to convert number")] - Conversion(#[from] std::num::TryFromIntError), - - #[error("unable to write output")] - Write(#[from] std::io::Error), - - #[error("sending message to backend from ui failed")] - Communication(#[from] tokio::sync::mpsc::error::SendError), -} - -/// How long the audio bar will be visible for when audio is adjusted. -/// This is in frames. -const AUDIO_BAR_DURATION: usize = 10; - -lazy_static! { - /// The volume timer, which controls how long the volume display should - /// show up and when it should disappear. - /// - /// When this is 0, it means that the audio bar shouldn't be displayed. - /// To make it start counting, you need to set it to 1. - static ref VOLUME_TIMER: AtomicUsize = AtomicUsize::new(0); -} - -/// Sets the volume timer to one, effectively flashing the audio display in lowfi's UI. -/// -/// The amount of frames the audio display is visible for is determined by [`AUDIO_BAR_DURATION`]. -pub fn flash_audio() { - VOLUME_TIMER.store(1, Ordering::Relaxed); -} - -/// Represents an abstraction for drawing the actual lowfi window itself. -/// -/// The main purpose of this struct is just to add the fancy border, -/// as well as clear the screen before drawing. -pub struct Window { - /// Whether or not to include borders in the output. - borderless: bool, - - /// The top & bottom borders, which are here since they can be - /// prerendered, as they don't change from window to window. - /// - /// If the option to not include borders is set, these will just be empty [String]s. - borders: [String; 2], - - /// The width of the window. - width: usize, - - /// The output, currently just an [`Stdout`]. - out: Stdout, -} - -impl Window { - /// Initializes a new [Window]. - /// - /// * `width` - Width of the windows. - /// * `borderless` - Whether to include borders in the window, or not. - pub fn new(width: usize, borderless: bool) -> Self { - let borders = if borderless { - [String::new(), String::new()] - } else { - let middle = "─".repeat(width + 2); - - [format!("┌{middle}┐"), format!("└{middle}┘")] - }; - - Self { - borders, - borderless, - width, - out: stdout(), - } - } - - /// Actually draws the window, with each element in `content` being on a new line. - pub fn draw(&mut self, content: Vec, space: bool) -> eyre::Result<(), UIError> { - let len: u16 = content.len().try_into()?; - - // Note that this will have a trailing newline, which we use later. - let menu: String = content.into_iter().fold(String::new(), |mut output, x| { - // Horizontal Padding & Border - let padding = if self.borderless { " " } else { "│" }; - let space = if space { - " ".repeat(self.width.saturating_sub(x.graphemes(true).count())) - } else { - String::new() - }; - write!(output, "{padding} {}{space} {padding}\r\n", x.reset()).unwrap(); - - output - }); - - // We're doing this because Windows is stupid and can't stand - // writing to the last line repeatedly. - #[cfg(windows)] - let (height, suffix) = (len + 2, "\r\n"); - #[cfg(not(windows))] - let (height, suffix) = (len + 1, ""); - - // There's no need for another newline after the main menu content, because it already has one. - let rendered = format!("{}\r\n{menu}{}{suffix}", self.borders[0], self.borders[1]); - - crossterm::execute!( - self.out, - Clear(ClearType::FromCursorDown), - MoveToColumn(0), - Print(rendered), - MoveToColumn(0), - MoveUp(height), - )?; - - Ok(()) - } -} - -/// The code for the terminal interface itself. -/// -/// * `minimalist` - All this does is hide the bottom control bar. -async fn interface( - player: Arc, - minimalist: bool, - borderless: bool, - debug: bool, - fps: u8, - width: usize, -) -> eyre::Result<(), UIError> { - let mut window = Window::new(width, borderless || debug); - - loop { - // Load `current` once so that it doesn't have to be loaded over and over - // again by different UI components. - let current = player.current.load(); - let current = current.as_ref(); - - let action = components::action(&player, current, width); - - let volume = player.sink.volume(); - let percentage = format!("{}%", (volume * 100.0).round().abs()); - - let timer = VOLUME_TIMER.load(Ordering::Relaxed); - let middle = match timer { - 0 => components::progress_bar(&player, current, width - 16), - _ => components::audio_bar(volume, &percentage, width - 17), - }; - - if timer > 0 && timer <= AUDIO_BAR_DURATION { - // We'll keep increasing the timer until it eventually hits `AUDIO_BAR_DURATION`. - VOLUME_TIMER.fetch_add(1, Ordering::Relaxed); - } else { - // If enough time has passed, we'll reset it back to 0. - VOLUME_TIMER.store(0, Ordering::Relaxed); - } - - let controls = components::controls(width); - - let menu = match (minimalist, debug, player.current.load().as_ref()) { - (true, _, _) => vec![action, middle], - (false, true, Some(x)) => vec![x.full_path.clone(), action, middle, controls], - _ => vec![action, middle, controls], - }; - - window.draw(menu, false)?; - - let delta = 1.0 / f32::from(fps); - sleep(Duration::from_secs_f32(delta)).await; - } -} - -/// Represents the terminal environment, and is used to properly -/// initialize and clean up the terminal. -pub struct Environment { - /// Whether keyboard enhancements are enabled. - enhancement: bool, - - /// Whether the terminal is in an alternate screen or not. - alternate: bool, -} - -impl Environment { - /// This prepares the terminal, returning an [Environment] helpful - /// for cleaning up afterwards. - pub fn ready(alternate: bool) -> eyre::Result { - let mut lock = stdout().lock(); - - crossterm::execute!(lock, Hide)?; - - if alternate { - crossterm::execute!(lock, EnterAlternateScreen, MoveTo(0, 0))?; - } - - terminal::enable_raw_mode()?; - let enhancement = terminal::supports_keyboard_enhancement()?; - - if enhancement { - crossterm::execute!( - lock, - PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) - )?; - } - - Ok(Self { - enhancement, - alternate, - }) - } - - /// Uses the information collected from initialization to safely close down - /// the terminal & restore it to it's previous state. - pub fn cleanup(&self) -> eyre::Result<(), UIError> { - let mut lock = stdout().lock(); - - if self.alternate { - crossterm::execute!(lock, LeaveAlternateScreen)?; - } - - crossterm::execute!(lock, Clear(ClearType::FromCursorDown), Show)?; - - if self.enhancement { - crossterm::execute!(lock, PopKeyboardEnhancementFlags)?; - } - - terminal::disable_raw_mode()?; - - eprintln!("bye! :)"); - - Ok(()) - } -} - -impl Drop for Environment { - /// Just a wrapper for [`Environment::cleanup`] which ignores any errors thrown. - fn drop(&mut self) { - // Well, we're dropping it, so it doesn't really matter if there's an error. - let _ = self.cleanup(); - } -} - -/// Initializes the UI, this will also start taking input from the user. -/// -/// `alternate` controls whether to use [`EnterAlternateScreen`] in order to hide -/// previous terminal history. -pub async fn start( - player: Arc, - sender: Sender, - args: Args, -) -> eyre::Result<(), UIError> { - let environment = Environment::ready(args.alternate)?; - - let interface = task::spawn(interface( - Arc::clone(&player), - args.minimalist, - args.borderless, - args.debug, - args.fps, - 21 + args.width.min(32) * 2, - )); - - input::listen(sender.clone()).await?; - interface.abort(); - - environment.cleanup()?; - - Ok(()) -} diff --git a/src/scrapers.rs b/src/scrapers.rs index 041a696..f051d13 100644 --- a/src/scrapers.rs +++ b/src/scrapers.rs @@ -1,3 +1,5 @@ +#![allow(clippy::all)] + use std::path::{Path, PathBuf}; use clap::ValueEnum; diff --git a/src/scrapers/archive.rs b/src/scrapers/archive.rs index 4348c0a..dd956d9 100644 --- a/src/scrapers/archive.rs +++ b/src/scrapers/archive.rs @@ -3,16 +3,16 @@ //! This command is completely optional, and as such isn't subject to the same //! quality standards as the rest of the codebase. +use std::sync::LazyLock; + use futures::{stream::FuturesOrdered, StreamExt}; -use lazy_static::lazy_static; use reqwest::Client; use scraper::{Html, Selector}; use crate::scrapers::{get, Source}; -lazy_static! { - static ref SELECTOR: Selector = Selector::parse("html > body > pre > a").unwrap(); -} +static SELECTOR: LazyLock = + LazyLock::new(|| Selector::parse("html > body > pre > a").unwrap()); async fn parse(client: &Client, path: &str) -> eyre::Result> { let document = get(client, path, super::Source::Lofigirl).await?; diff --git a/src/scrapers/chillhop.rs b/src/scrapers/chillhop.rs index 2dbb343..68e9e07 100644 --- a/src/scrapers/chillhop.rs +++ b/src/scrapers/chillhop.rs @@ -2,9 +2,8 @@ use eyre::eyre; use futures::stream::FuturesUnordered; use futures::StreamExt; use indicatif::ProgressBar; -use lazy_static::lazy_static; -use std::fmt; use std::str::FromStr; +use std::{fmt, sync::LazyLock}; use reqwest::Client; use scraper::{Html, Selector}; @@ -16,14 +15,13 @@ use tokio::fs; use crate::scrapers::{get, Source}; -lazy_static! { - static ref RELEASES: Selector = Selector::parse(".table-body > a").unwrap(); - static ref RELEASE_LABEL: Selector = Selector::parse("label").unwrap(); - // static ref RELEASE_DATE: Selector = Selector::parse(".release-feat-props > .text-xs").unwrap(); - // static ref RELEASE_NAME: Selector = Selector::parse(".release-feat-props > h2").unwrap(); - static ref RELEASE_AUTHOR: Selector = Selector::parse(".release-feat-props .artist-link").unwrap(); - static ref RELEASE_TEXTAREA: Selector = Selector::parse("textarea").unwrap(); -} +static RELEASES: LazyLock = LazyLock::new(|| Selector::parse(".table-body > a").unwrap()); +static RELEASE_LABEL: LazyLock = LazyLock::new(|| Selector::parse("label").unwrap()); +// static ref RELEASE_DATE: LazyLock = LazyLock::new(|| Selector::parse(".release-feat-props > .text-xs").unwrap()); +// static ref RELEASE_NAME: LazyLock = LazyLock::new(|| Selector::parse(".release-feat-props > h2").unwrap()); +// static RELEASE_AUTHOR: LazyLock = LazyLock::new(|| Selector::parse(".release-feat-props .artist-link").unwrap()); +static RELEASE_TEXTAREA: LazyLock = + LazyLock::new(|| Selector::parse("textarea").unwrap()); #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] diff --git a/src/scrapers/lofigirl.rs b/src/scrapers/lofigirl.rs index 41b40a8..a97706b 100644 --- a/src/scrapers/lofigirl.rs +++ b/src/scrapers/lofigirl.rs @@ -3,16 +3,16 @@ //! This command is completely optional, and as such isn't subject to the same //! quality standards as the rest of the codebase. +use std::sync::LazyLock; + use futures::{stream::FuturesOrdered, StreamExt}; -use lazy_static::lazy_static; use reqwest::Client; use scraper::{Html, Selector}; use crate::scrapers::get; -lazy_static! { - static ref SELECTOR: Selector = Selector::parse("html > body > pre > a").unwrap(); -} +static SELECTOR: LazyLock = + LazyLock::new(|| Selector::parse("html > body > pre > a").unwrap()); async fn parse(client: &Client, path: &str) -> eyre::Result> { let document = get(client, path, super::Source::Lofigirl).await?; diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..da313cd --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,5 @@ +#![allow(clippy::all, clippy::missing_docs_in_private_items)] + +mod bookmark; +mod tracks; +mod ui; diff --git a/src/tests/bookmark.rs b/src/tests/bookmark.rs new file mode 100644 index 0000000..8637b9a --- /dev/null +++ b/src/tests/bookmark.rs @@ -0,0 +1,58 @@ +#[cfg(test)] +mod bookmark { + use crate::{bookmark::Bookmarks, tracks::Info}; + + fn test_info(path: &str, display: &str) -> Info { + Info { + path: path.into(), + display: display.into(), + width: display.len(), + duration: None, + } + } + + #[test] + fn toggle_and_check() { + let mut bm = Bookmarks { entries: vec![] }; + let info = test_info("p.mp3", "Nice Track"); + + // initially not bookmarked + assert!(!bm.bookmarked(&info)); + + // bookmark it + let added = bm.bookmark(&info).unwrap(); + assert!(added); + assert!(bm.bookmarked(&info)); + + // un-bookmark it + let removed = bm.bookmark(&info).unwrap(); + assert!(!removed); + assert!(!bm.bookmarked(&info)); + } + + #[test] + fn multiple_bookmarks() { + let mut bm = Bookmarks { entries: vec![] }; + let info1 = test_info("track1.mp3", "Track One"); + let info2 = test_info("track2.mp3", "Track Two"); + + bm.bookmark(&info1).unwrap(); + bm.bookmark(&info2).unwrap(); + + assert!(bm.bookmarked(&info1)); + assert!(bm.bookmarked(&info2)); + assert_eq!(bm.entries.len(), 2); + } + + #[test] + fn duplicate_bookmark_removes() { + let mut bm = Bookmarks { entries: vec![] }; + let info = test_info("x.mp3", "X"); + + bm.bookmark(&info).unwrap(); + let is_added = bm.bookmark(&info).unwrap(); + + assert!(!is_added); + assert!(bm.entries.is_empty()); + } +} diff --git a/src/tests/tracks.rs b/src/tests/tracks.rs new file mode 100644 index 0000000..d783ae4 --- /dev/null +++ b/src/tests/tracks.rs @@ -0,0 +1,182 @@ +#[cfg(test)] +mod format { + use crate::tracks::format::name; + + #[test] + fn handles_all_numeric_name() { + let n = name("12345.mp3").unwrap(); + assert_eq!(n, "12345"); + } + + #[test] + fn decodes_url() { + let n = name("lofi%20track.mp3").unwrap(); + assert_eq!(n, "lofi track"); + } + + #[test] + fn handles_extension_only() { + let n = name(".mp3").unwrap(); + // Should handle edge case gracefully + assert!(!n.is_empty()); + } +} + +#[cfg(test)] +mod queued { + use crate::tracks::{format, Queued}; + use bytes::Bytes; + + #[test] + fn queued_uses_custom_display() { + let q = Queued::new( + "path/to/file.mp3".into(), + Bytes::from_static(b"abc"), + Some("Shown".into()), + ) + .unwrap(); + + assert_eq!(q.display, "Shown"); + assert_eq!(q.path, "path/to/file.mp3"); + } + + #[test] + fn queued_generates_display_if_none() { + let q = Queued::new( + "path/to/cool_track.mp3".into(), + Bytes::from_static(b"abc"), + None, + ) + .unwrap(); + + assert_eq!(q.display, format::name("path/to/cool_track.mp3").unwrap()); + } +} + +#[cfg(test)] +mod info { + use crate::tracks::Info; + use unicode_segmentation::UnicodeSegmentation; + + #[test] + fn to_entry_roundtrip() { + let info = Info { + path: "p.mp3".into(), + display: "Nice Track".into(), + width: 10, + duration: None, + }; + + assert_eq!(info.to_entry(), "p.mp3!Nice Track"); + } + + #[test] + fn width_counts_graphemes() { + // We cannot create a valid decoder for arbitrary bytes here, so test width through constructor logic directly. + let display = "a̐é"; // multiple-grapheme clusters + let width = display.graphemes(true).count(); + + let info = Info { + path: "x".into(), + display: display.into(), + width, + duration: None, + }; + + assert_eq!(info.width, width); + } +} + +#[cfg(test)] +mod decoded { + use crate::tracks::Queued; + use bytes::Bytes; + + #[tokio::test] + async fn decoded_fails_with_invalid_audio() { + let q = Queued::new( + "path.mp3".into(), + Bytes::from_static(b"not audio"), + Some("Name".into()), + ) + .unwrap(); + + let result = q.decode(); + assert!(result.is_err()); + } +} + +#[cfg(test)] +mod list { + use crate::{download::PROGRESS, tracks::List}; + use reqwest::Client; + + #[test] + fn base_works() { + let text = "http://base/\ntrack1\ntrack2"; + let list = List::new("test", text, None); + assert_eq!(list.header(), "http://base/"); + } + + #[test] + fn random_path_parses_custom_display() { + let text = "http://x/\npath!Display"; + let list = List::new("t", text, None); + + let (p, d) = list.random_path(); + assert_eq!(p, "path"); + assert_eq!(d, Some("Display".into())); + } + + #[test] + fn random_path_no_display() { + let text = "http://x/\ntrackA"; + let list = List::new("t", text, None); + + let (p, d) = list.random_path(); + assert_eq!(p, "trackA"); + assert!(d.is_none()); + } + + #[test] + fn new_trims_lines() { + let text = "base\na \nb "; + let list = List::new("name", text, None); + + assert_eq!(list.header(), "base"); + assert_eq!(list.lines[1], "a"); + assert_eq!(list.lines[2], "b"); + } + + #[test] + fn custom_display_with_exclamation() { + let text = "http://base/\nfile.mp3!My Custom Name"; + let list = List::new("t", text, None); + let (path, display) = list.random_path(); + assert_eq!(path, "file.mp3"); + assert_eq!(display, Some("My Custom Name".into())); + } + + #[test] + fn single_track() { + let text = "base\nonly_track.mp3"; + let list = List::new("name", text, None); + let (path, _) = list.random_path(); + assert_eq!(path, "only_track.mp3"); + } + + #[tokio::test] + async fn download() { + let text = "https://stream.chillhop.com/mp3/\n9476!Apple Juice"; + let list = List::new("name", text, None); + + let client = Client::new(); + let track = list.random(&client, &PROGRESS).await.unwrap(); + assert_eq!(track.display, "Apple Juice"); + assert_eq!(track.path, "https://stream.chillhop.com/mp3/9476"); + assert_eq!(track.data.len(), 3150424); + + let decoded = track.decode().unwrap(); + assert_eq!(decoded.info.duration.unwrap().as_secs(), 143); + } +} diff --git a/src/tests/ui.rs b/src/tests/ui.rs new file mode 100644 index 0000000..85d39e7 --- /dev/null +++ b/src/tests/ui.rs @@ -0,0 +1,251 @@ +/* The lowfi UI: +┌─────────────────────────────┐ +│ loading │ +│ [ ] 00:00/00:00 │ +│ [s]kip [p]ause [q]uit │ +└─────────────────────────────┘ +*/ + +#[cfg(test)] +mod components { + use crate::ui; + + use std::time::Duration; + + #[test] + fn format_duration_works() { + let d = Duration::from_secs(62); + assert_eq!(ui::components::format_duration(&d), "01:02"); + } + + #[test] + fn format_duration_zero() { + let d = Duration::from_secs(0); + assert_eq!(ui::components::format_duration(&d), "00:00"); + } + + #[test] + fn format_duration_hours_wrap() { + let d = Duration::from_secs(3661); // 1:01:01 + assert_eq!(ui::components::format_duration(&d), "61:01"); + } + + #[test] + fn audio_bar_contains_percentage() { + let s = ui::components::audio_bar(10, 0.5, "50%"); + assert!(s.contains("50%")); + assert!(s.starts_with(" volume:")); + } + + #[test] + fn audio_bar_muted_volume() { + let s = ui::components::audio_bar(8, 0.0, "0%"); + assert!(s.contains("0%")); + } + + #[test] + fn audio_bar_full_volume() { + let s = ui::components::audio_bar(10, 1.0, "100%"); + assert!(s.contains("100%")); + } + + #[test] + fn controls_has_items() { + let s = ui::components::controls(30); + assert!(s.contains("[s]")); + assert!(s.contains("[p]")); + assert!(s.contains("[q]")); + } +} + +#[cfg(test)] +mod window { + use crate::ui::window::Window; + + #[test] + fn new_border_strings() { + let w = Window::new(10, false); + assert!(w.borders[0].starts_with('┌')); + assert!(w.borders[1].starts_with('└')); + + let w2 = Window::new(5, true); + assert!(w2.borders[0].is_empty()); + assert!(w2.borders[1].is_empty()); + } + + fn sided(text: &str) -> String { + return format!("│ {text} │"); + } + + #[test] + fn simple() { + let w = Window::new(3, false); + let (render, height) = w.render(vec![String::from("abc")], false, true).unwrap(); + + const MIDDLE: &str = "─────"; + assert_eq!(format!("┌{MIDDLE}┐\n{}\n└{MIDDLE}┘", sided("abc")), render); + assert_eq!(height, 3); + } + + #[test] + fn spaced() { + let w = Window::new(3, false); + let (render, height) = w + .render( + vec![String::from("abc"), String::from(" b"), String::from("c")], + true, + true, + ) + .unwrap(); + + const MIDDLE: &str = "─────"; + assert_eq!( + format!( + "┌{MIDDLE}┐\n{}\n{}\n{}\n└{MIDDLE}┘", + sided("abc"), + sided(" b "), + sided("c "), + ), + render + ); + assert_eq!(height, 5); + } + + #[test] + fn zero_width_window() { + let w = Window::new(0, false); + assert!(!w.borders[0].is_empty()); + } +} + +#[cfg(test)] +mod interface { + use crossterm::style::Stylize; + use std::{sync::Arc, time::Duration}; + use tokio::time::Instant; + + use crate::{ + download::PROGRESS, + player::Current, + tracks, + ui::{ + interface::{self, Params}, + State, + }, + }; + + #[test] + fn loading() { + let sink = Arc::new(rodio::Sink::new().0); + let mut state = State::initial(sink, 3, String::from("test")); + let menu = interface::menu(&mut state, Params::default()); + + assert_eq!(menu[0], "loading "); + assert_eq!(menu[1], " [ ] 00:00/00:00 "); + assert_eq!( + menu[2], + format!( + "{}kip {}ause {}uit", + "[s]".bold(), + "[p]".bold(), + "[q]".bold() + ) + ); + } + + #[test] + fn volume() { + let sink = Arc::new(rodio::Sink::new().0); + sink.set_volume(0.5); + let mut state = State::initial(sink, 3, String::from("test")); + state.timer = Some(Instant::now()); + + let menu = interface::menu(&mut state, Params::default()); + + assert_eq!(menu[0], "loading "); + assert_eq!(menu[1], " volume: [///// ] 50% "); + assert_eq!( + menu[2], + format!( + "{}kip {}ause {}uit", + "[s]".bold(), + "[p]".bold(), + "[q]".bold() + ) + ); + } + + #[test] + fn progress() { + let sink = Arc::new(rodio::Sink::new().0); + PROGRESS.store(50, std::sync::atomic::Ordering::Relaxed); + let mut state = State::initial(sink, 3, String::from("test")); + state.current = Current::Loading(Some(&PROGRESS)); + + let menu = interface::menu(&mut state, Params::default()); + + assert_eq!(menu[0], format!("loading {} ", "50%".bold())); + assert_eq!(menu[1], " [ ] 00:00/00:00 "); + assert_eq!( + menu[2], + format!( + "{}kip {}ause {}uit", + "[s]".bold(), + "[p]".bold(), + "[q]".bold() + ) + ); + } + + #[test] + fn track() { + let sink = Arc::new(rodio::Sink::new().0); + let track = tracks::Info { + path: "/path".to_owned(), + display: "Test Track".to_owned(), + width: 4 + 1 + 5, + duration: Some(Duration::from_secs(8)), + }; + + let mut state = State::initial(sink, 3, String::from("test")); + state.current = Current::Track(track.clone()); + let menu = interface::menu(&mut state, Params::default()); + + assert_eq!( + menu[0], + format!("playing {} ", track.display.bold()) + ); + assert_eq!(menu[1], " [ ] 00:00/00:08 "); + assert_eq!( + menu[2], + format!( + "{}kip {}ause {}uit", + "[s]".bold(), + "[p]".bold(), + "[q]".bold() + ) + ); + } +} + +#[cfg(test)] +mod environment { + use crate::ui::Environment; + + #[test] + fn ready_and_cleanup_no_panic() { + // Try to create the environment but don't fail the test if the + // terminal isn't available. We just assert the API exists. + if let Ok(env) = Environment::ready(false) { + // cleanup should succeed + let _ = env.cleanup(true); + } + } + + #[test] + fn ready_with_alternate_screen() { + if let Ok(env) = Environment::ready(true) { + let _ = env.cleanup(false); + } + } +} diff --git a/src/tracks.rs b/src/tracks.rs index 7ecf6f8..c2fa33f 100644 --- a/src/tracks.rs +++ b/src/tracks.rs @@ -2,74 +2,76 @@ //! of tracks, as well as downloading them & finding new ones. //! //! There are several structs which represent the different stages -//! that go on in downloading and playing tracks. The proccess for fetching tracks, -//! and what structs are relevant in each step, are as follows. +//! that go on in downloading and playing tracks. When first queued, +//! the downloader will return a [`Queued`] track. //! -//! First Stage, when a track is initially fetched. -//! 1. Raw entry selected from track list. -//! 2. Raw entry split into path & display name. -//! 3. Track data fetched, and [`QueuedTrack`] is created which includes a [`TrackName`] that may be raw. -//! -//! Second Stage, when a track is played. -//! 1. Track data is decoded. -//! 2. [`Info`] created from decoded data. -//! 3. [`Decoded`] made from [`Info`] and the original decoded data. +//! Then, when it's time to play the track, it is decoded into +//! a [`Decoded`] track, which includes all the information +//! in the form of [`Info`]. -use std::{io::Cursor, path::Path, time::Duration}; +use std::{fmt::Debug, io::Cursor, time::Duration}; use bytes::Bytes; -use convert_case::{Case, Casing}; -use regex::Regex; use rodio::{Decoder, Source as _}; -use unicode_segmentation::UnicodeSegmentation; -use url::form_urlencoded; +use unicode_segmentation::UnicodeSegmentation as _; -pub mod error; pub mod list; +pub use list::List; +pub mod error; +pub mod format; +pub use error::{Error, Result}; -pub use error::Error; - -use crate::tracks::error::Context; -use lazy_static::lazy_static; +use crate::tracks::error::WithTrackContext as _; /// Just a shorthand for a decoded [Bytes]. pub type DecodedData = Decoder>; -/// Specifies a track's name, and specifically, -/// whether it has already been formatted or if it -/// is still in it's raw path form. -#[derive(Debug, Clone)] -pub enum TrackName { - /// Pulled straight from the list, - /// with no splitting done at all. - Raw(String), - - /// If a track has a custom specified name - /// in the list, then it should be defined with this variant. - Formatted(String), -} - /// Tracks which are still waiting in the queue, and can't be played yet. /// /// This means that only the data & track name are included. -pub struct QueuedTrack { - /// Name of the track, which may be raw. - pub name: TrackName, +#[derive(PartialEq, Eq)] +pub struct Queued { + /// Display name of the track. + pub display: String, /// Full downloadable path/url of the track. - pub full_path: String, + pub path: String, /// The raw data of the track, which is not decoded and /// therefore much more memory efficient. pub data: Bytes, } -impl QueuedTrack { +impl Debug for Queued { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Queued") + .field("display", &self.display) + .field("path", &self.path) + .field("data", &self.data.len()) + .finish() + } +} + +impl Queued { /// This will actually decode and format the track, /// returning a [`DecodedTrack`] which can be played /// and also has a duration & formatted name. - pub fn decode(self) -> eyre::Result { - DecodedTrack::new(self) + pub fn decode(self) -> Result { + Decoded::new(self) + } + + /// Creates a new queued track. + pub fn new(path: String, data: Bytes, display: Option) -> Result { + let display = match display { + None => self::format::name(&path)?, + Some(custom) => custom, + }; + + Ok(Self { + display, + path, + data, + }) } } @@ -80,13 +82,10 @@ impl QueuedTrack { #[derive(Debug, Eq, PartialEq, Clone)] pub struct Info { /// The full downloadable path/url of the track. - pub full_path: String, - - /// Whether the track entry included a custom name, or not. - pub custom_name: bool, + pub path: String, /// This is a formatted name, so it doesn't include the full path. - pub display_name: String, + pub display: String, /// This is the *actual* terminal width of the track name, used to make /// the UI consistent. @@ -97,128 +96,30 @@ pub struct Info { pub duration: Option, } -lazy_static! { - static ref MASTER_PATTERNS: [Regex; 5] = [ - // (master), (master v2) - Regex::new(r"\s*\(.*?master(?:\s*v?\d+)?\)$").unwrap(), - // mstr or - mstr or (mstr) — now also matches "mstr v3", "mstr2", etc. - Regex::new(r"\s*[-(]?\s*mstr(?:\s*v?\d+)?\s*\)?$").unwrap(), - // - master, master at end without parentheses - Regex::new(r"\s*[-]?\s*master(?:\s*v?\d+)?$").unwrap(), - // kupla master1, kupla master v2 (without parentheses or separator) - Regex::new(r"\s+kupla\s+master(?:\s*v?\d+|\d+)?$").unwrap(), - // (kupla master) followed by trailing parenthetical numbers, e.g. "... (kupla master) (1)" - Regex::new(r"\s*\(.*?master(?:\s*v?\d+)?\)(?:\s*\(\d+\))+$").unwrap(), - ]; - static ref ID_PATTERN: Regex = Regex::new(r"^[a-z]\d[ .]").unwrap(); -} - impl Info { /// Converts the info back into a full track list entry. pub fn to_entry(&self) -> String { - let mut entry = self.full_path.clone(); - - if self.custom_name { - entry.push('!'); - entry.push_str(&self.display_name); - } + let mut entry = self.path.clone(); + entry.push('!'); + entry.push_str(&self.display); entry } - /// Decodes a URL string into normal UTF-8. - fn decode_url(text: &str) -> String { - // The tuple contains smart pointers, so it's not really practical to use `into()`. - #[allow(clippy::tuple_array_conversions)] - form_urlencoded::parse(text.as_bytes()) - .map(|(key, val)| [key, val].concat()) - .collect() - } - - /// Formats a name with [`convert_case`]. - /// - /// This will also strip the first few numbers that are - /// usually present on most lofi tracks and do some other - /// formatting operations. - fn format_name(name: &str) -> eyre::Result { - let path = Path::new(name); - - let name = path - .file_stem() - .and_then(|x| x.to_str()) - .ok_or((name, error::Kind::InvalidName))?; - - let name = Self::decode_url(name).to_lowercase(); - let mut name = name - .replace("masster", "master") - .replace("(online-audio-converter.com)", "") // Some of these names, man... - .replace('_', " "); - - // Get rid of "master" suffix with a few regex patterns. - for regex in MASTER_PATTERNS.iter() { - name = regex.replace(&name, "").to_string(); - } - - name = ID_PATTERN.replace(&name, "").to_string(); - - let name = name - .replace("13lufs", "") - .to_case(Case::Title) - .replace(" .", "") - .replace(" Ft ", " ft. ") - .replace("Ft.", "ft.") - .replace("Feat.", "ft.") - .replace(" W ", " w/ "); - - // This is incremented for each digit in front of the song name. - let mut skip = 0; - - for character in name.as_bytes() { - if character.is_ascii_digit() - || *character == b'.' - || *character == b')' - || *character == b'(' - { - skip += 1; - } else { - break; - } - } - - // If the entire name of the track is a number, then just return it. - if skip == name.len() { - Ok(name.trim().to_string()) - } else { - // We've already checked before that the bound is at an ASCII digit. - #[allow(clippy::string_slice)] - Ok(String::from(name[skip..].trim())) - } - } - - /// Creates a new [`TrackInfo`] from a possibly raw name & decoded data. - pub fn new( - name: TrackName, - full_path: String, - decoded: &DecodedData, - ) -> eyre::Result { - let (display_name, custom_name) = match name { - TrackName::Raw(raw) => (Self::format_name(&raw)?, false), - TrackName::Formatted(custom) => (custom, true), - }; - + /// Creates a new [`Info`] from decoded data & the queued track. + pub fn new(decoded: &DecodedData, path: String, display: String) -> Result { Ok(Self { duration: decoded.total_duration(), - width: display_name.graphemes(true).count(), - full_path, - custom_name, - display_name, + width: display.graphemes(true).count(), + path, + display, }) } } -/// This struct is seperate from [Track] since it is generated lazily from +/// This struct is separate from [Track] since it is generated lazily from /// a track, and not when the track is first downloaded. -pub struct DecodedTrack { +pub struct Decoded { /// Has both the formatted name and some information from the decoded data. pub info: Info, @@ -226,18 +127,18 @@ pub struct DecodedTrack { pub data: DecodedData, } -impl DecodedTrack { +impl Decoded { /// Creates a new track. /// This is equivalent to [`QueuedTrack::decode`]. - pub fn new(track: QueuedTrack) -> eyre::Result { + pub fn new(track: Queued) -> Result { + let (path, display) = (track.path.clone(), track.display.clone()); let data = Decoder::builder() - .with_byte_len(track.data.len().try_into().unwrap()) + .with_byte_len(track.data.len().try_into()?) .with_data(Cursor::new(track.data)) .build() - .track(track.full_path.clone())?; - - let info = Info::new(track.name, track.full_path, &data)?; + .track(track.display)?; + let info = Info::new(&data, path, display)?; Ok(Self { info, data }) } } diff --git a/src/tracks/error.rs b/src/tracks/error.rs index 49a1e3c..4e712d4 100644 --- a/src/tracks/error.rs +++ b/src/tracks/error.rs @@ -1,3 +1,5 @@ +pub type Result = std::result::Result; + #[derive(Debug, thiserror::Error)] pub enum Kind { #[error("unable to decode: {0}")] @@ -17,19 +19,20 @@ pub enum Kind { #[error("unable to fetch data: {0}")] Request(#[from] reqwest::Error), + + #[error("couldn't handle integer track length: {0}")] + Integer(#[from] std::num::TryFromIntError), } #[derive(Debug, thiserror::Error)] -#[error("{kind} (track: {track})")] +#[error("{kind} (track: {track:?})")] pub struct Error { - pub track: String, - - #[source] + pub track: Option, pub kind: Kind, } impl Error { - pub fn is_timeout(&self) -> bool { + pub fn timeout(&self) -> bool { if let Kind::Request(x) = &self.kind { x.is_timeout() } else { @@ -45,22 +48,34 @@ where { fn from((track, err): (T, E)) -> Self { Self { - track: track.into(), + track: Some(track.into()), kind: Kind::from(err), } } } -pub trait Context { - fn track(self, name: impl Into) -> Result; +impl From for Error +where + Kind: From, +{ + fn from(err: E) -> Self { + Self { + track: None, + kind: Kind::from(err), + } + } } -impl Context for Result +pub trait WithTrackContext { + fn track(self, name: impl Into) -> Result; +} + +impl WithTrackContext for std::result::Result where (String, E): Into, E: Into, { - fn track(self, name: impl Into) -> Result { + fn track(self, name: impl Into) -> std::result::Result { self.map_err(|e| { let error = match e.into() { Kind::Request(e) => Kind::Request(e.without_url()), diff --git a/src/tracks/format.rs b/src/tracks/format.rs new file mode 100644 index 0000000..f59b223 --- /dev/null +++ b/src/tracks/format.rs @@ -0,0 +1,54 @@ +use std::path::Path; + +use super::error::WithTrackContext as _; +use url::form_urlencoded; + +/// Decodes a URL string into normal UTF-8. +fn decode_url(text: &str) -> String { + // The tuple contains smart pointers, so it's not really practical to use `into()`. + #[allow(clippy::tuple_array_conversions)] + form_urlencoded::parse(text.as_bytes()) + .map(|(key, val)| [key, val].concat()) + .collect() +} + +/// Formats a name with [`convert_case`]. +/// +/// This will also strip the first few numbers that are +/// usually present on most lofi tracks and do some other +/// formatting operations. +pub fn name(name: &str) -> super::Result { + let path = Path::new(name); + + let name = path + .file_stem() + .and_then(|x| x.to_str()) + .ok_or(super::error::Kind::InvalidName) + .track(name)?; + + let name = decode_url(name); + + // This is incremented for each digit in front of the song name. + let mut skip = 0; + + for character in name.as_bytes() { + if character.is_ascii_digit() + || *character == b'.' + || *character == b')' + || *character == b'(' + { + skip += 1; + } else { + break; + } + } + + // If the entire name of the track is a number, then just return it. + if skip == name.len() { + Ok(name.trim().to_owned()) + } else { + // We've already checked before that the bound is at an ASCII digit. + #[allow(clippy::string_slice)] + Ok(String::from(name[skip..].trim())) + } +} diff --git a/src/tracks/list.rs b/src/tracks/list.rs index e62b7c7..700e7f7 100644 --- a/src/tracks/list.rs +++ b/src/tracks/list.rs @@ -1,21 +1,25 @@ //! The module containing all of the logic behind track lists, //! as well as obtaining track names & downloading the raw audio data -use std::{cmp::min, sync::atomic::Ordering}; +use std::{ + cmp::min, + sync::atomic::{AtomicU8, Ordering}, +}; -use atomic_float::AtomicF32; -use bytes::{BufMut, Bytes, BytesMut}; -use eyre::OptionExt as _; -use futures::StreamExt; +use bytes::{BufMut as _, Bytes, BytesMut}; +use futures::StreamExt as _; use reqwest::Client; use tokio::fs; use crate::{ data_dir, - tracks::{self, error::Context}, + tracks::{ + self, + error::{self, WithTrackContext as _}, + }, }; -use super::QueuedTrack; +use super::Queued; /// Represents a list of tracks that can be played. /// @@ -28,7 +32,7 @@ pub struct List { /// Just the raw file, but seperated by `/n` (newlines). /// `lines[0]` is the base/heaeder, with the rest being tracks. - lines: Vec, + pub lines: Vec, /// The file path which the list was read from. #[allow(dead_code)] @@ -37,7 +41,7 @@ pub struct List { impl List { /// Gets the base URL of the [List]. - pub fn base(&self) -> &str { + pub fn header(&self) -> &str { self.lines[0].trim() } @@ -45,7 +49,7 @@ impl List { /// /// The second value in the tuple specifies whether the /// track has a custom display name. - fn random_path(&self) -> (String, Option) { + pub fn random_path(&self) -> (String, Option) { // We're getting from 1 here, since the base is at `self.lines[0]`. // // We're also not pre-trimming `self.lines` into `base` & `tracks` due to @@ -62,85 +66,75 @@ impl List { } /// Downloads a raw track, but doesn't decode it. - async fn download( + pub(crate) async fn download( &self, track: &str, client: &Client, - progress: Option<&AtomicF32>, - ) -> Result<(Bytes, String), tracks::Error> { + progress: Option<&AtomicU8>, + ) -> tracks::Result<(Bytes, String)> { // If the track has a protocol, then we should ignore the base for it. - let full_path = if track.contains("://") { + let path = if track.contains("://") { track.to_owned() } else { - format!("{}{}", self.base(), track) + format!("{}{}", self.header(), track) }; - let data: Bytes = if let Some(x) = full_path.strip_prefix("file://") { + let data: Bytes = if let Some(x) = path.strip_prefix("file://") { let path = if x.starts_with('~') { - let home_path = - dirs::home_dir().ok_or((track, tracks::error::Kind::InvalidPath))?; + let home_path = dirs::home_dir() + .ok_or(error::Kind::InvalidPath) + .track(track)?; let home = home_path .to_str() - .ok_or((track, tracks::error::Kind::InvalidPath))?; + .ok_or(error::Kind::InvalidPath) + .track(track)?; x.replace('~', home) } else { x.to_owned() }; - let result = tokio::fs::read(path.clone()).await.track(track)?; + let result = tokio::fs::read(path.clone()).await.track(x)?; result.into() } else { - let response = client.get(full_path.clone()).send().await.track(track)?; + let response = client.get(path.clone()).send().await.track(track)?; + let Some(progress) = progress else { + let bytes = response.bytes().await.track(track)?; + return Ok((bytes, path)); + }; - if let Some(progress) = progress { - let total = response - .content_length() - .ok_or((track, tracks::error::Kind::UnknownLength))?; - let mut stream = response.bytes_stream(); - let mut bytes = BytesMut::new(); - let mut downloaded: u64 = 0; + let total = response + .content_length() + .ok_or(error::Kind::UnknownLength) + .track(track)?; + let mut stream = response.bytes_stream(); + let mut bytes = BytesMut::new(); + let mut downloaded: u64 = 0; - while let Some(item) = stream.next().await { - let chunk = item.track(track)?; - let new = min(downloaded + (chunk.len() as u64), total); - downloaded = new; - progress.store((new as f32) / (total as f32), Ordering::Relaxed); + while let Some(item) = stream.next().await { + let chunk = item.track(track)?; + downloaded = min(downloaded + (chunk.len() as u64), total); + let rounded = ((downloaded as f64) / (total as f64) * 100.0).round() as u8; + progress.store(rounded, Ordering::Relaxed); - bytes.put(chunk); - } - - bytes.into() - } else { - response.bytes().await.track(track)? + bytes.put(chunk); } + + bytes.into() }; - Ok((data, full_path)) + Ok((data, path)) } /// Fetches and downloads a random track from the [List]. /// /// The Result's error is a bool, which is true if a timeout error occured, /// and false otherwise. This tells lowfi if it shouldn't wait to try again. - pub async fn random( - &self, - client: &Client, - progress: Option<&AtomicF32>, - ) -> Result { - let (path, custom_name) = self.random_path(); - let (data, full_path) = self.download(&path, client, progress).await?; + pub async fn random(&self, client: &Client, progress: &AtomicU8) -> tracks::Result { + let (path, display) = self.random_path(); + let (data, path) = self.download(&path, client, Some(progress)).await?; - let name = custom_name.map_or_else( - || super::TrackName::Raw(path.clone()), - super::TrackName::Formatted, - ); - - Ok(QueuedTrack { - name, - full_path, - data, - }) + Queued::new(path, data, display) } /// Parses text into a [List]. @@ -159,31 +153,34 @@ impl List { } /// Reads a [List] from the filesystem using the CLI argument provided. - pub async fn load(tracks: Option<&String>) -> eyre::Result { - if let Some(arg) = tracks { - // Check if the track is in ~/.local/share/lowfi, in which case we'll load that. - let path = data_dir()?.join(format!("{arg}.txt")); - let path = if path.exists() { path } else { arg.into() }; - - let raw = fs::read_to_string(path.clone()).await?; - - // Get rid of special noheader case for tracklists without a header. - let raw = raw - .strip_prefix("noheader") - .map_or(raw.as_ref(), |stripped| stripped); - - let name = path - .file_stem() - .and_then(|x| x.to_str()) - .ok_or_eyre("invalid track path")?; - - Ok(Self::new(name, raw, path.to_str())) - } else { - Ok(Self::new( + pub async fn load(tracks: &str) -> tracks::Result { + if tracks == "chillhop" { + return Ok(Self::new( "chillhop", include_str!("../../data/chillhop.txt"), None, - )) + )); } + + // Check if the track is in ~/.local/share/lowfi, in which case we'll load that. + let path = data_dir() + .map_err(|_| error::Kind::InvalidPath)? + .join(format!("{tracks}.txt")); + let path = if path.exists() { path } else { tracks.into() }; + + let raw = fs::read_to_string(path.clone()).await?; + + // Get rid of special noheader case for tracklists without a header. + let raw = raw + .strip_prefix("noheader") + .map_or_else(|| raw.as_ref(), |stripped| stripped); + + let name = path + .file_stem() + .and_then(|x| x.to_str()) + .ok_or(tracks::error::Kind::InvalidName) + .track(tracks)?; + + Ok(Self::new(name, raw, path.to_str())) } } diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..d0fb7da --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,192 @@ +use std::sync::Arc; + +use crate::{ + player::Current, + ui::{self, window::Window}, + Args, +}; +use tokio::{ + sync::{broadcast, mpsc::Sender}, + task::JoinHandle, + time::Instant, +}; +pub mod components; +pub mod environment; +pub use environment::Environment; +pub mod input; +pub mod interface; +pub mod window; + +#[cfg(feature = "mpris")] +pub mod mpris; + +/// Shorthand for a [`Result`] with a [`ui::Error`]. +type Result = std::result::Result; + +/// The error type for the UI, which is used to handle errors +/// that occur while drawing the UI or handling input. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("unable to convert number: {0}")] + Conversion(#[from] std::num::TryFromIntError), + + #[error("unable to write output: {0}")] + Write(#[from] std::io::Error), + + #[error("sending message to backend from ui failed: {0}")] + CrateSend(#[from] tokio::sync::mpsc::error::SendError), + + #[error("sharing state between backend and frontend failed: {0}")] + Send(#[from] tokio::sync::broadcast::error::SendError), + + #[cfg(feature = "mpris")] + #[error("mpris bus error: {0}")] + ZBus(#[from] mpris_server::zbus::Error), + + #[cfg(feature = "mpris")] + #[error("mpris fdo (zbus interface) error: {0}")] + Fdo(#[from] mpris_server::zbus::fdo::Error), +} + +/// The UI state, which is all of the information that +/// the user interface needs to display to the user. +/// +/// It should be noted that this is also used by MPRIS to keep +/// track of state. +#[derive(Clone)] +pub struct State { + /// The audio sink. + pub sink: Arc, + + /// The current track, which is updated by way of an [`Update`]. + pub current: Current, + + /// Whether the current track is bookmarked. + pub bookmarked: bool, + + /// The timer, which is used when the user changes volume to briefly display it. + pub(crate) timer: Option, + + /// The full inner width of the terminal window. + pub(crate) width: usize, + + /// The name of the playing tracklist, for MPRIS. + #[allow(dead_code)] + list: String, +} + +impl State { + /// Creates an initial UI state. + pub fn initial(sink: Arc, width: usize, list: String) -> Self { + let width = 21 + width.min(32) * 2; + Self { + width, + sink, + list, + current: Current::default(), + bookmarked: false, + timer: None, + } + } +} + +/// A UI update sent out by the main player thread, which may +/// not be immediately applied by the UI. +/// +/// This corresponds to user actions, like bookmarking a track, +/// skipping, or changing the volume. The difference is that it also +/// contains the new information about the track. +#[derive(Debug, Clone)] +pub enum Update { + Track(Current), + Bookmarked(bool), + Volume, + Quit, +} + +/// Just a simple wrapper for the two primary tasks that the UI +/// requires to function. +#[derive(Debug)] +struct Tasks { + /// The renderer, responsible for sending output to `stdout`. + render: JoinHandle>, + + /// The input, which receives data from `stdin` via [`crossterm`]. + input: JoinHandle>, +} + +/// The UI handle for controlling the state of the UI, as well as +/// updating MPRIS information and other small interfacing tasks. +pub struct Handle { + /// The terminal environment, which can be used for cleanup. + pub(crate) environment: Environment, + + /// The MPRIS server, which is more or less a handle to the actual MPRIS thread. + #[cfg(feature = "mpris")] + pub mpris: mpris::Server, + + /// The UI's running tasks. + tasks: Tasks, +} + +impl Drop for Handle { + fn drop(&mut self) { + self.tasks.input.abort(); + self.tasks.render.abort(); + } +} + +impl Handle { + /// The main UI process, which will both render the UI to the terminal + /// and also update state. + /// + /// It does both of these things at a fixed interval, due to things + /// like the track duration changing too frequently. + /// + /// `rx` is the receiver for state updates, `state` the initial state, + /// and `params` specifies aesthetic options that are specified by the user. + async fn ui( + mut rx: broadcast::Receiver, + mut state: State, + params: interface::Params, + ) -> Result<()> { + let mut interval = tokio::time::interval(params.delta); + let mut window = Window::new(state.width, params.borderless); + + loop { + if let Ok(message) = rx.try_recv() { + match message { + Update::Track(track) => state.current = track, + Update::Bookmarked(bookmarked) => state.bookmarked = bookmarked, + Update::Volume => state.timer = Some(Instant::now()), + Update::Quit => break, + } + } + + interface::draw(&mut state, &mut window, params)?; + interval.tick().await; + } + + Ok(()) + } + + /// Initializes the UI itself, along with all of the tasks that are related to it. + #[allow(clippy::unused_async)] + pub async fn init( + tx: Sender, + updater: broadcast::Receiver, + state: State, + args: &Args, + ) -> Result { + let environment = Environment::ready(args.alternate)?; + Ok(Self { + #[cfg(feature = "mpris")] + mpris: mpris::Server::new(state.clone(), tx.clone(), updater.resubscribe()).await?, + environment, + tasks: Tasks { + render: tokio::spawn(Self::ui(updater, state, interface::Params::from(args))), + input: tokio::spawn(input::listen(tx)), + }, + }) + } +} diff --git a/src/player/ui/components.rs b/src/ui/components.rs similarity index 70% rename from src/player/ui/components.rs rename to src/ui/components.rs index 6873850..7ce747d 100644 --- a/src/player/ui/components.rs +++ b/src/ui/components.rs @@ -1,12 +1,12 @@ //! Various different individual components that //! appear in lowfi's UI, like the progress bar. -use std::{ops::Deref as _, sync::Arc, time::Duration}; +use std::time::Duration; use crossterm::style::Stylize as _; use unicode_segmentation::UnicodeSegmentation as _; -use crate::{player::Player, tracks::Info}; +use crate::{player::Current, tracks, ui}; /// Small helper function to format durations. pub fn format_duration(duration: &Duration) -> String { @@ -17,23 +17,23 @@ pub fn format_duration(duration: &Duration) -> String { } /// Creates the progress bar, as well as all the padding needed. -pub fn progress_bar(player: &Player, current: Option<&Arc>, width: usize) -> String { +pub fn progress_bar(state: &ui::State, width: usize) -> String { let mut duration = Duration::new(0, 0); - let elapsed = if current.is_some() { - player.sink.get_pos() + let elapsed = if matches!(&state.current, Current::Track(_)) { + state.sink.get_pos() } else { Duration::new(0, 0) }; let mut filled = 0; - if let Some(current) = current { + if let Current::Track(current) = &state.current { if let Some(x) = current.duration { duration = x; let elapsed = elapsed.as_secs() as f32 / duration.as_secs() as f32; filled = (elapsed * width as f32).round() as usize; } - }; + } format!( " [{}{}] {}/{} ", @@ -45,7 +45,7 @@ pub fn progress_bar(player: &Player, current: Option<&Arc>, width: usize) } /// Creates the audio bar, as well as all the padding needed. -pub fn audio_bar(volume: f32, percentage: &str, width: usize) -> String { +pub fn audio_bar(width: usize, volume: f32, percentage: &str) -> String { let audio = (volume * width as f32).round() as usize; format!( @@ -60,13 +60,13 @@ pub fn audio_bar(volume: f32, percentage: &str, width: usize) -> String { /// This represents the main "action" bars state. enum ActionBar { /// When the app is paused. - Paused(Info), + Paused(tracks::Info), /// When the app is playing. - Playing(Info), + Playing(tracks::Info), /// When the app is loading. - Loading(f32), + Loading(Option), /// When the app is muted. Muted, @@ -77,12 +77,15 @@ impl ActionBar { /// The second value is the character length of the result. fn format(&self, star: bool) -> (String, usize) { let (word, subject) = match self { - Self::Playing(x) => ("playing", Some((x.display_name.clone(), x.width))), - Self::Paused(x) => ("paused", Some((x.display_name.clone(), x.width))), + Self::Playing(x) => ("playing", Some((x.display.clone(), x.width))), + Self::Paused(x) => ("paused", Some((x.display.clone(), x.width))), Self::Loading(progress) => { - let progress = format!("{: <2.0}%", (progress * 100.0).min(99.0)); + let progress = match *progress { + None | Some(0) => None, + Some(progress) => Some((format!("{: <2.0}%", progress.min(99)), 3)), + }; - ("loading", Some((progress, 3))) + ("loading", progress) } Self::Muted => { let msg = "+ to increase volume"; @@ -105,26 +108,23 @@ impl ActionBar { /// Creates the top/action bar, which has the name of the track and it's status. /// This also creates all the needed padding. -pub fn action(player: &Player, current: Option<&Arc>, width: usize) -> String { - let (main, len) = current - .map_or_else( - || ActionBar::Loading(player.progress.load(std::sync::atomic::Ordering::Acquire)), - |info| { - let info = info.deref().clone(); - - if player.sink.volume() < 0.01 { - return ActionBar::Muted; - } - - if player.sink.is_paused() { - ActionBar::Paused(info) - } else { - ActionBar::Playing(info) - } - }, - ) - .format(player.bookmarks.bookmarked()); +pub fn action(state: &ui::State, width: usize) -> String { + let action = match state.current.clone() { + Current::Loading(progress) => { + ActionBar::Loading(progress.map(|x| x.load(std::sync::atomic::Ordering::Relaxed))) + } + Current::Track(info) => { + if state.sink.volume() < 0.01 { + ActionBar::Muted + } else if state.sink.is_paused() { + ActionBar::Paused(info) + } else { + ActionBar::Playing(info) + } + } + }; + let (main, len) = action.format(state.bookmarked); if len > width { let chopped: String = main.graphemes(true).take(width + 1).collect(); diff --git a/src/ui/environment.rs b/src/ui/environment.rs new file mode 100644 index 0000000..a03bafe --- /dev/null +++ b/src/ui/environment.rs @@ -0,0 +1,76 @@ +use std::{io::stdout, panic}; + +use crossterm::{ + cursor::{Hide, MoveTo, Show}, + event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, + terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, +}; + +/// Represents the terminal environment, and is used to properly +/// initialize and clean up the terminal. +#[derive(Debug, Clone, Copy)] +pub struct Environment { + /// Whether keyboard enhancements are enabled. + enhancement: bool, + + /// Whether the terminal is in an alternate screen or not. + alternate: bool, +} + +impl Environment { + /// This prepares the terminal, returning an [Environment] helpful + /// for cleaning up afterwards. + pub fn ready(alternate: bool) -> super::Result { + let mut lock = stdout().lock(); + + crossterm::execute!(lock, Hide)?; + if alternate { + crossterm::execute!(lock, EnterAlternateScreen, MoveTo(0, 0))?; + } + + terminal::enable_raw_mode()?; + + let enhancement = terminal::supports_keyboard_enhancement().unwrap_or_default(); + if enhancement { + crossterm::execute!( + lock, + PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) + )?; + } + + let environment = Self { + enhancement, + alternate, + }; + + panic::set_hook(Box::new(move |info| { + let _ = environment.cleanup(false); + eprintln!("panic: {info}"); + })); + + Ok(environment) + } + + /// Uses the information collected from initialization to safely close down + /// the terminal & restore it to it's previous state. + pub fn cleanup(&self, elegant: bool) -> super::Result<()> { + let mut lock = stdout().lock(); + + if self.alternate { + crossterm::execute!(lock, LeaveAlternateScreen)?; + } + + crossterm::execute!(lock, Clear(ClearType::FromCursorDown), Show)?; + + if self.enhancement { + crossterm::execute!(lock, PopKeyboardEnhancementFlags)?; + } + + terminal::disable_raw_mode()?; + if elegant { + eprintln!("bye! :)"); + } + + Ok(()) + } +} diff --git a/src/player/ui/input.rs b/src/ui/input.rs similarity index 86% rename from src/player/ui/input.rs rename to src/ui/input.rs index 0177406..107fd6f 100644 --- a/src/player/ui/input.rs +++ b/src/ui/input.rs @@ -1,17 +1,13 @@ -//! Responsible for specifically recieving terminal input +//! Responsible for specifically receiving terminal input //! using [`crossterm`]. +use crate::Message; use crossterm::event::{self, EventStream, KeyCode, KeyEventKind, KeyModifiers}; use futures::{FutureExt as _, StreamExt as _}; use tokio::sync::mpsc::Sender; -use crate::player::{ - ui::{self, UIError}, - Message, -}; - -/// Starts the listener to recieve input from the terminal for various events. -pub async fn listen(sender: Sender) -> eyre::Result<(), UIError> { +/// Starts the listener to receive input from the terminal for various events. +pub async fn listen(sender: Sender) -> super::Result<()> { let mut reader = EventStream::new(); loop { @@ -66,10 +62,6 @@ pub async fn listen(sender: Sender) -> eyre::Result<(), UIError> { _ => continue, }; - if let Message::ChangeVolume(_) = messages { - ui::flash_audio(); - } - sender.send(messages).await?; } } diff --git a/src/ui/interface.rs b/src/ui/interface.rs new file mode 100644 index 0000000..6114e4f --- /dev/null +++ b/src/ui/interface.rs @@ -0,0 +1,64 @@ +use std::time::Duration; + +use crate::{ + ui::{self, components, window::Window}, + Args, +}; + +#[derive(Copy, Clone, Debug, Default)] +pub struct Params { + pub borderless: bool, + pub minimalist: bool, + pub delta: Duration, +} + +impl From<&Args> for Params { + fn from(args: &Args) -> Self { + let delta = 1.0 / f32::from(args.fps); + let delta = Duration::from_secs_f32(delta); + + Self { + delta, + minimalist: args.minimalist, + borderless: args.borderless, + } + } +} + +/// Creates a full "menu" from the [`ui::State`], which can be +/// easily put into a window for display. +/// +/// The menu really is just a [`Vec`] of the different components, +/// with padding already added. +pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec { + let action = components::action(state, state.width); + + let middle = match state.timer { + Some(timer) => { + let volume = state.sink.volume(); + let percentage = format!("{}%", (volume * 100.0).round().abs()); + if timer.elapsed() > Duration::from_secs(1) { + state.timer = None; + } + + components::audio_bar(state.width - 17, volume, &percentage) + } + None => components::progress_bar(state, state.width - 16), + }; + + let controls = components::controls(state.width); + if params.minimalist { + vec![action, middle] + } else { + vec![action, middle, controls] + } +} + +/// The code for the terminal interface itself. +/// +/// * `minimalist` - All this does is hide the bottom control bar. +pub fn draw(state: &mut ui::State, window: &mut Window, params: Params) -> super::Result<()> { + let menu = menu(state, params); + window.draw(menu, false)?; + Ok(()) +} diff --git a/src/player/mpris.rs b/src/ui/mpris.rs similarity index 55% rename from src/player/mpris.rs rename to src/ui/mpris.rs index d260d72..761b461 100644 --- a/src/player/mpris.rs +++ b/src/ui/mpris.rs @@ -1,27 +1,62 @@ //! Contains the code for the MPRIS server & other helper functions. -use std::{env, process, sync::Arc}; +use std::{ + env, + hash::{DefaultHasher, Hash, Hasher}, + process, + sync::Arc, +}; +use arc_swap::ArcSwap; use mpris_server::{ zbus::{self, fdo, Result}, LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface, Time, TrackId, Volume, }; -use tokio::sync::mpsc::Sender; +use rodio::Sink; +use tokio::sync::{broadcast, mpsc}; -use super::ui; -use super::Message; +use crate::{player::Current, ui::Update}; +use crate::{ui, Message}; const ERROR: fdo::Error = fdo::Error::Failed(String::new()); +struct Sender { + inner: mpsc::Sender, +} + +impl Sender { + pub fn new(inner: mpsc::Sender) -> Self { + Self { inner } + } + + pub async fn send(&self, message: Message) -> fdo::Result<()> { + self.inner + .send(message) + .await + .map_err(|x| fdo::Error::Failed(x.to_string())) + } + + pub async fn zbus(&self, message: Message) -> zbus::Result<()> { + self.inner + .send(message) + .await + .map_err(|x| zbus::Error::Failure(x.to_string())) + } +} + +impl Into for crate::Error { + fn into(self) -> fdo::Error { + fdo::Error::Failed(self.to_string()) + } +} + /// The actual MPRIS player. pub struct Player { - /// A reference to the [`super::Player`] itself. - pub player: Arc, - - /// The audio server sender, which is used to communicate with - /// the audio sender for skips and a few other inputs. - pub sender: Sender, + sink: Arc, + current: ArcSwap, + list: String, + sender: Sender, } impl RootInterface for Player { @@ -30,10 +65,7 @@ impl RootInterface for Player { } async fn quit(&self) -> fdo::Result<()> { - self.sender - .send(Message::Quit) - .await - .map_err(|_error| ERROR) + self.sender.send(Message::Quit).await } async fn can_quit(&self) -> fdo::Result { @@ -79,10 +111,7 @@ impl RootInterface for Player { impl PlayerInterface for Player { async fn next(&self) -> fdo::Result<()> { - self.sender - .send(Message::Next) - .await - .map_err(|_error| ERROR) + self.sender.send(Message::Next).await } async fn previous(&self) -> fdo::Result<()> { @@ -90,17 +119,11 @@ impl PlayerInterface for Player { } async fn pause(&self) -> fdo::Result<()> { - self.sender - .send(Message::Pause) - .await - .map_err(|_error| ERROR) + self.sender.send(Message::Pause).await } async fn play_pause(&self) -> fdo::Result<()> { - self.sender - .send(Message::PlayPause) - .await - .map_err(|_error| ERROR) + self.sender.send(Message::PlayPause).await } async fn stop(&self) -> fdo::Result<()> { @@ -108,10 +131,7 @@ impl PlayerInterface for Player { } async fn play(&self) -> fdo::Result<()> { - self.sender - .send(Message::Play) - .await - .map_err(|_error| ERROR) + self.sender.send(Message::Play).await } async fn seek(&self, _offset: Time) -> fdo::Result<()> { @@ -127,9 +147,9 @@ impl PlayerInterface for Player { } async fn playback_status(&self) -> fdo::Result { - Ok(if !self.player.current_exists() { + Ok(if self.current.load().loading() { PlaybackStatus::Stopped - } else if self.player.sink.is_paused() { + } else if self.sink.is_paused() { PlaybackStatus::Paused } else { PlaybackStatus::Playing @@ -145,11 +165,11 @@ impl PlayerInterface for Player { } async fn rate(&self) -> fdo::Result { - Ok(self.player.sink.speed().into()) + Ok(self.sink.speed().into()) } async fn set_rate(&self, rate: PlaybackRate) -> Result<()> { - self.player.sink.set_speed(rate as f32); + self.sink.set_speed(rate as f32); Ok(()) } @@ -162,15 +182,23 @@ impl PlayerInterface for Player { } async fn metadata(&self) -> fdo::Result { - let metadata = self - .player - .current - .load() - .as_ref() - .map_or_else(Metadata::new, |track| { + Ok(match self.current.load().as_ref() { + Current::Loading(_) => Metadata::new(), + Current::Track(track) => { + let mut hasher = DefaultHasher::new(); + track.path.hash(&mut hasher); + + let id = mpris_server::zbus::zvariant::ObjectPath::try_from(format!( + "/com/talwat/lowfi/{}/{}", + self.list, + hasher.finish() + )) + .unwrap(); + let mut metadata = Metadata::builder() - .title(track.display_name.clone()) - .album(self.player.list.name.clone()) + .trackid(id) + .title(track.display.clone()) + .album(self.list.clone()) .build(); metadata.set_length( @@ -180,26 +208,20 @@ impl PlayerInterface for Player { ); metadata - }); - - Ok(metadata) + } + }) } async fn volume(&self) -> fdo::Result { - Ok(self.player.sink.volume().into()) + Ok(self.sink.volume().into()) } async fn set_volume(&self, volume: Volume) -> Result<()> { - self.player.set_volume(volume as f32); - ui::flash_audio(); - - Ok(()) + self.sender.zbus(Message::SetVolume(volume as f32)).await } async fn position(&self) -> fdo::Result