diff --git a/Assets/.DS_Store b/Assets/.DS_Store
new file mode 100644
index 0000000..8cafa7c
Binary files /dev/null and b/Assets/.DS_Store differ
diff --git a/Assets/Demo.mov b/Assets/Demo.mov
new file mode 100644
index 0000000..2ce1990
Binary files /dev/null and b/Assets/Demo.mov differ
diff --git a/Assets/audioleaf-iOS-Default-1024x1024@1x.png b/Assets/audioleaf-iOS-Default-1024x1024@1x.png
new file mode 100644
index 0000000..e2d6b04
Binary files /dev/null and b/Assets/audioleaf-iOS-Default-1024x1024@1x.png differ
diff --git a/Assets/audioleaf-icon.svg b/Assets/audioleaf-icon.svg
new file mode 100644
index 0000000..9afc716
--- /dev/null
+++ b/Assets/audioleaf-icon.svg
@@ -0,0 +1,33 @@
+
diff --git a/Assets/audioleaf.icon/Assets/audioleaf-icon.svg b/Assets/audioleaf.icon/Assets/audioleaf-icon.svg
new file mode 100644
index 0000000..9afc716
--- /dev/null
+++ b/Assets/audioleaf.icon/Assets/audioleaf-icon.svg
@@ -0,0 +1,33 @@
+
diff --git a/Assets/audioleaf.icon/icon.json b/Assets/audioleaf.icon/icon.json
new file mode 100644
index 0000000..011195b
--- /dev/null
+++ b/Assets/audioleaf.icon/icon.json
@@ -0,0 +1,84 @@
+{
+ "fill-specializations" : [
+ {
+ "value" : {
+ "linear-gradient" : [
+ "display-p3:0.07092,1.00000,0.12784,1.00000",
+ "display-p3:0.23472,0.23753,0.96783,1.00000"
+ ],
+ "orientation" : {
+ "start" : {
+ "x" : 0.5,
+ "y" : 0
+ },
+ "stop" : {
+ "x" : 0.5,
+ "y" : 0.7
+ }
+ }
+ }
+ },
+ {
+ "appearance" : "dark",
+ "value" : {
+ "linear-gradient" : [
+ "display-p3:0.93358,0.26816,1.00000,1.00000",
+ "display-p3:0.26882,0.96783,0.82718,1.00000"
+ ],
+ "orientation" : {
+ "start" : {
+ "x" : 0.5,
+ "y" : 0
+ },
+ "stop" : {
+ "x" : 0.5,
+ "y" : 0.7
+ }
+ }
+ }
+ }
+ ],
+ "groups" : [
+ {
+ "layers" : [
+ {
+ "blend-mode-specializations" : [
+ {
+ "appearance" : "tinted",
+ "value" : "normal"
+ }
+ ],
+ "fill-specializations" : [
+ {
+ "appearance" : "tinted",
+ "value" : "automatic"
+ }
+ ],
+ "image-name" : "audioleaf-icon.svg",
+ "name" : "audioleaf-icon",
+ "position" : {
+ "scale" : 2.75,
+ "translation-in-points" : [
+ -38.89999999999998,
+ -14.71875
+ ]
+ }
+ }
+ ],
+ "shadow" : {
+ "kind" : "neutral",
+ "opacity" : 0.5
+ },
+ "translucency" : {
+ "enabled" : true,
+ "value" : 0.5
+ }
+ }
+ ],
+ "supported-platforms" : {
+ "circles" : [
+ "watchOS"
+ ],
+ "squares" : "shared"
+ }
+}
\ No newline at end of file
diff --git a/Assets/demo.gif b/Assets/demo.gif
new file mode 100644
index 0000000..bb0dbd6
Binary files /dev/null and b/Assets/demo.gif differ
diff --git a/Assets/gui.png b/Assets/gui.png
new file mode 100644
index 0000000..229e7fe
Binary files /dev/null and b/Assets/gui.png differ
diff --git a/Assets/icon_16.png b/Assets/icon_16.png
new file mode 100644
index 0000000..3f41b8d
Binary files /dev/null and b/Assets/icon_16.png differ
diff --git a/Assets/icon_32.png b/Assets/icon_32.png
new file mode 100644
index 0000000..b21f57b
Binary files /dev/null and b/Assets/icon_32.png differ
diff --git a/Assets/icon_64.png b/Assets/icon_64.png
new file mode 100644
index 0000000..866aaca
Binary files /dev/null and b/Assets/icon_64.png differ
diff --git a/Cargo.lock b/Cargo.lock
index 8387c67..e40080d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,15 +8,6 @@ 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"
@@ -110,15 +101,6 @@ 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"
@@ -127,7 +109,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "audioleaf"
-version = "3.5.0"
+version = "4.0.0"
dependencies = [
"anyhow",
"auto-palette",
@@ -142,7 +124,6 @@ dependencies = [
"objc2-foundation",
"palette",
"pollster",
- "ratatui",
"reqwest",
"serde",
"serde_json",
@@ -195,21 +176,6 @@ 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"
@@ -222,15 +188,6 @@ 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"
@@ -276,15 +233,6 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
-[[package]]
-name = "castaway"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
-dependencies = [
- "rustversion",
-]
-
[[package]]
name = "cc"
version = "1.2.57"
@@ -346,7 +294,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
[[package]]
@@ -386,29 +334,6 @@ dependencies = [
"memchr",
]
-[[package]]
-name = "compact_str"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
-dependencies = [
- "castaway",
- "cfg-if",
- "itoa",
- "rustversion",
- "ryu",
- "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"
@@ -479,15 +404,6 @@ dependencies = [
"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"
@@ -497,140 +413,12 @@ dependencies = [
"cfg-if",
]
-[[package]]
-name = "crossterm"
-version = "0.29.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
-dependencies = [
- "bitflags 2.11.0",
- "crossterm_winapi",
- "derive_more",
- "document-features",
- "mio",
- "parking_lot",
- "rustix",
- "signal-hook",
- "signal-hook-mio",
- "winapi",
-]
-
-[[package]]
-name = "crossterm_winapi"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
-dependencies = [
- "winapi",
-]
-
-[[package]]
-name = "crypto-common"
-version = "0.1.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.23.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
-dependencies = [
- "darling_core",
- "darling_macro",
-]
-
-[[package]]
-name = "darling_core"
-version = "0.23.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
-dependencies = [
- "ident_case",
- "proc-macro2",
- "quote",
- "strsim",
- "syn 2.0.117",
-]
-
-[[package]]
-name = "darling_macro"
-version = "0.23.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
-dependencies = [
- "darling_core",
- "quote",
- "syn 2.0.117",
-]
-
[[package]]
name = "dasp_sample"
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"
@@ -670,16 +458,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
- "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",
+ "syn",
]
[[package]]
@@ -688,12 +467,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
-[[package]]
-name = "either"
-version = "1.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
-
[[package]]
name = "encoding_rs"
version = "0.8.35"
@@ -709,35 +482,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
-[[package]]
-name = "errno"
-version = "0.3.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
-dependencies = [
- "libc",
- "windows-sys 0.61.2",
-]
-
-[[package]]
-name = "euclid"
-version = "0.22.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63"
-dependencies = [
- "num-traits",
-]
-
-[[package]]
-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"
@@ -753,35 +497,12 @@ 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.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 = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
-
[[package]]
name = "flate2"
version = "1.1.9"
@@ -804,12 +525,6 @@ 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"
@@ -884,16 +599,6 @@ dependencies = [
"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.17"
@@ -916,24 +621,11 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
- "r-efi 5.3.0",
+ "r-efi",
"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 6.0.0",
- "wasip2",
- "wasip3",
-]
-
[[package]]
name = "glam"
version = "0.27.0"
@@ -967,7 +659,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
- "foldhash 0.1.5",
+ "foldhash",
]
[[package]]
@@ -975,11 +667,6 @@ 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"
@@ -987,12 +674,6 @@ 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"
@@ -1176,18 +857,6 @@ 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"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
-
[[package]]
name = "idna"
version = "1.1.0"
@@ -1256,30 +925,6 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
- "serde",
- "serde_core",
-]
-
-[[package]]
-name = "indoc"
-version = "2.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
-dependencies = [
- "rustversion",
-]
-
-[[package]]
-name = "instability"
-version = "0.3.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
-dependencies = [
- "darling",
- "indoc",
- "proc-macro2",
- "quote",
- "syn 2.0.117",
]
[[package]]
@@ -1304,15 +949,6 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
-[[package]]
-name = "itertools"
-version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
-dependencies = [
- "either",
-]
-
[[package]]
name = "itoa"
version = "1.0.17"
@@ -1361,35 +997,6 @@ dependencies = [
"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.183"
@@ -1406,40 +1013,10 @@ dependencies = [
]
[[package]]
-name = "line-clipping"
-version = "0.3.5"
+name = "litemap"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a"
-dependencies = [
- "bitflags 2.11.0",
-]
-
-[[package]]
-name = "linux-raw-sys"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
-
-[[package]]
-name = "litemap"
-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"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
-dependencies = [
- "scopeguard",
-]
+checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "log"
@@ -1447,31 +1024,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
-[[package]]
-name = "lru"
-version = "0.16.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
-dependencies = [
- "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.5.0"
@@ -1516,33 +1074,12 @@ 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 = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
-dependencies = [
- "autocfg",
-]
-
[[package]]
name = "mime"
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"
@@ -1572,7 +1109,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
- "log",
"wasi",
"windows-sys 0.61.2",
]
@@ -1622,29 +1158,6 @@ 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"
@@ -1654,12 +1167,6 @@ 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"
@@ -1668,7 +1175,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
[[package]]
@@ -1699,16 +1206,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "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",
+ "syn",
]
[[package]]
@@ -1833,15 +1331,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
-[[package]]
-name = "ordered-float"
-version = "4.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
-dependencies = [
- "num-traits",
-]
-
[[package]]
name = "palette"
version = "0.7.6"
@@ -1863,30 +1352,7 @@ dependencies = [
"by_address",
"proc-macro2",
"quote",
- "syn 2.0.117",
-]
-
-[[package]]
-name = "parking_lot"
-version = "0.12.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
-dependencies = [
- "lock_api",
- "parking_lot_core",
-]
-
-[[package]]
-name = "parking_lot_core"
-version = "0.9.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
-dependencies = [
- "cfg-if",
- "libc",
- "redox_syscall",
- "smallvec",
- "windows-link",
+ "syn",
]
[[package]]
@@ -1895,49 +1361,6 @@ 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"
@@ -1948,16 +1371,6 @@ 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"
@@ -1978,7 +1391,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
[[package]]
@@ -2040,12 +1453,6 @@ 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"
@@ -2055,12 +1462,6 @@ 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"
@@ -2070,16 +1471,6 @@ 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.5.0"
@@ -2187,12 +1578,6 @@ 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"
@@ -2237,100 +1622,6 @@ dependencies = [
"getrandom 0.3.4",
]
-[[package]]
-name = "ratatui"
-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 = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293"
-dependencies = [
- "bitflags 2.11.0",
- "compact_str",
- "hashbrown 0.16.1",
- "indoc",
- "itertools",
- "kasuari",
- "lru",
- "strum",
- "thiserror 2.0.18",
- "unicode-segmentation",
- "unicode-truncate",
- "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]]
-name = "redox_syscall"
-version = "0.5.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
-dependencies = [
- "bitflags 2.11.0",
-]
-
[[package]]
name = "redox_users"
version = "0.5.2"
@@ -2342,35 +1633,6 @@ dependencies = [
"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.13.2"
@@ -2433,28 +1695,6 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
-[[package]]
-name = "rustc_version"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
-dependencies = [
- "semver",
-]
-
-[[package]]
-name = "rustix"
-version = "1.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
-dependencies = [
- "bitflags 2.11.0",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.61.2",
-]
-
[[package]]
name = "rustls"
version = "0.23.37"
@@ -2536,12 +1776,6 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
-[[package]]
-name = "ryu"
-version = "1.0.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
-
[[package]]
name = "same-file"
version = "1.0.6"
@@ -2560,12 +1794,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "scopeguard"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
-
[[package]]
name = "security-framework"
version = "3.7.0"
@@ -2589,12 +1817,6 @@ dependencies = [
"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"
@@ -2622,7 +1844,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
[[package]]
@@ -2647,54 +1869,12 @@ dependencies = [
"serde_core",
]
-[[package]]
-name = "sha2"
-version = "0.10.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-[[package]]
-name = "signal-hook"
-version = "0.3.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
-dependencies = [
- "libc",
- "signal-hook-registry",
-]
-
-[[package]]
-name = "signal-hook-mio"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
-dependencies = [
- "libc",
- "mio",
- "signal-hook",
-]
-
-[[package]]
-name = "signal-hook-registry"
-version = "1.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
-dependencies = [
- "errno",
- "libc",
-]
-
[[package]]
name = "simd-adler32"
version = "0.3.8"
@@ -2735,56 +1915,18 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
-[[package]]
-name = "static_assertions"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
-[[package]]
-name = "strum"
-version = "0.27.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
-dependencies = [
- "strum_macros",
-]
-
-[[package]]
-name = "strum_macros"
-version = "0.27.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
-dependencies = [
- "heck",
- "proc-macro2",
- "quote",
- "syn 2.0.117",
-]
-
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
-[[package]]
-name = "syn"
-version = "1.0.109"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
[[package]]
name = "syn"
version = "2.0.117"
@@ -2813,7 +1955,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
[[package]]
@@ -2837,69 +1979,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "terminfo"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
-dependencies = [
- "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]]
name = "thiserror"
version = "1.0.69"
@@ -2926,7 +2005,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
[[package]]
@@ -2937,30 +2016,9 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
-[[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"
@@ -3150,53 +2208,12 @@ 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.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-[[package]]
-name = "unicode-segmentation"
-version = "1.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
-
-[[package]]
-name = "unicode-truncate"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
-dependencies = [
- "itertools",
- "unicode-segmentation",
- "unicode-width",
-]
-
-[[package]]
-name = "unicode-width"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
-
-[[package]]
-name = "unicode-xid"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
-
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -3227,33 +2244,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
-[[package]]
-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 = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0"
-dependencies = [
- "utf8parse",
-]
-
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -3288,15 +2278,6 @@ 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 = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
-dependencies = [
- "wit-bindgen",
-]
-
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
@@ -3343,7 +2324,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
"wasm-bindgen-shared",
]
@@ -3356,40 +2337,6 @@ 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.91"
@@ -3419,78 +2366,6 @@ 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"
@@ -3575,7 +2450,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
[[package]]
@@ -3586,7 +2461,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
[[package]]
@@ -3879,88 +2754,6 @@ name = "wit-bindgen"
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 = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
-dependencies = [
- "anyhow",
- "id-arena",
- "indexmap",
- "log",
- "semver",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
- "wasmparser",
-]
[[package]]
name = "writeable"
@@ -3987,7 +2780,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
"synstructure",
]
@@ -4008,7 +2801,7 @@ checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
[[package]]
@@ -4028,7 +2821,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
"synstructure",
]
@@ -4068,7 +2861,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 3fb1038..7f742e1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "audioleaf"
-version = "3.5.0"
+version = "4.0.0"
edition = "2024"
authors = ["Antoni Zasada", "weekendsuperhero"]
description = "Manage your Nanoleaf devices (Canvas, Shapes, Elements, Light Panels) and visualize music straight from the terminal"
@@ -23,7 +23,6 @@ image = { version = "0.25", default-features = false, features = ["jpeg", "png",
num-complex = "0.4.6"
palette = "0.7.6"
pollster = "0.4"
-ratatui = "0.30"
reqwest = { version = "0.13", features = ["blocking", "json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
diff --git a/README.md b/README.md
index a00e4b7..e86e9aa 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,43 @@
# Audioleaf
-A TUI for managing and visualizing music on your Nanoleaf devices (Canvas, Shapes, Elements, and Light Panels)
+A real-time music visualizer for Nanoleaf devices (Shapes, Canvas, Elements, and Light Panels). Audioleaf listens to your system audio, analyzes it, and drives your Nanoleaf panels with reactive color animations — all rendered in a graphical window that mirrors your physical panel layout.
-
+
-> **Note:** This is a fork with macOS compatibility fixes and support for all Nanoleaf device types. See [CHANGELOG.md](CHANGELOG.md) for details.
+
+
+> **Note:** This is a fork with macOS compatibility fixes, a graphical UI, album art integration, and support for all Nanoleaf device types. See [CHANGELOG.md](CHANGELOG.md) for details.
+
+## Features
+
+- **Real-time audio visualization** — Three effects (Spectrum, Energy Wave, Pulse) that react to your music
+- **Graphical panel preview** — See your exact Nanoleaf layout rendered on screen with live color preview
+- **Album art integration** — Automatically extract color palettes from the currently playing track's album artwork (Spotify & Apple Music)
+- **11 built-in color palettes** — From ocean-nightclub to neon-rainbow, plus custom RGB palettes
+- **Panel sorting controls** — Adjust how colors map to your physical layout
+- **Cross-platform** — macOS and Linux
## Installation
-Install from cargo with `cargo install audioleaf`. Make sure that the directory with cargo binaries (by default `$HOME/.cargo/bin`) is added to your `$PATH`.
+Install from cargo:
+
+```bash
+cargo install audioleaf
+```
+
+Make sure `$HOME/.cargo/bin` is in your `$PATH`.
-For users of Arch-based distros, audioleaf is also available as a [package in the AUR](https://aur.archlinux.org/packages/audioleaf). You can install it with your AUR helper of choice, for example with yay: `yay -S audioleaf`.
+For Arch-based distros, audioleaf is also available in the [AUR](https://aur.archlinux.org/packages/audioleaf):
+
+```bash
+yay -S audioleaf
+```
## Usage
### First-Time Setup
-At first launch, `audioleaf` will automatically discover Nanoleaf devices on your local network and prompt you to choose one:
+At first launch, audioleaf discovers Nanoleaf devices on your local network:
```bash
audioleaf -n
@@ -24,20 +45,20 @@ audioleaf -n
This will:
-1. Scan your network for Nanoleaf devices (Canvas, Shapes, Elements, Light Panels)
-2. Display a list of discovered devices
-3. Prompt you to put your chosen device in pairing mode (hold power button until LEDs flash)
+1. Scan your network for Nanoleaf devices
+2. Display discovered devices
+3. Prompt you to put your device in pairing mode (hold power button until LEDs flash)
4. Save the device configuration
**Device data location:**
- **macOS**: `~/Library/Application Support/audioleaf/nl_devices.toml`
- **Linux**: `~/.config/audioleaf/nl_devices.toml`
-- **Custom**: Use `--devices /path/to/devices.toml` to specify a custom location
+- **Custom**: Use `--devices /path/to/devices.toml`
-### Running Audioleaf
+### Running
-After initial setup, simply run:
+After setup, simply run:
```bash
audioleaf
@@ -49,363 +70,146 @@ To connect to a specific device:
audioleaf -d "Shapes AC01"
```
-### TUI Controls
+### Controls
+
+Press ? in the app to see all keybinds.
-Lost in the TUI? Press ? to see the list of keybinds.
+| Key | Action |
+| --- | --- |
+| Esc / Q | Quit |
+| ? | Toggle help overlay |
+| Space | Toggle live panel color preview |
+| - / + | Decrease / increase gain (visual sensitivity) |
+| 1-9, 0 | Switch color palette |
+| E | Cycle effect: Spectrum / Energy Wave / Pulse |
+| A | Toggle primary sort axis (X / Y) |
+| P | Toggle primary sort (Asc / Desc) |
+| S | Toggle secondary sort (Asc / Desc) |
+| N | Use album art colors from current track |
+| R | Reset all panels to black |
-**Main shortcuts:**
+### Effects
-- Enter - Play selected effect
-- V - Toggle visualizer mode
-- j/k or ↓/↑ - Navigate effect list
-- +/- - Increase/decrease visualizer gain (sensitivity)
-- Q or Esc - Quit
-- ? - Show help
+- **Spectrum** — Each panel tracks a frequency band. Bass on one end, treble on the other.
+- **Energy Wave** — Audio energy cascades across panels as a traveling ripple.
+- **Pulse** — All panels pulse together, driven by audio transients. Snaps to the beat.
## Configuration
-All configuration is done through the `config.toml` file. The location depends on your operating system:
+Configuration lives in `config.toml`:
- **macOS**: `~/Library/Application Support/audioleaf/config.toml`
- **Linux**: `~/.config/audioleaf/config.toml`
-- **Custom**: Use `--config /path/to/config.toml` to override
+- **Custom**: Use `--config /path/to/config.toml`
-At first launch, a default config file will be automatically generated after connecting to your Nanoleaf device.
+A default config file is generated on first launch.
-### Complete Configuration Reference
+### Example Configuration
```toml
-# Specify which Nanoleaf device to use by default
default_nl_device_name = "Shapes AC01"
-[tui_config]
-# Display effect names in colors matching their palettes
-colorful_effect_names = true
-
[visualizer_config]
-# Audio input device name (see Audio Setup section below)
+# Audio input device (see Audio Setup below)
audio_backend = "BlackHole 2ch"
-# Frequency range to visualize (Hz) - [min, max]
-# Lower range = bass-focused, higher range = treble-focused
+# Frequency range to visualize [min_hz, max_hz]
freq_range = [20, 4500]
-# Color palette - use a named palette OR custom hue array
-# Named palette (easiest - see Color Palettes section for all options):
-hues = "ocean-nightclub"
-# OR custom hue array (0-360 on HSB color wheel, 360 = white):
-# hues = [195, 210, 240, 270, 285, 300, 180]
-# hues = [0, 120, 360] # Red, Green, White
+# Color palette — named palette or custom RGB array
+# Named: "ocean-nightclub", "sunset", "fire", "forest", "neon-rainbow", etc.
+colors = "ocean-nightclub"
+# Or custom RGB: colors = [[255, 0, 128], [0, 128, 255], [128, 255, 0]]
-# Audio sensitivity multiplier (default: 1.0)
-# BlackHole 2ch directly: 1 (recommended)
-# VB Cable or other virtual devices: may need 200-500
-# Physical microphones: typically 1-10
-default_gain = 1
+# Audio sensitivity (doesn't affect playback volume)
+default_gain = 1.0
-# Panel transition speed in 100ms units (1 = 100ms, 10 = 1s)
+# Panel transition speed in 100ms units (2 = 200ms)
transition_time = 2
# Audio sampling window in seconds
-# Smaller = more responsive, larger = smoother but laggy
time_window = 0.1875
-# Panel layout sorting (affects visualization direction)
-primary_axis = "Y" # "X" (left→right) or "Y" (bottom→top)
+# Panel sorting
+primary_axis = "Y" # "X" or "Y"
sort_primary = "Asc" # "Asc" or "Desc"
sort_secondary = "Asc" # "Asc" or "Desc"
-```
-
-### Configuration Options Explained
-
-#### TUI Configuration
-
-| Option | Type | Default | Description |
-| ----------------------- | ------- | ------- | -------------------------------------------------------- |
-| `colorful_effect_names` | boolean | `true` | Display effect names with colors matching their palettes |
-
-#### Visualizer Configuration
-
-| Option | Type | Default | Description |
-| ----------------- | -------------------- | --------------- | ----------------------------------------------------------------------------------- |
-| `audio_backend` | string | (auto-detected) | Name of audio input device to visualize |
-| `freq_range` | [int, int] | `[20, 4500]` | Min and max frequencies (Hz) to include in visualization |
-| `hues` | string or [int, ...] | varies | Named palette (e.g., `"sunset"`) OR custom HSB hue array (0-360, use 360 for white) |
-| `default_gain` | int/float | `1.0` | Audio amplification multiplier (doesn't affect playback volume) |
-| `transition_time` | int | `2` | Panel color transition time in 100ms units |
-| `time_window` | float | `0.1875` | Audio sampling window length in seconds |
-| `primary_axis` | string | `"Y"` | Primary sort direction: `"X"` or `"Y"` |
-| `sort_primary` | string | `"Asc"` | Primary sort order: `"Asc"` or `"Desc"` |
-| `sort_secondary` | string | `"Asc"` | Secondary sort order: `"Asc"` or `"Desc"` |
-
-### Color Palettes
-
-Colors are specified using **HSB (Hue, Saturation, Brightness)** hue values ranging from **0-360**. You can use either:
-
-1. **Named palettes** (easiest): `hues = "palette-name"`
-2. **Custom hue arrays**: `hues = [0, 60, 120, 180, 240, 300]`
- - Use values 0-359 for colors on the HSB color wheel
- - Use **360** for white/near-white colors
-
-#### Using Named Palettes
-
-Simply set `hues` to one of the palette names below:
-
-```toml
-[visualizer_config]
-hues = "sunset"
-```
-#### Available Named Palettes
-
-**ocean-nightclub** - Deep blues, purples, and teals
-
-```toml
-hues = "ocean-nightclub"
-# Equivalent to: hues = [195, 210, 240, 270, 285, 300, 180]
-```
-
-**sunset** - Warm oranges, reds, pinks, and purples
-
-```toml
-hues = "sunset"
-# Equivalent to: hues = [15, 25, 340, 350, 0, 10, 310, 280]
+# Visualization effect
+effect = "Spectrum" # "Spectrum", "EnergyWave", or "Pulse"
```
-**house-music-party** - Energetic magentas, purples, blues, and cyans
+### Available Palettes
-```toml
-hues = "house-music-party"
-# Equivalent to: hues = [300, 285, 270, 255, 240, 195, 180]
-```
+| Palette | Description |
+| --- | --- |
+| `ocean-nightclub` | Deep blues, purples, teals |
+| `sunset` | Warm oranges, reds, pinks |
+| `house-music-party` | Energetic magentas, purples, cyans |
+| `tropical-beach` | Turquoise, aqua, lime |
+| `fire` | Reds, oranges, yellows |
+| `forest` | Deep greens, yellow-green |
+| `neon-rainbow` | Full spectrum |
+| `pink-dreams` | Soft pinks through magentas |
+| `cool-blues` | Ice blues to navy |
+| `tmnt` | Turtle green + bandana colors |
+| `christmas` | Red, green, white |
-**tropical-beach** - Turquoise, aqua, lime, and sunny yellows
+## Audio Setup
-```toml
-hues = "tropical-beach"
-# Equivalent to: hues = [180, 175, 170, 160, 90, 75, 60]
-```
-
-**fire** - Intense reds, oranges, and yellows
-
-```toml
-hues = "fire"
-# Equivalent to: hues = [0, 10, 20, 30, 40, 50, 60]
-```
-
-**forest** - Deep greens with yellow-green highlights
-
-```toml
-hues = "forest"
-# Equivalent to: hues = [90, 100, 110, 120, 130, 140, 150]
-```
-
-**neon-rainbow** - Full spectrum bright colors
-
-```toml
-hues = "neon-rainbow"
-# Equivalent to: hues = [0, 60, 120, 180, 240, 300]
-```
-
-**pink-dreams** - Soft pinks through magentas
-
-```toml
-hues = "pink-dreams"
-# Equivalent to: hues = [320, 325, 330, 335, 340, 345, 350]
-```
-
-**cool-blues** - Ice blues to deep navy
-
-```toml
-hues = "cool-blues"
-# Equivalent to: hues = [190, 200, 210, 220, 230, 240, 250]
-```
-
-**tmnt** - Turtle green with Leonardo, Michelangelo, Raphael, and Donatello bandana colors
-
-```toml
-hues = "tmnt"
-# Equivalent to: hues = [125, 130, 240, 245, 25, 30, 0, 5, 280, 285]
-```
-
-**christmas** - Festive red, green, and white
-
-```toml
-hues = "christmas"
-# Equivalent to: hues = [0, 5, 120, 125, 360, 360]
-# Red (0, 5), Green (120, 125), White (360, 360)
-```
-
-### HSB Color Wheel Reference
-
-Hue values map to colors on the standard HSB color wheel (0-359 degrees):
-
-- **0° - Red**
-- 15° - Red-Orange
-- **30° - Orange**
-- 45° - Yellow-Orange
-- **60° - Yellow**
-- 90° - Yellow-Green
-- **120° - Green**
-- 150° - Blue-Green
-- **180° - Cyan**
-- 210° - Sky Blue
-- **240° - Blue**
-- 270° - Blue-Purple
-- **300° - Magenta**
-- 330° - Pink-Red
-- **359° - Back to Red**
-
-The Nanoleaf API uses **HSB (Hue, Saturation, Brightness)** color space where:
-
-- **Hue**: 0-359 (color position on the wheel)
-- **Saturation**: 0-100 (color intensity, controlled by device)
-- **Brightness**: 0-100 (lightness, controlled by device settings)
-
-In audioleaf configuration, you specify hue values (0-359). The visualizer dynamically adjusts brightness based on audio input.
-
-**Special white value**: Use **360** to create white/near-white colors. This adds high whiteness to the color, making it appear white or silvery when bright.
-
-Example with white:
-
-```toml
-hues = [0, 120, 360] # Red, Green, White
-```
-
-For more details, see the [HSB Color System guide](https://www.learnui.design/blog/the-hsb-color-system-practicioners-primer.html) or the [Nanoleaf API documentation](https://forum.nanoleaf.me/docs).
-
-### Audio Setup
-
-#### macOS
+### macOS
1. Install [BlackHole](https://existential.audio/blackhole/) (free virtual audio device)
-2. Open **Audio MIDI Setup** (Applications → Utilities)
-3. Create a **Multi-Output Device**:
- - Include your speakers/headphones
- - Include BlackHole 2ch
+2. Open **Audio MIDI Setup** (Applications > Utilities)
+3. Create a **Multi-Output Device** including your speakers + BlackHole 2ch
4. Set the Multi-Output Device as your system output
5. Set `audio_backend = "BlackHole 2ch"` in config.toml
-6. Set `default_gain = 1` (normal gain when targeting BlackHole directly)
-**Important**: Target `"BlackHole 2ch"` directly, not the Multi-Output Device aggregate. This provides proper audio levels without requiring extreme gain values.
+**Tip**: Target `"BlackHole 2ch"` directly, not the Multi-Output Device aggregate. This provides proper audio levels with `default_gain = 1`.
-#### Linux (PulseAudio/PipeWire)
+### Linux (PulseAudio/PipeWire)
-1. Run audioleaf once to see available devices
-2. Use `pavucontrol` (PulseAudio Volume Control)
-3. Go to **Recording** tab while audioleaf is running
-4. Set audioleaf's input to your media player's monitor
-5. Set `audio_backend` to match the device name
-6. Adjust `default_gain` as needed (typically 1-10)
+1. Run audioleaf
+2. Open `pavucontrol` (PulseAudio Volume Control)
+3. In the **Recording** tab, set audioleaf's input to your media player's monitor
+4. Set `audio_backend` in config.toml to match
-#### Windows
+## Dump Commands
-1. Install [VB Cable](https://vb-audio.com/Cable) (free virtual audio cable)
-2. Set VB Cable as your default playback device
-3. Route VB Cable output to your speakers using audio software
-4. Set `audio_backend = "VB Cable"` in config.toml
-5. Set `default_gain = 200` or higher
+Inspect your device without launching the full app:
-### Brightness Adjustment
+```bash
+# Show panel layout info
+audioleaf dump layout
-Brightness is controlled by your Nanoleaf device settings, not by audioleaf:
+# Interactive graphical layout view (click panels to flash them)
+audioleaf dump layout-graphical
-1. Open the Nanoleaf mobile app
-2. Select your device
-3. Adjust the brightness slider
-4. The visualizer will maintain this brightness level
+# List available color palettes
+audioleaf dump palettes
-Alternatively, you can adjust brightness directly on the Nanoleaf controller using the physical buttons.
+# Show raw device info
+audioleaf dump info
+```
## Troubleshooting
-### Visualizer Not Responding to Music
-
-**Symptom**: Panels don't react to audio, or react very weakly.
-
-**Solutions**:
-
-1. **Check audio routing**: Ensure audioleaf is receiving audio input
- - On Linux: Use `pavucontrol` → Recording tab to verify audio source
- - On macOS: Verify Multi-Output Device is selected in System Settings
- - On Windows: Confirm VB Cable routing is correct
-
-2. **Adjust gain**: Different audio sources need different gain levels
- - Press +/- in audioleaf to adjust gain in real-time
- - BlackHole 2ch (macOS): `default_gain = 1` when targeted directly
- - VB Cable (Windows): may need `default_gain = 200-500`
- - Physical microphones: typically `default_gain = 1-10`
-
-3. **Adjust frequency range**: Try different frequency ranges
- - Bass-heavy: `freq_range = [20, 500]`
- - Full range: `freq_range = [20, 4500]`
- - Treble-focused: `freq_range = [1000, 8000]`
-
-### Only One Panel Lights Up
+### Visualizer Not Responding
-**Symptom**: Only a single panel shows colors during visualization.
-
-**Solution**: This is fixed in this fork. The original version included the controller unit in the panel list. If you're still seeing this:
-
-- Update to the latest version of this fork
-- Run `audioleaf -n` to re-discover your device
-- Delete the devices file and reconnect
+1. **Check audio routing** — Verify audioleaf receives audio input (use `pavucontrol` on Linux, check Multi-Output Device on macOS)
+2. **Adjust gain** — Press +/- in the app, or set `default_gain` in config
+3. **Adjust frequency range** — Try `freq_range = [20, 500]` for bass-heavy, `[20, 4500]` for full range
### Device Not Discovered
-**Symptom**: "No Nanoleaf devices found on the local network"
-
-**Solutions**:
-
-1. Ensure your device is powered on and connected to the same network
-2. This fork supports all device types: Canvas (nl29), Shapes (nl42), Elements (nl52), and Light Panels (aurora)
-3. Check your firewall isn't blocking SSDP/UDP multicast
-4. Try running with `sudo` on Linux if permission issues exist
-5. Manually specify device in config: `default_nl_device_name = "Your Device Name"`
-
-### Enter Key Not Working in TUI
-
-**Symptom**: Pressing Enter doesn't select effects.
+1. Ensure device is powered on and on the same network
+2. Check firewall isn't blocking SSDP/UDP multicast
+3. Try `audioleaf -n` to re-discover
-**Solution**: This is fixed in this fork. If still experiencing issues:
+### Brightness
-- Ensure you're running the latest version
-- Try different terminal emulators (tested with Ghostty, iTerm2, Alacritty)
-- Check terminal supports standard key event handling
-
-### Config Changes Not Taking Effect
-
-**Symptom**: Changing config.toml has no effect.
-
-**Solutions**:
-
-1. **Verify correct config location**:
- - macOS: `~/Library/Application Support/audioleaf/config.toml`
- - Linux: `~/.config/audioleaf/config.toml`
- - Not in `~/.config/` on macOS!
-
-2. **Check TOML syntax**: Ensure proper formatting
- - Use integers OR floats for `default_gain` (both work)
- - Arrays use square brackets: `hues = [0, 60, 120]`
- - Strings use quotes: `audio_backend = "BlackHole 2ch"`
-
-3. **Restart audioleaf**: Changes only apply on launch
-
-### Audio Routing (Linux)
-
-Make sure that audioleaf's audio input is set to be your media player's output. Use any audio mixer software, for example [pavucontrol](https://freedesktop.org/software/pulseaudio/pavucontrol) (for PulseAudio or PipeWire):
-
-1. Go to the **Recording** tab while audioleaf is running
-2. Set the device in the dropdown menu to your media player's monitor
-
-
-
-### Audio Routing (Windows)
-
-Windows doesn't have a built-in way to route one program's audio output to another's input. Use third-party software such as [VB Cable](https://vb-audio.com/Cable).
+Brightness is controlled by your Nanoleaf device settings (mobile app or physical buttons), not by audioleaf. The visualizer dynamically adjusts color intensity based on audio.
## Contributing
-Audioleaf is a project made mainly in my spare time as a way to become familiar with Rust, making TUIs, and some basics of audio processing.
-
-Therefore, there are surely many ways to make it more robust, performant and nicer to use - feel free to open a pull request or start a Github issue if you see any potential for audioleaf's improvement. Thank you!
+Feel free to open a pull request or start a GitHub issue. Contributions welcome!
diff --git a/images/pavucontrol.png b/images/pavucontrol.png
deleted file mode 100644
index 6b146ab..0000000
Binary files a/images/pavucontrol.png and /dev/null differ
diff --git a/images/tui.png b/images/tui.png
deleted file mode 100644
index 17b42f0..0000000
Binary files a/images/tui.png and /dev/null differ
diff --git a/src/app.rs b/src/app.rs
index f93dde7..ab0a6c3 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,26 +1,15 @@
use crate::audio;
use crate::constants;
-use crate::event_handler::{self, Event};
-use crate::utils;
-use crate::visualizer::VisualizerMsg;
+use crate::layout_visualizer::PanelInfo;
+use crate::visualizer::{self, VisualizerMsg};
use crate::{
- config::{Axis, Effect, Sort, TuiConfig, VisualizerConfig},
- nanoleaf::{NlDevice, NlEffect},
- visualizer,
+ config::{Axis, Effect, Sort, VisualizerConfig},
+ nanoleaf::NlDevice,
};
use anyhow::Result;
-use ratatui::{
- Frame, Terminal,
- crossterm::event::KeyCode,
- layout::Margin,
- prelude::Backend,
- style::{Color, Style, Stylize},
- text::{Line, Span},
- widgets::{
- Block, Borders, HighlightSpacing, List, ListDirection, ListItem, ListState, Paragraph,
- Scrollbar, ScrollbarOrientation, ScrollbarState,
- },
-};
+use macroquad::prelude::*;
+use std::collections::HashMap;
+use std::f32::consts::PI;
use std::sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
@@ -28,142 +17,57 @@ use std::sync::{
};
use std::time::Duration;
-#[derive(Debug, Default)]
-enum AppState {
- #[default]
- RunningEffectList,
- RunningVisualizer,
- Done,
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
-enum AppView {
- #[default]
- EffectList,
- Visualizer,
- HelpScreen,
-}
-
-#[derive(Debug)]
-enum AppMsg {
- NoOp,
- Quit,
- ChangeView(AppView),
- PlayEffect(usize),
- ScrollDown(u16),
- ScrollUp(u16),
- ScrollToBottom,
- 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,
+ artwork_bytes: Option>,
}
-#[derive(Debug)]
-struct Scroll {
- pos: u16,
- state: ScrollbarState,
-}
+pub struct App {
+ // Device
+ nl_device: NlDevice,
+ panels: Vec,
+ global_orientation: u16,
-#[derive(Debug)]
-struct EffectList {
- list: Vec,
- state: ListState,
- scroll: Scroll,
-}
+ // Visualizer
+ visualizer_tx: mpsc::Sender,
+ shared_colors: Arc>>,
-#[derive(Debug)]
-struct Visualizer {
- tx: mpsc::Sender,
+ // Settings
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,
- global_orientation: u16,
-}
-#[derive(Debug)]
-pub struct App {
- state: AppState,
- prev_view: AppView,
- view: AppView,
- nl_device: NlDevice,
- effect_list: EffectList,
- visualizer: Visualizer,
- display_colors: bool,
+ // UI state
+ show_visualization: bool,
+ show_help: bool,
+
+ // Album art
+ viz_state: Arc>,
+ album_art_stop: Option>,
+ album_art_texture: Option,
+ /// Tracks which artwork bytes we've already loaded into the texture
+ loaded_artwork_len: usize,
}
impl App {
- /// Constructs a new `App` for TUI-based Nanoleaf effect selection and audio visualizer.
- ///
- /// Initializes:
- /// - Effect list from device API, selects current effect if running.
- /// - Visualizer with audio stream, UDP, config params, starts processing thread via `init()`.
- /// - State to EffectList view, running effects mode.
- /// - Palette names from predefined for switching.
- /// - Colorful names from TUI config.
- /// - Fetches device global orientation for panel sorting.
- ///
- /// # Arguments
+ /// Constructs a new `App` with macroquad-based graphical UI.
///
- /// * `nl_device` - Connected Nanoleaf device.
- /// * `tui_config` - UI settings like colorful effect names.
- /// * `visualizer_config` - Audio/viz params like gain, hues, sorting.
- ///
- /// # Errors
- ///
- /// From device API, visualizer new/init, or effect list fetch.
- pub fn new(
- nl_device: NlDevice,
- tui_config: TuiConfig,
- visualizer_config: VisualizerConfig,
- ) -> Result {
- let state = AppState::default();
- let prev_view = AppView::default();
- let view = AppView::default();
- let list = nl_device.get_effect_list()?;
- let list_pos = if let Some(cur_effect_name) = nl_device.cur_effect_name.as_deref() {
- list.iter()
- .position(|effect| effect.name == cur_effect_name)
- .unwrap_or(0)
- } else {
- 0
- };
- let scroll = Scroll {
- pos: 0,
- state: ScrollbarState::default(),
- };
- let effect_list = EffectList {
- list,
- state: ListState::default().with_selected(Some(list_pos)),
- scroll,
- };
+ /// Initializes the audio visualizer thread, fetches panel layout from the device,
+ /// and prepares all settings state. Requests UDP control immediately.
+ pub fn new(nl_device: NlDevice, visualizer_config: VisualizerConfig) -> Result {
let audio_stream = audio::AudioStream::new(visualizer_config.audio_backend.as_deref())?;
let gain = visualizer_config
.default_gain
.unwrap_or(constants::DEFAULT_GAIN);
+ #[cfg(debug_assertions)]
eprintln!("INFO: Starting with gain: {}", gain);
- // Get global orientation
let global_orientation = nl_device
.get_global_orientation()
.ok()
@@ -180,299 +84,619 @@ impl App {
.clone()
.unwrap_or_else(|| Vec::from(constants::DEFAULT_COLORS));
- let tx = visualizer::Visualizer::new(visualizer_config, audio_stream, &nl_device)?.init();
+ // Fetch panel layout for graphical rendering
+ let layout = nl_device.get_panel_layout()?;
+ let panels = crate::layout_visualizer::parse_layout(&layout)?;
+
+ // Shared color state: visualizer thread writes panel colors, UI reads them
+ let shared_colors: Arc>> = Arc::new(Mutex::new(HashMap::new()));
+
+ let tx = visualizer::Visualizer::new(
+ visualizer_config,
+ audio_stream,
+ &nl_device,
+ Arc::clone(&shared_colors),
+ )?
+ .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,
+ artwork_bytes: None,
}));
- let visualizer = Visualizer {
- tx,
+ nl_device.request_udp_control()?;
+
+ Ok(App {
+ nl_device,
+ panels,
+ global_orientation,
+ visualizer_tx: tx,
+ shared_colors,
gain,
current_palette_index: 0,
palette_names,
- viz_state,
- album_art_stop: None,
effect,
primary_axis,
sort_primary,
sort_secondary,
- global_orientation,
- };
- let display_colors = tui_config
- .colorful_effect_names
- .unwrap_or(constants::DEFAULT_COLORFUL_EFFECT_NAMES);
-
- Ok(App {
- state,
- prev_view,
- view,
- nl_device,
- effect_list,
- visualizer,
- display_colors,
+ show_visualization: false,
+ show_help: false,
+ viz_state,
+ album_art_stop: None,
+ album_art_texture: None,
+ loaded_artwork_len: 0,
})
}
- /// Executes the main TUI application loop.
- ///
- /// Creates event handler for key/tick events.
- /// Loop: draw current view, receive event, map to AppMsg, update app state.
- /// Breaks when state=Done (quit).
- /// On exit, sends End msg to visualizer to stop audio/UDP thread.
- ///
- /// # Arguments
- ///
- /// * `self` - Mutable app state.
- /// * `terminal` - Ratatui terminal for rendering frames.
- ///
- /// # Errors
- ///
- /// From event recv, update logic, or draw.
- pub fn run(&mut self, terminal: &mut Terminal) -> Result<()>
- where
- B::Error: Send + Sync + 'static,
- {
- let event_handler = event_handler::EventHandler::new();
+ /// Launches the macroquad window and runs the main graphical event loop.
+ /// Blocks until the window is closed.
+ pub fn run(self) {
+ let title = format!("Audioleaf - {}", self.nl_device.name);
+ macroquad::Window::from_config(
+ Conf {
+ window_title: title,
+ window_width: 1200,
+ window_height: 800,
+ window_resizable: true,
+ icon: Some(load_icon()),
+ ..Default::default()
+ },
+ async move {
+ let mut app = self;
+ app.main_loop().await;
+ },
+ );
+ }
+
+ async fn main_loop(&mut self) {
loop {
- terminal.draw(|frame| self.render_view(frame))?;
- let event = event_handler.next()?;
- let msg = self.event_to_msg(event);
- self.update(msg)?;
- if let AppState::Done = self.state {
+ clear_background(Color::from_rgba(20, 20, 30, 255));
+
+ if self.handle_input() {
break;
}
+
+ // Check if artwork bytes have changed and reload texture
+ self.update_album_art_texture();
+
+ self.draw_panels();
+ self.draw_hud();
+
+ if self.show_help {
+ self.draw_help_overlay();
+ }
+
+ next_frame().await;
}
- 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(())
+ // Shutdown
+ let _ = self.visualizer_tx.send(VisualizerMsg::End);
+ self.stop_album_art_watcher();
+ let _ = self.nl_device.set_state(Some(false), Some(0));
}
- /// Converts raw terminal events to `AppMsg` for state updates.
- ///
- /// Ignores ticks (NoOp).
- /// Maps KeyEvent codes to actions:
- /// - ESC/Q: Quit
- /// - Enter: Play selected effect in list view
- /// - Up/Down/j/k: Scroll list
- /// - g/G: Scroll top/bottom
- /// - V: Toggle EffectList <-> Visualizer view
- /// - ?: Toggle help screen
- /// - +/-=_ : Adjust visualizer gain by ±0.05
- /// - 0-9: Switch to numbered palette
- /// - 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.
- fn event_to_msg(&self, event: Event) -> AppMsg {
- match event {
- Event::Tick => AppMsg::NoOp,
- Event::Key(e) => match e.code {
- KeyCode::Esc | KeyCode::Char('Q') => AppMsg::Quit,
- KeyCode::Enter => {
- if let AppView::EffectList = self.view {
- if let Some(selected) = self.effect_list.state.selected() {
- AppMsg::PlayEffect(selected)
- } else {
- AppMsg::NoOp
- }
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Down | KeyCode::Char('j') => {
- if let AppView::EffectList = self.view {
- AppMsg::ScrollDown(1)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Up | KeyCode::Char('k') => {
- if let AppView::EffectList = self.view {
- AppMsg::ScrollUp(1)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('g') => AppMsg::ScrollToTop,
- KeyCode::Char('G') => AppMsg::ScrollToBottom,
- 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),
- },
- KeyCode::Char('?') => {
- if let AppView::HelpScreen = self.view {
- AppMsg::ChangeView(self.prev_view)
- } else {
- AppMsg::ChangeView(AppView::HelpScreen)
- }
- }
- KeyCode::Char('-') | KeyCode::Char('_') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangeGain(-0.05)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('=') | KeyCode::Char('+') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangeGain(0.05)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('1') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangePalette(0)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('2') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangePalette(1)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('3') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangePalette(2)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('4') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangePalette(3)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('5') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangePalette(4)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('6') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangePalette(5)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('7') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangePalette(6)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('8') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangePalette(7)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('9') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangePalette(8)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('0') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ChangePalette(9)
- } else {
- AppMsg::NoOp
- }
- }
- KeyCode::Char('a') | KeyCode::Char('A') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ToggleAxis
- } else {
- AppMsg::NoOp
- }
+ /// Process keyboard input. Returns true if quit was requested.
+ fn handle_input(&mut self) -> bool {
+ if is_key_pressed(KeyCode::Escape) {
+ return true;
+ }
+
+ while let Some(ch) = get_char_pressed() {
+ match ch {
+ 'Q' => return true,
+ '?' => self.show_help = !self.show_help,
+ _ if self.show_help => {} // Swallow other keys while help is shown
+ '-' | '_' => {
+ self.gain -= 0.05;
+ let _ = self.visualizer_tx.send(VisualizerMsg::SetGain(self.gain));
}
- KeyCode::Char('p') | KeyCode::Char('P') => {
- if let AppView::Visualizer = self.view {
- AppMsg::TogglePrimarySort
- } else {
- AppMsg::NoOp
- }
+ '=' | '+' => {
+ self.gain += 0.05;
+ let _ = self.visualizer_tx.send(VisualizerMsg::SetGain(self.gain));
}
- KeyCode::Char('s') | KeyCode::Char('S') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ToggleSecondarySort
- } else {
- AppMsg::NoOp
- }
+ '1'..='9' => {
+ let index = (ch as usize) - ('1' as usize);
+ self.change_palette(index);
}
- KeyCode::Char('e') | KeyCode::Char('E') => {
- if let AppView::Visualizer = self.view {
- AppMsg::CycleEffect
- } else {
- AppMsg::NoOp
- }
+ '0' => self.change_palette(9),
+ 'a' | 'A' => self.toggle_axis(),
+ 'p' | 'P' => self.toggle_primary_sort(),
+ 's' | 'S' => self.toggle_secondary_sort(),
+ 'e' | 'E' => self.cycle_effect(),
+ 'n' | 'N' => self.use_album_art_palette(),
+ 'r' | 'R' => {
+ let _ = self.visualizer_tx.send(VisualizerMsg::ResetPanels);
}
- KeyCode::Char('n') | KeyCode::Char('N') => {
- if let AppView::Visualizer = self.view {
- AppMsg::UseAlbumArtPalette
- } else {
- AppMsg::NoOp
- }
+ ' ' => self.show_visualization = !self.show_visualization,
+ _ => {}
+ }
+ }
+
+ false
+ }
+
+ // ── Panel rendering ──────────────────────────────────────────────────
+
+ fn draw_panels(&self) {
+ let sw = screen_width();
+ let sh = screen_height();
+
+ // Find the largest panel radius (in layout units) so we can include
+ // the full extent of edge panels in the bounding box, not just centers.
+ let max_panel_radius = self
+ .panels
+ .iter()
+ .filter(|p| p.shape_type.side_length >= 1.0)
+ .map(|p| {
+ let s = p.shape_type.side_length;
+ match p.shape_type.num_sides() {
+ 3 => s / f32::sqrt(3.0),
+ 4 => s / f32::sqrt(2.0),
+ _ => s,
}
- KeyCode::Char('r') | KeyCode::Char('R') => {
- if let AppView::Visualizer = self.view {
- AppMsg::ResetPanels
- } else {
- AppMsg::NoOp
- }
+ })
+ .fold(0.0_f32, f32::max);
+
+ let min_x = self.panels.iter().map(|p| p.x).min().unwrap_or(0) as f32 - max_panel_radius;
+ let max_x = self.panels.iter().map(|p| p.x).max().unwrap_or(0) as f32 + max_panel_radius;
+ let min_y = self.panels.iter().map(|p| p.y).min().unwrap_or(0) as f32 - max_panel_radius;
+ let max_y = self.panels.iter().map(|p| p.y).max().unwrap_or(0) as f32 + max_panel_radius;
+
+ let layout_width = (max_x - min_x).max(1.0);
+ let layout_height = (max_y - min_y).max(1.0);
+
+ let padding_top = 40.0;
+ let padding_bottom = 40.0;
+ let padding_sides = 40.0;
+ let available_width = sw - 2.0 * padding_sides;
+ let available_height = sh - padding_top - padding_bottom;
+
+ let scale = (available_width / layout_width).min(available_height / layout_height);
+
+ let offset_x = (sw - layout_width * scale) / 2.0;
+ let offset_y = padding_top + (available_height - layout_height * scale) / 2.0;
+
+ // Snapshot visualization colors once per frame
+ let vis_colors = if self.show_visualization {
+ self.shared_colors.lock().ok().map(|map| map.clone())
+ } else {
+ None
+ };
+
+ // First pass: compute rotated screen positions
+ let transformed: Vec<(f32, f32)> = self
+ .panels
+ .iter()
+ .map(|panel| {
+ let rel_x = (panel.x as f32 - min_x) - layout_width / 2.0;
+ let rel_y = (panel.y as f32 - min_y) - layout_height / 2.0;
+ let angle = -(self.global_orientation as f32).to_radians();
+ let rotated_x = rel_x * angle.cos() - rel_y * angle.sin();
+ let rotated_y = rel_x * angle.sin() + rel_y * angle.cos();
+ let screen_x = offset_x + (rotated_x + layout_width / 2.0) * scale;
+ let screen_y = offset_y + (layout_height / 2.0 - rotated_y) * scale;
+ (screen_x, screen_y)
+ })
+ .collect();
+
+ // Second pass: draw
+ for (i, panel) in self.panels.iter().enumerate() {
+ let (x, y) = transformed[i];
+ if panel.shape_type.side_length < 1.0 {
+ draw_controller(x, y, panel, scale, &self.panels, &transformed);
+ } else {
+ self.draw_light_panel(x, y, panel, scale, &vis_colors);
+ }
+ }
+ }
+
+ fn draw_light_panel(
+ &self,
+ x: f32,
+ y: f32,
+ panel: &PanelInfo,
+ scale: f32,
+ vis_colors: &Option>,
+ ) {
+ let num_sides = panel.shape_type.num_sides();
+ let side_length = panel.shape_type.side_length * scale;
+
+ let radius = match num_sides {
+ 3 => side_length / f32::sqrt(3.0),
+ 4 => side_length / f32::sqrt(2.0),
+ _ => side_length,
+ };
+
+ let start_angle = (panel.orientation as f32).to_radians();
+ let vertices: Vec = (0..num_sides)
+ .map(|i| {
+ let angle = start_angle + (i as f32 * 2.0 * PI / num_sides as f32);
+ Vec2::new(x + radius * angle.cos(), y + radius * angle.sin())
+ })
+ .collect();
+
+ // Determine fill color: live visualization or static shape-type color
+ let color = if let Some(colors_map) = vis_colors {
+ if let Some(&[r, g, b]) = colors_map.get(&panel.panel_id) {
+ Color::from_rgba(r, g, b, 255)
+ } else {
+ Color::from_rgba(30, 30, 40, 200)
+ }
+ } else {
+ match panel.shape_type.id {
+ 0 | 8 | 9 => Color::from_rgba(255, 100, 100, 200),
+ 2..=4 => Color::from_rgba(100, 255, 100, 200),
+ 7 | 14 | 15 => Color::from_rgba(100, 150, 255, 200),
+ 30..=32 => Color::from_rgba(255, 255, 100, 200),
+ _ => Color::from_rgba(150, 150, 150, 200),
+ }
+ };
+
+ // Fill polygon (triangle fan)
+ for i in 1..(num_sides - 1) {
+ draw_triangle(vertices[0], vertices[i], vertices[i + 1], color);
+ }
+
+ // Outline
+ let outline = if vis_colors.is_some() {
+ Color::from_rgba(60, 60, 80, 255)
+ } else {
+ WHITE
+ };
+ for i in 0..num_sides {
+ let next = (i + 1) % num_sides;
+ draw_line(
+ vertices[i].x,
+ vertices[i].y,
+ vertices[next].x,
+ vertices[next].y,
+ 2.0,
+ outline,
+ );
+ }
+ }
+
+ // ── Album art texture ─────────────────────────────────────────────────
+
+ fn update_album_art_texture(&mut self) {
+ let bytes_opt = self
+ .viz_state
+ .lock()
+ .ok()
+ .and_then(|s| s.artwork_bytes.clone());
+ if let Some(bytes) = bytes_opt {
+ // Only reload if the bytes actually changed
+ if bytes.len() != self.loaded_artwork_len {
+ if let Ok(img) = image::load_from_memory(&bytes) {
+ let rgba = img.to_rgba8();
+ let (w, h) = (rgba.width() as u16, rgba.height() as u16);
+ let tex = Texture2D::from_rgba8(w, h, rgba.as_raw());
+ tex.set_filter(FilterMode::Linear);
+ self.album_art_texture = Some(tex);
}
- _ => AppMsg::NoOp,
- },
+ self.loaded_artwork_len = bytes.len();
+ }
+ } else if self.album_art_texture.is_some() {
+ // Artwork cleared (switched to named palette)
+ self.album_art_texture = None;
+ self.loaded_artwork_len = 0;
}
}
- /// 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);
+ // ── HUD ──────────────────────────────────────────────────────────────
+
+ fn draw_hud(&self) {
+ let sw = screen_width();
+ let sh = screen_height();
+
+ // ── Top-left: device name ──
+ let name_text = format!("Connected to {}", self.nl_device.name);
+ let name_size = 28.0;
+ let nm = sharp_measure(name_size, &name_text);
+ draw_rectangle(
+ 5.0,
+ 2.0,
+ nm.width + 14.0,
+ nm.height + 10.0,
+ Color::from_rgba(0, 0, 0, 160),
+ );
+ sharp_text(
+ &name_text,
+ 12.0,
+ nm.height + 5.0,
+ name_size,
+ Color::from_rgba(220, 130, 255, 255),
+ );
+
+ // ── Top-right: preview toggle ──
+ let vis_text = if self.show_visualization {
+ "Preview: ON [Space]"
+ } else {
+ "Preview: OFF [Space]"
+ };
+ let vis_color = if self.show_visualization {
+ Color::from_rgba(100, 255, 100, 255)
+ } else {
+ Color::from_rgba(150, 150, 150, 255)
+ };
+ let vm = sharp_measure(22.0, vis_text);
+ draw_rectangle(
+ sw - vm.width - 19.0,
+ 2.0,
+ vm.width + 14.0,
+ vm.height + 10.0,
+ Color::from_rgba(0, 0, 0, 160),
+ );
+ sharp_text(
+ vis_text,
+ sw - vm.width - 12.0,
+ vm.height + 5.0,
+ 22.0,
+ vis_color,
+ );
+
+ // ── Bottom-left: effect + palette + colors ──
+ let effect_str = match self.effect {
+ Effect::Spectrum => "Spectrum",
+ Effect::EnergyWave => "Energy Wave",
+ Effect::Pulse => "Pulse",
+ };
+ let (palette_name, track_title) = {
+ let state = self.viz_state.lock().unwrap();
+ let name = if state.track_title.is_some() {
+ "album-art".to_string()
+ } else if self.current_palette_index < self.palette_names.len() {
+ self.palette_names[self.current_palette_index].clone()
+ } else {
+ "Unknown".to_string()
+ };
+ (name, state.track_title.clone())
+ };
+
+ let effect_text = format!("Effect: {} | Gain: {:.2}", effect_str, self.gain);
+ let mut palette_text = format!("Palette: {}", palette_name);
+ if let Some(title) = &track_title {
+ palette_text.push_str(&format!(" | Now playing: {}", title));
}
- if let Ok(mut state) = self.visualizer.viz_state.lock() {
- state.track_title = None;
+
+ let big = 26.0;
+ let em = sharp_measure(big, &effect_text);
+ let pm = sharp_measure(big, &palette_text);
+ let box_w = em.width.max(pm.width) + 14.0;
+
+ // Color swatches measurement
+ let colors = self.viz_state.lock().unwrap().colors.clone();
+ let swatch_total_w = 22.0 * colors.len() as f32;
+ let box_w = box_w.max(swatch_total_w + 80.0);
+
+ let box_h = big * 2.0 + 30.0 + 18.0; // two text lines + swatch row + padding
+ let box_y = sh - box_h - 5.0;
+
+ draw_rectangle(5.0, box_y, box_w, box_h, Color::from_rgba(0, 0, 0, 160));
+
+ let line1_y = box_y + big + 4.0;
+ sharp_text(&effect_text, 12.0, line1_y, big, WHITE);
+
+ let line2_y = line1_y + big + 4.0;
+ sharp_text(
+ &palette_text,
+ 12.0,
+ line2_y,
+ big,
+ Color::from_rgba(100, 255, 100, 255),
+ );
+
+ // Color swatches
+ let swatch_y = line2_y + 8.0;
+ let mut sx = 12.0;
+ for [r, g, b] in &colors {
+ draw_rectangle(sx, swatch_y, 18.0, 14.0, Color::from_rgba(*r, *g, *b, 255));
+ sx += 22.0;
+ }
+
+ // ── Bottom-right: album art + sorting ──
+ let mut art_bottom = 0.0_f32;
+ if let Some(tex) = &self.album_art_texture {
+ let art_size = 140.0;
+ let ax = sw - art_size - 10.0;
+ let ay = sh - art_size - 35.0;
+ // Semi-transparent background behind art
+ draw_rectangle(
+ ax - 4.0,
+ ay - 4.0,
+ art_size + 8.0,
+ art_size + 8.0,
+ Color::from_rgba(0, 0, 0, 160),
+ );
+ draw_texture_ex(
+ tex,
+ ax,
+ ay,
+ WHITE,
+ DrawTextureParams {
+ dest_size: Some(Vec2::new(art_size, art_size)),
+ ..Default::default()
+ },
+ );
+ art_bottom = ay + art_size + 4.0;
+ }
+ let axis_str = match self.primary_axis {
+ Axis::X => "X",
+ Axis::Y => "Y",
+ };
+ let pri_str = match self.sort_primary {
+ Sort::Asc => "Asc",
+ Sort::Desc => "Desc",
+ };
+ let sec_str = match self.sort_secondary {
+ Sort::Asc => "Asc",
+ Sort::Desc => "Desc",
+ };
+ let sort_text = format!(
+ "Sort: Axis={} Primary={} Secondary={}",
+ axis_str, pri_str, sec_str
+ );
+ let sm = sharp_measure(18.0, &sort_text);
+ let sort_y = if art_bottom > 0.0 {
+ art_bottom
+ } else {
+ sh - sm.height - 15.0
+ };
+ draw_rectangle(
+ sw - sm.width - 19.0,
+ sort_y,
+ sm.width + 14.0,
+ sm.height + 10.0,
+ Color::from_rgba(0, 0, 0, 160),
+ );
+ sharp_text(
+ &sort_text,
+ sw - sm.width - 12.0,
+ sort_y + sm.height + 3.0,
+ 18.0,
+ Color::from_rgba(255, 255, 100, 255),
+ );
+
+ // ── Bottom-center: controls hint ──
+ let hint = "? help | ESC quit | Space preview | -/+ gain | 1-0 palette | E effect | N album art | R reset";
+ let hm = sharp_measure(14.0, hint);
+ let hx = (sw - hm.width) / 2.0;
+ sharp_text(
+ hint,
+ hx,
+ sh - 10.0,
+ 14.0,
+ Color::from_rgba(100, 100, 100, 200),
+ );
+ }
+
+ fn draw_help_overlay(&self) {
+ let sw = screen_width();
+ let sh = screen_height();
+
+ draw_rectangle(0.0, 0.0, sw, sh, Color::from_rgba(0, 0, 0, 200));
+
+ let x = sw / 2.0 - 280.0;
+ let mut y = sh / 2.0 - 180.0;
+
+ sharp_text("Keybinds", x, y, 28.0, WHITE);
+ y += 40.0;
+
+ let binds = [
+ ("ESC / Q", "Quit"),
+ ("?", "Toggle this help"),
+ ("Space", "Toggle panel visualization preview"),
+ ("- / +", "Decrease / increase gain"),
+ ("1-9, 0", "Switch color palette"),
+ ("E", "Cycle effect: Spectrum / Energy Wave / Pulse"),
+ ("A", "Toggle primary axis (X / Y)"),
+ ("P", "Toggle primary sort (Asc / Desc)"),
+ ("S", "Toggle secondary sort (Asc / Desc)"),
+ ("N", "Use album art colors from current track"),
+ ("R", "Reset all panels to black"),
+ ];
+
+ for (key, desc) in &binds {
+ sharp_text(key, x, y, 20.0, Color::from_rgba(255, 255, 100, 255));
+ sharp_text(desc, x + 120.0, y, 20.0, WHITE);
+ y += 26.0;
}
+
+ sharp_text(
+ "(Gain only affects visuals, not your music volume)",
+ x,
+ y + 14.0,
+ 16.0,
+ GRAY,
+ );
+ }
+
+ // ── Settings changes ─────────────────────────────────────────────────
+
+ fn change_palette(&mut self, index: usize) {
+ if index < self.palette_names.len() {
+ let palette_name = &self.palette_names[index];
+ if let Some(colors) = crate::palettes::get_palette(palette_name) {
+ self.current_palette_index = index;
+ self.stop_album_art_watcher();
+ if let Ok(mut state) = self.viz_state.lock() {
+ state.colors = colors.clone();
+ state.track_title = None;
+ }
+ let _ = self.visualizer_tx.send(VisualizerMsg::SetPalette(colors));
+ }
+ }
+ }
+
+ fn toggle_axis(&mut self) {
+ self.primary_axis = match self.primary_axis {
+ Axis::X => Axis::Y,
+ Axis::Y => Axis::X,
+ };
+ self.send_sorting();
+ }
+
+ fn toggle_primary_sort(&mut self) {
+ self.sort_primary = match self.sort_primary {
+ Sort::Asc => Sort::Desc,
+ Sort::Desc => Sort::Asc,
+ };
+ self.send_sorting();
+ }
+
+ fn toggle_secondary_sort(&mut self) {
+ self.sort_secondary = match self.sort_secondary {
+ Sort::Asc => Sort::Desc,
+ Sort::Desc => Sort::Asc,
+ };
+ self.send_sorting();
+ }
+
+ fn send_sorting(&self) {
+ let _ = self.visualizer_tx.send(VisualizerMsg::SetSorting {
+ primary_axis: self.primary_axis,
+ sort_primary: self.sort_primary,
+ sort_secondary: self.sort_secondary,
+ global_orientation: self.global_orientation,
+ });
+ }
+
+ fn cycle_effect(&mut self) {
+ self.effect = match self.effect {
+ Effect::Spectrum => Effect::EnergyWave,
+ Effect::EnergyWave => Effect::Pulse,
+ Effect::Pulse => Effect::Spectrum,
+ };
+ let _ = self
+ .visualizer_tx
+ .send(VisualizerMsg::SetEffect(self.effect));
}
- /// 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) {
+ /// Fetches album art + palette on a background thread so the render loop
+ /// never blocks on HTTP downloads or color extraction.
+ fn use_album_art_palette(&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);
+ self.album_art_stop = Some(Arc::clone(&stop));
+ let tx = self.visualizer_tx.clone();
+ let viz_state = Arc::clone(&self.viz_state);
std::thread::spawn(move || {
+ // Initial fetch
+ if let Some((artwork, colors)) = crate::now_playing::fetch_artwork_and_palette() {
+ let title = crate::now_playing::get_track_title();
+ if let Ok(mut state) = viz_state.lock() {
+ state.colors = colors.clone();
+ state.track_title = title;
+ state.artwork_bytes = Some(artwork);
+ }
+ let _ = tx.send(VisualizerMsg::SetPalette(colors));
+ }
+
+ // Then poll for track changes
let mut last_title = crate::now_playing::get_track_title();
loop {
std::thread::sleep(Duration::from_secs(3));
@@ -482,10 +706,12 @@ impl App {
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 Some((artwork, colors)) = crate::now_playing::fetch_artwork_and_palette()
+ {
if let Ok(mut state) = viz_state.lock() {
state.colors = colors.clone();
state.track_title = title;
+ state.artwork_bytes = Some(artwork);
}
let _ = tx.send(VisualizerMsg::SetPalette(colors));
}
@@ -494,375 +720,165 @@ impl App {
});
}
- /// Applies an `AppMsg` to update application state, views, or external components.
- ///
- /// Match on msg type:
- /// - NoOp/Quit: Idle or set Done state.
- /// - Scroll: Adjust effect list selection and scrollbar position.
- /// - View change: Switch views, pause/resume visualizer or effects mode, enable UDP if needed.
- /// - PlayEffect: Calls device.play_effect by selected name.
- /// - ChangeGain/Palette: Send SetGain/SetPalette to visualizer tx.
- /// - Toggles (Axis/Sorts): Flip enum, send SetSorting with current params to visualizer.
- ///
- /// Syncs list state with scrollbar for rendering.
- /// Ensures state transitions (e.g., resume viz only if switching to it).
- ///
- /// # Errors
- ///
- /// From device API calls or visualizer msg send.
- fn update(&mut self, msg: AppMsg) -> Result<()> {
- match msg {
- AppMsg::NoOp => Ok(()),
- AppMsg::Quit => {
- self.stop_album_art_watcher();
- self.state = AppState::Done;
- Ok(())
- }
- AppMsg::ScrollDown(k) => {
- self.effect_list.state.scroll_down_by(k);
- self.effect_list.scroll.pos = self.effect_list.scroll.pos.saturating_add(k);
- self.effect_list.scroll.state = self
- .effect_list
- .scroll
- .state
- .position(self.effect_list.scroll.pos as usize);
- Ok(())
- }
- AppMsg::ScrollUp(k) => {
- self.effect_list.state.scroll_up_by(k);
- self.effect_list.scroll.pos = self.effect_list.scroll.pos.saturating_sub(k);
- self.effect_list.scroll.state = self
- .effect_list
- .scroll
- .state
- .position(self.effect_list.scroll.pos as usize);
- Ok(())
- }
- AppMsg::ScrollToBottom => {
- self.effect_list.state.select_last();
- self.effect_list.scroll.pos = self.effect_list.list.len() as u16 - 1;
- self.effect_list.scroll.state = self
- .effect_list
- .scroll
- .state
- .position(self.effect_list.scroll.pos as usize);
- Ok(())
- }
- AppMsg::ScrollToTop => {
- self.effect_list.state.select_first();
- self.effect_list.scroll.pos = 0;
- self.effect_list.scroll.state = self
- .effect_list
- .scroll
- .state
- .position(self.effect_list.scroll.pos as usize);
- Ok(())
- }
- AppMsg::ChangeView(view) => {
- self.prev_view = self.view;
- self.view = view;
- match self.view {
- AppView::Visualizer => {
- if !matches!(self.state, AppState::RunningVisualizer) {
- self.nl_device.request_udp_control()?;
- self.visualizer.tx.send(VisualizerMsg::Resume)?;
- }
- self.state = AppState::RunningVisualizer;
- }
- AppView::EffectList => {
- if !matches!(self.state, AppState::RunningEffectList) {
- self.visualizer.tx.send(VisualizerMsg::Pause)?;
- }
- self.state = AppState::RunningEffectList;
- }
- AppView::HelpScreen => (),
- };
- Ok(())
- }
- AppMsg::PlayEffect(i) => {
- let effect_name = self.effect_list.list[i].name.as_str();
- self.nl_device.play_effect(effect_name)?;
- Ok(())
- }
- AppMsg::ChangeGain(delta) => {
- self.visualizer.gain += delta;
- self.visualizer
- .tx
- .send(VisualizerMsg::SetGain(self.visualizer.gain))?;
- Ok(())
- }
- AppMsg::ChangePalette(index) => {
- if index < self.visualizer.palette_names.len() {
- let palette_name = &self.visualizer.palette_names[index];
- if let Some(colors) = crate::palettes::get_palette(palette_name) {
- self.visualizer.current_palette_index = index;
- 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,
- Axis::Y => Axis::X,
- };
- self.visualizer.tx.send(VisualizerMsg::SetSorting {
- primary_axis: self.visualizer.primary_axis,
- sort_primary: self.visualizer.sort_primary,
- sort_secondary: self.visualizer.sort_secondary,
- global_orientation: self.visualizer.global_orientation,
- })?;
- Ok(())
- }
- AppMsg::TogglePrimarySort => {
- self.visualizer.sort_primary = match self.visualizer.sort_primary {
- Sort::Asc => Sort::Desc,
- Sort::Desc => Sort::Asc,
- };
- self.visualizer.tx.send(VisualizerMsg::SetSorting {
- primary_axis: self.visualizer.primary_axis,
- sort_primary: self.visualizer.sort_primary,
- sort_secondary: self.visualizer.sort_secondary,
- global_orientation: self.visualizer.global_orientation,
- })?;
- Ok(())
- }
- AppMsg::ToggleSecondarySort => {
- self.visualizer.sort_secondary = match self.visualizer.sort_secondary {
- Sort::Asc => Sort::Desc,
- Sort::Desc => Sort::Asc,
- };
- self.visualizer.tx.send(VisualizerMsg::SetSorting {
- primary_axis: self.visualizer.primary_axis,
- sort_primary: self.visualizer.sort_primary,
- sort_secondary: self.visualizer.sort_secondary,
- global_orientation: self.visualizer.global_orientation,
- })?;
- Ok(())
- }
+ fn stop_album_art_watcher(&mut self) {
+ if let Some(stop) = self.album_art_stop.take() {
+ stop.store(true, Ordering::Relaxed);
+ }
+ if let Ok(mut state) = self.viz_state.lock() {
+ state.track_title = None;
+ state.artwork_bytes = None;
}
}
+}
- /// Renders the active view (effect list, visualizer, or help) into the terminal frame.
- ///
- /// Creates bordered main block with device name in magenta left title, right-aligned "? for help".
- /// Dispatches to view-specific rendering:
- /// - `EffectList`: Stateful list of NlEffect items, optional colorful char styling via palette,
- /// highlight with >> symbol, synced scrollbar with padding.
- /// - Other views (Visualizer/HelpScreen): Implementation details for spectrum display or key bindings.
- /// - Updates scrollbar state post-render if needed.
- fn render_view(&mut self, frame: &mut Frame) {
- let main_block = Block::new()
- .borders(Borders::ALL)
- .title_top(
- Line::from(vec![
- "Connected to ".into(),
- self.nl_device.name.as_str().magenta(),
- ])
- .left_aligned(),
- )
- .title_top(
- Line::from(vec!["Press ".into(), "?".magenta(), " for help".into()])
- .right_aligned(),
- );
- match self.view {
- AppView::EffectList => {
- frame.render_stateful_widget(
- List::new(self.effect_list.list.iter().map(|x| {
- let name = x.name.as_str();
- if self.display_colors {
- ListItem::new(utils::colorful_effect_name(name, &x.palette))
- } else {
- ListItem::new(name)
- }
- }))
- .scroll_padding(2)
- .block(main_block)
- .highlight_style(Style::new().bold())
- .highlight_symbol(">> ")
- .highlight_spacing(HighlightSpacing::Always)
- .direction(ListDirection::TopToBottom),
- frame.area(),
- &mut self.effect_list.state,
- );
- self.effect_list.scroll.state = self
- .effect_list
- .scroll
- .state
- .content_length(self.effect_list.list.len());
- frame.render_stateful_widget(
- Scrollbar::new(ScrollbarOrientation::VerticalRight)
- .track_symbol(Some("│"))
- .begin_symbol(Some("↑"))
- .end_symbol(Some("↓")),
- frame.area().inner(Margin {
- vertical: 1,
- horizontal: 0,
- }),
- &mut self.effect_list.scroll.state,
- );
+// ── Free-standing controller drawing ─────────────────────────────────────
+
+fn draw_controller(
+ x: f32,
+ y: f32,
+ _panel: &PanelInfo,
+ scale: f32,
+ all_panels: &[PanelInfo],
+ transformed_positions: &[(f32, f32)],
+) {
+ // Find nearest parent light panel
+ let mut min_dist = f32::MAX;
+ let mut nearest_idx = 0;
+ for (i, other) in all_panels.iter().enumerate() {
+ if other.shape_type.side_length >= 1.0 {
+ let (ox, oy) = transformed_positions[i];
+ let dist = ((x - ox).powi(2) + (y - oy).powi(2)).sqrt();
+ if dist < min_dist {
+ min_dist = dist;
+ nearest_idx = i;
}
- AppView::Visualizer => {
- // 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 {
- Axis::X => "X",
- Axis::Y => "Y",
- };
- let primary_str = match self.visualizer.sort_primary {
- Sort::Asc => "Asc",
- Sort::Desc => "Desc",
- };
- let secondary_str = match self.visualizer.sort_secondary {
- Sort::Asc => "Asc",
- 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(""),
- Line::from(vec![
- "Amplitude gain: ".into(),
- 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![
- " Primary Axis [A]: ".into(),
- axis_str.yellow(),
- " | Primary [P]: ".into(),
- primary_str.yellow(),
- " | Secondary [S]: ".into(),
- secondary_str.yellow(),
- ]),
- 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 {
- "0".to_string()
- } else {
- (i + 1).to_string()
- };
- let is_current = i == self.visualizer.current_palette_index;
- let line = if is_current {
- Line::from(vec![
- key.bold().yellow(),
- " - ".into(),
- palette_name.as_str().green().bold(),
- " ◀".green(),
- ])
- } else {
- Line::from(vec![key.bold(), " - ".into(), palette_name.as_str().into()])
- };
- lines.push(line);
- }
+ let (parent_x, parent_y) = transformed_positions[nearest_idx];
+ let parent = &all_panels[nearest_idx];
+ let dx = x - parent_x;
+ let dy = y - parent_y;
+ let angle_to_ctrl = dy.atan2(dx);
+ let num_sides = parent.shape_type.num_sides();
+ let parent_side = parent.shape_type.side_length * scale;
- frame.render_widget(
- Paragraph::new(lines).block(main_block).centered(),
- frame.area(),
- );
- }
- AppView::HelpScreen => {
- frame.render_widget(
- Paragraph::new(vec![
- Line::from("Keybinds:".bold()),
- Line::from(vec!["?".bold(), " - toggle help".into()]),
- Line::from(vec!["Q/Esc".bold(), " - quit".into()]),
- 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/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)
- .centered(),
- frame.area(),
- );
- }
- };
+ let parent_radius = match num_sides {
+ 3 => parent_side / f32::sqrt(3.0),
+ 4 => parent_side / f32::sqrt(2.0),
+ _ => parent_side,
+ };
+
+ let parent_ori = (parent.orientation as f32).to_radians();
+ let angle_per_side = 2.0 * PI / num_sides as f32;
+
+ let mut closest_edge = 0;
+ let mut min_angle_diff = f32::MAX;
+ for i in 0..num_sides {
+ let va = parent_ori + (i as f32 * angle_per_side);
+ let raw = (angle_to_ctrl - va).abs() % (2.0 * PI);
+ let diff = raw.min((2.0 * PI) - raw);
+ if diff < min_angle_diff {
+ min_angle_diff = diff;
+ closest_edge = i;
+ }
+ }
+
+ let v1a = parent_ori + (closest_edge as f32 * angle_per_side);
+ let v2a = parent_ori + ((closest_edge + 1) as f32 * angle_per_side);
+ let v1x = parent_x + parent_radius * v1a.cos();
+ let v1y = parent_y + parent_radius * v1a.sin();
+ let v2x = parent_x + parent_radius * v2a.cos();
+ let v2y = parent_y + parent_radius * v2a.sin();
+
+ let trap_h = 20.0;
+ let mid_x = (v1x + v2x) / 2.0;
+ let mid_y = (v1y + v2y) / 2.0;
+ let pdx = mid_x - parent_x;
+ let pdy = mid_y - parent_y;
+ let plen = (pdx * pdx + pdy * pdy).sqrt();
+ let pnx = pdx / plen;
+ let pny = pdy / plen;
+ let nr = 0.6;
+
+ let verts = [
+ Vec2::new(v1x, v1y),
+ Vec2::new(v2x, v2y),
+ Vec2::new(
+ v2x + pnx * trap_h - (v2x - mid_x) * (1.0 - nr),
+ v2y + pny * trap_h - (v2y - mid_y) * (1.0 - nr),
+ ),
+ Vec2::new(
+ v1x + pnx * trap_h - (v1x - mid_x) * (1.0 - nr),
+ v1y + pny * trap_h - (v1y - mid_y) * (1.0 - nr),
+ ),
+ ];
+
+ let fill = Color::from_rgba(255, 200, 0, 255);
+ draw_triangle(verts[0], verts[1], verts[2], fill);
+ draw_triangle(verts[0], verts[2], verts[3], fill);
+
+ let outline = Color::from_rgba(200, 150, 0, 255);
+ for i in 0..4 {
+ let next = (i + 1) % 4;
+ draw_line(
+ verts[i].x,
+ verts[i].y,
+ verts[next].x,
+ verts[next].y,
+ 2.0,
+ outline,
+ );
+ }
+
+ let ts = 10.0;
+ let td = sharp_measure(ts, "C");
+ let lx = (verts[0].x + verts[1].x + verts[2].x + verts[3].x) / 4.0;
+ let ly = (verts[0].y + verts[1].y + verts[2].y + verts[3].y) / 4.0;
+ sharp_text("C", lx - td.width / 2.0, ly + ts / 3.0, ts, BLACK);
+}
+
+// ── Sharp text helpers (DPI-aware via camera_font_scale) ─────────────────
+
+fn sharp_text(text: &str, x: f32, y: f32, logical_size: f32, color: Color) {
+ let (fs, sx, sy) = camera_font_scale(logical_size);
+ draw_text_ex(
+ text,
+ x,
+ y,
+ TextParams {
+ font_size: fs,
+ font_scale: sx,
+ font_scale_aspect: sy / sx,
+ color,
+ ..Default::default()
+ },
+ );
+}
+
+fn sharp_measure(logical_size: f32, text: &str) -> TextDimensions {
+ let (fs, sx, sy) = camera_font_scale(logical_size);
+ measure_text(text, None, fs, sx * (sy / sx))
+}
+
+// ── Window icon ──────────────────────────────────────────────────────────
+
+fn load_icon() -> miniquad::conf::Icon {
+ fn decode_rgba(png_bytes: &[u8], size: u32) -> Vec {
+ let img = image::load_from_memory(png_bytes)
+ .expect("embedded icon PNG is valid")
+ .resize_exact(size, size, image::imageops::FilterType::Lanczos3)
+ .into_rgba8();
+ img.into_raw()
+ }
+
+ let small = decode_rgba(include_bytes!("../Assets/icon_16.png"), 16);
+ let medium = decode_rgba(include_bytes!("../Assets/icon_32.png"), 32);
+ let big = decode_rgba(include_bytes!("../Assets/icon_64.png"), 64);
+
+ miniquad::conf::Icon {
+ small: small.try_into().expect("16x16 RGBA = 1024 bytes"),
+ medium: medium.try_into().expect("32x32 RGBA = 4096 bytes"),
+ big: big.try_into().expect("64x64 RGBA = 16384 bytes"),
}
}
diff --git a/src/audio.rs b/src/audio.rs
index 92e4fa5..d1e5f76 100644
--- a/src/audio.rs
+++ b/src/audio.rs
@@ -55,6 +55,7 @@ impl AudioStream {
if let Ok(name) = device.description().map(|d| d.name().to_string())
&& loopback_names.iter().any(|lb| name.contains(lb))
{
+ #[cfg(debug_assertions)]
eprintln!("INFO: Found loopback device: {}", name);
loopback_device = Some(device);
break;
diff --git a/src/config.rs b/src/config.rs
index 9ccb250..0e8c89c 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -54,23 +54,6 @@ pub enum DumpType {
LayoutGraphical,
}
-#[derive(Debug, Serialize)]
-pub struct TuiConfig {
- pub colorful_effect_names: Option,
-}
-
-impl TuiConfig {
- /// Returns the default TUI configuration.
- ///
- /// Sets `colorful_effect_names` to the constant `DEFAULT_COLORFUL_EFFECT_NAMES` (typically false,
- /// meaning effect names in the effect list are displayed without per-character coloring based on palette).
- pub fn default() -> Self {
- TuiConfig {
- colorful_effect_names: Some(constants::DEFAULT_COLORFUL_EFFECT_NAMES),
- }
- }
-}
-
#[derive(Copy, Clone, Debug, Default, Serialize)]
pub enum Axis {
X,
@@ -155,7 +138,6 @@ impl VisualizerConfig {
#[derive(Debug, Serialize)]
pub struct Config {
pub default_nl_device_name: Option,
- pub tui_config: TuiConfig,
pub visualizer_config: VisualizerConfig,
}
@@ -167,48 +149,17 @@ impl Config {
/// # Arguments
///
/// * `default_nl_device_name` - Optional default Nanoleaf device name for quick selection.
- /// * `tui_config` - Optional TUI settings; defaults to `TuiConfig::default()`.
/// * `visualizer_config` - Optional visualizer params; defaults to `VisualizerConfig::default()`.
pub fn new(
default_nl_device_name: Option,
- tui_config: Option,
visualizer_config: Option,
) -> Self {
Config {
default_nl_device_name,
- tui_config: tui_config.unwrap_or(TuiConfig::default()),
visualizer_config: visualizer_config.unwrap_or(VisualizerConfig::default()),
}
}
- /// Parses a TOML table into the fields of a mutable `TuiConfig`.
- ///
- /// Supports key "colorful_effect_names" as boolean value.
- /// Ignores unknown keys? No, bails with error on invalid keys.
- /// Updates `tui_config` in place.
- ///
- /// # Arguments
- ///
- /// * `tui_config` - Mutable reference to populate.
- /// * `t` - TOML table from config section.
- ///
- /// # Errors
- ///
- /// `anyhow::Error` for invalid key types or unknown keys.
- pub fn parse_tui_config(tui_config: &mut TuiConfig, t: toml::Table) -> Result<()> {
- for (key, val) in t {
- match (key.as_str(), val) {
- ("colorful_effect_names", Value::Boolean(b)) => {
- tui_config.colorful_effect_names = Some(b);
- }
- (key, _) => {
- bail!(format!("invalid key `{}`", key));
- }
- }
- }
- Ok(())
- }
-
/// Parses a TOML table into the fields of a mutable `VisualizerConfig`.
///
/// Supports comprehensive field validation and type conversion:
@@ -316,10 +267,12 @@ impl Config {
}
}
("default_gain", Value::Float(x)) => {
+ #[cfg(debug_assertions)]
eprintln!("DEBUG: Parsed default_gain as Float: {}", x);
visualizer_config.default_gain = Some(x as f32);
}
("default_gain", Value::Integer(x)) => {
+ #[cfg(debug_assertions)]
eprintln!("DEBUG: Parsed default_gain as Integer: {}", x);
visualizer_config.default_gain = Some(x as f32);
}
@@ -413,24 +366,24 @@ impl Config {
///
/// File I/O errors, TOML deserialization failures, or validation bails.
pub fn parse_from_file(path: &Path) -> Result {
+ #[cfg(debug_assertions)]
eprintln!("DEBUG: Reading config from: {}", path.display());
let mut config_file = File::open(path)?;
let mut contents = String::new();
config_file.read_to_string(&mut contents)?;
+ #[cfg(debug_assertions)]
eprintln!("DEBUG: Config file contents:\n{}", contents);
let data = contents.parse::()?;
let mut default_nl_device_name = None;
- let mut tui_config = TuiConfig::default();
let mut visualizer_config = VisualizerConfig::default();
for (key, val) in data {
match (key.as_str(), val) {
("default_nl_device_name", Value::String(s)) => {
default_nl_device_name = Some(s);
}
- ("tui_config", Value::Table(t)) => {
- Self::parse_tui_config(&mut tui_config, t)?;
- }
+ // Silently ignore legacy tui_config section for backwards compatibility
+ ("tui_config", Value::Table(_)) => {}
("visualizer_config", Value::Table(t)) => {
Self::parse_visualizer_config(&mut visualizer_config, t)?;
}
@@ -439,11 +392,7 @@ impl Config {
}
}
}
- Ok(Config::new(
- default_nl_device_name,
- Some(tui_config),
- Some(visualizer_config),
- ))
+ Ok(Config::new(default_nl_device_name, Some(visualizer_config)))
}
/// Serializes and writes the configuration to a TOML file at the given path.
diff --git a/src/constants.rs b/src/constants.rs
index f0e4fb6..5ce3069 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -1,6 +1,3 @@
-// tui config
-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);
@@ -31,4 +28,3 @@ pub const DEFAULT_DEVICES_FILE: &str = "nl_devices.toml";
pub const DEFAULT_BACKTRACE_FILE: &str = "audioleaf_backtrace.log";
pub const NL_API_PORT: u16 = 16021;
pub const NL_UDP_PORT: u16 = 60222;
-pub const TICKRATE: u64 = 32;
diff --git a/src/event_handler.rs b/src/event_handler.rs
deleted file mode 100644
index f6eb1ae..0000000
--- a/src/event_handler.rs
+++ /dev/null
@@ -1,58 +0,0 @@
-use crate::{constants, panic};
-use anyhow::Result;
-use ratatui::crossterm::event;
-use std::{sync::mpsc, thread, time::Duration};
-
-pub enum Event {
- Key(event::KeyEvent),
- Tick,
-}
-
-pub struct EventHandler {
- rx: mpsc::Receiver,
-}
-
-impl EventHandler {
- /// Creates a new `EventHandler` that polls for terminal events and ticks.
- ///
- /// Spawns a background thread to:
- /// - Poll for key events (only processes KeyEventKind::Press to avoid repeats).
- /// - Send `Event::Key` or `Event::Tick` via mpsc channel at `constants::TICKRATE` ms intervals.
- /// - Registers panic handler in the thread for crash logging.
- ///
- /// # Returns
- ///
- /// `EventHandler` with receiver channel for events.
- pub fn new() -> Self {
- let tickrate = Duration::from_millis(constants::TICKRATE);
- let (tx, rx) = mpsc::channel();
- thread::spawn(move || {
- panic::register_backtrace_panic_handler();
- loop {
- if event::poll(tickrate).expect("event poll failed") {
- let event = event::read().expect("event read failed");
- if let event::Event::Key(key_event) = event {
- // Only process Press events to avoid duplicate triggers
- if key_event.kind == event::KeyEventKind::Press {
- tx.send(Event::Key(key_event)).expect("event send failed");
- }
- }
- }
- tx.send(Event::Tick).expect("tick send failed");
- }
- });
-
- EventHandler { rx }
- }
-
- /// Blocks and receives the next event from the event handler channel.
- ///
- /// Used in the main loop to process keyboard input or ticks for UI updates.
- ///
- /// # Returns
- ///
- /// `Result` - The received event, or error if channel recv fails (e.g., sender dropped).
- pub fn next(&self) -> Result {
- Ok(self.rx.recv()?)
- }
-}
diff --git a/src/main.rs b/src/main.rs
index 365ec7a..a0df67d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,7 +5,6 @@ mod app;
mod audio;
mod config;
mod constants;
-mod event_handler;
mod graphical_layout;
mod layout_visualizer;
mod nanoleaf;
@@ -50,7 +49,7 @@ async fn main_async() -> Result<()> {
} = cli_options;
let ((config_file_path, config_file_exists), (devices_file_path, devices_file_exists)) =
config::resolve_paths(config_file_path, devices_file_path)?;
- let (nl_device, tui_config, visualizer_config) = if !add_new && devices_file_exists {
+ let (nl_device, visualizer_config) = if !add_new && devices_file_exists {
if config_file_exists {
let config = config::Config::parse_from_file(&config_file_path)?;
let name_to_search = if device_name.is_some() {
@@ -60,13 +59,13 @@ async fn main_async() -> Result<()> {
};
let nl_device =
nanoleaf::NlDevice::find_in_file(&devices_file_path, name_to_search.as_deref())?;
- (nl_device, config.tui_config, config.visualizer_config)
+ (nl_device, config.visualizer_config)
} else {
let nl_device =
nanoleaf::NlDevice::find_in_file(&devices_file_path, device_name.as_deref())?;
- let config = config::Config::new(Some(nl_device.name.clone()), None, None);
+ let config = config::Config::new(Some(nl_device.name.clone()), None);
config.write_to_file(&config_file_path)?;
- (nl_device, config.tui_config, config.visualizer_config)
+ (nl_device, config.visualizer_config)
}
} else {
let ip = config::get_ip()?;
@@ -77,19 +76,17 @@ async fn main_async() -> Result<()> {
config.default_nl_device_name = Some(nl_device.name.clone());
config
} else {
- config::Config::new(Some(nl_device.name.clone()), None, None)
+ config::Config::new(Some(nl_device.name.clone()), None)
};
config.write_to_file(&config_file_path)?;
- (nl_device, config.tui_config, config.visualizer_config)
+ (nl_device, config.visualizer_config)
};
// Ensure device is powered on and has brightness set
nl_device.ensure_device_ready()?;
- let mut app = app::App::new(nl_device, tui_config, visualizer_config)?;
- let mut terminal = utils::init_tui()?;
- app.run(&mut terminal)?;
- utils::destroy_tui()?;
+ let app = app::App::new(nl_device, visualizer_config)?;
+ app.run();
Ok(())
}
diff --git a/src/nanoleaf.rs b/src/nanoleaf.rs
index 37b5280..f05010d 100644
--- a/src/nanoleaf.rs
+++ b/src/nanoleaf.rs
@@ -3,7 +3,7 @@ use crate::{
constants, utils,
};
use anyhow::{Result, bail};
-use palette::{FromColor, Hsv, Oklch, Srgb};
+use palette::{FromColor, Oklch, Srgb};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{
@@ -13,20 +13,11 @@ use std::{
path::Path,
};
-#[derive(Debug)]
-pub struct NlEffect {
- pub name: String,
- pub palette: Vec>,
-}
-
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NlDevice {
pub name: String,
pub ip: Ipv4Addr,
pub token: String,
- #[serde(skip)]
- #[serde(default)]
- pub cur_effect_name: Option,
}
/// wrapper struct for TOML serialization
@@ -58,7 +49,7 @@ impl NlDevice {
///
/// # Returns
///
- /// `Result` with name, ip, token, optional cur_effect_name.
+ /// `Result` with name, ip, and token.
///
/// # Errors
///
@@ -66,13 +57,7 @@ impl NlDevice {
pub fn new(ip: Ipv4Addr) -> Result {
let token = Self::get_token(&ip)?;
let name = Self::get_name(&ip, &token)?;
- let cur_effect_name = Self::get_cur_effect_name(&ip, &token)?;
- Ok(NlDevice {
- name,
- ip,
- token,
- cur_effect_name,
- })
+ Ok(NlDevice { name, ip, token })
}
fn get_token(ip: &Ipv4Addr) -> Result {
@@ -101,27 +86,6 @@ impl NlDevice {
Ok(String::from(res_json["name"].as_str().unwrap()))
}
- pub fn get_cur_effect_name(ip: &Ipv4Addr, token: &str) -> Result