diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..24c4286 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,11 @@ +#!/bin/sh +# Pre-commit hook: runs cargo fmt and clippy checks. +# Install with: git config core.hooksPath .githooks + +set -e + +echo "Running cargo fmt --check..." +cargo fmt --all -- --check +echo "Running cargo clippy..." +cargo clippy --all-targets -- -D warnings +echo "All checks passed." diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f4d246b..291600c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,6 +6,10 @@ on: branches: - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ !github.ref_protected }} + jobs: build: name: Build and Test @@ -61,7 +65,22 @@ jobs: run: cargo clippy --all-targets --all-features -- -D warnings - name: Build + if: matrix.os != 'macos-latest' run: cargo build --release --verbose + - name: Add macOS targets + if: matrix.os == 'macos-latest' + run: rustup target add x86_64-apple-darwin aarch64-apple-darwin + + - name: Build universal binary (macOS) + if: matrix.os == 'macos-latest' + run: | + cargo build --release --target x86_64-apple-darwin + cargo build --release --target aarch64-apple-darwin + lipo -create \ + target/x86_64-apple-darwin/release/audioleaf \ + target/aarch64-apple-darwin/release/audioleaf \ + -output target/release/audioleaf-universal + - name: Run tests run: cargo test --verbose diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bedca6..1332236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,44 @@ All notable changes to this fork of audioleaf are documented in this file. This fork focuses on macOS compatibility, support for all Nanoleaf device types, and enhanced color palette features. +## [3.5.0] - 2026-03-17 + +### Added + +- **Album art visualizer**: Press `N` in the visualizer view to extract colors from the currently playing track's album artwork + - Spotify: artwork URL fetched via ScriptingBridge (`SBApplication`), downloaded via reqwest + - Apple Music: raw artwork bytes read directly from `MusicArtwork.rawData` via ScriptingBridge + - Falls back to osascript if ScriptingBridge is unavailable + - Apple Music osascript fallback uses iTunes Search API for artwork URL + - Background watcher thread polls every 3 seconds and auto-updates colors on song change + - Press any number key (`1`-`0`) to switch back to a named palette and stop the watcher + +- **Now playing display in TUI**: Visualizer view shows track title and color swatches + - "Now playing: *track title*" line appears when album art mode is active + - Color swatch bar shows the active palette colors as colored blocks + - Both update automatically when the watcher detects a song change + - Shared state between watcher thread and TUI via `Arc>` + +- **Universal macOS binary**: Build a fat binary supporting both Intel and Apple Silicon + - `make universal` runs `cargo build` for both `x86_64-apple-darwin` and `aarch64-apple-darwin`, then combines with `lipo` + - CI updated to build universal binary on macOS runners + - `make build` for single-arch release, `make clean` for cleanup + +- **Pre-commit hook**: `.githooks/pre-commit` runs `cargo fmt --check` and `cargo clippy -D warnings` + - Activate with `git config core.hooksPath .githooks` + - Prevents formatting and lint issues from reaching CI + +### Changed + +- **Even color distribution across panels**: `colors_from_rgb` now spreads palette colors evenly instead of padding with the last color + - 4 colors across 12 panels: each color covers 3 panels (was 1-1-1-9) + - Works correctly for all palette sizes + +- **Color extraction**: Switched from `color-thief` to `auto-palette` crate + - Native Oklch color space support (`to_oklch()`, `lightness()`, `is_dark()`) + - Extracts the 4 most dominant colors sorted by pixel population + - Filters out near-black colors using Oklch lightness (`l > 0.15`) since they can't be represented on LED panels + ## [3.2.0] - 2025-12-05 ### Added diff --git a/Cargo.lock b/Cargo.lock index df4ad8b..8387c67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -16,21 +25,21 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alsa" -version = "0.9.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "libc", ] [[package]] name = "alsa-sys" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" dependencies = [ "libc", "pkg-config", @@ -38,9 +47,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -53,15 +62,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -88,9 +97,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "approx" @@ -101,6 +110,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -109,15 +127,19 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "audioleaf" -version = "3.3.0" +version = "3.5.0" dependencies = [ "anyhow", + "auto-palette", "clap", "cpal", "dasp_sample", "dirs", + "image 0.25.10", "macroquad", "num-complex", + "objc2", + "objc2-foundation", "palette", "pollster", "ratatui", @@ -127,18 +149,67 @@ dependencies = [ "toml", ] +[[package]] +name = "auto-palette" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3a3dd8bb9a7414943e5ccc56d75083721bf2286f2b25e6e2eb9eda8ac97d7f" +dependencies = [ + "image 0.25.10", + "num-traits", + "rustc-hash", + "thiserror 2.0.18", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -147,15 +218,33 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "by_address" @@ -165,9 +254,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -176,16 +265,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "bytes" -version = "1.11.0" +name = "byteorder-lite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] -name = "cassowary" -version = "0.3.0" +name = "bytes" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "castaway" @@ -198,11 +287,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -218,11 +309,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -230,9 +327,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -242,21 +339,30 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] [[package]] name = "color_quant" @@ -266,9 +372,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -282,9 +388,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" dependencies = [ "castaway", "cfg-if", @@ -294,6 +400,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -304,6 +419,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -312,11 +437,11 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "coreaudio-rs" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +checksum = "d15c3c3cee7c087938f7ad1c3098840b3ef1f1bdc7f6e496336c3b1e7a6f3914" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.11.0", "libc", "objc2-audio-toolbox", "objc2-core-audio", @@ -326,9 +451,9 @@ dependencies = [ [[package]] name = "cpal" -version = "0.16.0" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" +checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" dependencies = [ "alsa", "coreaudio-rs", @@ -341,15 +466,28 @@ dependencies = [ "ndk-context", "num-derive", "num-traits", + "objc2", "objc2-audio-toolbox", + "objc2-avf-audio", "objc2-core-audio", "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "windows", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -361,15 +499,17 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossterm_winapi", + "derive_more", + "document-features", "mio", "parking_lot", - "rustix 0.38.44", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -384,11 +524,31 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "darling" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -396,27 +556,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -425,6 +584,53 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[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" @@ -448,11 +654,11 @@ dependencies = [ [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", ] @@ -464,9 +670,24 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -499,16 +720,29 @@ dependencies = [ ] [[package]] -name = "fast-srgb8" -version = "1.0.0" +name = "euclid" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] [[package]] -name = "fastrand" -version = "2.3.0" +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fdeflate" @@ -519,17 +753,40 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -547,6 +804,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "fontdue" version = "0.9.3" @@ -557,21 +820,6 @@ dependencies = [ "ttf-parser", ] -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -581,11 +829,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -593,33 +847,33 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -627,19 +881,30 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] +[[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 = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -647,11 +912,26 @@ name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", + "wasip3", ] [[package]] @@ -662,9 +942,9 @@ checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -687,7 +967,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -695,6 +975,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -702,6 +987,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.4.0" @@ -780,34 +1071,17 @@ dependencies = [ ] [[package]] -name = "hyper-tls" -version = "0.6.0" +name = "hyper-util" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64", "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", + "futures-channel", + "futures-util", + "http", + "http-body", "hyper", "ipnet", "libc", @@ -902,6 +1176,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -939,17 +1219,45 @@ dependencies = [ "byteorder", "color_quant", "num-traits", - "png", + "png 0.17.16", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", ] [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -963,28 +1271,28 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" dependencies = [ "darling", "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -998,18 +1306,18 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jni" @@ -1033,43 +1341,84 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.10.0", "libc", ] [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "line-clipping" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.11.0", +] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1077,6 +1426,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1094,18 +1449,34 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.12.5" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", ] [[package]] name = "mach2" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" dependencies = [ "libc", ] @@ -1118,7 +1489,7 @@ checksum = "d2befbae373456143ef55aa93a73594d080adfb111dc32ec96a1123a3e4ff4ae" dependencies = [ "fontdue", "glam", - "image", + "image 0.24.9", "macroquad_macro", "miniquad", "quad-rand", @@ -1141,9 +1512,24 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] [[package]] name = "mime" @@ -1151,6 +1537,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniquad" version = "0.4.8" @@ -1186,20 +1578,13 @@ dependencies = [ ] [[package]] -name = "native-tls" -version = "0.2.14" +name = "moxcms" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", + "num-traits", + "pxfm", ] [[package]] @@ -1208,7 +1593,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", @@ -1237,6 +1622,29 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -1246,6 +1654,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "num-derive" version = "0.4.2" @@ -1254,7 +1668,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1268,9 +1682,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -1278,14 +1692,23 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", ] [[package]] @@ -1299,9 +1722,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -1312,7 +1735,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "objc2", "objc2-core-audio", @@ -1321,6 +1744,16 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-audio" version = "0.3.2" @@ -1331,6 +1764,7 @@ dependencies = [ "objc2", "objc2-core-audio-types", "objc2-core-foundation", + "objc2-foundation", ] [[package]] @@ -1339,7 +1773,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", ] @@ -1349,8 +1783,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", + "block2", "dispatch2", + "libc", "objc2", ] @@ -1366,14 +1802,18 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", "objc2", + "objc2-core-foundation", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1382,55 +1822,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] -name = "openssl-probe" -version = "0.1.6" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] -name = "openssl-sys" -version = "0.9.111" +name = "ordered-float" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", + "num-traits", ] -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "palette" version = "0.7.6" @@ -1452,7 +1863,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1478,18 +1889,55 @@ dependencies = [ "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.11.3" @@ -1500,6 +1948,16 @@ dependencies = [ "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + [[package]] name = "phf_generator" version = "0.11.3" @@ -1507,7 +1965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -1520,7 +1978,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1534,9 +1992,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1563,12 +2021,31 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "pollster" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1578,35 +2055,128 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + [[package]] name = "quad-rand" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1617,13 +2187,39 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1632,25 +2228,98 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "ratatui" -version = "0.29.0" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" dependencies = [ - "bitflags 2.10.0", - "cassowary", + "bitflags 2.11.0", "compact_str", - "crossterm", + "hashbrown 0.16.1", "indoc", - "instability", "itertools", + "kasuari", "lru", - "paste", "strum", + "thiserror 2.0.18", "unicode-segmentation", "unicode-truncate", - "unicode-width 0.2.0", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width", ] [[package]] @@ -1659,7 +2328,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -1668,16 +2337,45 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "reqwest" -version = "0.12.25" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64", "bytes", @@ -1691,21 +2389,21 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", "mime", - "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -1723,44 +2421,47 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] -name = "rustix" -version = "0.38.44" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "semver", ] [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ + "aws-lc-rs", "once_cell", "rustls-pki-types", "rustls-webpki", @@ -1768,21 +2469,62 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1796,9 +2538,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -1811,9 +2553,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -1826,12 +2568,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -1839,14 +2581,20 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -1874,41 +2622,40 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "sha2" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -1940,10 +2687,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -1955,15 +2703,15 @@ checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -1973,12 +2721,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2001,24 +2749,23 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn", + "syn 2.0.117", ] [[package]] @@ -2029,9 +2776,20 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.111" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2055,17 +2813,17 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.10.0", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2080,16 +2838,66 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.23.0" +name = "terminfo" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix 1.1.2", - "windows-sys 0.61.2", + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.11.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", ] [[package]] @@ -2103,11 +2911,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -2118,20 +2926,41 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "tinystr" version = "0.8.2" @@ -2143,27 +2972,32 @@ dependencies = [ ] [[package]] -name = "tokio" -version = "1.48.0" +name = "tinyvec" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "windows-sys 0.61.2", + "tinyvec_macros", ] [[package]] -name = "tokio-native-tls" -version = "0.3.1" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "native-tls", - "tokio", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", ] [[package]] @@ -2178,9 +3012,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -2191,9 +3025,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" dependencies = [ "indexmap", "serde_core", @@ -2206,18 +3040,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap", "toml_datetime", @@ -2227,24 +3061,24 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -2261,7 +3095,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -2287,9 +3121,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-core", @@ -2297,9 +3131,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -2316,11 +3150,23 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -2330,26 +3176,26 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" -version = "1.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ "itertools", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width", ] [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] -name = "unicode-width" -version = "0.2.0" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -2359,9 +3205,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -2382,10 +3228,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "vcpkg" -version = "0.2.15" +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "atomic", + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] [[package]] name = "walkdir" @@ -2414,18 +3281,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -2436,11 +3312,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -2449,9 +3326,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2459,36 +3336,161 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2522,22 +3524,69 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.54.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ "windows-core", - "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.54.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2547,23 +3596,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-registry" -version = "0.6.1" +name = "windows-numerics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ + "windows-core", "windows-link", - "windows-result 0.4.1", - "windows-strings", ] [[package]] -name = "windows-result" -version = "0.1.2" +name = "windows-registry" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-targets 0.52.6", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -2602,15 +3652,6 @@ 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" @@ -2677,6 +3718,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -2817,18 +3867,100 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -2855,10 +3987,30 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zerofrom" version = "0.1.6" @@ -2876,7 +4028,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -2916,5 +4068,26 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +dependencies = [ + "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 6c1e281..3fb1038 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,34 +1,40 @@ [package] name = "audioleaf" -version = "3.3.0" -edition = "2021" +version = "3.5.0" +edition = "2024" authors = ["Antoni Zasada", "weekendsuperhero"] description = "Manage your Nanoleaf devices (Canvas, Shapes, Elements, Light Panels) and visualize music straight from the terminal" -keywords = ["nanoleaf", "audio", "music", "visualizer"] +keywords = ["audio", "music", "nanoleaf", "visualizer"] license = "MIT" readme = "README.md" -repository = "https://github.com/weekendsuperhero/audioleaf" +repository = "https://github.com/weekendsuperhero-io/audioleaf" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.86" -clap = { version = "4.5.16", features = ["derive"] } -cpal = "0.16.0" +anyhow = "1.0.102" +clap = { version = "4.6.0", features = ["derive"] } +auto-palette = "0.9" +cpal = "0.17.3" dasp_sample = "0.11.0" dirs = "6.0.0" macroquad = "0.4" +image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] } num-complex = "0.4.6" palette = "0.7.6" pollster = "0.4" -ratatui = "0.29.0" -reqwest = { version = "0.12.24", features = ["blocking", "json"] } -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.145" -toml = "0.9.8" +ratatui = "0.30" +reqwest = { version = "0.13", features = ["blocking", "json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "1" -[profile.release] -panic = "abort" +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.6" +objc2-foundation = { version = "0.3", features = ["NSString", "NSData"] } [profile.dev] panic = "abort" + +[profile.release] +panic = "abort" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f4a0a48 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: build universal clean + +build: + cargo build --release + +universal: + rustup target add x86_64-apple-darwin aarch64-apple-darwin + cargo build --release --target x86_64-apple-darwin + cargo build --release --target aarch64-apple-darwin + lipo -create \ + target/x86_64-apple-darwin/release/audioleaf \ + target/aarch64-apple-darwin/release/audioleaf \ + -output target/release/audioleaf-universal + +clean: + cargo clean diff --git a/src/app.rs b/src/app.rs index 8f7bae3..f93dde7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,24 +4,29 @@ use crate::event_handler::{self, Event}; use crate::utils; use crate::visualizer::VisualizerMsg; use crate::{ - config::{Axis, Sort, TuiConfig, VisualizerConfig}, + config::{Axis, Effect, Sort, TuiConfig, VisualizerConfig}, nanoleaf::{NlDevice, NlEffect}, visualizer, }; use anyhow::Result; use ratatui::{ + Frame, Terminal, crossterm::event::KeyCode, layout::Margin, prelude::Backend, - style::{Style, Stylize}, - text::Line, + style::{Color, Style, Stylize}, + text::{Line, Span}, widgets::{ Block, Borders, HighlightSpacing, List, ListDirection, ListItem, ListState, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, }, - Frame, Terminal, }; -use std::sync::mpsc; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + mpsc, +}; +use std::time::Duration; #[derive(Debug, Default)] enum AppState { @@ -51,11 +56,23 @@ enum AppMsg { ScrollToTop, ChangeGain(f32), ChangePalette(usize), + CycleEffect, + ResetPanels, + UseAlbumArtPalette, ToggleAxis, TogglePrimarySort, ToggleSecondarySort, } +/// Display state shared between the main thread and the album art watcher thread. +#[derive(Debug)] +struct VizState { + /// The RGB colors currently driving the visualizer palette. + colors: Vec<[u8; 3]>, + /// Set when album art mode is active; cleared when switching to a named palette. + track_title: Option, +} + #[derive(Debug)] struct Scroll { pos: u16, @@ -75,6 +92,9 @@ struct Visualizer { gain: f32, current_palette_index: usize, palette_names: Vec, + viz_state: Arc>, + album_art_stop: Option>, + effect: Effect, primary_axis: Axis, sort_primary: Sort, sort_secondary: Sort, @@ -153,18 +173,31 @@ impl App { let primary_axis = visualizer_config.primary_axis.unwrap_or_default(); let sort_primary = visualizer_config.sort_primary.unwrap_or_default(); let sort_secondary = visualizer_config.sort_secondary.unwrap_or_default(); + let effect = visualizer_config.effect.unwrap_or_default(); + + let initial_colors = visualizer_config + .colors + .clone() + .unwrap_or_else(|| Vec::from(constants::DEFAULT_COLORS)); let tx = visualizer::Visualizer::new(visualizer_config, audio_stream, &nl_device)?.init(); // Initialize palette list let mut palette_names = crate::palettes::get_palette_names(); palette_names.sort(); + let viz_state = Arc::new(Mutex::new(VizState { + colors: initial_colors, + track_title: None, + })); let visualizer = Visualizer { tx, gain, current_palette_index: 0, palette_names, + viz_state, + album_art_stop: None, + effect, primary_axis, sort_primary, sort_secondary, @@ -200,7 +233,10 @@ impl App { /// # Errors /// /// From event recv, update logic, or draw. - pub fn run(&mut self, terminal: &mut Terminal) -> Result<()> { + pub fn run(&mut self, terminal: &mut Terminal) -> Result<()> + where + B::Error: Send + Sync + 'static, + { let event_handler = event_handler::EventHandler::new(); loop { terminal.draw(|frame| self.render_view(frame))?; @@ -212,6 +248,14 @@ impl App { } } self.visualizer.tx.send(VisualizerMsg::End)?; + self.shutdown() + } + + /// Shuts down the device by turning it off and setting brightness to 0. + /// + /// Called after the main TUI loop exits to restore the device to an off state. + pub fn shutdown(&self) -> Result<()> { + self.nl_device.set_state(Some(false), Some(0))?; Ok(()) } @@ -230,6 +274,7 @@ impl App { /// - a/A: Toggle primary axis X/Y /// - p/P: Toggle primary sort Asc/Desc /// - s/S: Toggle secondary sort Asc/Desc + /// - e/E: Cycle visual effect (Spectrum / Energy Wave) /// - Defaults to NoOp for unhandled. /// /// View-specific logic, e.g., scroll only in EffectList. @@ -265,7 +310,7 @@ impl App { } KeyCode::Char('g') => AppMsg::ScrollToTop, KeyCode::Char('G') => AppMsg::ScrollToBottom, - KeyCode::Char('V') => match self.view { + KeyCode::Char('V') | KeyCode::Char('v') => match self.view { AppView::HelpScreen => AppMsg::NoOp, AppView::EffectList => AppMsg::ChangeView(AppView::Visualizer), AppView::Visualizer => AppMsg::ChangeView(AppView::EffectList), @@ -382,11 +427,73 @@ impl App { AppMsg::NoOp } } + KeyCode::Char('e') | KeyCode::Char('E') => { + if let AppView::Visualizer = self.view { + AppMsg::CycleEffect + } else { + AppMsg::NoOp + } + } + KeyCode::Char('n') | KeyCode::Char('N') => { + if let AppView::Visualizer = self.view { + AppMsg::UseAlbumArtPalette + } else { + AppMsg::NoOp + } + } + KeyCode::Char('r') | KeyCode::Char('R') => { + if let AppView::Visualizer = self.view { + AppMsg::ResetPanels + } else { + AppMsg::NoOp + } + } _ => AppMsg::NoOp, }, } } + /// Signals the album art watcher thread to stop, if one is running. + fn stop_album_art_watcher(&mut self) { + if let Some(stop) = self.visualizer.album_art_stop.take() { + stop.store(true, Ordering::Relaxed); + } + if let Ok(mut state) = self.visualizer.viz_state.lock() { + state.track_title = None; + } + } + + /// Spawns a background thread that polls the current track title every 3 seconds. + /// When the title changes it fetches the new album art and sends SetPalette directly + /// to the visualizer thread. Stopped by setting the AtomicBool stop flag. + fn start_album_art_watcher(&mut self) { + self.stop_album_art_watcher(); + let stop = Arc::new(AtomicBool::new(false)); + self.visualizer.album_art_stop = Some(Arc::clone(&stop)); + let tx = self.visualizer.tx.clone(); + let viz_state = Arc::clone(&self.visualizer.viz_state); + std::thread::spawn(move || { + let mut last_title = crate::now_playing::get_track_title(); + loop { + std::thread::sleep(Duration::from_secs(3)); + if stop.load(Ordering::Relaxed) { + break; + } + let title = crate::now_playing::get_track_title(); + if title != last_title { + last_title = title.clone(); + if let Some(colors) = crate::now_playing::fetch_palette() { + if let Ok(mut state) = viz_state.lock() { + state.colors = colors.clone(); + state.track_title = title; + } + let _ = tx.send(VisualizerMsg::SetPalette(colors)); + } + } + } + }); + } + /// Applies an `AppMsg` to update application state, views, or external components. /// /// Match on msg type: @@ -407,6 +514,7 @@ impl App { match msg { AppMsg::NoOp => Ok(()), AppMsg::Quit => { + self.stop_album_art_watcher(); self.state = AppState::Done; Ok(()) } @@ -486,13 +594,46 @@ impl App { AppMsg::ChangePalette(index) => { if index < self.visualizer.palette_names.len() { let palette_name = &self.visualizer.palette_names[index]; - if let Some(hues) = crate::palettes::get_palette(palette_name) { + if let Some(colors) = crate::palettes::get_palette(palette_name) { self.visualizer.current_palette_index = index; - self.visualizer.tx.send(VisualizerMsg::SetPalette(hues))?; + self.stop_album_art_watcher(); + if let Ok(mut state) = self.visualizer.viz_state.lock() { + state.colors = colors.clone(); + state.track_title = None; + } + self.visualizer.tx.send(VisualizerMsg::SetPalette(colors))?; } } Ok(()) } + AppMsg::UseAlbumArtPalette => { + eprintln!("DEBUG app: UseAlbumArtPalette triggered"); + if let Some(colors) = crate::now_playing::fetch_palette() { + let title = crate::now_playing::get_track_title(); + if let Ok(mut state) = self.visualizer.viz_state.lock() { + state.colors = colors.clone(); + state.track_title = title; + } + self.visualizer.tx.send(VisualizerMsg::SetPalette(colors))?; + self.start_album_art_watcher(); + } + Ok(()) + } + AppMsg::CycleEffect => { + self.visualizer.effect = match self.visualizer.effect { + Effect::Spectrum => Effect::EnergyWave, + Effect::EnergyWave => Effect::Pulse, + Effect::Pulse => Effect::Spectrum, + }; + self.visualizer + .tx + .send(VisualizerMsg::SetEffect(self.visualizer.effect))?; + Ok(()) + } + AppMsg::ResetPanels => { + self.visualizer.tx.send(VisualizerMsg::ResetPanels)?; + Ok(()) + } AppMsg::ToggleAxis => { self.visualizer.primary_axis = match self.visualizer.primary_axis { Axis::X => Axis::Y, @@ -595,12 +736,26 @@ impl App { ); } AppView::Visualizer => { - let current_palette = if self.visualizer.current_palette_index - < self.visualizer.palette_names.len() - { - &self.visualizer.palette_names[self.visualizer.current_palette_index] - } else { - "Unknown" + // Snapshot display state from shared VizState (also written by watcher thread). + let (current_palette_name, track_title, palette_colors) = { + let state = self.visualizer.viz_state.lock().unwrap(); + let name = if state.track_title.is_some() { + "album-art".to_string() + } else if self.visualizer.current_palette_index + < self.visualizer.palette_names.len() + { + self.visualizer.palette_names[self.visualizer.current_palette_index].clone() + } else { + "Unknown".to_string() + }; + (name, state.track_title.clone(), state.colors.clone()) + }; + let current_palette = current_palette_name.as_str(); + + let effect_str = match self.visualizer.effect { + Effect::Spectrum => "Spectrum", + Effect::EnergyWave => "Energy Wave", + Effect::Pulse => "Pulse", }; let axis_str = match self.visualizer.primary_axis { @@ -616,6 +771,16 @@ impl App { Sort::Desc => "Desc", }; + // Build color swatch line: two spaces per color with background set. + let mut swatch_spans: Vec = vec!["Colors: ".into()]; + for [r, g, b] in &palette_colors { + swatch_spans.push(Span::styled( + " ", + Style::default().bg(Color::Rgb(*r, *g, *b)), + )); + swatch_spans.push(" ".into()); + } + let mut lines = vec![ Line::from("Music Visualizer".bold().cyan()), Line::from(""), @@ -624,6 +789,16 @@ impl App { format!("{:.2}", self.visualizer.gain).blue(), ]), Line::from(vec!["Current palette: ".into(), current_palette.green()]), + ]; + if let Some(title) = &track_title { + lines.push(Line::from(vec![ + "Now playing: ".into(), + title.as_str().cyan().italic(), + ])); + } + lines.push(Line::from(swatch_spans)); + lines.extend([ + Line::from(vec!["Effect [E]: ".into(), effect_str.magenta()]), Line::from(""), Line::from("Panel Sorting:".bold()), Line::from(vec![ @@ -636,7 +811,7 @@ impl App { ]), Line::from(""), Line::from("Available Palettes (press number to switch):".bold()), - ]; + ]); for (i, palette_name) in self.visualizer.palette_names.iter().enumerate().take(10) { let key = if i == 9 { @@ -672,12 +847,15 @@ impl App { Line::from(vec!["g/G".bold(), " - go to the top/bottom of the list".into()]), Line::from(vec!["j/Down, k/Up".bold(), " - scroll down and up".into()]), Line::from(vec!["Enter".bold(), " - play selected effect".into()]), - Line::from(vec!["V".bold(), " - toggle music visualizer mode".into()]), + Line::from(vec!["V/v".bold(), " - toggle music visualizer mode".into()]), Line::from(vec!["-/+".bold(), " - decrease/increase gain (in visualizer mode)".into()]), Line::from(vec!["1-9, 0".bold(), " - switch color palette (in visualizer mode)".into()]), Line::from(vec!["A".bold(), " - toggle primary axis X/Y (in visualizer mode)".into()]), Line::from(vec!["P".bold(), " - toggle primary sort Asc/Desc (in visualizer mode)".into()]), Line::from(vec!["S".bold(), " - toggle secondary sort Asc/Desc (in visualizer mode)".into()]), + Line::from(vec!["E".bold(), " - cycle visual effect: Spectrum / Energy Wave / Pulse (in visualizer mode)".into()]), + Line::from(vec!["N".bold(), " - use album art colors from current track (in visualizer mode)".into()]), + Line::from(vec!["R".bold(), " - reset all panels to black (in visualizer mode)".into()]), Line::from(vec!["(note that gain doesn't affect your music volume, only the visuals are amplified)".italic()]), ]) .block(main_block) diff --git a/src/audio.rs b/src/audio.rs index 890c6c5..92e4fa5 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -1,6 +1,6 @@ use crate::constants; -use anyhow::{bail, Result}; -use cpal::{traits::*, Device, SampleFormat, StreamConfig}; +use anyhow::{Result, bail}; +use cpal::{Device, SampleFormat, StreamConfig, traits::*}; pub struct AudioStream { pub device: Device, @@ -52,12 +52,12 @@ impl AudioStream { let mut loopback_device = None; if let Ok(devices) = host.input_devices() { for device in devices { - if let Ok(name) = device.name() { - if loopback_names.iter().any(|lb| name.contains(lb)) { - eprintln!("INFO: Found loopback device: {}", name); - loopback_device = Some(device); - break; - } + if let Ok(name) = device.description().map(|d| d.name().to_string()) + && loopback_names.iter().any(|lb| name.contains(lb)) + { + eprintln!("INFO: Found loopback device: {}", name); + loopback_device = Some(device); + break; } } } @@ -69,9 +69,11 @@ impl AudioStream { host.default_input_device() }) } - _ => host - .input_devices()? - .find(|x| x.name().map(|y| y == device_name).unwrap_or(false)), + _ => host.input_devices()?.find(|x| { + x.description() + .map(|d| d.name() == device_name) + .unwrap_or(false) + }), }; let Some(device) = device else { @@ -79,7 +81,10 @@ impl AudioStream { "Audio backend `{}` not found, available options: {}", device_name, host.input_devices()? - .map(|dev| dev.name().unwrap_or_default()) + .map(|dev| dev + .description() + .map(|d| d.name().to_string()) + .unwrap_or_default()) .collect::>() .join(", ") )); diff --git a/src/config.rs b/src/config.rs index 251c671..9ccb250 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,5 @@ use crate::{constants, ssdp, utils}; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use clap::Parser; use serde::Serialize; use std::fs::File; @@ -85,17 +85,36 @@ pub enum Sort { Desc, } +/// Visual effect mode for the audio visualizer. +/// +/// Controls how audio data maps to panel brightness/color animation. +#[derive(Copy, Clone, Debug, Default, Serialize)] +pub enum Effect { + /// Each panel independently tracks a logarithmic frequency band. + /// Brightness pulses with audio energy in that band (fast attack, slow decay). + #[default] + Spectrum, + /// Audio energy enters from one end and cascades across panels as a traveling wave. + /// Creates a flowing ripple effect driven by overall audio amplitude. + EnergyWave, + /// All panels pulse together, driven directly by audio transients. + /// Very fast attack snaps to each beat; smooth exponential decay fades between hits. + /// The music's own rhythm drives the animation — no fixed oscillation. + Pulse, +} + #[derive(Debug, Serialize)] pub struct VisualizerConfig { pub audio_backend: Option, pub freq_range: Option<(u16, u16)>, - pub hues: Option>, + pub colors: Option>, pub default_gain: Option, - pub transition_time: Option, + pub transition_time: Option, pub time_window: Option, pub primary_axis: Option, pub sort_primary: Option, pub sort_secondary: Option, + pub effect: Option, } impl VisualizerConfig { @@ -104,7 +123,7 @@ impl VisualizerConfig { /// Initializes with constants: /// - `audio_backend`: "default" /// - `freq_range`: (20, 4500) Hz - /// - `hues`: Standard rainbow-like [30,0,330,...] + /// - `colors`: RGB color array for panel visualization /// - `default_gain`: 1.0 /// - `transition_time`: 2 (200ms) /// - `time_window`: 0.1875 s @@ -113,13 +132,22 @@ impl VisualizerConfig { VisualizerConfig { audio_backend: Some("default".to_string()), freq_range: Some(constants::DEFAULT_FREQ_RANGE), - hues: Some(vec![30, 0, 330, 300, 270, 240, 210]), + colors: Some(vec![ + [255, 128, 0], + [255, 0, 0], + [255, 0, 128], + [255, 0, 255], + [128, 0, 255], + [0, 0, 255], + [0, 128, 255], + ]), default_gain: Some(constants::DEFAULT_GAIN), transition_time: Some(constants::DEFAULT_TRANSITION_TIME), time_window: Some(constants::DEFAULT_TIME_WINDOW), primary_axis: Some(Axis::default()), sort_primary: Some(Sort::default()), sort_secondary: Some(Sort::default()), + effect: Some(Effect::default()), } } } @@ -186,14 +214,14 @@ impl Config { /// Supports comprehensive field validation and type conversion: /// - `audio_backend`: String for device name. /// - `freq_range`: 2-element array of u16 [min_hz, max_hz]. - /// - `hues`: Array of u16 (0-360) or string name of predefined palette (e.g., "ocean-nightclub"). + /// - `colors`: Array of [R,G,B] triplets or string name of predefined palette (e.g., "ocean-nightclub"). /// - `default_gain`: f32 or i64, applied to spectrum amplitudes. - /// - `transition_time`: i16 (-1 for instant, positive in 100ms units for Nanoleaf transitions). + /// - `transition_time`: u16 in 100ms units for Nanoleaf transitions (0 = instant). /// - `time_window`: f32 seconds for smoothing window. /// - `primary_axis`: "X" or "Y" enum. /// - `sort_primary`/`sort_secondary`: "Asc" or "Desc". /// - /// Validates ranges (e.g., hues 0-360, transition_time >= -1) and bails on errors or unknown keys. + /// Validates ranges (e.g., RGB 0-255, transition_time >= 0) and bails on errors or unknown keys. /// Palette names checked against available predefined palettes. /// /// # Arguments @@ -223,10 +251,10 @@ impl Config { visualizer_config.freq_range = Some((u16::try_from(low)?, u16::try_from(high)?)); } - ("hues", Value::String(s)) => { + ("colors" | "hues", Value::String(s)) => { // Named palette support match crate::palettes::get_palette(&s) { - Some(hues) => visualizer_config.hues = Some(hues), + Some(colors) => visualizer_config.colors = Some(colors), None => { let available = crate::palettes::get_palette_names().join(", "); bail!( @@ -237,21 +265,55 @@ impl Config { } } } - ("hues", Value::Array(v)) => { + ("colors" | "hues", Value::Array(v)) => { if v.is_empty() { - bail!("hues cannot be an empty array"); + bail!("colors cannot be an empty array"); } - if v.iter().map(|x| x.as_integer()).any(|x| match x.as_ref() { - Some(x) => !(0..=360).contains(x), - None => true, - }) { - bail!("hues must be integers from 0 to 360 inclusive (360 = white)"); + // Detect format: if first element is an integer, treat as legacy hue array; + // if first element is an array, treat as RGB color array. + if v[0].is_integer() { + // Legacy hue format: [30, 0, 330, ...] → convert HSV hues to RGB + let mut colors = Vec::with_capacity(v.len()); + for (i, entry) in v.iter().enumerate() { + let Some(hue_val) = entry.as_integer() else { + bail!("hues[{}] must be an integer (0-360)", i); + }; + if !(0..=360).contains(&hue_val) { + bail!("hues[{}] must be 0-360, got {}", i, hue_val); + } + if hue_val == 360 { + colors.push([255, 255, 255]); // white + } else { + // Convert HSV hue (S=1, V=1) to RGB + let rgb = hsv_hue_to_rgb(hue_val as f32); + colors.push(rgb); + } + } + visualizer_config.colors = Some(colors); + } else { + // New RGB format: [[255, 0, 0], [0, 255, 0], ...] + let mut colors = Vec::with_capacity(v.len()); + for (i, entry) in v.iter().enumerate() { + let Some(rgb_arr) = entry.as_array() else { + bail!("colors[{}] must be a [R, G, B] array", i); + }; + if rgb_arr.len() != 3 { + bail!("colors[{}] must be a 3-element [R, G, B] array", i); + } + let mut rgb = [0u8; 3]; + for (j, component) in rgb_arr.iter().enumerate() { + let Some(val) = component.as_integer() else { + bail!("colors[{}][{}] must be an integer (0-255)", i, j); + }; + if !(0..=255).contains(&val) { + bail!("colors[{}][{}] must be 0-255, got {}", i, j, val); + } + rgb[j] = val as u8; + } + colors.push(rgb); + } + visualizer_config.colors = Some(colors); } - let hues: Vec = v - .into_iter() - .map(|x| u16::try_from(x.as_integer().unwrap()).unwrap()) - .collect(); - visualizer_config.hues = Some(hues); } ("default_gain", Value::Float(x)) => { eprintln!("DEBUG: Parsed default_gain as Float: {}", x); @@ -262,10 +324,11 @@ impl Config { visualizer_config.default_gain = Some(x as f32); } ("transition_time", Value::Integer(x)) => { - let trans_time = i16::try_from(x)?; - if trans_time < -1 { - bail!("transition_time must be -1 (instant) or a positive value. Note: units are in 100ms (1 = 100ms, 2 = 200ms, etc.)"); - } + let trans_time = u16::try_from(x).map_err(|_| { + anyhow::anyhow!( + "transition_time must be 0-65535. Note: units are in 100ms (0 = instant, 1 = 100ms, 2 = 200ms, etc.)" + ) + })?; visualizer_config.transition_time = Some(trans_time); } ("time_window", Value::Float(x)) => { @@ -304,6 +367,21 @@ impl Config { }; visualizer_config.sort_secondary = sort; } + ("effect", Value::String(s)) => { + let effect = match s.as_str() { + "Spectrum" | "spectrum" => Some(Effect::Spectrum), + "EnergyWave" | "energy_wave" | "energy-wave" => Some(Effect::EnergyWave), + "Pulse" | "pulse" => Some(Effect::Pulse), + _ => None, + }; + if effect.is_none() { + bail!( + "effect must be `Spectrum`, `EnergyWave`, or `Pulse`, got `{}`", + s + ); + }; + visualizer_config.effect = effect; + } (key, _) => { bail!(format!("invalid key `{}`", key)); } @@ -446,6 +524,24 @@ pub fn resolve_paths( )) } +/// Converts an HSV hue (with S=1, V=1) to an RGB triplet. +/// +/// Used for backwards compatibility with legacy config files that specify +/// colors as hue angles (0-360) instead of RGB arrays. +fn hsv_hue_to_rgb(hue: f32) -> [u8; 3] { + let h = hue / 60.0; + let x = 1.0 - (h % 2.0 - 1.0).abs(); + let (r, g, b) = match h as u32 { + 0 => (1.0, x, 0.0), + 1 => (x, 1.0, 0.0), + 2 => (0.0, 1.0, x), + 3 => (0.0, x, 1.0), + 4 => (x, 0.0, 1.0), + _ => (1.0, 0.0, x), + }; + [(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8] +} + /// Interactively discovers Nanoleaf devices via SSDP or accepts manual IP input. /// /// Performs SSDP M-SEARCH to find devices on network, lists names/IPs for user choice. diff --git a/src/constants.rs b/src/constants.rs index 4017977..f0e4fb6 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -4,12 +4,24 @@ pub const DEFAULT_COLORFUL_EFFECT_NAMES: bool = false; // visualizer config pub const DEFAULT_AUDIO_BACKEND: &str = "default"; pub const DEFAULT_FREQ_RANGE: (u16, u16) = (20, 4500); -pub const DEFAULT_HUES: [u16; 12] = [50, 30, 10, 350, 330, 310, 290, 270, 250, 230, 210, 190]; +pub const DEFAULT_COLORS: [[u8; 3]; 12] = [ + [255, 213, 0], // golden + [255, 128, 0], // orange + [255, 43, 0], // scarlet + [255, 0, 43], // rose + [255, 0, 128], // hot pink + [255, 0, 213], // fuchsia + [213, 0, 255], // purple + [128, 0, 255], // violet + [43, 0, 255], // indigo + [0, 43, 255], // cobalt + [0, 128, 255], // azure + [0, 213, 255], // sky blue +]; pub const DEFAULT_GAIN: f32 = 1.0; -// Transition time in units of 100ms (1 = 100ms, 2 = 200ms, etc.) -// -1 is special: instant transition (start frame), used to avoid lengthy initial transitions +// Transition time in units of 100ms (0 = instant, 1 = 100ms, 2 = 200ms, etc.) // Recommended: Use values that align with your time_window for smooth transitions -pub const DEFAULT_TRANSITION_TIME: i16 = 2; +pub const DEFAULT_TRANSITION_TIME: u16 = 2; pub const DEFAULT_TIME_WINDOW: f32 = 0.1875; // other diff --git a/src/graphical_layout.rs b/src/graphical_layout.rs index 0e50d22..189cc73 100644 --- a/src/graphical_layout.rs +++ b/src/graphical_layout.rs @@ -1,7 +1,7 @@ use crate::layout_visualizer::PanelInfo; use crate::nanoleaf::NlDevice; use macroquad::prelude::*; -use palette::Hwb; +use palette::Oklch; use std::f32::consts::PI; use std::thread; use std::time::Duration; @@ -146,7 +146,16 @@ async fn visualize_loop(panels: Vec, global_orientation: u16, device: // Initialize optional UDP controller using device IP and token for flashing panels. // Gracefully handles initialization failure by disabling clicks but continuing render. let nl_controller = match crate::nanoleaf::NlUdp::new(&device) { - Ok(controller) => Some(controller), + Ok(controller) => { + // Blank all panels to black so only clicked panels light up + let black_colors: Vec = panels + .iter() + .filter(|p| p.shape_type.side_length >= 1.0) + .map(|_| Oklch::new(0.0, 0.0, 0.0)) + .collect(); + let _ = controller.update_panels(&black_colors, 0); + Some(controller) + } Err(e) => { eprintln!("Warning: Could not initialize Nanoleaf controller: {}", e); None @@ -504,7 +513,7 @@ fn draw_panel( /// Flashes a specific panel white briefly by sending UDP color updates. /// -/// Sets the clicked panel to white (Hwb(359,0,0)) and all other light panels to black (Hwb(0,0,1)). +/// Sets the clicked panel to white (Oklch L=1, C=0) and all other light panels to black (Oklch L=0, C=0). /// Updates immediately (transition=1), waits 300ms, then sets all light panels back to black. /// /// Only affects panels with side_length >=1.0 (light panels, skips controllers). @@ -519,16 +528,16 @@ fn flash_panel( all_panels: &[PanelInfo], clicked_panel_id: u16, ) { - // Construct per-panel Hwb colors for UDP update: white (Hwb::new(359.0, 0.0, 0.0)) for clicked, - // black (Hwb::new(0.0, 0.0, 1.0)) for other light panels; exclude controllers from array. - let colors: Vec = all_panels + // Construct per-panel Oklch colors for UDP update: white (L=1, C=0) for clicked, + // black (L=0, C=0) for other light panels; exclude controllers from array. + let colors: Vec = all_panels .iter() .filter(|panel| panel.shape_type.side_length >= 1.0) .map(|panel| { if panel.panel_id == clicked_panel_id { - Hwb::new(359.0, 0.0, 0.0) // White + Oklch::new(1.0, 0.0, 0.0) // White } else { - Hwb::new(0.0, 0.0, 1.0) // Black + Oklch::new(0.0, 0.0, 0.0) // Black } }) .collect(); @@ -540,11 +549,10 @@ fn flash_panel( thread::sleep(Duration::from_millis(300)); // Reset flash: update all light panels to black, effectively turning off the highlight - // (note: this overrides any current effect; in practice, may need to restore original colors for seamless integration). - let black_colors: Vec = all_panels + let black_colors: Vec = all_panels .iter() .filter(|panel| panel.shape_type.side_length >= 1.0) - .map(|_| Hwb::new(0.0, 0.0, 1.0)) + .map(|_| Oklch::new(0.0, 0.0, 0.0)) .collect(); let _ = controller.update_panels(&black_colors, 1); } diff --git a/src/main.rs b/src/main.rs index 89ae9a8..365ec7a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod event_handler; mod graphical_layout; mod layout_visualizer; mod nanoleaf; +mod now_playing; mod palettes; mod panic; mod processing; @@ -145,6 +146,8 @@ async fn handle_dump_command( println!("Panel Layout Information for: {}", nl_device.name); println!("Device IP: {}", nl_device.ip); + nl_device.ensure_device_ready()?; + let layout = nl_device.get_panel_layout()?; let orientation = nl_device.get_global_orientation()?; let global_orientation = orientation["value"].as_u64().unwrap_or(0) as u16; @@ -159,14 +162,20 @@ async fn handle_dump_command( println!("\n=== Raw Global Orientation JSON ==="); println!("{}", serde_json::to_string_pretty(&orientation)?); + nl_device.set_state(Some(false), Some(0))?; Ok(()) } config::DumpType::Palettes => { println!("Available Color Palettes:\n"); - let palette_names = palettes::get_palette_names(); + let mut palette_names = palettes::get_palette_names(); + palette_names.sort(); for name in palette_names { - let hues = palettes::get_palette(&name).unwrap(); - println!(" {} = {:?}", name, hues); + let colors = palettes::get_palette(&name).unwrap(); + let color_strs: Vec = colors + .iter() + .map(|[r, g, b]| format!("[{}, {}, {}]", r, g, b)) + .collect(); + println!(" {} = [{}]", name, color_strs.join(", ")); } Ok(()) } @@ -197,6 +206,10 @@ async fn handle_dump_command( )? }; + nl_device.ensure_device_ready()?; + // Request UDP control so panel flash commands work + nl_device.request_udp_control()?; + let layout = nl_device.get_panel_layout()?; let orientation = nl_device.get_global_orientation()?; let global_orientation = orientation["value"].as_u64().unwrap_or(0) as u16; @@ -204,8 +217,9 @@ async fn handle_dump_command( let panels = layout_visualizer::parse_layout(&layout)?; // Call the graphical visualizer - it has its own macroquad::main wrapper - graphical_layout::visualize_graphical(panels, global_orientation, nl_device); + graphical_layout::visualize_graphical(panels, global_orientation, nl_device.clone()); + nl_device.set_state(Some(false), Some(0))?; Ok(()) } config::DumpType::Info => { @@ -237,10 +251,14 @@ async fn handle_dump_command( println!("Device Information for: {}", nl_device.name); println!("Device IP: {}", nl_device.ip); + + nl_device.ensure_device_ready()?; + println!("\n=== Device Info (from /api/v1/) ==="); let info = nl_device.get_device_info()?; println!("{}", serde_json::to_string_pretty(&info)?); + nl_device.set_state(Some(false), Some(0))?; Ok(()) } } diff --git a/src/nanoleaf.rs b/src/nanoleaf.rs index 4055193..37b5280 100644 --- a/src/nanoleaf.rs +++ b/src/nanoleaf.rs @@ -2,8 +2,8 @@ use crate::{ config::{Axis, Sort}, constants, utils, }; -use anyhow::{bail, Result}; -use palette::{FromColor, Hsv, Srgb}; +use anyhow::{Result, bail}; +use palette::{FromColor, Hsv, Oklch, Srgb}; use serde::{Deserialize, Serialize}; use serde_json::json; use std::{ @@ -19,7 +19,7 @@ pub struct NlEffect { pub palette: Vec>, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct NlDevice { pub name: String, pub ip: Ipv4Addr, @@ -219,24 +219,18 @@ impl NlDevice { let is_on = info["state"]["on"]["value"].as_bool().unwrap_or(true); let brightness = info["state"]["brightness"]["value"].as_u64().unwrap_or(100) as u8; - let mut needs_power = false; - let mut needs_brightness = false; + let needs_power = !is_on; + let needs_brightness = brightness != 100; - if !is_on { + if needs_power { eprintln!("Device is off. Turning on..."); - needs_power = true; } - - if brightness == 0 { - eprintln!("Device brightness is 0. Setting to 100..."); - needs_brightness = true; + if needs_brightness { + eprintln!("Device brightness is {}. Setting to 100...", brightness); } if needs_power || needs_brightness { - self.set_state( - if needs_power { Some(true) } else { None }, - if needs_brightness { Some(100) } else { None }, - )?; + self.set_state(if needs_power { Some(true) } else { None }, Some(100))?; // Give the device a moment to respond to the state change std::thread::sleep(std::time::Duration::from_millis(500)); } @@ -528,7 +522,7 @@ impl NlUdp { (rotated_x, rotated_y) } - pub fn update_panels(&self, colors: &[palette::Hwb], trans_time: i16) -> Result<()> { + pub fn update_panels(&self, colors: &[Oklch], trans_time: u16) -> Result<()> { let mut buf = vec![0; 8 * self.panels.len() + 2]; (buf[0], buf[1]) = utils::split_into_bytes(self.panels.len() as u16); for (i, color) in colors.iter().enumerate() { @@ -537,7 +531,7 @@ impl NlUdp { green: g, blue: b, .. - } = palette::Srgb::from_color(*color).into_format::(); + } = Srgb::from_color(*color).into_format::(); let offset = 8 * i + 2; (buf[offset], buf[offset + 1]) = utils::split_into_bytes(self.panels[i].id); ( @@ -546,14 +540,8 @@ impl NlUdp { buf[offset + 4], buf[offset + 5], ) = (r, g, b, 0); - // Convert i16 to bytes (supporting -1 for instant transition) - // trans_time is in units of 100ms: 1 = 100ms, 2 = 200ms, -1 = instant - let trans_time_u16 = if trans_time == -1 { - 0xFFFF // -1 as u16 (two's complement) - } else { - trans_time as u16 - }; - (buf[offset + 6], buf[offset + 7]) = utils::split_into_bytes(trans_time_u16); + // trans_time is in units of 100ms: 0 = instant, 1 = 100ms, 2 = 200ms + (buf[offset + 6], buf[offset + 7]) = utils::split_into_bytes(trans_time); } self.socket.send(&buf)?; diff --git a/src/now_playing.rs b/src/now_playing.rs new file mode 100644 index 0000000..d17ca34 --- /dev/null +++ b/src/now_playing.rs @@ -0,0 +1,350 @@ +//! Album art color extraction for the visualizer. +//! +//! macOS: Uses ScriptingBridge.framework (via objc2) to query running media +//! players directly through the ObjC bridge — no subprocess spawning. +//! - Spotify: title, artist, artwork URL (downloaded via reqwest). +//! - Apple Music: title, artist, raw artwork bytes from MusicArtwork.rawData. +//! - Falls back to osascript if ScriptingBridge fails. +//! +//! Linux: Uses `playerctl` subprocess. + +/// Returns the title of the currently playing track. +pub fn get_track_title() -> Option { + #[cfg(target_os = "macos")] + { + macos::get_track_info().map(|info| info.title) + } + #[cfg(target_os = "linux")] + { + linux::get_track_title() + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + None + } +} + +/// Returns dominant RGB colors extracted from the current track's album artwork. +pub fn fetch_palette() -> Option> { + #[cfg(target_os = "macos")] + { + macos::fetch_palette() + } + #[cfg(target_os = "linux")] + { + linux::fetch_palette() + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + None + } +} + +// ── macOS — ScriptingBridge + osascript fallback ────────────────────────────── + +#[cfg(target_os = "macos")] +mod macos { + use objc2::msg_send; + use objc2::rc::Retained; + use objc2::runtime::{AnyClass, AnyObject}; + use objc2_foundation::{NSData, NSString}; + + #[link(name = "ScriptingBridge", kind = "framework")] + unsafe extern "C" {} + + // Four-char code for "playing" state (shared by Spotify and Apple Music). + const EPLAYER_PLAYING: u32 = 0x6b505350; // 'kPSP' + + pub struct TrackInfo { + pub title: String, + } + + pub fn get_track_info() -> Option { + sb_spotify_title() + .or_else(sb_apple_music_title) + .or_else(osascript_title) + .map(|title| TrackInfo { title }) + } + + pub fn fetch_palette() -> Option> { + let bytes = sb_spotify_artwork() + .or_else(sb_apple_music_artwork) + .or_else(osascript_artwork)?; + extract_colors(&bytes) + } + + // ── ScriptingBridge helpers ─────────────────────────────────────────────── + + /// Open a scripting bridge to a running application. Returns None if not running. + fn sb_app(bundle_id: &str) -> Option> { + unsafe { + let cls = AnyClass::get(c"SBApplication")?; + let bid = NSString::from_str(bundle_id); + let app: Option> = + msg_send![cls, applicationWithBundleIdentifier: &*bid]; + let app = app?; + let running: bool = msg_send![&*app, isRunning]; + if !running { + return None; + } + let state: u32 = msg_send![&*app, playerState]; + if state != EPLAYER_PLAYING { + return None; + } + Some(app) + } + } + + // ── Spotify via ScriptingBridge ─────────────────────────────────────────── + + fn sb_spotify_title() -> Option { + let app = sb_app("com.spotify.client")?; + unsafe { + let track: Option> = msg_send![&*app, currentTrack]; + let track = track?; + let name: Option> = msg_send![&*track, name]; + name.map(|s| s.to_string()) + } + } + + fn sb_spotify_artwork() -> Option> { + let app = sb_app("com.spotify.client")?; + unsafe { + let track: Option> = msg_send![&*app, currentTrack]; + let track = track?; + let url: Option> = msg_send![&*track, artworkUrl]; + let url = url?; + reqwest::blocking::get(&*url.to_string()) + .ok()? + .bytes() + .ok() + .map(|b| b.to_vec()) + } + } + + // ── Apple Music via ScriptingBridge ─────────────────────────────────────── + + fn sb_apple_music_title() -> Option { + let app = sb_app("com.apple.Music")?; + unsafe { + let track: Option> = msg_send![&*app, currentTrack]; + let track = track?; + let name: Option> = msg_send![&*track, name]; + name.map(|s| s.to_string()) + } + } + + fn sb_apple_music_artwork() -> Option> { + let app = sb_app("com.apple.Music")?; + unsafe { + let track: Option> = msg_send![&*app, currentTrack]; + let track = track?; + let artworks: Option> = msg_send![&*track, artworks]; + let artworks = artworks?; + let count: usize = msg_send![&*artworks, count]; + if count == 0 { + return None; + } + let artwork: Option> = msg_send![&*artworks, objectAtIndex: 0usize]; + let artwork = artwork?; + let raw: Option> = msg_send![&*artwork, rawData]; + let raw = raw?; + // rawData returns NSData with JPEG/PNG image bytes. + raw.downcast_ref::().map(|d| d.to_vec()) + } + } + + // ── osascript fallback ─────────────────────────────────────────────────── + + fn osascript_title() -> Option { + for script in [ + r#"tell application "System Events" + if not (exists process "Spotify") then return "NOT_RUNNING" + end tell + tell application "Spotify" + if player state is not playing then return "NOT_PLAYING" + return name of current track + end tell"#, + r#"tell application "System Events" + if not (exists process "Music") then return "NOT_RUNNING" + end tell + tell application "Music" + if player state is not playing then return "NOT_PLAYING" + return name of current track + end tell"#, + ] { + if let Some(title) = run_osascript(script) { + return Some(title); + } + } + None + } + + fn osascript_artwork() -> Option> { + // Spotify: artwork URL. + let script = r#"tell application "System Events" + if not (exists process "Spotify") then return "NOT_RUNNING" + end tell + tell application "Spotify" + if player state is not playing then return "NOT_PLAYING" + return artwork url of current track + end tell"#; + if let Some(url) = run_osascript(script) + && let Some(bytes) = reqwest::blocking::get(&url) + .ok() + .and_then(|r| r.bytes().ok()) + .map(|b| b.to_vec()) + { + return Some(bytes); + } + // Apple Music: use iTunes Search API. + let script = r#"tell application "System Events" + if not (exists process "Music") then return "NOT_RUNNING" + end tell + tell application "Music" + if player state is not playing then return "NOT_PLAYING" + return (name of current track) & " " & (artist of current track) + end tell"#; + if let Some(query) = run_osascript(script) + && let Some(url) = itunes_artwork_url(&query) + { + return reqwest::blocking::get(&url) + .ok() + .and_then(|r| r.bytes().ok()) + .map(|b| b.to_vec()); + } + None + } + + fn run_osascript(script: &str) -> Option { + let output = std::process::Command::new("osascript") + .args(["-e", script]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if text.is_empty() || text == "NOT_RUNNING" || text == "NOT_PLAYING" { + return None; + } + Some(text) + } + + fn itunes_artwork_url(query: &str) -> Option { + let url = format!( + "https://itunes.apple.com/search?term={}&media=music&limit=1", + urlencoded(query) + ); + let resp: serde_json::Value = reqwest::blocking::get(&url).ok()?.json().ok()?; + let art = resp["results"][0]["artworkUrl100"].as_str()?; + Some(art.replace("100x100", "600x600")) + } + + fn urlencoded(s: &str) -> String { + let mut out = String::with_capacity(s.len() * 2); + for b in s.bytes() { + match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' => { + out.push(b as char); + } + b' ' => out.push('+'), + _ => { + out.push('%'); + out.push_str(&format!("{:02X}", b)); + } + } + } + out + } + + fn extract_colors(image_bytes: &[u8]) -> Option> { + use auto_palette::{ImageData, Palette}; + + let img = image::load_from_memory(image_bytes).ok()?; + let rgba = img.to_rgba8(); + let image_data = ImageData::new(rgba.width(), rgba.height(), rgba.as_raw()).ok()?; + let palette: Palette = Palette::extract(&image_data).ok()?; + // Sort by population (most prevalent colors first) and take the + // top 4 that aren't near-black — these are the actual dominant + // colors of the album art, not theme-scored "interesting" picks. + let mut swatches = palette.swatches().to_vec(); + swatches.sort_by_key(|s| std::cmp::Reverse(s.population())); + let colors: Vec<[u8; 3]> = swatches + .iter() + .filter(|s| s.color().to_oklch().l > 0.15) + .take(4) + .map(|s| { + let rgb = s.color().to_rgb(); + [rgb.r, rgb.g, rgb.b] + }) + .collect(); + if colors.is_empty() { + None + } else { + Some(colors) + } + } +} + +// ── Linux ───────────────────────────────────────────────────────────────────── + +#[cfg(target_os = "linux")] +mod linux { + pub fn get_track_title() -> Option { + let output = std::process::Command::new("playerctl") + .args(["metadata", "title"]) + .output() + .ok()?; + if output.status.success() { + let title = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !title.is_empty() { + return Some(title); + } + } + None + } + + pub fn fetch_palette() -> Option> { + let output = std::process::Command::new("playerctl") + .args(["metadata", "mpris:artUrl"]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let url = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if url.is_empty() { + return None; + } + + let bytes: Vec = if url.starts_with("file://") { + std::fs::read(url.trim_start_matches("file://")).ok()? + } else { + reqwest::blocking::get(&url).ok()?.bytes().ok()?.to_vec() + }; + + use auto_palette::{ImageData, Palette}; + + let img = image::load_from_memory(&bytes).ok()?; + let rgba = img.to_rgba8(); + let image_data = ImageData::new(rgba.width(), rgba.height(), rgba.as_raw()).ok()?; + let palette: Palette = Palette::extract(&image_data).ok()?; + let mut swatches = palette.swatches().to_vec(); + swatches.sort_by_key(|s| std::cmp::Reverse(s.population())); + let colors: Vec<[u8; 3]> = swatches + .iter() + .filter(|s| s.color().to_oklch().l > 0.15) + .take(4) + .map(|s| { + let rgb = s.color().to_rgb(); + [rgb.r, rgb.g, rgb.b] + }) + .collect(); + if colors.is_empty() { + None + } else { + Some(colors) + } + } +} diff --git a/src/palettes.rs b/src/palettes.rs index 1c239ea..a42436d 100644 --- a/src/palettes.rs +++ b/src/palettes.rs @@ -1,11 +1,13 @@ /// Predefined color palettes /// /// This module contains named color palettes that users can reference -/// in their config.toml instead of manually specifying hue arrays. +/// in their config.toml instead of manually specifying RGB color arrays. +/// Each color is an [R, G, B] triplet. At runtime these are converted +/// to Oklch so the visualizer can animate perceptually-uniform lightness. use std::collections::HashMap; /// Get a predefined palette by name -pub fn get_palette(name: &str) -> Option> { +pub fn get_palette(name: &str) -> Option> { let palettes = get_all_palettes(); palettes.get(name).cloned() } @@ -16,51 +18,165 @@ pub fn get_palette_names() -> Vec { } /// Get all predefined palettes -fn get_all_palettes() -> HashMap> { +fn get_all_palettes() -> HashMap> { let mut palettes = HashMap::new(); + // Deep sky blues, indigos, magentas, and cyans palettes.insert( "ocean-nightclub".to_string(), - vec![195, 210, 240, 270, 285, 300, 180], + vec![ + [0, 191, 255], // deep sky blue + [0, 128, 255], // azure + [0, 0, 255], // blue + [128, 0, 255], // violet + [191, 0, 255], // purple + [255, 0, 255], // magenta + [0, 255, 255], // cyan + ], ); + // Warm reds, oranges, deep pinks, and purples palettes.insert( "sunset".to_string(), - vec![15, 25, 340, 350, 0, 10, 310, 280], + vec![ + [255, 64, 0], // red-orange + [255, 106, 0], // orange + [255, 0, 85], // crimson + [255, 0, 43], // rose + [255, 0, 0], // red + [255, 43, 0], // scarlet + [255, 0, 128], // hot pink + [170, 0, 255], // violet + ], ); + // Magentas, purples, indigos, and cyans palettes.insert( "house-music-party".to_string(), - vec![300, 285, 270, 255, 240, 195, 180], + vec![ + [255, 0, 255], // magenta + [191, 0, 255], // purple + [128, 0, 255], // violet + [64, 0, 255], // indigo + [0, 0, 255], // blue + [0, 191, 255], // deep sky blue + [0, 255, 255], // cyan + ], ); + // Cyans, teals, greens, and limes palettes.insert( "tropical-beach".to_string(), - vec![180, 175, 170, 160, 90, 75, 60], + vec![ + [0, 255, 255], // cyan + [0, 255, 234], // turquoise + [0, 255, 213], // aquamarine + [0, 255, 170], // spring green + [128, 255, 0], // lime + [191, 255, 0], // chartreuse + [255, 255, 0], // yellow + ], ); - palettes.insert("fire".to_string(), vec![0, 10, 20, 30, 40, 50, 60]); + // Reds through oranges to yellow + palettes.insert( + "fire".to_string(), + vec![ + [255, 0, 0], // red + [255, 43, 0], // scarlet + [255, 85, 0], // vermillion + [255, 128, 0], // orange + [255, 170, 0], // amber + [255, 213, 0], // golden + [255, 255, 0], // yellow + ], + ); - palettes.insert("forest".to_string(), vec![90, 100, 110, 120, 130, 140, 150]); + // Chartreuse through greens to teal + palettes.insert( + "forest".to_string(), + vec![ + [128, 255, 0], // lime + [85, 255, 0], // green-yellow + [43, 255, 0], // lawn green + [0, 255, 0], // green + [0, 255, 43], // emerald + [0, 255, 85], // jade + [0, 255, 128], // mint + ], + ); - palettes.insert("neon-rainbow".to_string(), vec![0, 60, 120, 180, 240, 300]); + // Full spectrum rainbow + palettes.insert( + "neon-rainbow".to_string(), + vec![ + [255, 0, 0], // red + [255, 255, 0], // yellow + [0, 255, 0], // green + [0, 255, 255], // cyan + [0, 0, 255], // blue + [255, 0, 255], // magenta + ], + ); + // Pinks from deep to light rose palettes.insert( "pink-dreams".to_string(), - vec![320, 325, 330, 335, 340, 345, 350], + vec![ + [255, 0, 170], // deep pink + [255, 0, 149], // hot pink + [255, 0, 128], // pink + [255, 0, 106], // rose + [255, 0, 85], // fuchsia-rose + [255, 0, 64], // crimson-rose + [255, 0, 43], // warm rose + ], ); + // Cool blue spectrum palettes.insert( "cool-blues".to_string(), - vec![190, 200, 210, 220, 230, 240, 250], + vec![ + [0, 170, 255], // sky blue + [0, 128, 255], // azure + [0, 85, 255], // royal blue + [0, 43, 255], // cobalt + [0, 0, 255], // blue + [43, 0, 255], // indigo + [64, 0, 255], // deep indigo + ], ); + // Teenage Mutant Ninja Turtles: character bandana colors + turtle green palettes.insert( "tmnt".to_string(), - vec![125, 130, 240, 245, 25, 30, 0, 5, 280, 285], + vec![ + [0, 200, 0], // turtle green + [0, 80, 255], // Leonardo blue + [128, 0, 255], // Donatello purple + [255, 128, 0], // Michelangelo orange + [255, 0, 0], // Raphael red + [0, 255, 64], // sewer green + [0, 128, 255], // Leonardo azure + [170, 0, 255], // Donatello violet + [255, 170, 0], // Michelangelo amber + [255, 43, 0], // Raphael scarlet + ], ); - palettes.insert("christmas".to_string(), vec![360, 0, 120, 5, 125, 5, 360]); + // Red, white, and green + palettes.insert( + "christmas".to_string(), + vec![ + [255, 0, 0], // red + [255, 0, 0], // red + [0, 255, 0], // green + [255, 21, 0], // warm red + [0, 255, 43], // festive green + [255, 21, 0], // warm red + [255, 255, 255], // white + ], + ); palettes } diff --git a/src/panic.rs b/src/panic.rs index fa7a307..4da3f2c 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -2,7 +2,7 @@ use crate::constants; use ratatui::crossterm::{execute, terminal}; use std::{ backtrace, fs, - io::{stdout, Write}, + io::{Write, stdout}, }; /// Registers a custom panic hook to handle application crashes gracefully. diff --git a/src/processing.rs b/src/processing.rs index f12860b..0da0e79 100644 --- a/src/processing.rs +++ b/src/processing.rs @@ -1,6 +1,5 @@ use crate::utils; use num_complex::Complex32; -use palette::Hwb; /// Recursive in-place radix-2 Cooley-Tukey FFT implementation. /// @@ -65,34 +64,37 @@ pub fn process(samples: Vec, gain: f32) -> Vec { .collect::>() } -/// Updates the blackness component of HWB colors based on audio frequency spectrum for animated visualization. +/// Updates per-panel brightness values based on audio frequency spectrum for animated visualization. /// -/// Divides the frequency range [min_freq, max_freq] into `colors.len()` logarithmic intervals. -/// For each interval, tracks maximum amplitude (with equal loudness correction) and updates blackness +/// Divides the frequency range [min_freq, max_freq] into `brightness.len()` logarithmic intervals. +/// For each interval, tracks maximum amplitude (with equal loudness correction) and updates brightness /// with velocity-based decay/increase for smooth transitions. Uses cubic easing functions for rates. /// +/// Brightness is a multiplier in [0,1] applied to the base Oklch color's lightness. +/// At 0 the panel is black; at 1 it shows the exact target color the user specified. +/// /// # Arguments /// /// * `spectrum` - FFT-derived amplitudes for frequency bins. /// * `hz_per_bin` - Frequency resolution (Hz per bin in spectrum). /// * `min_freq`/`max_freq` - Frequency range to consider for color mapping. -/// * `colors` - Mutable slice of HWB colors; updates their blackness [0,1] (1=white, 0=saturated). +/// * `brightness` - Mutable slice of brightness multipliers [0,1] per panel (mutated). /// * `prev_max` - Previous max amplitudes per interval for delta computation (mutated). -/// * `speed` - Velocity accumulators for blackness changes per interval (mutated). +/// * `speed` - Velocity accumulators for brightness changes per interval (mutated). /// /// # Panics /// /// May panic if spectrum length insufficient or invalid frequency params. -pub fn update_colors( +pub fn update_brightness( spectrum: Vec, hz_per_bin: u32, min_freq: u16, max_freq: u16, - colors: &mut [Hwb], + brightness: &mut [f32], prev_max: &mut [f32], speed: &mut [f32], ) { - let n_panels = colors.len(); + let n_panels = brightness.len(); let (min_freq, max_freq) = (min_freq as f32, max_freq as f32); let multiplier = (max_freq / min_freq).powf(1.0 / n_panels as f32); let (mut intervals, mut cutoff) = (Vec::new(), min_freq); @@ -108,13 +110,24 @@ pub fn update_colors( let cur_freq = (i as u32) * hz_per_bin + hz_per_bin / 2; if cur_freq > intervals[cur_interval] || i == n_bins - 1 { let ampl_delta = cur_max - prev_max[cur_interval]; - if ampl_delta.is_sign_positive() { - speed[cur_interval] = -rate_func_inc(ampl_delta); + if ampl_delta > 0.0 { + // Audio getting louder → increase brightness + speed[cur_interval] = rate_func_inc(ampl_delta); + } else if cur_max > 0.01 { + // Audio getting quieter but still present → normal decay + speed[cur_interval] = -(rate_func_dec(-ampl_delta).max(0.01)); } else { - speed[cur_interval] = rate_func_dec(-ampl_delta).max(1e-4); + // Audio is essentially silent → strong decay so panels actually go dark + // Without this, the tiny 1e-4 floor makes panels take minutes to fade out + speed[cur_interval] = -(0.15_f32.max(brightness[cur_interval] * 0.3)); + } + brightness[cur_interval] = + (brightness[cur_interval] + speed[cur_interval]).clamp(0.0, 1.0); + + // Floor very small values to true zero so panels go fully dark + if brightness[cur_interval] < 0.005 { + brightness[cur_interval] = 0.0; } - colors[cur_interval].blackness = - (colors[cur_interval].blackness + speed[cur_interval]).clamp(0.0, 1.0); prev_max[cur_interval] = cur_max; cur_max = 0.0; @@ -126,3 +139,164 @@ pub fn update_colors( cur_max = cur_max.max(utils::equalize(ampl, cur_freq).min(1.0)); } } + +/// Updates per-panel brightness using an energy wave / cascade animation. +/// +/// Instead of each panel independently tracking a frequency band (like `update_brightness`), +/// this effect creates a traveling wave of light across the panels: +/// +/// 1. Compute overall audio energy from the full spectrum (max equalized amplitude). +/// 2. Cascade: shift each panel's brightness to the next panel (with per-step decay). +/// 3. Feed new energy into the first panel with smooth attack/decay. +/// +/// The result is a flowing ripple of light that propagates across panels, +/// with intensity driven by audio amplitude. Visually very different from the +/// per-band spectrum effect. +/// +/// # Arguments +/// +/// * `spectrum` - FFT-derived amplitudes for frequency bins. +/// * `hz_per_bin` - Frequency resolution (Hz per bin in spectrum). +/// * `min_freq`/`max_freq` - Frequency range to consider. +/// * `brightness` - Mutable slice of brightness multipliers [0,1] per panel (mutated). +/// * `prev_max` - Previous overall energy for delta computation (only index 0 used). +/// * `speed` - Velocity accumulator for the lead panel's brightness (only index 0 used). +pub fn update_brightness_wave( + spectrum: Vec, + hz_per_bin: u32, + min_freq: u16, + max_freq: u16, + brightness: &mut [f32], + prev_max: &mut [f32], + speed: &mut [f32], +) { + let n_panels = brightness.len(); + if n_panels == 0 { + return; + } + let (min_freq, max_freq) = (min_freq as u32, max_freq as u32); + + // 1. Compute overall audio energy: max equalized amplitude in the frequency range + let mut overall_energy = 0.0_f32; + for (i, &l) in spectrum.iter().enumerate() { + let cur_freq = (i as u32) * hz_per_bin + hz_per_bin / 2; + if cur_freq < min_freq { + continue; + } + if cur_freq > max_freq { + break; + } + overall_energy = overall_energy.max(utils::equalize(ampl, cur_freq).min(1.0)); + } + + // 2. Cascade: shift brightness values from left to right with per-step decay + // This creates the traveling wave — each panel inherits its left neighbor's value + let cascade_decay = 0.92; + for i in (1..n_panels).rev() { + brightness[i] = brightness[i - 1] * cascade_decay; + } + + // 3. Feed new energy into the lead panel (index 0) with smooth attack/decay + let rate_func_inc = |x: f32| -> f32 { 1.0 - (1.0 - x).powi(3) }; + let rate_func_dec = |x: f32| -> f32 { 0.9 * (1.0 - (1.0 - x).powi(4)) }; + + let energy_delta = overall_energy - prev_max[0]; + if energy_delta > 0.0 { + speed[0] = rate_func_inc(energy_delta); + } else if overall_energy > 0.01 { + // Audio getting quieter but still present → normal decay + speed[0] = -(rate_func_dec(-energy_delta).max(0.01)); + } else { + // Audio is essentially silent → strong decay so panels actually go dark + speed[0] = -(0.15_f32.max(brightness[0] * 0.3)); + } + brightness[0] = (brightness[0] + speed[0]).clamp(0.0, 1.0); + prev_max[0] = overall_energy; + + // Floor very small values to true zero so panels go fully dark + for b in brightness.iter_mut() { + if *b < 0.005 { + *b = 0.0; + } + } +} + +/// Updates all panels with a unified beat-reactive pulse animation. +/// +/// All panels flash together on audio transients (beats, hits, kicks) and +/// fade quickly between them. The music's own rhythm drives the animation — +/// no fixed-rate oscillation. +/// +/// Uses asymmetric attack/decay with signal boost: +/// - **Boost**: `sqrt(energy)` expands the dynamic range so moderate audio +/// levels (0.25 → 0.5, 0.5 → 0.71) still produce visible flashes. +/// - **Attack**: When boosted energy exceeds current brightness, snap to it +/// almost instantly (90% of the gap per frame). Every kick/snare/transient +/// produces an immediate flash. +/// - **Decay**: When energy drops, brightness decays exponentially (×0.72 +/// per frame ≈ 250ms to half-brightness at 5.3 fps). The fast falloff +/// creates strong contrast between beats — panels go noticeably dark +/// before the next hit lands. +/// +/// State layout: +/// - `prev_max[0]`: current display brightness level (smoothed) +/// +/// # Arguments +/// +/// * `spectrum` - FFT-derived amplitudes for frequency bins. +/// * `hz_per_bin` - Frequency resolution (Hz per bin in spectrum). +/// * `min_freq`/`max_freq` - Frequency range to consider. +/// * `brightness` - Mutable slice of brightness multipliers [0,1] per panel (all set to same value). +/// * `prev_max` - Index 0 stores the current pulse brightness across frames. +/// * `speed` - Unused by this effect (reserved for compatibility). +pub fn update_brightness_pulse( + spectrum: Vec, + hz_per_bin: u32, + min_freq: u16, + max_freq: u16, + brightness: &mut [f32], + prev_max: &mut [f32], + _speed: &mut [f32], +) { + if brightness.is_empty() { + return; + } + let (min_freq, max_freq) = (min_freq as u32, max_freq as u32); + + // 1. Compute overall audio energy: max equalized amplitude in the frequency range + let mut overall_energy = 0.0_f32; + for (i, &l) in spectrum.iter().enumerate() { + let cur_freq = (i as u32) * hz_per_bin + hz_per_bin / 2; + if cur_freq < min_freq { + continue; + } + if cur_freq > max_freq { + break; + } + overall_energy = overall_energy.max(utils::equalize(ampl, cur_freq).min(1.0)); + } + + // 2. Boost signal: sqrt expands the dynamic range so moderate levels produce visible pulses + // Without this, typical music energy (0.2-0.5) barely lights the panels + let boosted = overall_energy.sqrt(); + + // 3. Asymmetric attack/decay for punchy beat-reactive pulse + if boosted > prev_max[0] { + // Near-instant attack: snap 90% of the gap per frame + // Every transient produces an immediate flash + prev_max[0] += 0.9 * (boosted - prev_max[0]); + } else { + // Fast exponential decay between beats + // 0.72 per frame at ~5.3 fps ≈ 250ms to half-brightness + // Creates strong contrast so the next beat has real impact + prev_max[0] *= 0.72; + } + + // Floor very small values to true zero so panels go fully dark + if prev_max[0] < 0.005 { + prev_max[0] = 0.0; + } + + // 4. All panels pulse together at the same brightness + brightness.fill(prev_max[0]); +} diff --git a/src/utils.rs b/src/utils.rs index aa5b81d..decb73e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,20 +1,20 @@ -use anyhow::{bail, Result}; -use palette::rgb::Srgb; +use anyhow::{Result, bail}; +use palette::{IntoColor, Oklch, Srgb}; use ratatui::{ - backend::{Backend, CrosstermBackend}, + Terminal, + backend::CrosstermBackend, crossterm::{ event::{self, Event}, execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }, style::{Color, Stylize}, text::Line, - Terminal, }; use reqwest::blocking::Client; use std::{ fmt::Display, - io::{self, stdout, Write}, + io::{self, Stdout, Write, stdout}, net::Ipv4Addr, }; @@ -45,15 +45,18 @@ pub fn choose_ip(v: &[impl Display]) -> Result { } let n = v.len(); loop { - print!("Choose an option (by entering its number), enter 'M' to provide the IP adress manually or enter 'Q' to quit: "); + print!( + "Choose an option (by entering its number), enter 'M' to provide the IP adress manually or enter 'Q' to quit: " + ); io::stdout().flush()?; let mut input = String::new(); match io::stdin().read_line(&mut input) { Ok(_) => { - if let Ok(x) = input.trim().parse::() { - if x >= 1 && x <= n { - return Ok(Choice::Automatic(x - 1)); - } + if let Ok(x) = input.trim().parse::() + && x >= 1 + && x <= n + { + return Ok(Choice::Automatic(x - 1)); } if input.trim() == "M" { return Ok(Choice::Manual); @@ -125,8 +128,8 @@ pub fn wait_for_any_key() -> Result<()> { /// /// # Returns /// -/// `Result>>` wrapped in impl Backend. -pub fn init_tui() -> Result> { +/// `Result>>`. +pub fn init_tui() -> Result>> { enable_raw_mode()?; execute!(stdout(), EnterAlternateScreen)?; Terminal::new(CrosstermBackend::new(stdout())).map_err(anyhow::Error::from) @@ -226,23 +229,24 @@ pub fn request_get(url: &str) -> Result { Ok(res.text()?.to_string()) } -/// Generates HWB colors from a list of hues, expanding or truncating to exact count `n`. -/// -/// Repeats last hue if fewer than `n`, cycles if more? No, resizes vec from hues slice. -/// Hue=360 special-cased as white (H=0.1, W=1.0, B=0.0); others H=hue f32, W=0, B=1 (saturated full brightness). -/// -/// Used to map palette hues to panel colors in visualizer. -pub fn colors_from_hues(hues: &[u16], n: usize) -> Vec { - let mut res = Vec::from(hues); - res.resize(n, *hues.last().unwrap()); - res.into_iter() - .map(|hue| { - // Use special hue value 360 to represent white (high whiteness) - if hue == 360 { - palette::Hwb::new(0.1, 1.0, 0.0) // White: any hue, high whiteness, starts black - } else { - palette::Hwb::new(hue as f32, 0.0, 1.0) // Normal colors: no whiteness - } +/// Generates Oklch base colors from a list of RGB values, expanding or truncating to exact count `n`. +/// +/// Repeats last color if fewer than `n`. +/// Each RGB triplet is converted to Oklch preserving the perceptually correct hue, chroma, and lightness. +/// The returned colors represent the **target** appearance at full brightness — the visualizer +/// animates a separate brightness multiplier [0,1] that scales lightness, ensuring the original +/// RGB color is faithfully reproduced at peak audio amplitude. +/// +/// Used to map palette colors to panel base colors in visualizer. +pub fn colors_from_rgb(rgb_colors: &[[u8; 3]], n: usize) -> Vec { + // Spread colors evenly across n panels. With 4 colors and 12 panels + // each color covers 3 panels instead of the last color filling 9. + (0..n) + .map(|i| { + let color_idx = i * rgb_colors.len() / n; + let [r, g, b] = rgb_colors[color_idx]; + let srgb = Srgb::new(r, g, b).into_format::(); + srgb.into_color() }) .collect() } diff --git a/src/visualizer.rs b/src/visualizer.rs index e821e27..c6da7e0 100644 --- a/src/visualizer.rs +++ b/src/visualizer.rs @@ -1,13 +1,14 @@ use crate::{ audio::AudioStream, - config::VisualizerConfig, + config::{Effect, VisualizerConfig}, constants, nanoleaf::{self, NlDevice, NlUdp}, panic, processing, utils, }; use anyhow::Result; -use cpal::{traits::*, InputCallbackInfo, SampleFormat, SizedSample}; +use cpal::{InputCallbackInfo, SampleFormat, SizedSample, traits::*}; use dasp_sample::conv::ToSample; +use palette::Oklch; use std::{ sync::mpsc::{self, TryRecvError}, thread, @@ -27,7 +28,9 @@ pub enum VisualizerMsg { Resume, End, SetGain(f32), - SetPalette(Vec), + SetPalette(Vec<[u8; 3]>), + SetEffect(Effect), + ResetPanels, SetSorting { primary_axis: crate::config::Axis, sort_primary: crate::config::Sort, @@ -38,14 +41,16 @@ pub enum VisualizerMsg { pub struct Visualizer { state: VisualizerState, + nl_device: NlDevice, nl_udp: NlUdp, audio_stream: AudioStream, gain: f32, time_window: f32, - trans_time: i16, + trans_time: u16, min_freq: u16, max_freq: u16, - hues: Vec, + hues: Vec<[u8; 3]>, + effect: Effect, } impl Visualizer { @@ -94,9 +99,13 @@ impl Visualizer { .transition_time .unwrap_or(constants::DEFAULT_TRANSITION_TIME); let (min_freq, max_freq) = config.freq_range.unwrap_or(constants::DEFAULT_FREQ_RANGE); - let hues = config.hues.unwrap_or(Vec::from(constants::DEFAULT_HUES)); + let hues = config + .colors + .unwrap_or(Vec::from(constants::DEFAULT_COLORS)); + let effect = config.effect.unwrap_or_default(); Ok(Visualizer { state, + nl_device: nl_device.clone(), nl_udp, audio_stream, gain, @@ -105,28 +114,67 @@ impl Visualizer { min_freq, max_freq, hues, + effect, }) } + /// Sends a UDP frame setting all panels to black with instant transition. + /// + /// Used when starting the visualizer or changing effects/palettes to clear + /// any lingering colors from the previous state before the new effect begins. + fn send_black_frame(&self, n_panels: usize) { + let black = vec![Oklch::new(0.0, 0.0, 0.0); n_panels]; + let _ = self.nl_udp.update_panels(&black, 0); + } + /// Updates visualizer internal state or parameters based on received message. /// /// Dispatched in processing thread loop. - /// Modifies self fields and/or regenerates `colors` vector from hues. + /// Modifies self fields and/or regenerates `base_colors` vector from hues. + /// When palette or sorting changes, brightness is reset to 0 so panels start dark. /// For SetSorting, updates UDP panel order with orientation. /// /// # Arguments /// /// * `event` - Control message type. - /// * `colors` - Mutable reference to current HWB color array for panels (updated if palette/sort changes). - fn update_state(&mut self, event: VisualizerMsg, colors: &mut Vec) { + /// * `base_colors` - Mutable reference to base Oklch colors with original lightness (updated if palette/sort changes). + /// * `brightness` - Mutable reference to per-panel brightness multipliers (reset on palette/sort changes). + /// * `prev_max` - Mutable reference to previous max amplitudes (reset on effect change). + /// * `speed` - Mutable reference to velocity/phase accumulators (reset on effect change). + fn update_state( + &mut self, + event: VisualizerMsg, + base_colors: &mut Vec, + brightness: &mut Vec, + prev_max: &mut [f32], + speed: &mut [f32], + ) { match event { VisualizerMsg::Resume => self.state = VisualizerState::Running, VisualizerMsg::Pause => self.state = VisualizerState::Paused, VisualizerMsg::End => self.state = VisualizerState::Done, VisualizerMsg::SetGain(gain) => self.gain = gain, - VisualizerMsg::SetPalette(hues) => { - self.hues = hues; - *colors = utils::colors_from_hues(&self.hues, colors.len()); + VisualizerMsg::SetEffect(effect) => { + self.effect = effect; + brightness.fill(0.0); + // Reset state arrays — each effect uses prev_max/speed differently + prev_max.fill(0.0); + speed.fill(0.0); + // Immediately send a black frame so the old effect's colors don't linger + self.send_black_frame(base_colors.len()); + } + VisualizerMsg::SetPalette(new_colors) => { + self.hues = new_colors; + *base_colors = utils::colors_from_rgb(&self.hues, base_colors.len()); + brightness.fill(0.0); + // Immediately send a black frame so the old palette's colors don't linger + self.send_black_frame(base_colors.len()); + } + VisualizerMsg::ResetPanels => { + brightness.fill(0.0); + prev_max.fill(0.0); + speed.fill(0.0); + self.send_black_frame(base_colors.len()); } VisualizerMsg::SetSorting { primary_axis, @@ -140,7 +188,11 @@ impl Visualizer { Some(sort_secondary), global_orientation, ); - *colors = utils::colors_from_hues(&self.hues, self.nl_udp.panels.len()); + *base_colors = utils::colors_from_rgb(&self.hues, self.nl_udp.panels.len()); + brightness.resize(base_colors.len(), 0.0); + brightness.fill(0.0); + // Immediately send a black frame so the old sort order's colors don't linger + self.send_black_frame(base_colors.len()); } } } @@ -192,8 +244,10 @@ impl Visualizer { /// Spawns thread that: /// - Registers panic handler. /// - Loops receiving audio samples and control messages. - /// - Processes FFT spectrum, updates colors with gain/time_window/equalize. - /// - Applies sorting and sends HWB colors to panels via UDP with transition time. + /// - Processes FFT spectrum, updates per-panel brightness multiplier [0,1] from audio. + /// - Computes display colors: for each panel, scales base Oklch lightness by brightness, + /// so at peak audio the output exactly matches the user's original RGB palette color. + /// - Sends display colors to panels via UDP with transition time. /// - Handles pause/resume/end states. /// /// Returns sender for sending `VisualizerMsg` to control runtime behavior. @@ -236,19 +290,37 @@ impl Visualizer { stream.play().expect("running the audio stream failed"); let n = self.nl_udp.panels.len(); - let sample_rate = self.audio_stream.stream_config.sample_rate.0; - let mut colors = utils::colors_from_hues(&self.hues, n); + let sample_rate = self.audio_stream.stream_config.sample_rate; + // Base colors hold the target Oklch values (with original lightness from the user's RGB) + let mut base_colors = utils::colors_from_rgb(&self.hues, n); + // Brightness multiplier [0,1] per panel — animated by audio amplitude + // At 0 the panel is black; at 1 it shows the exact target color + let mut brightness = vec![0.0_f32; n]; let mut prev_max = vec![0.0; n]; let mut speed = vec![0.0; n]; + // Clear any colors left over from a previous Nanoleaf scene or effect + self.send_black_frame(n); loop { match self.state { VisualizerState::Done => break, VisualizerState::Paused => { let event = rx_events.recv().expect("events sender disconnected"); - self.update_state(event, &mut colors); + self.update_state( + event, + &mut base_colors, + &mut brightness, + &mut prev_max, + &mut speed, + ); } VisualizerState::Running => match rx_events.try_recv() { - Ok(event) => self.update_state(event, &mut colors), + Ok(event) => self.update_state( + event, + &mut base_colors, + &mut brightness, + &mut prev_max, + &mut speed, + ), Err(err) => { if err == TryRecvError::Disconnected { panic!("events sender disconnected"); @@ -264,18 +336,52 @@ impl Visualizer { } let spectrum = processing::process(samples, self.gain); let hz_per_bin = (sample_rate / 2) / (spectrum.len() as u32); - processing::update_colors( - spectrum, - hz_per_bin, - self.min_freq, - self.max_freq, - &mut colors, - &mut prev_max, - &mut speed, - ); - self.nl_udp - .update_panels(&colors, self.trans_time) - .expect("updating panels failed"); + match self.effect { + Effect::Spectrum => processing::update_brightness( + spectrum, + hz_per_bin, + self.min_freq, + self.max_freq, + &mut brightness, + &mut prev_max, + &mut speed, + ), + Effect::EnergyWave => processing::update_brightness_wave( + spectrum, + hz_per_bin, + self.min_freq, + self.max_freq, + &mut brightness, + &mut prev_max, + &mut speed, + ), + Effect::Pulse => processing::update_brightness_pulse( + spectrum, + hz_per_bin, + self.min_freq, + self.max_freq, + &mut brightness, + &mut prev_max, + &mut speed, + ), + } + // Compute display colors: scale base lightness by brightness multiplier + // This ensures at brightness=1.0, the output exactly matches the user's original RGB + let display_colors: Vec = base_colors + .iter() + .zip(brightness.iter()) + .map(|(base, &b)| Oklch::new(base.l * b, base.chroma, base.hue)) + .collect(); + if self + .nl_udp + .update_panels(&display_colors, self.trans_time) + .is_err() + { + // UDP send failed (e.g. extControl timed out) — re-request and retry once + if self.nl_device.request_udp_control().is_ok() { + let _ = self.nl_udp.update_panels(&display_colors, self.trans_time); + } + } } });