MQTT Theme Support, test debs, release debs

4b251af · gmorell · 2026-01-12 02:39

8 files +595 -12

Files changed

modified .github/workflows/rust-ci.yml
+45 −3
@@ -19,7 +19,7 @@ jobs:
19 19 include:
20 20 - runner: ubuntu-latest
21 21 arch: x86_64
22 − - runner: [self-hosted, runner-aarch64]
22 + - runner: ubuntu-24.04-arm
23 23 arch: aarch64
24 24 runs-on: ${{ matrix.runner }}
25 25 permissions:
@@ -72,6 +72,30 @@ jobs:
72 72 - name: Check
73 73 run: cargo check
74 74
75 + - name: Build release artifact
76 + run: cargo build --release --verbose
77 +
78 + - name: Strip binary
79 + run: strip target/release/koco
80 +
81 + - name: Install cargo-deb
82 + run: cargo install cargo-deb
83 +
84 + - name: Build test Debian package
85 + run: cargo deb --no-build --no-strip
86 +
87 + - name: Upload test artifact
88 + uses: actions/upload-artifact@v4
89 + with:
90 + name: koco-test-${{ matrix.arch }}-linux
91 + path: target/release/koco
92 +
93 + - name: Upload test Debian package
94 + uses: actions/upload-artifact@v4
95 + with:
96 + name: koco-test-${{ matrix.arch }}-linux.deb
97 + path: target/debian/*.deb
98 +
75 99
76 100 release:
77 101 name: Release
@@ -88,7 +112,7 @@ jobs:
88 112 archive_cmd: tar -czf
89 113 binary_ext: ""
90 114 rust: "1.92"
91 − - os: [self-hosted, runner-aarch64]
115 + - os: ubuntu-24.04-arm
92 116 name: koco-aarch64-linux-gnu.tar.gz
93 117 archive_cmd: tar -czf
94 118 binary_ext: ""
@@ -159,12 +183,27 @@ jobs:
159 183 7z a ../../${{ matrix.name }} koco.exe
160 184 cd ../..
161 185
186 + - name: Install cargo-deb (Linux)
187 + if: runner.os == 'Linux'
188 + run: cargo install cargo-deb
189 +
190 + - name: Build Debian package (Linux)
191 + if: runner.os == 'Linux'
192 + run: cargo deb --no-build --no-strip
193 +
162 194 - name: Upload release archive
163 195 uses: actions/upload-artifact@v4
164 196 with:
165 197 name: ${{ matrix.name }}
166 198 path: ${{ matrix.name }}
167 199
200 + - name: Upload Debian package
201 + if: runner.os == 'Linux'
202 + uses: actions/upload-artifact@v4
203 + with:
204 + name: koco-${{ matrix.os }}.deb
205 + path: target/debian/*.deb
206 +
168 207 create-release:
169 208 name: Create Release
170 209 runs-on: ubuntu-latest
@@ -203,7 +242,10 @@ jobs:
203 242 - name: Create GitHub Release
204 243 uses: softprops/action-gh-release@v1
205 244 with:
206 − files: artifacts/*/*.tar.gz,artifacts/*/*.zip
245 + files: |
246 + artifacts/*/*.tar.gz
247 + artifacts/*/*.zip
248 + artifacts/*/*.deb
207 249 body_path: release_notes.md
208 250 draft: false
209 251 prerelease: ${{ contains(github.ref_name, '-') }}
modified Cargo.lock
+264 −9
@@ -70,7 +70,7 @@ checksum = "301e55b39cfc15d9c48943ce5f572204a551646700d0e8efa424585f94fec528"
70 70 dependencies = [
71 71 "accesskit",
72 72 "accesskit_atspi_common",
73 − "async-channel",
73 + "async-channel 2.5.0",
74 74 "async-executor",
75 75 "async-task",
76 76 "atspi",
@@ -127,6 +127,15 @@ dependencies = [
127 127 "zerocopy",
128 128 ]
129 129
130 +[[package]]
131 +name = "aho-corasick"
132 +version = "1.1.4"
133 +source = "registry+https://github.com/rust-lang/crates.io-index"
134 +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
135 +dependencies = [
136 + "memchr",
137 +]
138 +
130 139 [[package]]
131 140 name = "android-activity"
132 141 version = "0.6.0"
@@ -163,6 +172,56 @@ dependencies = [
163 172 "libc",
164 173 ]
165 174
175 +[[package]]
176 +name = "anstream"
177 +version = "0.6.21"
178 +source = "registry+https://github.com/rust-lang/crates.io-index"
179 +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
180 +dependencies = [
181 + "anstyle",
182 + "anstyle-parse",
183 + "anstyle-query",
184 + "anstyle-wincon",
185 + "colorchoice",
186 + "is_terminal_polyfill",
187 + "utf8parse",
188 +]
189 +
190 +[[package]]
191 +name = "anstyle"
192 +version = "1.0.13"
193 +source = "registry+https://github.com/rust-lang/crates.io-index"
194 +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
195 +
196 +[[package]]
197 +name = "anstyle-parse"
198 +version = "0.2.7"
199 +source = "registry+https://github.com/rust-lang/crates.io-index"
200 +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
201 +dependencies = [
202 + "utf8parse",
203 +]
204 +
205 +[[package]]
206 +name = "anstyle-query"
207 +version = "1.1.5"
208 +source = "registry+https://github.com/rust-lang/crates.io-index"
209 +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
210 +dependencies = [
211 + "windows-sys 0.61.2",
212 +]
213 +
214 +[[package]]
215 +name = "anstyle-wincon"
216 +version = "3.0.11"
217 +source = "registry+https://github.com/rust-lang/crates.io-index"
218 +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
219 +dependencies = [
220 + "anstyle",
221 + "once_cell_polyfill",
222 + "windows-sys 0.61.2",
223 +]
224 +
166 225 [[package]]
167 226 name = "arboard"
168 227 version = "3.6.1"
@@ -216,12 +275,23 @@ version = "0.7.2"
216 275 source = "registry+https://github.com/rust-lang/crates.io-index"
217 276 checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
218 277 dependencies = [
219 − "event-listener",
278 + "event-listener 5.4.1",
220 279 "event-listener-strategy",
221 280 "futures-core",
222 281 "pin-project-lite",
223 282 ]
224 283
284 +[[package]]
285 +name = "async-channel"
286 +version = "1.9.0"
287 +source = "registry+https://github.com/rust-lang/crates.io-index"
288 +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
289 +dependencies = [
290 + "concurrent-queue",
291 + "event-listener 2.5.3",
292 + "futures-core",
293 +]
294 +
225 295 [[package]]
226 296 name = "async-channel"
227 297 version = "2.5.0"
@@ -272,7 +342,7 @@ version = "3.4.2"
272 342 source = "registry+https://github.com/rust-lang/crates.io-index"
273 343 checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
274 344 dependencies = [
275 − "event-listener",
345 + "event-listener 5.4.1",
276 346 "event-listener-strategy",
277 347 "pin-project-lite",
278 348 ]
@@ -283,14 +353,14 @@ version = "2.5.0"
283 353 source = "registry+https://github.com/rust-lang/crates.io-index"
284 354 checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
285 355 dependencies = [
286 − "async-channel",
356 + "async-channel 2.5.0",
287 357 "async-io",
288 358 "async-lock",
289 359 "async-signal",
290 360 "async-task",
291 361 "blocking",
292 362 "cfg-if",
293 − "event-listener",
363 + "event-listener 5.4.1",
294 364 "futures-lite",
295 365 "rustix 1.1.3",
296 366 ]
@@ -466,7 +536,7 @@ version = "1.6.2"
466 536 source = "registry+https://github.com/rust-lang/crates.io-index"
467 537 checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
468 538 dependencies = [
469 − "async-channel",
539 + "async-channel 2.5.0",
470 540 "async-task",
471 541 "futures-io",
472 542 "futures-lite",
@@ -629,6 +699,15 @@ dependencies = [
629 699 "error-code",
630 700 ]
631 701
702 +[[package]]
703 +name = "cmake"
704 +version = "0.1.57"
705 +source = "registry+https://github.com/rust-lang/crates.io-index"
706 +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
707 +dependencies = [
708 + "cc",
709 +]
710 +
632 711 [[package]]
633 712 name = "codespan-reporting"
634 713 version = "0.12.0"
@@ -640,6 +719,12 @@ dependencies = [
640 719 "unicode-width",
641 720 ]
642 721
722 +[[package]]
723 +name = "colorchoice"
724 +version = "1.0.4"
725 +source = "registry+https://github.com/rust-lang/crates.io-index"
726 +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
727 +
643 728 [[package]]
644 729 name = "combine"
645 730 version = "4.6.7"
@@ -738,6 +823,15 @@ dependencies = [
738 823 "cfg-if",
739 824 ]
740 825
826 +[[package]]
827 +name = "crossbeam-channel"
828 +version = "0.5.15"
829 +source = "registry+https://github.com/rust-lang/crates.io-index"
830 +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
831 +dependencies = [
832 + "crossbeam-utils",
833 +]
834 +
741 835 [[package]]
742 836 name = "crossbeam-utils"
743 837 version = "0.8.21"
@@ -1036,6 +1130,29 @@ dependencies = [
1036 1130 "syn",
1037 1131 ]
1038 1132
1133 +[[package]]
1134 +name = "env_filter"
1135 +version = "0.1.4"
1136 +source = "registry+https://github.com/rust-lang/crates.io-index"
1137 +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2"
1138 +dependencies = [
1139 + "log",
1140 + "regex",
1141 +]
1142 +
1143 +[[package]]
1144 +name = "env_logger"
1145 +version = "0.11.8"
1146 +source = "registry+https://github.com/rust-lang/crates.io-index"
1147 +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
1148 +dependencies = [
1149 + "anstream",
1150 + "anstyle",
1151 + "env_filter",
1152 + "jiff",
1153 + "log",
1154 +]
1155 +
1039 1156 [[package]]
1040 1157 name = "epaint"
1041 1158 version = "0.33.3"
@@ -1082,6 +1199,12 @@ version = "3.3.2"
1082 1199 source = "registry+https://github.com/rust-lang/crates.io-index"
1083 1200 checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
1084 1201
1202 +[[package]]
1203 +name = "event-listener"
1204 +version = "2.5.3"
1205 +source = "registry+https://github.com/rust-lang/crates.io-index"
1206 +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
1207 +
1085 1208 [[package]]
1086 1209 name = "event-listener"
1087 1210 version = "5.4.1"
@@ -1099,7 +1222,7 @@ version = "0.5.4"
1099 1222 source = "registry+https://github.com/rust-lang/crates.io-index"
1100 1223 checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
1101 1224 dependencies = [
1102 − "event-listener",
1225 + "event-listener 5.4.1",
1103 1226 "pin-project-lite",
1104 1227 ]
1105 1228
@@ -1223,6 +1346,21 @@ dependencies = [
1223 1346 "percent-encoding",
1224 1347 ]
1225 1348
1349 +[[package]]
1350 +name = "futures"
1351 +version = "0.3.31"
1352 +source = "registry+https://github.com/rust-lang/crates.io-index"
1353 +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
1354 +dependencies = [
1355 + "futures-channel",
1356 + "futures-core",
1357 + "futures-executor",
1358 + "futures-io",
1359 + "futures-sink",
1360 + "futures-task",
1361 + "futures-util",
1362 +]
1363 +
1226 1364 [[package]]
1227 1365 name = "futures-channel"
1228 1366 version = "0.3.31"
@@ -1239,6 +1377,17 @@ version = "0.3.31"
1239 1377 source = "registry+https://github.com/rust-lang/crates.io-index"
1240 1378 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
1241 1379
1380 +[[package]]
1381 +name = "futures-executor"
1382 +version = "0.3.31"
1383 +source = "registry+https://github.com/rust-lang/crates.io-index"
1384 +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
1385 +dependencies = [
1386 + "futures-core",
1387 + "futures-task",
1388 + "futures-util",
1389 +]
1390 +
1242 1391 [[package]]
1243 1392 name = "futures-io"
1244 1393 version = "0.3.31"
@@ -1281,12 +1430,19 @@ version = "0.3.31"
1281 1430 source = "registry+https://github.com/rust-lang/crates.io-index"
1282 1431 checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
1283 1432
1433 +[[package]]
1434 +name = "futures-timer"
1435 +version = "3.0.3"
1436 +source = "registry+https://github.com/rust-lang/crates.io-index"
1437 +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
1438 +
1284 1439 [[package]]
1285 1440 name = "futures-util"
1286 1441 version = "0.3.31"
1287 1442 source = "registry+https://github.com/rust-lang/crates.io-index"
1288 1443 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
1289 1444 dependencies = [
1445 + "futures-channel",
1290 1446 "futures-core",
1291 1447 "futures-io",
1292 1448 "futures-macro",
@@ -1833,12 +1989,42 @@ dependencies = [
1833 1989 "serde",
1834 1990 ]
1835 1991
1992 +[[package]]
1993 +name = "is_terminal_polyfill"
1994 +version = "1.70.2"
1995 +source = "registry+https://github.com/rust-lang/crates.io-index"
1996 +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
1997 +
1836 1998 [[package]]
1837 1999 name = "itoa"
1838 2000 version = "1.0.16"
1839 2001 source = "registry+https://github.com/rust-lang/crates.io-index"
1840 2002 checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
1841 2003
2004 +[[package]]
2005 +name = "jiff"
2006 +version = "0.2.17"
2007 +source = "registry+https://github.com/rust-lang/crates.io-index"
2008 +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8"
2009 +dependencies = [
2010 + "jiff-static",
2011 + "log",
2012 + "portable-atomic",
2013 + "portable-atomic-util",
2014 + "serde_core",
2015 +]
2016 +
2017 +[[package]]
2018 +name = "jiff-static"
2019 +version = "0.2.17"
2020 +source = "registry+https://github.com/rust-lang/crates.io-index"
2021 +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58"
2022 +dependencies = [
2023 + "proc-macro2",
2024 + "quote",
2025 + "syn",
2026 +]
2027 +
1842 2028 [[package]]
1843 2029 name = "jni"
1844 2030 version = "0.21.1"
@@ -1900,15 +2086,17 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
1900 2086
1901 2087 [[package]]
1902 2088 name = "koco"
1903 −version = "0.1.1"
2089 +version = "0.1.2"
1904 2090 dependencies = [
1905 2091 "chrono",
1906 2092 "dirs",
1907 2093 "eframe",
1908 2094 "egui",
1909 2095 "egui_material_icons",
2096 + "env_logger",
1910 2097 "futures-util",
1911 2098 "log",
2099 + "paho-mqtt",
1912 2100 "reqwest",
1913 2101 "serde",
1914 2102 "serde_json",
@@ -2484,6 +2672,12 @@ version = "1.21.3"
2484 2672 source = "registry+https://github.com/rust-lang/crates.io-index"
2485 2673 checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
2486 2674
2675 +[[package]]
2676 +name = "once_cell_polyfill"
2677 +version = "1.70.2"
2678 +source = "registry+https://github.com/rust-lang/crates.io-index"
2679 +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
2680 +
2487 2681 [[package]]
2488 2682 name = "openssl"
2489 2683 version = "0.10.75"
@@ -2571,6 +2765,32 @@ dependencies = [
2571 2765 "ttf-parser",
2572 2766 ]
2573 2767
2768 +[[package]]
2769 +name = "paho-mqtt"
2770 +version = "0.13.3"
2771 +source = "registry+https://github.com/rust-lang/crates.io-index"
2772 +checksum = "e2a9dc54c16b2e290ab3045b214a2a4effb61a42cf920cc83aa250367a4cceed"
2773 +dependencies = [
2774 + "async-channel 1.9.0",
2775 + "crossbeam-channel",
2776 + "futures",
2777 + "futures-timer",
2778 + "libc",
2779 + "log",
2780 + "paho-mqtt-sys",
2781 + "thiserror 1.0.69",
2782 +]
2783 +
2784 +[[package]]
2785 +name = "paho-mqtt-sys"
2786 +version = "0.10.3"
2787 +source = "registry+https://github.com/rust-lang/crates.io-index"
2788 +checksum = "4d3094e35ab92c9ffa90775d8faad0e8ecac0a0ed4c409c3e683cbf84f784287"
2789 +dependencies = [
2790 + "cmake",
2791 + "openssl-sys",
2792 +]
2793 +
2574 2794 [[package]]
2575 2795 name = "parking"
2576 2796 version = "2.2.1"
@@ -2880,6 +3100,35 @@ dependencies = [
2880 3100 "thiserror 1.0.69",
2881 3101 ]
2882 3102
3103 +[[package]]
3104 +name = "regex"
3105 +version = "1.12.2"
3106 +source = "registry+https://github.com/rust-lang/crates.io-index"
3107 +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
3108 +dependencies = [
3109 + "aho-corasick",
3110 + "memchr",
3111 + "regex-automata",
3112 + "regex-syntax",
3113 +]
3114 +
3115 +[[package]]
3116 +name = "regex-automata"
3117 +version = "0.4.13"
3118 +source = "registry+https://github.com/rust-lang/crates.io-index"
3119 +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
3120 +dependencies = [
3121 + "aho-corasick",
3122 + "memchr",
3123 + "regex-syntax",
3124 +]
3125 +
3126 +[[package]]
3127 +name = "regex-syntax"
3128 +version = "0.8.8"
3129 +source = "registry+https://github.com/rust-lang/crates.io-index"
3130 +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
3131 +
2883 3132 [[package]]
2884 3133 name = "renderdoc-sys"
2885 3134 version = "1.1.0"
@@ -3822,6 +4071,12 @@ version = "1.0.4"
3822 4071 source = "registry+https://github.com/rust-lang/crates.io-index"
3823 4072 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
3824 4073
4074 +[[package]]
4075 +name = "utf8parse"
4076 +version = "0.2.2"
4077 +source = "registry+https://github.com/rust-lang/crates.io-index"
4078 +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
4079 +
3825 4080 [[package]]
3826 4081 name = "uuid"
3827 4082 version = "1.19.0"
@@ -4978,7 +5233,7 @@ dependencies = [
4978 5233 "async-trait",
4979 5234 "blocking",
4980 5235 "enumflags2",
4981 − "event-listener",
5236 + "event-listener 5.4.1",
4982 5237 "futures-core",
4983 5238 "futures-lite",
4984 5239 "hex",
modified Cargo.toml
+2 −0
@@ -9,6 +9,7 @@ eframe = "0.33.3"
9 9 egui = "0.33.3"
10 10 egui_material_icons = "0.5.0"
11 11 log = "0.4"
12 +env_logger = "0.11"
12 13 serde = { version = "1.0", features = ["derive", "default"] }
13 14 serde_json = "1.0"
14 15 toml = "0.8.0"
@@ -17,3 +18,4 @@ reqwest = { version = "0.12", features = ["blocking", "json"] }
17 18 tokio = { version = "1", features = ["full"] }
18 19 tokio-tungstenite = "0.24"
19 20 futures-util = "0.3"
21 +paho-mqtt = "0.13"
modified README.md
+1 −0
@@ -1,4 +1,5 @@
1 1 # Koco
2 +[![THEME THIS SHIT STAT](https://neiam.org/images/themethisshit.svg)](https://theme-this-shit.neiam.org)
2 3
3 4 A Minimal Desktop **Ko**di **Co**ntroller
4 5
modified src/main.rs
+283 −0
@@ -114,6 +114,7 @@ struct Koco {
114 114 config_ui_state: ConfigUIState,
115 115 now_playing: Arc<Mutex<NowPlaying>>,
116 116 runtime: Arc<Runtime>,
117 + theme_changed: Arc<Mutex<bool>>,
117 118 }
118 119
119 120 #[derive(Default, PartialEq)]
@@ -121,6 +122,7 @@ enum SettingsTab {
121 122 #[default]
122 123 Edit,
123 124 Add,
125 + MqttConfig,
124 126 }
125 127
126 128 #[derive(Default)]
@@ -142,6 +144,13 @@ struct ConfigUIState {
142 144 new_password: String,
143 145 new_events: bool,
144 146 new_events_port: String,
147 +
148 + // For MQTT config
149 + mqtt_enabled: bool,
150 + mqtt_host: String,
151 + mqtt_username: String,
152 + mqtt_password: String,
153 + mqtt_topic: String,
145 154 }
146 155
147 156 impl ConfigUIState {
@@ -154,6 +163,14 @@ impl ConfigUIState {
154 163 self.events = instance.events;
155 164 self.events_port = instance.events_port.to_string();
156 165 }
166 +
167 + fn load_mqtt_config(&mut self, config: &KocoConfig) {
168 + self.mqtt_enabled = config.mqtt_enabled;
169 + self.mqtt_host = config.mqtt_host.clone();
170 + self.mqtt_username = config.mqtt_username.clone().unwrap_or_default();
171 + self.mqtt_password = config.mqtt_password.clone().unwrap_or_default();
172 + self.mqtt_topic = config.mqtt_topic.clone();
173 + }
157 174
158 175 fn to_instance(&self) -> Result<Instance, String> {
159 176 let http_port = self
@@ -249,6 +266,24 @@ struct KocoConfig {
249 266 instances: Vec<Instance>,
250 267 current_theme: String,
251 268 themes: Vec<Theme>,
269 + #[serde(default)]
270 + mqtt_enabled: bool,
271 + #[serde(default = "default_mqtt_host")]
272 + mqtt_host: String,
273 + #[serde(default)]
274 + mqtt_username: Option<String>,
275 + #[serde(default)]
276 + mqtt_password: Option<String>,
277 + #[serde(default = "default_mqtt_topic")]
278 + mqtt_topic: String,
279 +}
280 +
281 +fn default_mqtt_host() -> String {
282 + "tcp://localhost:1883".to_string()
283 +}
284 +
285 +fn default_mqtt_topic() -> String {
286 + "neiam/sync/theme".to_string()
252 287 }
253 288
254 289 fn create_default_themes() -> Vec<Theme> {
@@ -387,6 +422,11 @@ impl Default for KocoConfig {
387 422 }],
388 423 current_theme: "Default".to_string(),
389 424 themes: create_default_themes(),
425 + mqtt_enabled: false,
426 + mqtt_host: default_mqtt_host(),
427 + mqtt_username: None,
428 + mqtt_password: None,
429 + mqtt_topic: default_mqtt_topic(),
390 430 }
391 431 }
392 432 }
@@ -481,9 +521,12 @@ impl Koco {
481 521 if !config.instances.is_empty() {
482 522 config_ui_state.load_from_instance(&config.instances[0]);
483 523 }
524 +
525 + config_ui_state.load_mqtt_config(&config);
484 526
485 527 let now_playing = Arc::new(Mutex::new(NowPlaying::default()));
486 528 let runtime = Arc::new(Runtime::new().expect("Failed to create Tokio runtime"));
529 + let theme_changed = Arc::new(Mutex::new(false));
487 530
488 531 let app = Self {
489 532 config,
@@ -493,6 +536,7 @@ impl Koco {
493 536 config_ui_state,
494 537 now_playing,
495 538 runtime,
539 + theme_changed,
496 540 };
497 541
498 542 // Connect to websocket on startup
@@ -501,6 +545,9 @@ impl Koco {
501 545 // Start position updater
502 546 app.start_position_updater();
503 547
548 + // Start MQTT subscriber if enabled
549 + app.start_mqtt_subscriber();
550 +
504 551 app
505 552 }
506 553
@@ -747,6 +794,121 @@ impl Koco {
747 794 });
748 795 }
749 796
797 + fn start_mqtt_subscriber(&self) {
798 + if !self.config.mqtt_enabled {
799 + info!("MQTT subscription is disabled");
800 + return;
801 + }
802 +
803 + let mqtt_host = self.config.mqtt_host.clone();
804 + let mqtt_topic = self.config.mqtt_topic.clone();
805 + let mqtt_username = self.config.mqtt_username.clone();
806 + let mqtt_password = self.config.mqtt_password.clone();
807 + let config = Arc::new(Mutex::new(self.config.clone()));
808 + let theme_changed = Arc::clone(&self.theme_changed);
809 +
810 + info!("Starting MQTT subscriber for topic: {}", mqtt_topic);
811 +
812 + self.runtime.spawn(async move {
813 + loop {
814 + // Create MQTT client
815 + let create_opts = match paho_mqtt::CreateOptionsBuilder::new()
816 + .server_uri(&mqtt_host)
817 + .client_id("koco-theme-subscriber")
818 + .finalize()
819 + {
820 + opts => opts,
821 + };
822 +
823 + let mut client = match paho_mqtt::AsyncClient::new(create_opts) {
824 + Ok(c) => c,
825 + Err(e) => {
826 + warn!("Failed to create MQTT client: {}", e);
827 + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
828 + continue;
829 + }
830 + };
831 +
832 + // Set up connection options
833 + let conn_opts = {
834 + let mut conn_opts_builder = paho_mqtt::ConnectOptionsBuilder::new();
835 + conn_opts_builder.keep_alive_interval(std::time::Duration::from_secs(20));
836 + conn_opts_builder.clean_session(true);
837 + conn_opts_builder.automatic_reconnect(std::time::Duration::from_secs(1), std::time::Duration::from_secs(30));
838 +
839 + if let (Some(username), Some(password)) = (&mqtt_username, &mqtt_password) {
840 + conn_opts_builder.user_name(username).password(password);
841 + }
842 + conn_opts_builder.finalize()
843 + };
844 +
845 + // Connect and subscribe
846 + match client.connect(conn_opts).await {
847 + Ok(_) => {
848 + info!("Connected to MQTT broker at {}", mqtt_host);
849 + match client.subscribe(&mqtt_topic, 1).await {
850 + Ok(_) => info!("Subscribed to MQTT topic: {}", mqtt_topic),
851 + Err(e) => {
852 + warn!("Failed to subscribe to MQTT topic: {}", e);
853 + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
854 + continue;
855 + }
856 + }
857 +
858 + // Start consuming messages
859 + let mut stream = client.get_stream(25);
860 + while let Some(msg_opt) = stream.next().await {
861 + if let Some(msg) = msg_opt {
862 + info!("Received MQTT message: {}", msg.payload_str());
863 +
864 + // Parse the theme payload
865 + if let Ok(payload) = serde_json::from_str::<serde_json::Value>(&msg.payload_str()) {
866 + if let Some(theme_name) = payload.get("theme").and_then(|t| t.as_str()) {
867 + info!("Received theme update: {}", theme_name);
868 +
869 + // Update config with new theme
870 + if let Ok(mut cfg) = config.lock() {
871 + // Search for theme by name (case-insensitive)
872 + let matching_theme = cfg.themes.iter()
873 + .find(|t| t.name.eq_ignore_ascii_case(theme_name));
874 +
875 + if let Some(theme) = matching_theme {
876 + let theme_name_found = theme.name.clone();
877 +
878 + if cfg.current_theme != theme_name_found {
879 + cfg.current_theme = theme_name_found.clone();
880 + if let Err(e) = cfg.save() {
881 + warn!("Failed to save theme update: {}", e);
882 + } else {
883 + info!("Updated theme to: {}", theme_name_found);
884 + // Signal that the theme has changed
885 + if let Ok(mut changed) = theme_changed.lock() {
886 + *changed = true;
887 + }
888 + }
889 + }
890 + } else {
891 + warn!("Theme '{}' not found in available themes, keeping current", theme_name);
892 + }
893 + }
894 + }
895 + }
896 + }
897 + }
898 +
899 + warn!("MQTT stream ended, reconnecting...");
900 + }
901 + Err(e) => {
902 + warn!("Failed to connect to MQTT broker: {}", e);
903 + }
904 + }
905 +
906 + // Wait before reconnecting
907 + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
908 + }
909 + });
910 + }
911 +
750 912 fn apply_theme(&self, ctx: &egui::Context) {
751 913 let theme = self.config.get_current_theme_or_default();
752 914 let mut visuals = egui::Visuals::dark();
@@ -842,6 +1004,11 @@ impl Koco {
842 1004 SettingsTab::Add,
843 1005 "Add Instance",
844 1006 );
1007 + ui.selectable_value(
1008 + &mut self.settings_tab,
1009 + SettingsTab::MqttConfig,
1010 + "MQTT Theme Sync",
1011 + );
845 1012 });
846 1013
847 1014 ui.separator();
@@ -993,6 +1160,71 @@ impl Koco {
993 1160 }
994 1161 }
995 1162 }
1163 + SettingsTab::MqttConfig => {
1164 + // MQTT Configuration
1165 + ui.heading("MQTT Theme Synchronization");
1166 + ui.add_space(5.0);
1167 +
1168 + ui.label("Subscribe to theme updates from theme-sender");
1169 + ui.add_space(10.0);
1170 +
1171 + ui.checkbox(&mut self.config_ui_state.mqtt_enabled, "Enable MQTT Subscription");
1172 + ui.add_space(10.0);
1173 +
1174 + if self.config_ui_state.mqtt_enabled {
1175 + ui.horizontal(|ui| {
1176 + ui.label("MQTT Host:");
1177 + ui.text_edit_singleline(&mut self.config_ui_state.mqtt_host);
1178 + });
1179 + ui.label("Example: tcp://localhost:1883");
1180 + ui.add_space(5.0);
1181 +
1182 + ui.horizontal(|ui| {
1183 + ui.label("Topic:");
1184 + ui.text_edit_singleline(&mut self.config_ui_state.mqtt_topic);
1185 + });
1186 + ui.label("Example: neiam/sync/theme");
1187 + ui.add_space(5.0);
1188 +
1189 + ui.horizontal(|ui| {
1190 + ui.label("Username (optional):");
1191 + ui.text_edit_singleline(&mut self.config_ui_state.mqtt_username);
1192 + });
1193 +
1194 + ui.horizontal(|ui| {
1195 + ui.label("Password (optional):");
1196 + ui.add(
1197 + egui::TextEdit::singleline(&mut self.config_ui_state.mqtt_password)
1198 + .password(true),
1199 + );
1200 + });
1201 + }
1202 +
1203 + ui.add_space(15.0);
1204 +
1205 + if ui.button("Save MQTT Configuration").clicked() {
1206 + self.config.mqtt_enabled = self.config_ui_state.mqtt_enabled;
1207 + self.config.mqtt_host = self.config_ui_state.mqtt_host.clone();
1208 + self.config.mqtt_topic = self.config_ui_state.mqtt_topic.clone();
1209 + self.config.mqtt_username = if self.config_ui_state.mqtt_username.is_empty() {
1210 + None
1211 + } else {
1212 + Some(self.config_ui_state.mqtt_username.clone())
1213 + };
1214 + self.config.mqtt_password = if self.config_ui_state.mqtt_password.is_empty() {
1215 + None
1216 + } else {
1217 + Some(self.config_ui_state.mqtt_password.clone())
1218 + };
1219 +
1220 + if let Err(e) = self.config.save() {
1221 + warn!("Failed to save config: {}", e);
1222 + } else {
1223 + info!("MQTT configuration saved");
1224 + // TODO: Restart MQTT subscription with new settings
1225 + }
1226 + }
1227 + }
996 1228 }
997 1229
998 1230 ui.separator();
@@ -1014,6 +1246,26 @@ impl Koco {
1014 1246
1015 1247 impl eframe::App for Koco {
1016 1248 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
1249 + // Check if theme was changed via MQTT and reload config + apply theme
1250 + if let Ok(mut changed) = self.theme_changed.lock() {
1251 + if *changed {
1252 + *changed = false;
1253 + // Reload config from disk
1254 + self.config = KocoConfig::load();
1255 + // Apply the new theme
1256 + self.apply_theme(ctx);
1257 + // Update UI state to match
1258 + self.config_ui_state.load_mqtt_config(&self.config);
1259 + // Request repaint to apply visual changes
1260 + ctx.request_repaint();
1261 + }
1262 + }
1263 +
1264 + // Request periodic repaints to check for MQTT theme updates
1265 + if self.config.mqtt_enabled {
1266 + ctx.request_repaint_after(std::time::Duration::from_millis(100));
1267 + }
1268 +
1017 1269 // Update window title with now-playing info
1018 1270 let window_title = if let Ok(np) = self.now_playing.lock() {
1019 1271 if !np.title.is_empty() {
@@ -1034,7 +1286,35 @@ impl eframe::App for Koco {
1034 1286
1035 1287 // Handle keyboard input
1036 1288 if let Some(instance) = self.current_instance() {
1289 + // Handle mouse scroll events for volume control
1290 + ctx.input(|i| {
1291 + let scroll_delta = i.smooth_scroll_delta.y;
1292 +
1293 + if scroll_delta > 0.0 {
1294 + // Scroll up = volume up
1295 + if let Err(e) = instance.send_key("volumeup") {
1296 + info!("Failed to send volumeup command: {}", e);
1297 + }
1298 + } else if scroll_delta < 0.0 {
1299 + // Scroll down = volume down
1300 + if let Err(e) = instance.send_key("volumedown") {
1301 + info!("Failed to send volumedown command: {}", e);
1302 + }
1303 + }
1304 + });
1305 +
1306 + // Handle keyboard input including Page Up/Down for volume
1037 1307 ctx.input(|i| {
1308 + if i.key_pressed(egui::Key::PageUp)
1309 + && let Err(e) = instance.send_key("volumeup")
1310 + {
1311 + info!("Failed to send volumeup command: {}", e);
1312 + }
1313 + if i.key_pressed(egui::Key::PageDown)
1314 + && let Err(e) = instance.send_key("volumedown")
1315 + {
1316 + info!("Failed to send volumedown command: {}", e);
1317 + }
1038 1318 if i.key_pressed(egui::Key::ArrowUp)
1039 1319 && let Err(e) = instance.send_key("up")
1040 1320 {
@@ -1274,6 +1554,9 @@ impl eframe::App for Koco {
1274 1554 }
1275 1555
1276 1556 fn main() -> eframe::Result {
1557 + // Initialize env_logger to enable RUST_LOG support
1558 + env_logger::init();
1559 +
1277 1560 let options = eframe::NativeOptions {
1278 1561 viewport: egui::ViewportBuilder::default().with_inner_size([280.0, 400.0]),
1279 1562 ..Default::default()

Parents: 13c382d