├── .gitignore ├── .vscode └── launch.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── build.rs ├── demo.mp4 ├── krustify.png ├── res └── themes │ ├── compact │ ├── notifications.png │ ├── res.qrc │ └── template.ui │ └── default │ ├── notifications.png │ ├── res.qrc │ └── template.ui ├── rust-toolchain.toml └── src ├── dbus_signal.rs ├── errors.rs ├── image_handler.rs ├── main.rs ├── notification.rs ├── notification_spawner.rs ├── notification_widget.rs ├── settings.rs └── tray_menu.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'krustyfy'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=krustyfy", 15 | "--package=krustyfy" 16 | ], 17 | "filter": { 18 | "name": "krustyfy", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'krustyfy'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=krustyfy", 34 | "--package=krustyfy" 35 | ], 36 | "filter": { 37 | "name": "krustyfy", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.17.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "0.7.18" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anyhow" 31 | version = "1.0.63" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "a26fa4d7e3f2eebadf743988fc8aec9fa9a9e82611acafd77c1462ed6262440a" 34 | 35 | [[package]] 36 | name = "async-broadcast" 37 | version = "0.4.1" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" 40 | dependencies = [ 41 | "event-listener", 42 | "futures-core", 43 | "parking_lot", 44 | ] 45 | 46 | [[package]] 47 | name = "async-recursion" 48 | version = "0.3.2" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" 51 | dependencies = [ 52 | "proc-macro2", 53 | "quote", 54 | "syn", 55 | ] 56 | 57 | [[package]] 58 | name = "async-trait" 59 | version = "0.1.57" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" 62 | dependencies = [ 63 | "proc-macro2", 64 | "quote", 65 | "syn", 66 | ] 67 | 68 | [[package]] 69 | name = "atty" 70 | version = "0.2.14" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 73 | dependencies = [ 74 | "hermit-abi", 75 | "libc", 76 | "winapi 0.3.9", 77 | ] 78 | 79 | [[package]] 80 | name = "autocfg" 81 | version = "1.1.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 84 | 85 | [[package]] 86 | name = "backtrace" 87 | version = "0.3.66" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" 90 | dependencies = [ 91 | "addr2line", 92 | "cc", 93 | "cfg-if", 94 | "libc", 95 | "miniz_oxide", 96 | "object", 97 | "rustc-demangle", 98 | ] 99 | 100 | [[package]] 101 | name = "bincode" 102 | version = "1.3.3" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 105 | dependencies = [ 106 | "serde", 107 | ] 108 | 109 | [[package]] 110 | name = "bitflags" 111 | version = "1.3.2" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 114 | 115 | [[package]] 116 | name = "byteorder" 117 | version = "1.4.3" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 120 | 121 | [[package]] 122 | name = "bytes" 123 | version = "1.2.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" 126 | 127 | [[package]] 128 | name = "cc" 129 | version = "1.0.73" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 132 | 133 | [[package]] 134 | name = "cfg-if" 135 | version = "1.0.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 138 | 139 | [[package]] 140 | name = "copy_to_output" 141 | version = "2.0.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "38b0be9c79199e3ea04d8526600ebe0d00378bb0797b828170e4bfde85b26796" 144 | dependencies = [ 145 | "anyhow", 146 | "fs_extra", 147 | ] 148 | 149 | [[package]] 150 | name = "core-foundation" 151 | version = "0.9.3" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 154 | dependencies = [ 155 | "core-foundation-sys", 156 | "libc", 157 | ] 158 | 159 | [[package]] 160 | name = "core-foundation-sys" 161 | version = "0.8.3" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 164 | 165 | [[package]] 166 | name = "cpp_core" 167 | version = "0.6.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "5ebd6ba9742a158232afe2d07ec5d9d5d80d058baf700c5f9aa0e014fe3f24ad" 170 | dependencies = [ 171 | "libc", 172 | ] 173 | 174 | [[package]] 175 | name = "derivative" 176 | version = "2.2.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 179 | dependencies = [ 180 | "proc-macro2", 181 | "quote", 182 | "syn", 183 | ] 184 | 185 | [[package]] 186 | name = "device_query" 187 | version = "1.1.1" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "8b54c3f0350a597abbed2c10fff7b233f92744c9422697d5fb58dc5397ab6fae" 190 | dependencies = [ 191 | "lazy_static", 192 | "macos-accessibility-client", 193 | "pkg-config", 194 | "readkey", 195 | "readmouse", 196 | "winapi 0.3.9", 197 | "x11", 198 | ] 199 | 200 | [[package]] 201 | name = "dirs" 202 | version = "4.0.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" 205 | dependencies = [ 206 | "dirs-sys", 207 | ] 208 | 209 | [[package]] 210 | name = "dirs-sys" 211 | version = "0.3.7" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 214 | dependencies = [ 215 | "libc", 216 | "redox_users", 217 | "winapi 0.3.9", 218 | ] 219 | 220 | [[package]] 221 | name = "dunce" 222 | version = "1.0.2" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541" 225 | 226 | [[package]] 227 | name = "either" 228 | version = "1.8.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" 231 | 232 | [[package]] 233 | name = "enumflags2" 234 | version = "0.7.5" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" 237 | dependencies = [ 238 | "enumflags2_derive", 239 | "serde", 240 | ] 241 | 242 | [[package]] 243 | name = "enumflags2_derive" 244 | version = "0.7.4" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" 247 | dependencies = [ 248 | "proc-macro2", 249 | "quote", 250 | "syn", 251 | ] 252 | 253 | [[package]] 254 | name = "env_logger" 255 | version = "0.7.1" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 258 | dependencies = [ 259 | "atty", 260 | "humantime", 261 | "log", 262 | "regex", 263 | "termcolor", 264 | ] 265 | 266 | [[package]] 267 | name = "event-listener" 268 | version = "2.5.3" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 271 | 272 | [[package]] 273 | name = "failure" 274 | version = "0.1.8" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" 277 | dependencies = [ 278 | "backtrace", 279 | "failure_derive", 280 | ] 281 | 282 | [[package]] 283 | name = "failure_derive" 284 | version = "0.1.8" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" 287 | dependencies = [ 288 | "proc-macro2", 289 | "quote", 290 | "syn", 291 | "synstructure", 292 | ] 293 | 294 | [[package]] 295 | name = "fastrand" 296 | version = "1.8.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 299 | dependencies = [ 300 | "instant", 301 | ] 302 | 303 | [[package]] 304 | name = "fs_extra" 305 | version = "1.2.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" 308 | 309 | [[package]] 310 | name = "futures-core" 311 | version = "0.3.23" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" 314 | 315 | [[package]] 316 | name = "futures-sink" 317 | version = "0.3.23" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765" 320 | 321 | [[package]] 322 | name = "futures-task" 323 | version = "0.3.23" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" 326 | 327 | [[package]] 328 | name = "futures-util" 329 | version = "0.3.23" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" 332 | dependencies = [ 333 | "futures-core", 334 | "futures-sink", 335 | "futures-task", 336 | "pin-project-lite", 337 | "pin-utils", 338 | "slab", 339 | ] 340 | 341 | [[package]] 342 | name = "getrandom" 343 | version = "0.2.7" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 346 | dependencies = [ 347 | "cfg-if", 348 | "libc", 349 | "wasi", 350 | ] 351 | 352 | [[package]] 353 | name = "gimli" 354 | version = "0.26.2" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" 357 | 358 | [[package]] 359 | name = "glob" 360 | version = "0.3.0" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 363 | 364 | [[package]] 365 | name = "hermit-abi" 366 | version = "0.1.19" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 369 | dependencies = [ 370 | "libc", 371 | ] 372 | 373 | [[package]] 374 | name = "hex" 375 | version = "0.4.3" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 378 | 379 | [[package]] 380 | name = "humantime" 381 | version = "1.3.0" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 384 | dependencies = [ 385 | "quick-error", 386 | ] 387 | 388 | [[package]] 389 | name = "instant" 390 | version = "0.1.12" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 393 | dependencies = [ 394 | "cfg-if", 395 | ] 396 | 397 | [[package]] 398 | name = "itertools" 399 | version = "0.8.2" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" 402 | dependencies = [ 403 | "either", 404 | ] 405 | 406 | [[package]] 407 | name = "itoa" 408 | version = "1.0.3" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" 411 | 412 | [[package]] 413 | name = "kernel32-sys" 414 | version = "0.2.2" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 417 | dependencies = [ 418 | "winapi 0.2.8", 419 | "winapi-build", 420 | ] 421 | 422 | [[package]] 423 | name = "krustyfy" 424 | version = "0.1.7" 425 | dependencies = [ 426 | "copy_to_output", 427 | "cpp_core", 428 | "device_query", 429 | "glob", 430 | "lazy_static", 431 | "linked-hash-map", 432 | "qt_core", 433 | "qt_gui", 434 | "qt_ui_tools", 435 | "qt_widgets", 436 | "tokio", 437 | "uuid", 438 | "zbus", 439 | "zvariant", 440 | ] 441 | 442 | [[package]] 443 | name = "lazy_static" 444 | version = "1.4.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 447 | 448 | [[package]] 449 | name = "libc" 450 | version = "0.2.132" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" 453 | 454 | [[package]] 455 | name = "linked-hash-map" 456 | version = "0.5.6" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 459 | 460 | [[package]] 461 | name = "lock_api" 462 | version = "0.4.7" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" 465 | dependencies = [ 466 | "autocfg", 467 | "scopeguard", 468 | ] 469 | 470 | [[package]] 471 | name = "log" 472 | version = "0.4.17" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 475 | dependencies = [ 476 | "cfg-if", 477 | ] 478 | 479 | [[package]] 480 | name = "macos-accessibility-client" 481 | version = "0.0.1" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "edf7710fbff50c24124331760978fb9086d6de6288dcdb38b25a97f8b1bdebbb" 484 | dependencies = [ 485 | "core-foundation", 486 | "core-foundation-sys", 487 | ] 488 | 489 | [[package]] 490 | name = "memchr" 491 | version = "2.5.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 494 | 495 | [[package]] 496 | name = "memoffset" 497 | version = "0.6.5" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 500 | dependencies = [ 501 | "autocfg", 502 | ] 503 | 504 | [[package]] 505 | name = "miniz_oxide" 506 | version = "0.5.3" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" 509 | dependencies = [ 510 | "adler", 511 | ] 512 | 513 | [[package]] 514 | name = "mio" 515 | version = "0.8.4" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" 518 | dependencies = [ 519 | "libc", 520 | "log", 521 | "wasi", 522 | "windows-sys", 523 | ] 524 | 525 | [[package]] 526 | name = "nix" 527 | version = "0.24.2" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" 530 | dependencies = [ 531 | "bitflags", 532 | "cfg-if", 533 | "libc", 534 | "memoffset", 535 | ] 536 | 537 | [[package]] 538 | name = "num_cpus" 539 | version = "1.13.1" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 542 | dependencies = [ 543 | "hermit-abi", 544 | "libc", 545 | ] 546 | 547 | [[package]] 548 | name = "object" 549 | version = "0.29.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" 552 | dependencies = [ 553 | "memchr", 554 | ] 555 | 556 | [[package]] 557 | name = "once_cell" 558 | version = "1.13.1" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" 561 | 562 | [[package]] 563 | name = "ordered-stream" 564 | version = "0.0.1" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" 567 | dependencies = [ 568 | "futures-core", 569 | "pin-project-lite", 570 | ] 571 | 572 | [[package]] 573 | name = "parking_lot" 574 | version = "0.12.1" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 577 | dependencies = [ 578 | "lock_api", 579 | "parking_lot_core", 580 | ] 581 | 582 | [[package]] 583 | name = "parking_lot_core" 584 | version = "0.9.3" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" 587 | dependencies = [ 588 | "cfg-if", 589 | "libc", 590 | "redox_syscall", 591 | "smallvec", 592 | "windows-sys", 593 | ] 594 | 595 | [[package]] 596 | name = "pathdiff" 597 | version = "0.1.0" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "a3bf70094d203e07844da868b634207e71bfab254fe713171fae9a6e751ccf31" 600 | 601 | [[package]] 602 | name = "pin-project-lite" 603 | version = "0.2.9" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 606 | 607 | [[package]] 608 | name = "pin-utils" 609 | version = "0.1.0" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 612 | 613 | [[package]] 614 | name = "pkg-config" 615 | version = "0.3.25" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 618 | 619 | [[package]] 620 | name = "ppv-lite86" 621 | version = "0.2.16" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 624 | 625 | [[package]] 626 | name = "proc-macro-crate" 627 | version = "1.2.1" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" 630 | dependencies = [ 631 | "once_cell", 632 | "thiserror", 633 | "toml 0.5.9", 634 | ] 635 | 636 | [[package]] 637 | name = "proc-macro-hack" 638 | version = "0.5.19" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" 641 | 642 | [[package]] 643 | name = "proc-macro2" 644 | version = "1.0.43" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" 647 | dependencies = [ 648 | "unicode-ident", 649 | ] 650 | 651 | [[package]] 652 | name = "qt_core" 653 | version = "0.5.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "778fa84e9ee19abcf687ff0544f9bb9cd25059fe1ac479736c04475fd8d83a7a" 656 | dependencies = [ 657 | "cpp_core", 658 | "proc-macro-hack", 659 | "qt_macros", 660 | "qt_ritual_build", 661 | ] 662 | 663 | [[package]] 664 | name = "qt_gui" 665 | version = "0.5.0" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "dcc8366c6860b47084283cecc51397045e8401f492a98ac72b6bf1c7bad21837" 668 | dependencies = [ 669 | "cpp_core", 670 | "qt_core", 671 | "qt_ritual_build", 672 | ] 673 | 674 | [[package]] 675 | name = "qt_macros" 676 | version = "0.1.1" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "42b6fce195b624df4031efe9499c6500304b6b50e4e8c014709bddbbea2eaae3" 679 | dependencies = [ 680 | "proc-macro-hack", 681 | "proc-macro2", 682 | "quote", 683 | "syn", 684 | ] 685 | 686 | [[package]] 687 | name = "qt_ritual_build" 688 | version = "0.5.0" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "2a9c99db64e5bc0ab7404d199b7f2da51d77ea3c1d78d01a9390f01aefbd3979" 691 | dependencies = [ 692 | "env_logger", 693 | "itertools", 694 | "qt_ritual_common", 695 | "ritual_build", 696 | "semver", 697 | ] 698 | 699 | [[package]] 700 | name = "qt_ritual_common" 701 | version = "0.4.0" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "7ba1c7e05524bb83823426694e918d635b82d1366f20833887d7d62ea6b12a70" 704 | dependencies = [ 705 | "log", 706 | "ritual_common", 707 | "semver", 708 | ] 709 | 710 | [[package]] 711 | name = "qt_ui_tools" 712 | version = "0.5.0" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "8ad3bc61b5903878bb272e60080b1b4eb395ab1c6bf8808dbfb78b9fc63bc10c" 715 | dependencies = [ 716 | "cpp_core", 717 | "qt_core", 718 | "qt_gui", 719 | "qt_macros", 720 | "qt_ritual_build", 721 | "qt_widgets", 722 | ] 723 | 724 | [[package]] 725 | name = "qt_widgets" 726 | version = "0.5.0" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "d93cdec198a17bae150564e2da536bf3cb9df91aea63eb48b1b260b216a0b7f6" 729 | dependencies = [ 730 | "cpp_core", 731 | "qt_core", 732 | "qt_gui", 733 | "qt_ritual_build", 734 | ] 735 | 736 | [[package]] 737 | name = "quick-error" 738 | version = "1.2.3" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 741 | 742 | [[package]] 743 | name = "quote" 744 | version = "1.0.21" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 747 | dependencies = [ 748 | "proc-macro2", 749 | ] 750 | 751 | [[package]] 752 | name = "rand" 753 | version = "0.8.5" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 756 | dependencies = [ 757 | "libc", 758 | "rand_chacha", 759 | "rand_core", 760 | ] 761 | 762 | [[package]] 763 | name = "rand_chacha" 764 | version = "0.3.1" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 767 | dependencies = [ 768 | "ppv-lite86", 769 | "rand_core", 770 | ] 771 | 772 | [[package]] 773 | name = "rand_core" 774 | version = "0.6.3" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 777 | dependencies = [ 778 | "getrandom", 779 | ] 780 | 781 | [[package]] 782 | name = "readkey" 783 | version = "0.1.7" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "86d401b6d6a1725a59f1b4e813275d289dff3ad09c72b373a10a7a8217ba3146" 786 | 787 | [[package]] 788 | name = "readmouse" 789 | version = "0.2.1" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "be105c72a1e6a5a1198acee3d5b506a15676b74a02ecd78060042a447f408d94" 792 | 793 | [[package]] 794 | name = "redox_syscall" 795 | version = "0.2.16" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 798 | dependencies = [ 799 | "bitflags", 800 | ] 801 | 802 | [[package]] 803 | name = "redox_users" 804 | version = "0.4.3" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 807 | dependencies = [ 808 | "getrandom", 809 | "redox_syscall", 810 | "thiserror", 811 | ] 812 | 813 | [[package]] 814 | name = "regex" 815 | version = "1.6.0" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 818 | dependencies = [ 819 | "aho-corasick", 820 | "memchr", 821 | "regex-syntax", 822 | ] 823 | 824 | [[package]] 825 | name = "regex-syntax" 826 | version = "0.6.27" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 829 | 830 | [[package]] 831 | name = "remove_dir_all" 832 | version = "0.5.3" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 835 | dependencies = [ 836 | "winapi 0.3.9", 837 | ] 838 | 839 | [[package]] 840 | name = "ritual_build" 841 | version = "0.4.0" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "e308b6d715de5f46f5c0980169c2813c5e3fbec42dd4938fdfbf648248fb7ea7" 844 | dependencies = [ 845 | "log", 846 | "ritual_common", 847 | ] 848 | 849 | [[package]] 850 | name = "ritual_common" 851 | version = "0.4.0" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "59377d74284596d82c84a994b6abbabd7ae9cc1c3d39fcb3421e0ffcaf112f88" 854 | dependencies = [ 855 | "bincode", 856 | "dunce", 857 | "failure", 858 | "itertools", 859 | "lazy_static", 860 | "log", 861 | "num_cpus", 862 | "pathdiff", 863 | "regex", 864 | "semver", 865 | "serde", 866 | "serde_derive", 867 | "serde_json", 868 | "shell-words", 869 | "term-painter", 870 | "toml 0.4.10", 871 | ] 872 | 873 | [[package]] 874 | name = "rustc-demangle" 875 | version = "0.1.21" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" 878 | 879 | [[package]] 880 | name = "ryu" 881 | version = "1.0.11" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 884 | 885 | [[package]] 886 | name = "scopeguard" 887 | version = "1.1.0" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 890 | 891 | [[package]] 892 | name = "semver" 893 | version = "0.9.0" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" 896 | dependencies = [ 897 | "semver-parser", 898 | ] 899 | 900 | [[package]] 901 | name = "semver-parser" 902 | version = "0.7.0" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 905 | 906 | [[package]] 907 | name = "serde" 908 | version = "1.0.144" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" 911 | dependencies = [ 912 | "serde_derive", 913 | ] 914 | 915 | [[package]] 916 | name = "serde_derive" 917 | version = "1.0.144" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" 920 | dependencies = [ 921 | "proc-macro2", 922 | "quote", 923 | "syn", 924 | ] 925 | 926 | [[package]] 927 | name = "serde_json" 928 | version = "1.0.85" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" 931 | dependencies = [ 932 | "itoa", 933 | "ryu", 934 | "serde", 935 | ] 936 | 937 | [[package]] 938 | name = "serde_repr" 939 | version = "0.1.9" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" 942 | dependencies = [ 943 | "proc-macro2", 944 | "quote", 945 | "syn", 946 | ] 947 | 948 | [[package]] 949 | name = "sha1" 950 | version = "0.6.1" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" 953 | dependencies = [ 954 | "sha1_smol", 955 | ] 956 | 957 | [[package]] 958 | name = "sha1_smol" 959 | version = "1.0.0" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" 962 | 963 | [[package]] 964 | name = "shell-words" 965 | version = "0.1.0" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "39acde55a154c4cd3ae048ac78cc21c25f3a0145e44111b523279113dce0d94a" 968 | 969 | [[package]] 970 | name = "signal-hook-registry" 971 | version = "1.4.0" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 974 | dependencies = [ 975 | "libc", 976 | ] 977 | 978 | [[package]] 979 | name = "slab" 980 | version = "0.4.7" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 983 | dependencies = [ 984 | "autocfg", 985 | ] 986 | 987 | [[package]] 988 | name = "smallvec" 989 | version = "1.9.0" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 992 | 993 | [[package]] 994 | name = "socket2" 995 | version = "0.4.4" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 998 | dependencies = [ 999 | "libc", 1000 | "winapi 0.3.9", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "static_assertions" 1005 | version = "1.1.0" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1008 | 1009 | [[package]] 1010 | name = "syn" 1011 | version = "1.0.99" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" 1014 | dependencies = [ 1015 | "proc-macro2", 1016 | "quote", 1017 | "unicode-ident", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "synstructure" 1022 | version = "0.12.6" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" 1025 | dependencies = [ 1026 | "proc-macro2", 1027 | "quote", 1028 | "syn", 1029 | "unicode-xid", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "tempfile" 1034 | version = "3.3.0" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 1037 | dependencies = [ 1038 | "cfg-if", 1039 | "fastrand", 1040 | "libc", 1041 | "redox_syscall", 1042 | "remove_dir_all", 1043 | "winapi 0.3.9", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "term" 1048 | version = "0.4.6" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "fa63644f74ce96fbeb9b794f66aff2a52d601cbd5e80f4b97123e3899f4570f1" 1051 | dependencies = [ 1052 | "kernel32-sys", 1053 | "winapi 0.2.8", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "term-painter" 1058 | version = "0.2.4" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "dcaa948f0e3e38470cd8dc8dcfe561a75c9e43f28075bb183845be2b9b3c08cf" 1061 | dependencies = [ 1062 | "term", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "termcolor" 1067 | version = "1.1.3" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 1070 | dependencies = [ 1071 | "winapi-util", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "thiserror" 1076 | version = "1.0.32" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" 1079 | dependencies = [ 1080 | "thiserror-impl", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "thiserror-impl" 1085 | version = "1.0.32" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" 1088 | dependencies = [ 1089 | "proc-macro2", 1090 | "quote", 1091 | "syn", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "tokio" 1096 | version = "1.21.0" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" 1099 | dependencies = [ 1100 | "autocfg", 1101 | "bytes", 1102 | "libc", 1103 | "memchr", 1104 | "mio", 1105 | "num_cpus", 1106 | "once_cell", 1107 | "parking_lot", 1108 | "pin-project-lite", 1109 | "signal-hook-registry", 1110 | "socket2", 1111 | "tokio-macros", 1112 | "winapi 0.3.9", 1113 | ] 1114 | 1115 | [[package]] 1116 | name = "tokio-macros" 1117 | version = "1.8.0" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" 1120 | dependencies = [ 1121 | "proc-macro2", 1122 | "quote", 1123 | "syn", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "toml" 1128 | version = "0.4.10" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" 1131 | dependencies = [ 1132 | "serde", 1133 | ] 1134 | 1135 | [[package]] 1136 | name = "toml" 1137 | version = "0.5.9" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" 1140 | dependencies = [ 1141 | "serde", 1142 | ] 1143 | 1144 | [[package]] 1145 | name = "tracing" 1146 | version = "0.1.36" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" 1149 | dependencies = [ 1150 | "cfg-if", 1151 | "pin-project-lite", 1152 | "tracing-attributes", 1153 | "tracing-core", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "tracing-attributes" 1158 | version = "0.1.22" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" 1161 | dependencies = [ 1162 | "proc-macro2", 1163 | "quote", 1164 | "syn", 1165 | ] 1166 | 1167 | [[package]] 1168 | name = "tracing-core" 1169 | version = "0.1.29" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" 1172 | dependencies = [ 1173 | "once_cell", 1174 | ] 1175 | 1176 | [[package]] 1177 | name = "uds_windows" 1178 | version = "1.0.2" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" 1181 | dependencies = [ 1182 | "tempfile", 1183 | "winapi 0.3.9", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "unicode-ident" 1188 | version = "1.0.3" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" 1191 | 1192 | [[package]] 1193 | name = "unicode-xid" 1194 | version = "0.2.3" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" 1197 | 1198 | [[package]] 1199 | name = "uuid" 1200 | version = "1.1.2" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" 1203 | dependencies = [ 1204 | "getrandom", 1205 | "rand", 1206 | "uuid-macro-internal", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "uuid-macro-internal" 1211 | version = "1.1.2" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "548f7181a5990efa50237abb7ebca410828b57a8955993334679f8b50b35c97d" 1214 | dependencies = [ 1215 | "proc-macro2", 1216 | "quote", 1217 | "syn", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "wasi" 1222 | version = "0.11.0+wasi-snapshot-preview1" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1225 | 1226 | [[package]] 1227 | name = "winapi" 1228 | version = "0.2.8" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 1231 | 1232 | [[package]] 1233 | name = "winapi" 1234 | version = "0.3.9" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1237 | dependencies = [ 1238 | "winapi-i686-pc-windows-gnu", 1239 | "winapi-x86_64-pc-windows-gnu", 1240 | ] 1241 | 1242 | [[package]] 1243 | name = "winapi-build" 1244 | version = "0.1.1" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 1247 | 1248 | [[package]] 1249 | name = "winapi-i686-pc-windows-gnu" 1250 | version = "0.4.0" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1253 | 1254 | [[package]] 1255 | name = "winapi-util" 1256 | version = "0.1.5" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1259 | dependencies = [ 1260 | "winapi 0.3.9", 1261 | ] 1262 | 1263 | [[package]] 1264 | name = "winapi-x86_64-pc-windows-gnu" 1265 | version = "0.4.0" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1268 | 1269 | [[package]] 1270 | name = "windows-sys" 1271 | version = "0.36.1" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 1274 | dependencies = [ 1275 | "windows_aarch64_msvc", 1276 | "windows_i686_gnu", 1277 | "windows_i686_msvc", 1278 | "windows_x86_64_gnu", 1279 | "windows_x86_64_msvc", 1280 | ] 1281 | 1282 | [[package]] 1283 | name = "windows_aarch64_msvc" 1284 | version = "0.36.1" 1285 | source = "registry+https://github.com/rust-lang/crates.io-index" 1286 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 1287 | 1288 | [[package]] 1289 | name = "windows_i686_gnu" 1290 | version = "0.36.1" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 1293 | 1294 | [[package]] 1295 | name = "windows_i686_msvc" 1296 | version = "0.36.1" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 1299 | 1300 | [[package]] 1301 | name = "windows_x86_64_gnu" 1302 | version = "0.36.1" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 1305 | 1306 | [[package]] 1307 | name = "windows_x86_64_msvc" 1308 | version = "0.36.1" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 1311 | 1312 | [[package]] 1313 | name = "x11" 1314 | version = "2.20.0" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "f7ae97874a928d821b061fce3d1fc52f08071dd53c89a6102bc06efcac3b2908" 1317 | dependencies = [ 1318 | "libc", 1319 | "pkg-config", 1320 | ] 1321 | 1322 | [[package]] 1323 | name = "zbus" 1324 | version = "3.0.0" 1325 | source = "registry+https://github.com/rust-lang/crates.io-index" 1326 | checksum = "1faa83cd7c79d3a669220c634528577b98ff43c35aa7c827ab3e9990692f7868" 1327 | dependencies = [ 1328 | "async-broadcast", 1329 | "async-recursion", 1330 | "async-trait", 1331 | "byteorder", 1332 | "derivative", 1333 | "dirs", 1334 | "enumflags2", 1335 | "event-listener", 1336 | "futures-core", 1337 | "futures-sink", 1338 | "futures-util", 1339 | "hex", 1340 | "lazy_static", 1341 | "nix", 1342 | "once_cell", 1343 | "ordered-stream", 1344 | "rand", 1345 | "serde", 1346 | "serde_repr", 1347 | "sha1", 1348 | "static_assertions", 1349 | "tokio", 1350 | "tracing", 1351 | "uds_windows", 1352 | "winapi 0.3.9", 1353 | "zbus_macros", 1354 | "zbus_names", 1355 | "zvariant", 1356 | ] 1357 | 1358 | [[package]] 1359 | name = "zbus_macros" 1360 | version = "3.0.0" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "fd5874c328b945cab1865a299e31f855072fa528bafbbfa3249394b352d5742b" 1363 | dependencies = [ 1364 | "proc-macro-crate", 1365 | "proc-macro2", 1366 | "quote", 1367 | "regex", 1368 | "syn", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "zbus_names" 1373 | version = "2.2.0" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "41a408fd8a352695690f53906dc7fd036be924ec51ea5e05666ff42685ed0af5" 1376 | dependencies = [ 1377 | "serde", 1378 | "static_assertions", 1379 | "zvariant", 1380 | ] 1381 | 1382 | [[package]] 1383 | name = "zvariant" 1384 | version = "3.6.0" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "1bd68e4e6432ef19df47d7e90e2e72b5e7e3d778e0ae3baddf12b951265cc758" 1387 | dependencies = [ 1388 | "byteorder", 1389 | "enumflags2", 1390 | "libc", 1391 | "serde", 1392 | "static_assertions", 1393 | "zvariant_derive", 1394 | ] 1395 | 1396 | [[package]] 1397 | name = "zvariant_derive" 1398 | version = "3.6.0" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "08e977eaa3af652f63d479ce50d924254ad76722a6289ec1a1eac3231ca30430" 1401 | dependencies = [ 1402 | "proc-macro-crate", 1403 | "proc-macro2", 1404 | "quote", 1405 | "syn", 1406 | ] 1407 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "krustyfy" 3 | version = "0.1.7" 4 | edition = "2021" 5 | build = "build.rs" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | cpp_core = "0.6.0" 11 | qt_core = { version = "0.5.0" } 12 | qt_gui = { version = "0.5.0" } 13 | qt_widgets = { version = "0.5.0" } 14 | qt_ui_tools = { version = "0.5.0" } 15 | zbus = {version = "3.0.0", default-features = false, features = ["tokio"]} 16 | zvariant = "3.6.0" 17 | tokio={version="1.21.0", features = ["full"]} 18 | device_query = "1.1.1" 19 | linked-hash-map = "0.5.6" 20 | lazy_static = "1.4.0" 21 | 22 | [dependencies.uuid] 23 | version = "1.1.2" 24 | features = [ 25 | "v4", # Lets you generate random UUIDs 26 | "fast-rng", # Use a faster (but still sufficiently random) RNG 27 | "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs 28 | ] 29 | 30 | [build-dependencies] 31 | copy_to_output = "2.0.0" 32 | glob = "0.3.0" 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Abigail 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # krustyfy 2 | 3 |

4 | 5 |

6 | 7 | 8 | Unobtrusive notification daemon made in Rust and Qt. 9 | 10 | Notifications **can't be interacted with** (unless you keep Left Alt key pressed) and **mouse input goes right through them** :) 11 | 12 | 13 | https://user-images.githubusercontent.com/112440538/188256590-9793e49d-8265-4d85-a5f7-c2c3f3ed01bd.mp4 14 | 15 | ## Configuration 16 | 17 | Most settings can be changed directly from the **res/themes/{current theme}/template.ui** config file. From the layout of the notification itself to settings like duration, monitor, shadow color, etc. More settings comming soon. :) 18 | 19 |

20 | 21 |

22 | 23 | Take into account that some widgets MUST exist in the template.ui file, otherwise it'll crash. Modify it wisely. 24 | 25 | 26 | 27 | ## Theming 28 | 29 | Themes are located in the **res/themes** subdirectory. Each folder corresponds to a theme, which must have a **template.ui** file. Themes are loaded when the app is launched, and can be changed from the system tray: 30 | 31 | ![themes](https://user-images.githubusercontent.com/112440538/190928867-c006a63a-97ee-4eb5-8e2a-ccf012671547.png) 32 | 33 | Currently we have two built in themes: **default** and **compact**, and they can be changed, and even modified, while the app is running: 34 | 35 | 36 | 37 | https://user-images.githubusercontent.com/112440538/190928912-c352c2ad-a002-4d9a-aed8-0429989bc1d5.mp4 38 | 39 | 40 | 41 | ## Name 42 | 43 | **K**: Because it's made in Qt, so it works nice with KDE. 44 | 45 | **Rust**: Because it's made in rust. 46 | 47 | **Krusty**: Because it's made by a dumb sad clown. 48 | 49 | **Krustyfy**: Because it just had to have the "ify" suffix, as in "notify". 50 | 51 | ## How to contribute and help the project 52 | I don't know, I never thought I'd get this far. Also since I'm just learning about how to code in rust it's probably full of bad practices and awful code. :) 53 | 54 | ## Building 55 | 56 | Tested with Debian 11 netinst + KDE 57 | 58 | ### Requirements 59 | 1. Install Rustup (https://rustup.rs/) 60 | 2. Install required dev packages: `#apt-get install qt5-qmake qtbase5-dev cmake build-essential pkg-config qttools5-dev` 61 | 62 | ### Clone and build 63 | 1. Clone from git `$git clone https://github.com/abigaliz/krustyfy.git` 64 | 2. `cd krustyfy` 65 | 3. `cargo build --release` 66 | 67 | ### Running it 68 | 1. Disable KDE Notifications from the system tray by going to the System Tray Settings and marking Notifications as Disabled: 69 | ![image](https://user-images.githubusercontent.com/112440538/195397565-2d15242f-be1e-40ba-b7f1-4b1b9e6c0457.png) 70 | 2. Log out of the current session. 71 | 3. Run krustyfy from `$HOME/krustyfy/target/release/krustyfy` 72 | 73 | You can also set it to run on startup from KDE System Settings: 74 | 75 | ![image](https://user-images.githubusercontent.com/112440538/195398592-cc36fac4-95a0-4633-9b5b-22852101f138.png) 76 | 77 | Remember to set up the work path to `$HOME/krustyfy/target/release/`, otherwise it won't be able to access the notification templates folder. 78 | 79 | 80 | ## Usage 81 | 82 | By pressing **Left Alt key** you freeze all notifications (new notifications still come in, but start frozen) and you're able to click on them to interact. 83 | 84 | Otherwise, they are semi-transparent and get blurry and even less opaque when your cursor is over it. Also you click through them, so if a notification spanws just when you were about to click, you don't have to worry; the click will be processed as if the notification was nothing at all, nothing at all, nothing at all. 85 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use copy_to_output::copy_to_output; 2 | use std::env; 3 | 4 | fn main() { 5 | // Re-runs script if any files in res are changed 6 | println!("cargo:rerun-if-changed=res/*"); 7 | copy_to_output("res", &env::var("PROFILE").unwrap()).expect("Could not copy"); 8 | } 9 | -------------------------------------------------------------------------------- /demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abigaliz/krustyfy/851a673acc8bfd166b2af0bae9127ff12a558ccb/demo.mp4 -------------------------------------------------------------------------------- /krustify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abigaliz/krustyfy/851a673acc8bfd166b2af0bae9127ff12a558ccb/krustify.png -------------------------------------------------------------------------------- /res/themes/compact/notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abigaliz/krustyfy/851a673acc8bfd166b2af0bae9127ff12a558ccb/res/themes/compact/notifications.png -------------------------------------------------------------------------------- /res/themes/compact/res.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | notifications.png 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/themes/compact/template.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | notificationTemplate 4 | 5 | 6 | 7 | 0 8 | 0 9 | 321 10 | 293 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 0.900000000000000 18 | 19 | 20 | 0.800000000000000 21 | 22 | 23 | 0.200000000000000 24 | 25 | 26 | 1.000000000000000 27 | 28 | 29 | 10.000000000000000 30 | 31 | 32 | -1 33 | 34 | 35 | 15.000000000000000 36 | 37 | 38 | 6500 39 | 40 | 41 | 200 42 | 43 | 44 | 600 45 | 46 | 47 | 48 | 0 49 | 0 50 | 0 51 | 52 | 53 | 54 | 55 | 255 56 | 255 57 | 255 58 | 59 | 60 | 61 | 62 | 0 63 | 0 64 | 0 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 0 72 | 321 73 | 81 74 | 75 | 76 | 77 | 78 | 79 | 10 80 | 10 81 | 271 82 | 63 83 | 84 | 85 | 86 | border-radius: 10px; 87 | background-color: black; 88 | border-style:none; 89 | 90 | 91 | QFrame::StyledPanel 92 | 93 | 94 | QFrame::Raised 95 | 96 | 97 | 6 98 | 99 | 100 | 3 101 | 102 | 103 | 104 | 105 | 0 106 | 0 107 | 270 108 | 63 109 | 110 | 111 | 112 | background-color:rgba(255, 255, 255, 0); 113 | border-style: none; 114 | 115 | 116 | 117 | 118 | 0 119 | 0 120 | 270 121 | 70 122 | 123 | 124 | 125 | background-color:rgba(255, 255, 255, 0); 126 | border-style: none; 127 | 128 | 129 | 130 | 1 131 | 132 | 133 | QLayout::SetMaximumSize 134 | 135 | 136 | 4 137 | 138 | 139 | 0 140 | 141 | 142 | 1 143 | 144 | 145 | 146 | 147 | 148 | 0 149 | 0 150 | 151 | 152 | 153 | 154 | 25 155 | 50 156 | 157 | 158 | 159 | 160 | 0 161 | 50 162 | 163 | 164 | 165 | background-color:rgba(255, 255, 255, 0); 166 | border-style: none; 167 | 168 | 169 | 170 | 171 | 172 | true 173 | 174 | 175 | Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 49 184 | 0 185 | 186 | 187 | 188 | 189 | 200 190 | 0 191 | 192 | 193 | 194 | 195 | 290 196 | 90 197 | 198 | 199 | 200 | 201 | 500 202 | 0 203 | 204 | 205 | 206 | 207 | 300 208 | 0 209 | 210 | 211 | 212 | background-color:rgba(255, 255, 255, 0); 213 | border-style: none; 214 | 215 | 216 | 217 | 2 218 | 219 | 220 | QLayout::SetNoConstraint 221 | 222 | 223 | 2 224 | 225 | 226 | 227 | 228 | 229 | 0 230 | 0 231 | 232 | 233 | 234 | 235 | 200 236 | 0 237 | 238 | 239 | 240 | 241 | 227 242 | 20 243 | 244 | 245 | 246 | 247 | 75 248 | true 249 | 250 | 251 | 252 | background-color:rgba(255, 255, 255, 0); 253 | border-style: none; 254 | 255 | 256 | Test notification subject 257 | 258 | 259 | Qt::MarkdownText 260 | 261 | 262 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 0 271 | 0 272 | 273 | 274 | 275 | 276 | 0 277 | 30 278 | 279 | 280 | 281 | 282 | 300 283 | 40 284 | 285 | 286 | 287 | background-color:rgba(255, 255, 255, 0); 288 | border-style: none; 289 | 290 | 291 | Test notification body. Everything written here will be overwritten by the actual notification. 292 | 293 | 294 | Qt::MarkdownText 295 | 296 | 297 | false 298 | 299 | 300 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 301 | 302 | 303 | true 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 12 318 | 12 319 | 20 320 | 20 321 | 322 | 323 | 324 | 325 | 0 326 | 0 327 | 328 | 329 | 330 | 331 | 20 332 | 20 333 | 334 | 335 | 336 | 337 | 20 338 | 20 339 | 340 | 341 | 342 | false 343 | 344 | 345 | background-color:rgba(255, 255, 255, 0); 346 | border-style: none; 347 | 348 | 349 | 350 | 351 | 352 | :/default_icon/notifications.png 353 | 354 | 355 | false 356 | 357 | 358 | 359 | 360 | 361 | 362 | 10 363 | 160 364 | 301 365 | 121 366 | 367 | 368 | 369 | 370 | 371 | 0 372 | 0 373 | 301 374 | 121 375 | 376 | 377 | 378 | PointingHandCursor 379 | 380 | 381 | The button that will close the notification and 382 | execute the default action. 383 | 384 | This will never be visible (opacity: 0). 385 | 386 | 387 | false 388 | 389 | 390 | false 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | -------------------------------------------------------------------------------- /res/themes/default/notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abigaliz/krustyfy/851a673acc8bfd166b2af0bae9127ff12a558ccb/res/themes/default/notifications.png -------------------------------------------------------------------------------- /res/themes/default/res.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | notifications.png 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/themes/default/template.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | notificationTemplate 4 | 5 | 6 | 7 | 0 8 | 0 9 | 321 10 | 293 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 0.900000000000000 18 | 19 | 20 | 0.800000000000000 21 | 22 | 23 | 0.200000000000000 24 | 25 | 26 | 1.000000000000000 27 | 28 | 29 | 10.000000000000000 30 | 31 | 32 | -1 33 | 34 | 35 | 15.000000000000000 36 | 37 | 38 | 6500 39 | 40 | 41 | 200 42 | 43 | 44 | 600 45 | 46 | 47 | 48 | 0 49 | 0 50 | 0 51 | 52 | 53 | 54 | 55 | 255 56 | 255 57 | 255 58 | 59 | 60 | 61 | 62 | 0 63 | 0 64 | 0 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 0 72 | 321 73 | 141 74 | 75 | 76 | 77 | 78 | 79 | 10 80 | 10 81 | 301 82 | 121 83 | 84 | 85 | 86 | border-radius: 15px; 87 | background-color: black; 88 | border-style:none; 89 | 90 | 91 | QFrame::StyledPanel 92 | 93 | 94 | QFrame::Raised 95 | 96 | 97 | 6 98 | 99 | 100 | 3 101 | 102 | 103 | 104 | 105 | 0 106 | 0 107 | 301 108 | 29 109 | 110 | 111 | 112 | background-color:rgba(255, 255, 255, 0); 113 | border-style: none; 114 | 115 | 116 | 117 | 118 | 0 119 | 0 120 | 301 121 | 34 122 | 123 | 124 | 125 | background-color:rgba(255, 255, 255, 0); 126 | border-style: none; 127 | 128 | 129 | 130 | 6 131 | 132 | 133 | QLayout::SetFixedSize 134 | 135 | 136 | 5 137 | 138 | 139 | 3 140 | 141 | 142 | 143 | 144 | 145 | 25 146 | 25 147 | 148 | 149 | 150 | 151 | 25 152 | 25 153 | 154 | 155 | 156 | false 157 | 158 | 159 | background-color:rgba(255, 255, 255, 0); 160 | border-style: none; 161 | 162 | 163 | 164 | 165 | 166 | :/default_icon/notifications.png 167 | 168 | 169 | false 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 0 178 | 20 179 | 180 | 181 | 182 | 183 | 300 184 | 20 185 | 186 | 187 | 188 | background-color:rgba(255, 255, 255, 0); 189 | border-style: none; 190 | 191 | 192 | Test title 193 | 194 | 195 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 0 206 | 30 207 | 301 208 | 91 209 | 210 | 211 | 212 | background-color:rgba(255, 255, 255, 0); 213 | border-style: none; 214 | 215 | 216 | 217 | 218 | 0 219 | 0 220 | 301 221 | 94 222 | 223 | 224 | 225 | background-color:rgba(255, 255, 255, 0); 226 | border-style: none; 227 | 228 | 229 | 230 | 3 231 | 232 | 233 | QLayout::SetMaximumSize 234 | 235 | 236 | 4 237 | 238 | 239 | 0 240 | 241 | 242 | 3 243 | 244 | 245 | 246 | 247 | 248 | 0 249 | 0 250 | 251 | 252 | 253 | 254 | 0 255 | 85 256 | 257 | 258 | 259 | 260 | 0 261 | 85 262 | 263 | 264 | 265 | background-color:rgba(255, 255, 255, 0); 266 | border-style: none; 267 | 268 | 269 | 270 | 271 | 272 | true 273 | 274 | 275 | Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 49 284 | 0 285 | 286 | 287 | 288 | 289 | 200 290 | 0 291 | 292 | 293 | 294 | 295 | 290 296 | 90 297 | 298 | 299 | 300 | 301 | 500 302 | 0 303 | 304 | 305 | 306 | 307 | 300 308 | 0 309 | 310 | 311 | 312 | background-color:rgba(255, 255, 255, 0); 313 | border-style: none; 314 | 315 | 316 | 317 | 2 318 | 319 | 320 | QLayout::SetNoConstraint 321 | 322 | 323 | 2 324 | 325 | 326 | 327 | 328 | 329 | 0 330 | 0 331 | 332 | 333 | 334 | 335 | 200 336 | 0 337 | 338 | 339 | 340 | 341 | 280 342 | 20 343 | 344 | 345 | 346 | 347 | 75 348 | true 349 | 350 | 351 | 352 | background-color:rgba(255, 255, 255, 0); 353 | border-style: none; 354 | 355 | 356 | Test notification subject 357 | 358 | 359 | Qt::MarkdownText 360 | 361 | 362 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 0 371 | 0 372 | 373 | 374 | 375 | 376 | 0 377 | 64 378 | 379 | 380 | 381 | 382 | 300 383 | 64 384 | 385 | 386 | 387 | background-color:rgba(255, 255, 255, 0); 388 | border-style: none; 389 | 390 | 391 | Test notification body. Everything written here will be overwritten by the actual notification. 392 | 393 | 394 | Qt::MarkdownText 395 | 396 | 397 | false 398 | 399 | 400 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 401 | 402 | 403 | true 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 10 419 | 160 420 | 301 421 | 121 422 | 423 | 424 | 425 | 426 | 427 | 0 428 | 0 429 | 301 430 | 121 431 | 432 | 433 | 434 | PointingHandCursor 435 | 436 | 437 | The button that will close the notification and 438 | execute the default action. 439 | 440 | This will never be visible (opacity: 0). 441 | 442 | 443 | false 444 | 445 | 446 | false 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" -------------------------------------------------------------------------------- /src/dbus_signal.rs: -------------------------------------------------------------------------------- 1 | use crate::Notification; 2 | 3 | #[derive(Debug)] 4 | pub enum DbusSignal { 5 | ActionInvoked { notification_id: i32 }, 6 | NotificationClosed { notification_id: u32, reason: u32 }, 7 | } 8 | 9 | #[derive(Debug)] 10 | pub enum DbusMethod { 11 | CloseNotification { notification_id: u32 }, 12 | Notify { notification: Notification }, 13 | } 14 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::NulError, sync::PoisonError}; 2 | 3 | use crate::dbus_signal::{DbusMethod, DbusSignal}; 4 | 5 | /// 6 | /// Catchall error type for converting errors from between different libraries as needed 7 | /// 8 | /// <3 9 | /// 10 | #[derive(Debug)] 11 | pub enum KrustifyError { 12 | ZbusFdo(zbus::fdo::Error), 13 | Zvariant(zvariant::Error), 14 | DbusMethodSend(tokio::sync::mpsc::error::SendError), 15 | DbusSignalSend(tokio::sync::mpsc::error::SendError), 16 | CStr(NulError), 17 | FindChild(qt_core::FindChildError), 18 | Other { message: String }, 19 | } 20 | 21 | impl From for KrustifyError { 22 | fn from(err: zbus::Error) -> Self { 23 | match err { 24 | zbus::Error::FDO(e) => Self::ZbusFdo(*e), 25 | zbus::Error::Variant(e) => Self::Zvariant(e), 26 | _ => Self::Other { 27 | message: err.to_string(), 28 | }, 29 | } 30 | } 31 | } 32 | 33 | impl From for KrustifyError { 34 | fn from(err: zvariant::Error) -> Self { 35 | Self::Zvariant(err) 36 | } 37 | } 38 | 39 | impl From> for KrustifyError { 40 | fn from(err: tokio::sync::mpsc::error::SendError) -> Self { 41 | KrustifyError::DbusMethodSend(err) 42 | } 43 | } 44 | 45 | impl From for KrustifyError { 46 | fn from(err: NulError) -> Self { 47 | KrustifyError::CStr(err) 48 | } 49 | } 50 | 51 | impl From for KrustifyError { 52 | fn from(err: qt_core::FindChildError) -> Self { 53 | KrustifyError::FindChild(err) 54 | } 55 | } 56 | 57 | impl From>> for KrustifyError { 58 | fn from(err: PoisonError>) -> Self { 59 | Self::Other { 60 | message: err.to_string(), 61 | } 62 | } 63 | } 64 | 65 | impl From> for KrustifyError { 66 | fn from(err: tokio::sync::mpsc::error::SendError) -> Self { 67 | Self::DbusSignalSend(err) 68 | } 69 | } 70 | 71 | impl From for zbus::fdo::Error { 72 | fn from(err: KrustifyError) -> Self { 73 | match err { 74 | KrustifyError::ZbusFdo(e) => e, 75 | KrustifyError::Other { message } => zbus::fdo::Error::Failed(message), 76 | KrustifyError::Zvariant(e) => zbus::fdo::Error::Failed(e.to_string()), 77 | KrustifyError::DbusMethodSend(e) => zbus::fdo::Error::Failed(e.to_string()), 78 | KrustifyError::CStr(e) => zbus::fdo::Error::Failed(e.to_string()), 79 | KrustifyError::FindChild(e) => zbus::fdo::Error::Failed(e.to_string()), 80 | KrustifyError::DbusSignalSend(e) => zbus::fdo::Error::Failed(e.to_string()), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/image_handler.rs: -------------------------------------------------------------------------------- 1 | use cpp_core::{CppBox, Ref}; 2 | 3 | use qt_core::{qs, QFileInfo, QString}; 4 | use qt_gui::{QIcon, QImage, QPixmap}; 5 | use qt_widgets::QFileIconProvider; 6 | 7 | use crate::notification::ImageData; 8 | 9 | const DEFAULT_ICON: &str = "notifications"; 10 | 11 | pub unsafe fn find_icon(desktop_entry: &String) -> CppBox { 12 | let desktop_entry_lowercase = desktop_entry.as_str().to_lowercase(); 13 | 14 | let qstr = QString::from_std_str(desktop_entry_lowercase.as_str()); 15 | let qdesktop_entry = qstr.as_ref(); 16 | 17 | let icon_name: Ref; 18 | if QIcon::has_theme_icon(qdesktop_entry) { 19 | icon_name = qdesktop_entry; 20 | } else { 21 | let info = QFileInfo::new(); 22 | 23 | let path = format!("/usr/share/applications/{}.desktop", desktop_entry); 24 | 25 | info.set_file_q_string(QString::from_std_str(path).as_ref()); 26 | 27 | let icon_provider = QFileIconProvider::new(); 28 | 29 | if info.exists_0a() { 30 | let icon = icon_provider.icon_q_file_info(info.as_ref()); 31 | 32 | let pixmap = icon.pixmap_int(64); 33 | 34 | return pixmap; 35 | } 36 | 37 | return QIcon::from_theme_1a(QString::from_std_str(DEFAULT_ICON).as_ref()).pixmap_int(64); 38 | } 39 | 40 | QIcon::from_theme_1a(icon_name).pixmap_int(64) 41 | } 42 | 43 | pub unsafe fn parse_image(image_data: ImageData) -> CppBox { 44 | let pixmap = QPixmap::new(); 45 | 46 | let image_format = if image_data.has_alpha { 47 | qt_gui::q_image::Format::FormatRGBA8888 48 | } else { 49 | qt_gui::q_image::Format::FormatRGB888 50 | }; 51 | 52 | let data = image_data.data.as_ptr(); 53 | 54 | let qimage = QImage::from_uchar3_int_format2( 55 | data, 56 | image_data.width, 57 | image_data.height, 58 | image_data.rowstride, 59 | image_format, 60 | ); 61 | 62 | pixmap.convert_from_image_1a(qimage.as_ref()); 63 | 64 | pixmap 65 | } 66 | 67 | pub unsafe fn load_image(image_path: String) -> CppBox { 68 | let pixmap = QPixmap::new(); 69 | 70 | let qimage = QImage::from_q_string(&qs(image_path)); 71 | 72 | pixmap.convert_from_image_1a(qimage.as_ref()); 73 | 74 | pixmap 75 | } 76 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::convert::TryFrom; 3 | use std::error::Error; 4 | use std::time::Duration; 5 | 6 | use errors::KrustifyError; 7 | use qt_core::{ 8 | qs, ConnectionType, QCoreApplication, QString, SignalOfInt, SignalOfQString, WidgetAttribute, 9 | WindowType, 10 | }; 11 | use qt_widgets::{QApplication, QFrame, QMainWindow}; 12 | use tokio::{ 13 | self, 14 | sync::mpsc::{self, Sender}, 15 | }; 16 | use uuid::Uuid; 17 | use zbus::{dbus_interface, zvariant::Array, ConnectionBuilder}; 18 | use zvariant::Value; 19 | 20 | use notification::{ImageData, Notification}; 21 | use notification_spawner::NotificationSpawner; 22 | 23 | use crate::dbus_signal::{DbusMethod, DbusSignal}; 24 | use crate::settings::{load_settings, SETTINGS}; 25 | use crate::tray_menu::generate_tray; 26 | 27 | mod dbus_signal; 28 | mod errors; 29 | mod image_handler; 30 | mod notification; 31 | mod notification_spawner; 32 | mod notification_widget; 33 | mod settings; 34 | mod tray_menu; 35 | 36 | //static 37 | struct NotificationHandler { 38 | count: u32, 39 | dbus_method_sender: Sender, 40 | } 41 | 42 | #[dbus_interface(name = "org.freedesktop.Notifications")] 43 | impl NotificationHandler { 44 | #[dbus_interface(name = "CloseNotification")] 45 | async fn close_notification(&mut self, notification_id: u32) -> zbus::fdo::Result<()> { 46 | self.dbus_method_sender 47 | .send(DbusMethod::CloseNotification { notification_id }) 48 | .await 49 | .map_err(KrustifyError::from)?; 50 | 51 | Ok(()) 52 | } 53 | 54 | #[dbus_interface(name = "Notify")] 55 | async fn notify( 56 | &mut self, 57 | app_name: String, 58 | replaces_id: u32, 59 | app_icon: String, 60 | summary: String, 61 | body: String, 62 | actions: Vec, 63 | hints: HashMap>, 64 | expire_timeout: i32, 65 | ) -> zbus::fdo::Result { 66 | let desktop_entry = if hints.contains_key("desktop-entry") { 67 | zbus::zvariant::Str::try_from(&hints["desktop-entry"]) 68 | .map_err(KrustifyError::from)? 69 | .to_string() 70 | } else { 71 | String::new() 72 | }; 73 | 74 | let image_data_property_name = if hints.contains_key("image-data") { 75 | Some("image-data") 76 | } else if hints.contains_key("image_data") { 77 | Some("image_data") 78 | } else if hints.contains_key("icon-data") { 79 | Some("icon-data") 80 | } else if hints.contains_key("icon_data") { 81 | Some("icon_data") 82 | } else { 83 | None 84 | }; 85 | 86 | let image_data = if let Some(name) = image_data_property_name { 87 | let image_structure = 88 | zbus::zvariant::Structure::try_from(&hints[name]).map_err(KrustifyError::from)?; 89 | 90 | let fields = image_structure.fields(); 91 | let width_value = &fields[0]; 92 | let height_value = &fields[1]; 93 | let rowstride_value = &fields[2]; 94 | let has_alpha_value = &fields[3]; 95 | let bits_per_sample_value = &fields[4]; 96 | let channels_value = &fields[5]; 97 | let data_value = &fields[6]; 98 | 99 | let image_raw_bytes_array = Array::try_from(data_value) 100 | .map_err(KrustifyError::from)? 101 | .get() 102 | .to_vec(); 103 | 104 | let width = i32::try_from(width_value).map_err(KrustifyError::from)?; 105 | let height = i32::try_from(height_value).map_err(KrustifyError::from)?; 106 | let rowstride = i32::try_from(rowstride_value).map_err(KrustifyError::from)?; 107 | let has_alpha = bool::try_from(has_alpha_value).map_err(KrustifyError::from)?; 108 | let bits_per_sample = 109 | i32::try_from(bits_per_sample_value).map_err(KrustifyError::from)?; 110 | let channels = i32::try_from(channels_value).map_err(KrustifyError::from)?; 111 | 112 | // TODO: this one's tricky 113 | let data = image_raw_bytes_array 114 | .iter() 115 | .map(|value| { 116 | u8::try_from(value) 117 | .ok() 118 | .expect("value in image data was not a u8") 119 | }) 120 | .collect::>(); 121 | 122 | Some(ImageData::new( 123 | width, 124 | height, 125 | rowstride, 126 | has_alpha, 127 | bits_per_sample, 128 | channels, 129 | data, 130 | )) 131 | } else { 132 | None 133 | }; 134 | 135 | let image_path = if hints.contains_key("image-path") { 136 | Some( 137 | zbus::zvariant::Str::try_from(&hints["image-path"]) 138 | .map_err(KrustifyError::from)? 139 | .to_string(), 140 | ) 141 | } else { 142 | None 143 | }; 144 | 145 | let notification_id = if replaces_id == 0 { 146 | self.count += 1; 147 | self.count 148 | } else { 149 | replaces_id 150 | }; 151 | 152 | let notification = Notification { 153 | app_name, 154 | replaces_id, 155 | app_icon, 156 | summary, 157 | body, 158 | actions, 159 | image_data, 160 | image_path, 161 | expire_timeout, 162 | notification_id, 163 | desktop_entry, 164 | }; 165 | 166 | self.dbus_method_sender 167 | .send(DbusMethod::Notify { notification }) 168 | .await 169 | .map_err(KrustifyError::from)?; 170 | 171 | Ok(notification_id) 172 | } 173 | 174 | #[dbus_interface( 175 | out_args("name", "vendor", "version", "spec_version"), 176 | name = "GetServerInformation" 177 | )] 178 | fn get_server_information(&mut self) -> zbus::fdo::Result<(String, String, String, String)> { 179 | let name = String::from("Notification Daemon"); 180 | let vendor = String::from(env!("CARGO_PKG_NAME")); 181 | let version = String::from(env!("CARGO_PKG_VERSION")); 182 | let specification_version = String::from("1.2"); 183 | 184 | Ok((name, vendor, version, specification_version)) 185 | } 186 | 187 | #[dbus_interface(name = "GetCapabilities")] 188 | fn get_capabilities(&mut self) -> zbus::fdo::Result> { 189 | let capabilities = vec![ 190 | "action-icons", 191 | "actions", 192 | "body", 193 | "body-hyperlinks", 194 | "body-images", 195 | "body-markup", 196 | "icon-multi", 197 | "icon-static", 198 | "persistence", 199 | "sound", 200 | ]; 201 | 202 | Ok(capabilities) 203 | } 204 | } 205 | 206 | #[tokio::main] 207 | async fn main() -> Result<(), Box> { 208 | let (dbus_method_sender, mut dbus_method_receiver) = mpsc::channel(5); 209 | let (dbus_signal_sender, mut dbus_signal_receiver) = mpsc::unbounded_channel(); 210 | 211 | let notification_handler = NotificationHandler { 212 | count: 0, 213 | dbus_method_sender, 214 | }; 215 | let connection = ConnectionBuilder::session()? 216 | .name("org.freedesktop.Notifications")? 217 | .serve_at("/org/freedesktop/Notifications", notification_handler)? 218 | .build() 219 | .await?; 220 | 221 | tokio::spawn(async move { 222 | while let Some(signal) = dbus_signal_receiver.recv().await { 223 | match signal { 224 | DbusSignal::ActionInvoked { notification_id } => { 225 | connection 226 | .emit_signal( 227 | None::<()>, 228 | "/org/freedesktop/Notifications", 229 | "org.freedesktop.Notifications", 230 | "ActionInvoked", 231 | &(notification_id as u32, "default"), 232 | ) 233 | .await 234 | .expect("could not emit ActionInvoked signal"); 235 | } 236 | DbusSignal::NotificationClosed { 237 | notification_id, 238 | reason, 239 | } => { 240 | connection 241 | .emit_signal( 242 | None::<()>, 243 | "/org/freedesktop/Notifications", 244 | "org.freedesktop.Notifications", 245 | "NotificationClosed", 246 | &(notification_id, reason), 247 | ) 248 | .await 249 | .expect("could not emit NotificationClosed signal"); 250 | } 251 | } 252 | } 253 | }); 254 | 255 | QApplication::init(|_app| unsafe { 256 | QCoreApplication::set_organization_name(&qs(env!("CARGO_PKG_NAME"))); 257 | QCoreApplication::set_application_name(&qs(env!("CARGO_PKG_NAME"))); 258 | 259 | load_settings(); 260 | 261 | let main_window = QMainWindow::new_0a(); 262 | 263 | let desktop = QApplication::desktop(); 264 | 265 | let topleft = desktop 266 | .screen_geometry_int(SETTINGS.screen.id.clone()) 267 | .top_left(); 268 | 269 | main_window.set_window_flags( 270 | WindowType::WindowTransparentForInput 271 | | WindowType::WindowStaysOnTopHint 272 | | WindowType::FramelessWindowHint 273 | | WindowType::BypassWindowManagerHint 274 | | WindowType::X11BypassWindowManagerHint, 275 | ); 276 | 277 | main_window.set_attribute_1a(WidgetAttribute::WATranslucentBackground); 278 | main_window.set_attribute_1a(WidgetAttribute::WADeleteOnClose); 279 | main_window.set_attribute_1a(WidgetAttribute::WANoSystemBackground); 280 | main_window.set_style_sheet(&qs("background-color: transparent;")); 281 | 282 | let main_frame = QFrame::new_1a(main_window.as_ptr()); 283 | 284 | main_frame.set_attribute_1a(WidgetAttribute::WATranslucentBackground); 285 | main_frame.set_style_sheet(&qs("background-color: transparent;")); 286 | 287 | main_window.set_geometry_4a(topleft.x(), 0, 0, 0); 288 | 289 | main_window.show(); 290 | 291 | let spawner = NotificationSpawner::new(dbus_signal_sender, main_frame); 292 | 293 | spawner.init(); 294 | 295 | let notitification_signal = SignalOfQString::new(); 296 | notitification_signal.connect_with_type( 297 | ConnectionType::QueuedConnection, 298 | &spawner.slot_on_spawn_notification(), 299 | ); 300 | 301 | let closed_notification_signal = SignalOfInt::new(); 302 | closed_notification_signal.connect_with_type( 303 | ConnectionType::QueuedConnection, 304 | &spawner.slot_on_external_close(), 305 | ); 306 | 307 | let ref_notification_signal = notitification_signal 308 | .as_raw_ref() 309 | .expect("could not get a reference to notification_signal"); 310 | let ref_closed_notification_signal = closed_notification_signal 311 | .as_raw_ref() 312 | .expect("could not get a reference to notification signal"); 313 | 314 | tokio::spawn(async move { 315 | while let Some(method) = dbus_method_receiver.recv().await { 316 | match method { 317 | DbusMethod::CloseNotification { notification_id } => { 318 | tokio::spawn(async move { 319 | tokio::time::sleep(Duration::from_millis(100)).await; 320 | ref_closed_notification_signal.emit(notification_id as i32); 321 | }); 322 | } 323 | DbusMethod::Notify { notification } => { 324 | if !SETTINGS.do_not_disturb.value { 325 | let guid = Uuid::new_v4().to_string(); 326 | let mut list = notification_spawner::NOTIFICATION_LIST 327 | .lock() 328 | .expect("could not acquire lock to notification list"); 329 | list.insert(guid.clone(), notification); 330 | ref_notification_signal.emit(&QString::from_std_str(&guid)); 331 | } 332 | } 333 | } 334 | } 335 | }); 336 | 337 | let _tray_icon = generate_tray(); 338 | 339 | QApplication::exec() 340 | }) 341 | } 342 | -------------------------------------------------------------------------------- /src/notification.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct ImageData { 3 | pub width: i32, 4 | pub height: i32, 5 | pub rowstride: i32, 6 | pub has_alpha: bool, 7 | pub bits_per_sample: i32, 8 | pub channels: i32, 9 | pub data: Vec, 10 | } 11 | 12 | impl ImageData { 13 | pub fn new( 14 | width: i32, 15 | height: i32, 16 | rowstride: i32, 17 | has_alpha: bool, 18 | bits_per_sample: i32, 19 | channels: i32, 20 | data: Vec, 21 | ) -> ImageData { 22 | ImageData { 23 | width, 24 | height, 25 | rowstride, 26 | has_alpha, 27 | bits_per_sample, 28 | channels, 29 | data, 30 | } 31 | } 32 | } 33 | 34 | #[derive(Debug)] 35 | pub struct Notification { 36 | pub app_name: String, 37 | pub replaces_id: u32, 38 | pub app_icon: String, 39 | pub summary: String, 40 | pub body: String, 41 | pub actions: Vec, 42 | pub image_data: Option, 43 | pub image_path: Option, 44 | pub expire_timeout: i32, 45 | pub notification_id: u32, 46 | pub desktop_entry: String, 47 | } 48 | -------------------------------------------------------------------------------- /src/notification_spawner.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::rc::Rc; 3 | use std::sync::{Mutex, MutexGuard}; 4 | 5 | use cpp_core::{Ptr, Ref, StaticUpcast}; 6 | 7 | use linked_hash_map::LinkedHashMap; 8 | 9 | use qt_widgets::QFrame; 10 | use tokio::sync::mpsc::UnboundedSender; 11 | 12 | use lazy_static::lazy_static; 13 | 14 | use qt_core::{ 15 | qs, slot, ConnectionType, QBox, QObject, QString, QTimer, SignalNoArgs, SignalOfInt, 16 | SignalOfQString, SlotNoArgs, SlotOfInt, SlotOfQString, 17 | }; 18 | use uuid::Uuid; 19 | 20 | use crate::errors::KrustifyError; 21 | use crate::{ 22 | dbus_signal::DbusSignal, 23 | image_handler, 24 | notification::{ImageData, Notification}, 25 | notification_widget::notifications::NotificationWidget, 26 | }; 27 | 28 | lazy_static! { 29 | pub static ref NOTIFICATION_LIST: Mutex> = 30 | Mutex::new(HashMap::new()); 31 | } 32 | 33 | pub struct NotificationSpawner { 34 | widget_list: Mutex>>, 35 | check_hover: QBox, 36 | signal_sender: UnboundedSender, 37 | timer: QBox, 38 | reorder_signal: QBox, 39 | action_signal: QBox, 40 | close_signal: QBox, 41 | qobject: QBox, 42 | main_window: QBox, 43 | } 44 | 45 | impl StaticUpcast for NotificationSpawner { 46 | unsafe fn static_upcast(ptr: Ptr) -> Ptr { 47 | ptr.qobject.as_ptr().static_upcast() 48 | } 49 | } 50 | 51 | impl NotificationSpawner { 52 | pub fn new( 53 | signal_sender: UnboundedSender, 54 | main_window: QBox, 55 | ) -> Rc { 56 | unsafe { 57 | let widget_list = Mutex::new(LinkedHashMap::new()); 58 | 59 | let timer = QTimer::new_0a(); 60 | timer.set_interval(100); 61 | 62 | let check_hover = SignalNoArgs::new(); 63 | 64 | timer.timeout().connect(&check_hover); 65 | 66 | let reorder_signal = SignalNoArgs::new(); 67 | 68 | let action_signal = SignalOfInt::new(); 69 | 70 | let close_signal = SignalOfQString::new(); 71 | 72 | let qobject = QObject::new_0a(); 73 | 74 | Rc::new(Self { 75 | widget_list, 76 | check_hover, 77 | signal_sender, 78 | timer, 79 | reorder_signal, 80 | action_signal, 81 | close_signal, 82 | qobject, 83 | main_window, 84 | }) 85 | } 86 | } 87 | 88 | pub unsafe fn init(self: &Rc) { 89 | self.timer.start_0a(); 90 | 91 | self.reorder_signal 92 | .connect_with_type(ConnectionType::QueuedConnection, &self.slot_on_reorder()); 93 | 94 | self.close_signal.connect_with_type( 95 | ConnectionType::QueuedConnection, 96 | &self.slot_on_widget_close(), 97 | ); 98 | 99 | self.action_signal 100 | .connect_with_type(ConnectionType::QueuedConnection, &self.slot_on_action()); 101 | } 102 | 103 | #[slot(SlotOfQString)] 104 | pub unsafe fn on_spawn_notification(self: &Rc, guid: Ref) { 105 | let mut list = NOTIFICATION_LIST.lock().expect("failed to aquire lock"); 106 | let notification_option = list.remove(&guid.to_std_string()); 107 | 108 | if let Some(notification) = notification_option { 109 | self.spawn_notification( 110 | notification.app_name, 111 | notification.replaces_id, 112 | notification.app_icon, 113 | notification.summary, 114 | notification.body, 115 | notification.actions, 116 | notification.image_data, 117 | notification.image_path, 118 | notification.expire_timeout, 119 | notification.notification_id, 120 | notification.desktop_entry, 121 | ) 122 | .expect("failed to spawn notification"); 123 | } else { 124 | return; 125 | } 126 | } 127 | 128 | pub unsafe fn get_already_existing_notification<'a>( 129 | self: &Rc, 130 | list: &'a MutexGuard>>, 131 | app_name: &String, 132 | replaces_id: u32, 133 | ) -> Option<&'a Rc> { 134 | for widget in list.values() { 135 | let _replaces_id = widget.notification_id.borrow().to_owned(); 136 | 137 | if _replaces_id == replaces_id { 138 | return Some(widget); 139 | } 140 | 141 | if app_name.eq("discord") && _replaces_id == replaces_id - 1 142 | // Fuck you Discord 143 | { 144 | widget.notification_id.replace(replaces_id); 145 | return Some(widget); 146 | } 147 | } 148 | 149 | None 150 | } 151 | 152 | pub unsafe fn spawn_notification( 153 | self: &Rc, 154 | app_name: String, 155 | replaces_id: u32, 156 | _app_icon: String, 157 | summary: String, 158 | body: String, 159 | _actions: Vec, 160 | image_data: Option, 161 | image_path: Option, 162 | _expire_timeout: i32, 163 | notification_id: u32, 164 | desktop_entry: String, 165 | ) -> Result<(), KrustifyError> { 166 | let mut list = self.widget_list.lock()?; 167 | 168 | let already_existing_notification = 169 | self.get_already_existing_notification(&list, &app_name, replaces_id); 170 | 171 | if let Some(notification_widget) = already_existing_notification { 172 | notification_widget.reset_timer(); 173 | 174 | self.set_notification_contents( 175 | app_name, 176 | image_data, 177 | image_path, 178 | desktop_entry, 179 | summary, 180 | body, 181 | notification_widget, 182 | ); 183 | } else { 184 | let guid = Uuid::new_v4().to_string(); 185 | 186 | let _notification_widget = NotificationWidget::new( 187 | &self.main_window, 188 | &self.close_signal, 189 | &self.action_signal, 190 | notification_id, 191 | guid.clone(), 192 | )?; 193 | 194 | self.set_notification_contents( 195 | app_name, 196 | image_data, 197 | image_path, 198 | desktop_entry, 199 | summary, 200 | body, 201 | &_notification_widget, 202 | ); 203 | 204 | self.check_hover 205 | .connect(&_notification_widget.slot_check_hover()); 206 | 207 | list.insert(guid, _notification_widget); 208 | 209 | self.reorder(); 210 | }; 211 | 212 | Ok(()) 213 | } 214 | 215 | unsafe fn set_notification_contents( 216 | self: &Rc, 217 | app_name: String, 218 | image_data: Option, 219 | image_path: Option, 220 | desktop_entry: String, 221 | summary: String, 222 | body: String, 223 | notification_widget: &Rc, 224 | ) { 225 | let icon = if !desktop_entry.is_empty() { 226 | image_handler::find_icon(&desktop_entry) 227 | } else { 228 | image_handler::find_icon(&app_name) 229 | }; 230 | 231 | if image_data.is_none() && image_path.is_none() { 232 | notification_widget.set_content_no_image(qs(app_name), qs(summary), qs(body), icon); 233 | } else { 234 | let pixmap = if image_data.is_some() { 235 | image_handler::parse_image(image_data.expect("literally the imposible")) 236 | } else { 237 | image_handler::load_image(image_path.expect("damn, stupid cosmic rays")) 238 | }; 239 | 240 | notification_widget.set_content_with_image( 241 | qs(app_name), 242 | qs(summary), 243 | qs(body), 244 | pixmap, 245 | icon, 246 | ); 247 | } 248 | } 249 | 250 | unsafe fn reorder(self: &Rc) { 251 | self.reorder_signal.emit(); 252 | } 253 | 254 | #[slot(SlotNoArgs)] 255 | unsafe fn on_reorder(self: &Rc) { 256 | let list = self.widget_list.lock().expect("failed to acquire locks"); 257 | 258 | let mut height_accumulator = 0; 259 | let mut biggest_width = 0; 260 | let mut end_height = 0; 261 | 262 | for widget in list.values() { 263 | widget.animate_entry_signal.emit(height_accumulator); 264 | height_accumulator += widget.widget.height(); 265 | biggest_width = if biggest_width < widget.widget.width() { 266 | widget.widget.width() 267 | } else { 268 | biggest_width 269 | }; 270 | 271 | end_height = if height_accumulator < widget.widget.geometry().bottom() { 272 | widget.widget.geometry().bottom() 273 | } else { 274 | height_accumulator 275 | } 276 | } 277 | 278 | self.main_window 279 | .set_geometry_4a(0, 0, biggest_width, end_height); 280 | let window_geometry = self.main_window.window().geometry(); 281 | self.main_window.window().set_geometry_4a( 282 | window_geometry.x(), 283 | window_geometry.y(), 284 | biggest_width, 285 | end_height, 286 | ); 287 | } 288 | 289 | #[slot(SlotOfInt)] 290 | unsafe fn on_action(self: &Rc, notifcation_id: i32) { 291 | self.signal_sender 292 | .send(DbusSignal::ActionInvoked { 293 | notification_id: notifcation_id, 294 | }) 295 | .expect("failed to send signal"); 296 | } 297 | 298 | #[slot(SlotOfQString)] 299 | unsafe fn on_widget_close(self: &Rc, closed_widget: Ref) { 300 | let mut list = self.widget_list.lock().expect("failed to acquire lock"); 301 | 302 | let widget = list 303 | .remove(&closed_widget.to_std_string()) 304 | .expect("failed to remove widget"); 305 | widget.widget.close(); 306 | widget.overlay.close(); 307 | 308 | self.signal_sender 309 | .send(DbusSignal::NotificationClosed { 310 | notification_id: widget.notification_id.take(), 311 | reason: widget.close_reason.take(), 312 | }) 313 | .expect("failed to send signal"); 314 | 315 | self.reorder(); 316 | } 317 | 318 | #[slot(SlotOfInt)] 319 | pub unsafe fn on_external_close(self: &Rc, notification_id: i32) { 320 | let list = self.widget_list.lock().expect("failed to acquire lock"); 321 | 322 | for widget in list.values() { 323 | let _notification_id = widget.notification_id.borrow().to_owned(); 324 | if _notification_id as i32 == notification_id { 325 | widget.close_reason.replace(3); 326 | widget.on_close(); 327 | break; 328 | } 329 | } 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/notification_widget.rs: -------------------------------------------------------------------------------- 1 | pub mod notifications { 2 | use std::ffi::{CStr, CString}; 3 | use std::{cell::RefCell, rc::Rc}; 4 | 5 | use cpp_core::{CppBox, CppDeletable, Ptr, Ref, StaticUpcast}; 6 | use device_query::{DeviceQuery, DeviceState, Keycode}; 7 | 8 | use crate::errors::KrustifyError; 9 | use crate::settings::SETTINGS; 10 | use qt_core::{ 11 | q_abstract_animation, q_io_device::OpenModeFlag, qs, slot, AspectRatioMode, ConnectionType, 12 | GlobalColor, QBox, QByteArray, QEasingCurve, QFile, QFlags, QObject, 13 | QParallelAnimationGroup, QPropertyAnimation, QPtr, QRect, QSequentialAnimationGroup, 14 | QString, QVariant, SignalNoArgs, SignalOfInt, SignalOfQString, SlotNoArgs, SlotOfInt, 15 | TextElideMode, TransformationMode, WidgetAttribute, WindowType, 16 | }; 17 | use qt_gui::{q_painter::RenderHint, QColor, QCursor, QPainter, QPainterPath, QPixmap}; 18 | use qt_widgets::{ 19 | QDialog, QFrame, QGraphicsBlurEffect, QGraphicsDropShadowEffect, QGraphicsOpacityEffect, 20 | QLabel, QPushButton, QStackedLayout, QWidget, 21 | }; 22 | 23 | #[derive(Debug)] 24 | pub struct NotificationWidget { 25 | pub widget: QBox, 26 | // Animations 27 | entry_animation: QBox, 28 | exit_animation: QBox, 29 | blur_animation: QBox, 30 | exit_animation_group: QBox, 31 | parallel_animation: QBox, 32 | // Content 33 | icon_label: QPtr, 34 | app_name_label: QPtr, 35 | image_label: QPtr, 36 | title_label: QPtr, 37 | body_label: QPtr, 38 | close_signal: Ref, 39 | pub animate_entry_signal: QBox, 40 | blur_effect: QBox, 41 | opacity_effect: QBox, 42 | action_button: QPtr, 43 | pub notification_id: RefCell, 44 | pub overlay: QBox, 45 | frame_shadow: QBox, 46 | action_signal: Ref, 47 | guid: String, 48 | parallel_hover_animation: QBox, 49 | default_opacity: CppBox, 50 | default_blur: CppBox, 51 | end_blur: CppBox, 52 | notification_duration: CppBox, 53 | spawn_duration: CppBox, 54 | disappear_duration: CppBox, 55 | default_shadow_color: CppBox, 56 | focused_shadow_color: CppBox, 57 | pub close_reason: RefCell, 58 | } 59 | 60 | impl StaticUpcast for NotificationWidget { 61 | unsafe fn static_upcast(ptr: Ptr) -> Ptr { 62 | ptr.widget.as_ptr().static_upcast() 63 | } 64 | } 65 | 66 | impl NotificationWidget { 67 | pub fn new( 68 | main_window: &QBox, 69 | close_signal: &QBox, 70 | action_signal: &QBox, 71 | _notification_id: u32, 72 | guid: String, 73 | ) -> Result, KrustifyError> { 74 | unsafe { 75 | // Set the notification widget 76 | let widget = QWidget::new_1a(main_window); 77 | 78 | widget.set_object_name(&qs(&guid)); 79 | 80 | // Set flags 81 | 82 | widget.set_attribute_1a(WidgetAttribute::WATranslucentBackground); 83 | widget.set_attribute_1a(WidgetAttribute::WADeleteOnClose); 84 | widget.set_attribute_1a(WidgetAttribute::WANoSystemBackground); 85 | 86 | let widget_layout = QStackedLayout::new(); 87 | 88 | widget.set_layout(widget_layout.as_ptr()); 89 | 90 | let theme = &SETTINGS.theme.name; 91 | 92 | let template_file = 93 | QFile::from_q_string(&qs(format!("./res/themes/{theme}/template.ui"))); 94 | template_file.open(QFlags::from(OpenModeFlag::ReadOnly)); 95 | let loader = qt_ui_tools::QUiLoader::new_1a(&widget); 96 | let template = loader.load_1a(template_file.as_ptr()); 97 | template_file.reset(); 98 | template_file.close(); 99 | template_file.delete(); 100 | loader.delete(); 101 | 102 | // Load properties 103 | let default_opacity = 104 | template.property(CStr::as_ptr(&CString::new("defaultOpacity")?)); 105 | let hovered_opacity = 106 | template.property(CStr::as_ptr(&CString::new("hoveredOpacity")?)); 107 | let default_blur = template.property(CStr::as_ptr(&CString::new("defaultBlur")?)); 108 | let hovered_blur = template.property(CStr::as_ptr(&CString::new("hoveredBlur")?)); 109 | let end_blur = template.property(CStr::as_ptr(&CString::new("endBlur")?)); 110 | let notification_duration = 111 | template.property(CStr::as_ptr(&CString::new("notificationDuration")?)); 112 | let spawn_duration = 113 | template.property(CStr::as_ptr(&CString::new("spawnDuration")?)); 114 | let disappear_duration = 115 | template.property(CStr::as_ptr(&CString::new("disappearDuration")?)); 116 | let default_shadow_color = 117 | template.property(CStr::as_ptr(&CString::new("defaultShadowColor")?)); 118 | let focused_shadow_color = 119 | template.property(CStr::as_ptr(&CString::new("focusedShadowColor")?)); 120 | let text_shadow_color = 121 | template.property(CStr::as_ptr(&CString::new("textShadowColor")?)); 122 | 123 | let notification: QPtr = template.find_child("notification")?; 124 | 125 | widget.layout().add_widget(¬ification); 126 | 127 | let overlay_widget: QPtr = template.find_child("overlay")?; 128 | // Set the default action overlay 129 | let overlay = QDialog::new_1a(&widget); 130 | overlay.set_object_name(&qs("overlay")); 131 | 132 | overlay.set_window_flags( 133 | WindowType::WindowStaysOnTopHint 134 | | WindowType::Tool 135 | | WindowType::FramelessWindowHint 136 | | WindowType::BypassWindowManagerHint, 137 | ); 138 | 139 | overlay.set_attribute_1a(WidgetAttribute::WADeleteOnClose); 140 | 141 | overlay.set_window_opacity(0.0); 142 | 143 | let overlay_layout = QStackedLayout::new(); 144 | overlay_layout.set_object_name(&qs("overlay_layout")); 145 | overlay.set_layout(overlay_layout.as_ptr()); 146 | overlay_layout.add_widget(&overlay_widget); 147 | 148 | let action_button: QPtr = overlay_widget.find_child("pushButton")?; 149 | 150 | let blur_effect = QGraphicsBlurEffect::new_1a(&widget); 151 | blur_effect.set_object_name(&qs("blur_effect")); 152 | 153 | let opacity_effect = QGraphicsOpacityEffect::new_1a(¬ification); 154 | opacity_effect.set_object_name(&qs("opacity_effect")); 155 | 156 | widget.set_graphics_effect(&blur_effect); 157 | notification.set_graphics_effect(&opacity_effect); 158 | 159 | opacity_effect.set_opacity(default_opacity.to_double_0a()); 160 | blur_effect.set_blur_radius(default_blur.to_double_0a()); 161 | 162 | widget.set_geometry_4a( 163 | 0, 164 | 0 - notification.geometry().height(), 165 | notification.geometry().width(), 166 | notification.geometry().height(), 167 | ); 168 | 169 | widget.set_window_opacity(default_opacity.to_double_0a()); 170 | 171 | // Set animations 172 | let y_property = QByteArray::new(); 173 | y_property.add_assign_q_string(&qs("geometry")); 174 | 175 | let blur_radius_property = QByteArray::new(); 176 | blur_radius_property.add_assign_q_string(&qs("blurRadius")); 177 | 178 | let opacity_property = QByteArray::new(); 179 | opacity_property.add_assign_q_string(&qs("opacity")); 180 | 181 | let entry_animation = QPropertyAnimation::new_2a(&widget, &y_property); 182 | entry_animation.set_object_name(&qs("entry_animation")); 183 | let exit_animation = QPropertyAnimation::new_2a(&opacity_effect, &opacity_property); 184 | exit_animation.set_object_name(&qs("exit_animation")); 185 | let blur_animation = 186 | QPropertyAnimation::new_2a(&blur_effect, &blur_radius_property); 187 | blur_animation.set_object_name(&qs("blur_animation")); 188 | let blur_hover_animation = 189 | QPropertyAnimation::new_2a(&blur_effect, &blur_radius_property); 190 | blur_hover_animation.set_object_name(&qs("blur_hover_animation")); 191 | let opacity_hover_animation = 192 | QPropertyAnimation::new_2a(&opacity_effect, &opacity_property); 193 | opacity_hover_animation.set_object_name(&qs("opacity_hover_animation")); 194 | let exit_animation_group = QSequentialAnimationGroup::new_1a(&widget); 195 | exit_animation_group.set_object_name(&qs("exit_animation_group")); 196 | let parallel_animation = QParallelAnimationGroup::new_1a(&widget); 197 | parallel_animation.set_object_name(&qs("parallel_animation")); 198 | let parallel_hover_animation = QParallelAnimationGroup::new_1a(&widget); 199 | parallel_hover_animation.set_object_name(&qs("parallel_hover_animation")); 200 | 201 | blur_hover_animation.set_start_value(&default_blur); 202 | blur_hover_animation.set_end_value(&hovered_blur); 203 | blur_hover_animation.set_duration(100); 204 | opacity_hover_animation.set_start_value(&default_opacity); 205 | opacity_hover_animation.set_end_value(&hovered_opacity); 206 | opacity_hover_animation.set_duration(100); 207 | 208 | parallel_hover_animation.add_animation(&blur_hover_animation); 209 | parallel_hover_animation.add_animation(&opacity_hover_animation); 210 | 211 | let frame: QPtr = widget.find_child("notificationFrame")?; 212 | 213 | let frame_shadow = QGraphicsDropShadowEffect::new_1a(&frame); 214 | frame_shadow.set_object_name(&qs("frame_shadow")); 215 | 216 | frame_shadow.set_blur_radius(10.0); 217 | frame_shadow.set_x_offset(1.0); 218 | frame_shadow.set_y_offset(1.0); 219 | 220 | frame.set_graphics_effect(&frame_shadow); 221 | 222 | // Set up content 223 | let icon_label: QPtr = 224 | widget.find_child("iconLabel").unwrap_or(QPtr::null()); 225 | let app_name_label: QPtr = 226 | widget.find_child("appNameLabel").unwrap_or(QPtr::null()); 227 | 228 | if !app_name_label.is_null() { 229 | let app_name_label_shadow = QGraphicsDropShadowEffect::new_1a(&app_name_label); 230 | app_name_label_shadow.set_object_name(&qs("app_name_label_shadow")); 231 | 232 | app_name_label_shadow.set_blur_radius(1.0); 233 | app_name_label_shadow.set_x_offset(0.0); 234 | app_name_label_shadow.set_y_offset(0.0); 235 | app_name_label_shadow 236 | .set_color(&QColor::from_q_string(&text_shadow_color.to_string())); 237 | 238 | app_name_label.set_graphics_effect(&app_name_label_shadow); 239 | } 240 | 241 | let image_label: QPtr = 242 | widget.find_child("imageLabel").unwrap_or(QPtr::null()); 243 | 244 | let title_label: QPtr = 245 | widget.find_child("titleLabel").unwrap_or(QPtr::null()); 246 | 247 | if !title_label.is_null() { 248 | let title_label_shadow = QGraphicsDropShadowEffect::new_1a(&title_label); 249 | 250 | title_label_shadow.set_object_name(&qs("title_label_shadow")); 251 | 252 | title_label_shadow.set_blur_radius(1.0); 253 | title_label_shadow.set_x_offset(0.0); 254 | title_label_shadow.set_y_offset(0.0); 255 | title_label_shadow 256 | .set_color(&QColor::from_q_string(&text_shadow_color.to_string())); 257 | 258 | title_label.set_graphics_effect(&title_label_shadow); 259 | } 260 | 261 | let body_label: QPtr = 262 | widget.find_child("bodyLabel").unwrap_or(QPtr::null()); 263 | 264 | if !body_label.is_null() { 265 | let body_label_shadow = QGraphicsDropShadowEffect::new_1a(&body_label); 266 | body_label_shadow.set_object_name(&qs("body_label_shadow")); 267 | 268 | body_label_shadow.set_blur_radius(1.0); 269 | body_label_shadow.set_x_offset(0.0); 270 | body_label_shadow.set_y_offset(0.0); 271 | body_label_shadow 272 | .set_color(&QColor::from_q_string(&text_shadow_color.to_string())); 273 | 274 | body_label.set_graphics_effect(&body_label_shadow); 275 | } 276 | 277 | let animate_entry_signal = SignalOfInt::new(); 278 | 279 | widget.show(); 280 | overlay.show(); 281 | overlay.hide(); 282 | 283 | // not sure about these ones 284 | let close = close_signal 285 | .as_ref() 286 | .expect("couldn't get reference to close signal"); 287 | let action = action_signal 288 | .as_ref() 289 | .expect("coudln't get reference to action signal"); 290 | 291 | let notification_id = RefCell::new(_notification_id); 292 | 293 | template.close(); 294 | template.delete(); 295 | 296 | let this = Rc::new(Self { 297 | widget, 298 | entry_animation, 299 | exit_animation, 300 | blur_animation, 301 | exit_animation_group, 302 | parallel_animation, 303 | icon_label, 304 | app_name_label, 305 | image_label, 306 | title_label, 307 | body_label, 308 | close_signal: close, 309 | animate_entry_signal, 310 | blur_effect, 311 | opacity_effect, 312 | action_signal: action, 313 | action_button, 314 | notification_id, 315 | overlay, 316 | frame_shadow, 317 | guid, 318 | parallel_hover_animation, 319 | default_opacity, 320 | default_blur, 321 | end_blur, 322 | notification_duration, 323 | spawn_duration, 324 | disappear_duration, 325 | default_shadow_color, 326 | focused_shadow_color, 327 | close_reason: RefCell::new(1), 328 | }); 329 | this.init(); 330 | this.animate_exit(); 331 | Ok(this) 332 | } 333 | } 334 | 335 | #[slot(SlotNoArgs)] 336 | unsafe fn ellide(self: &Rc) { 337 | if !self.title_label.is_null() { 338 | let ellided_title = self.title_label.font_metrics().elided_text_3a( 339 | &self.title_label.text(), 340 | TextElideMode::ElideRight, 341 | self.title_label.width(), 342 | ); 343 | 344 | self.title_label.set_text(&ellided_title); 345 | } 346 | } 347 | 348 | unsafe fn set_content( 349 | self: &Rc, 350 | app_name: CppBox, 351 | title: CppBox, 352 | body: CppBox, 353 | icon: CppBox, 354 | ) { 355 | if !self.app_name_label.is_null() { 356 | self.app_name_label.set_text(&app_name); 357 | } 358 | 359 | if !self.body_label.is_null() { 360 | self.body_label.set_text(&body); 361 | } 362 | 363 | if !self.title_label.is_null() { 364 | self.title_label.set_text(&title); 365 | } 366 | 367 | if !self.icon_label.is_null() { 368 | let scaled_icon = icon.scaled_2_int_aspect_ratio_mode_transformation_mode( 369 | self.icon_label.width(), 370 | self.icon_label.height(), 371 | AspectRatioMode::IgnoreAspectRatio, 372 | TransformationMode::SmoothTransformation, 373 | ); 374 | 375 | self.icon_label.set_pixmap(&scaled_icon); 376 | } 377 | 378 | let signal = SignalNoArgs::new(); 379 | signal.connect_with_type(ConnectionType::QueuedConnection, &self.slot_ellide()); 380 | signal.emit(); 381 | } 382 | 383 | pub unsafe fn set_content_no_image( 384 | self: &Rc, 385 | app_name: CppBox, 386 | title: CppBox, 387 | body: CppBox, 388 | icon: CppBox, 389 | ) { 390 | self.set_content(app_name, title, body, icon); 391 | } 392 | 393 | pub unsafe fn set_content_with_image( 394 | self: &Rc, 395 | app_name: CppBox, 396 | title: CppBox, 397 | body: CppBox, 398 | image: CppBox, 399 | icon: CppBox, 400 | ) { 401 | if !self.image_label.is_null() { 402 | let scaled_image = self.resize_image(image); 403 | 404 | self.image_label.set_pixmap(&scaled_image); 405 | 406 | self.image_label.set_maximum_size_2a( 407 | self.image_label.maximum_height(), 408 | self.image_label.maximum_height(), 409 | ); 410 | self.image_label.set_minimum_size_2a( 411 | self.image_label.maximum_height(), 412 | self.image_label.maximum_height(), 413 | ); 414 | } 415 | 416 | self.set_content(app_name, title, body, icon); 417 | } 418 | 419 | unsafe fn resize_image(self: &Rc, pixmap: CppBox) -> CppBox { 420 | let target = QPixmap::from_2_int( 421 | self.image_label.maximum_height(), 422 | self.image_label.maximum_height(), 423 | ); 424 | 425 | target.fill_1a(&QColor::from_global_color(GlobalColor::Transparent)); 426 | 427 | let painter = QPainter::new_1a(&target); 428 | 429 | painter.set_render_hints_2a( 430 | RenderHint::HighQualityAntialiasing 431 | | RenderHint::SmoothPixmapTransform 432 | | RenderHint::Antialiasing, 433 | true, 434 | ); 435 | 436 | let path = QPainterPath::new_0a(); 437 | path.add_round_rect_6a( 438 | 0.0, 439 | 0.0, 440 | self.image_label.maximum_height() as f64, 441 | self.image_label.maximum_height() as f64, 442 | 25, 443 | 25, 444 | ); 445 | 446 | painter.set_clip_path_1a(&path); 447 | 448 | let scaled_pixmap = pixmap.scaled_2_int_aspect_ratio_mode_transformation_mode( 449 | self.image_label.maximum_height(), 450 | self.image_label.maximum_height(), 451 | AspectRatioMode::IgnoreAspectRatio, 452 | TransformationMode::SmoothTransformation, 453 | ); 454 | 455 | painter.draw_pixmap_q_rect_q_pixmap(&target.rect(), &scaled_pixmap); 456 | 457 | target 458 | } 459 | 460 | pub unsafe fn reset_timer(self: &Rc) { 461 | self.exit_animation_group.set_current_time(0); 462 | self.exit_animation_group.start_0a(); 463 | } 464 | 465 | #[slot(SlotNoArgs)] 466 | pub unsafe fn check_hover(self: &Rc) { 467 | let device_state = DeviceState::new(); 468 | 469 | let keys: Vec = device_state.get_keys(); 470 | 471 | if keys.contains(&Keycode::LAlt) { 472 | self.freeze(); 473 | } else { 474 | self.unfreeze(); 475 | } 476 | 477 | let rect = QRect::new(); 478 | rect.set_x(self.widget.x() + self.widget.window().x()); 479 | rect.set_y(self.widget.y() + self.widget.window().y()); 480 | rect.set_width(self.widget.geometry().width()); 481 | rect.set_height(self.widget.geometry().height()); 482 | 483 | let pos = QCursor::pos_0a(); 484 | 485 | if rect.contains_q_point(pos.as_ref()) { 486 | self.hover(); 487 | } else { 488 | self.unhover(); 489 | } 490 | } 491 | 492 | pub unsafe fn hover(self: &Rc) { 493 | if self.overlay.is_visible() { 494 | self.blur_effect 495 | .set_blur_radius(self.default_blur.to_double_0a()); 496 | self.opacity_effect.set_opacity(0.99); // For some reason setting it to 1.0 shifts the widget slightly to the bottom-right 497 | self.frame_shadow.set_blur_radius(15.0); 498 | 499 | let color = QColor::from_q_string(&self.focused_shadow_color.to_string()); 500 | 501 | self.frame_shadow.set_color(&color); 502 | self.frame_shadow.set_offset_2_double(0.0, 0.0); 503 | } else if self.exit_animation.state() != q_abstract_animation::State::Running { 504 | self.parallel_hover_animation 505 | .set_direction(q_abstract_animation::Direction::Forward); 506 | 507 | if self.parallel_hover_animation.state() == q_abstract_animation::State::Stopped 508 | && self.parallel_hover_animation.current_time() == 0 509 | { 510 | self.parallel_hover_animation.start_0a(); 511 | } 512 | } 513 | } 514 | 515 | pub unsafe fn unhover(self: &Rc) { 516 | if self.overlay.is_visible() { 517 | self.blur_effect 518 | .set_blur_radius(self.default_blur.to_double_0a()); 519 | self.opacity_effect 520 | .set_opacity(self.default_opacity.to_double_0a()); 521 | } else if self.exit_animation.state() != q_abstract_animation::State::Running { 522 | if self.parallel_hover_animation.state() == q_abstract_animation::State::Stopped 523 | && self.parallel_hover_animation.current_time() > 0 524 | { 525 | self.parallel_hover_animation 526 | .set_direction(q_abstract_animation::Direction::Backward); 527 | self.parallel_hover_animation.start_0a(); 528 | } 529 | } 530 | 531 | self.frame_shadow.set_blur_radius(10.0); 532 | 533 | let color = QColor::from_q_string(&self.default_shadow_color.to_string()); 534 | 535 | self.frame_shadow.set_color(&color); 536 | self.frame_shadow.set_offset_2_double(1.0, 1.0); 537 | } 538 | 539 | #[slot(SlotOfInt)] 540 | pub unsafe fn animate_entry(self: &Rc, height: i32) { 541 | self.entry_animation 542 | .set_duration(self.spawn_duration.to_int_0a()); 543 | 544 | let start_value = self.widget.geometry(); 545 | let end_value = QRect::from_4_int( 546 | start_value.left(), 547 | height, 548 | start_value.width(), 549 | start_value.height(), 550 | ); 551 | 552 | self.entry_animation 553 | .set_start_value(&QVariant::from_q_rect(start_value)); 554 | self.entry_animation 555 | .set_end_value(&QVariant::from_q_rect(&end_value)); 556 | self.entry_animation.start_0a(); 557 | } 558 | 559 | #[slot(SlotNoArgs)] 560 | unsafe fn animate_exit(self: &Rc) { 561 | self.exit_animation 562 | .set_duration(self.disappear_duration.to_int_0a()); 563 | self.exit_animation.set_start_value(&self.default_opacity); 564 | self.exit_animation 565 | .set_end_value(&QVariant::from_float(0.0)); 566 | self.exit_animation.set_easing_curve(&QEasingCurve::new_1a( 567 | qt_core::q_easing_curve::Type::OutCurve, 568 | )); 569 | 570 | self.blur_animation 571 | .set_duration(self.disappear_duration.to_int_0a()); 572 | self.blur_animation.set_start_value(&self.default_blur); 573 | self.blur_animation.set_end_value(&self.end_blur); 574 | 575 | self.parallel_animation.add_animation(&self.blur_animation); 576 | self.parallel_animation.add_animation(&self.exit_animation); 577 | 578 | self.exit_animation_group 579 | .add_pause(self.notification_duration.to_int_0a()) 580 | .finished() 581 | .connect(&self.slot_on_init_exit()); 582 | self.exit_animation_group 583 | .add_animation(&self.parallel_animation); 584 | 585 | self.exit_animation_group.start_0a(); 586 | 587 | self.exit_animation_group 588 | .finished() 589 | .connect(&self.slot_on_close()); 590 | } 591 | 592 | unsafe fn init(self: &Rc) { 593 | self.animate_entry_signal 594 | .connect(&self.slot_animate_entry()); 595 | self.action_button 596 | .clicked() 597 | .connect(&self.slot_on_button_clicked()); 598 | } 599 | 600 | #[slot(SlotNoArgs)] 601 | pub unsafe fn on_close(self: &Rc) { 602 | self.close_signal.emit(&qs(&self.guid)); 603 | } 604 | 605 | #[slot(SlotNoArgs)] 606 | unsafe fn on_init_exit(self: &Rc) { 607 | self.parallel_hover_animation.stop(); 608 | self.exit_animation 609 | .set_start_value(&qt_core::QVariant::from_double( 610 | self.opacity_effect.opacity(), 611 | )); 612 | self.blur_animation 613 | .set_start_value(&qt_core::QVariant::from_double( 614 | self.blur_effect.blur_radius(), 615 | )); 616 | } 617 | 618 | unsafe fn freeze(self: &Rc) { 619 | let rect = QRect::new(); 620 | rect.set_x(self.widget.x() + self.widget.window().x()); 621 | rect.set_y(self.widget.y() + self.widget.window().y()); 622 | rect.set_width(self.widget.geometry().width()); 623 | rect.set_height(self.widget.geometry().height()); 624 | 625 | self.overlay.set_geometry_1a(rect.as_ref()); 626 | self.action_button.set_geometry_4a( 627 | 0, 628 | 0, 629 | self.widget.geometry().width(), 630 | self.widget.geometry().height(), 631 | ); 632 | if self.overlay.is_visible() { 633 | return; 634 | } 635 | self.overlay.set_visible(true); 636 | if self.exit_animation_group.state() == q_abstract_animation::State::Paused { 637 | return; 638 | } 639 | self.exit_animation_group.pause(); 640 | self.blur_effect 641 | .set_blur_radius(self.default_blur.to_double_0a()); 642 | self.opacity_effect 643 | .set_opacity(self.default_opacity.to_double_0a()); 644 | } 645 | 646 | unsafe fn unfreeze(self: &Rc) { 647 | if !self.overlay.is_visible() { 648 | return; 649 | } 650 | self.overlay.set_visible(false); 651 | self.frame_shadow.set_blur_radius(10.0); 652 | 653 | let color = QColor::from_q_string(&self.default_shadow_color.to_string()); 654 | 655 | self.frame_shadow.set_color(&color); 656 | self.frame_shadow.set_offset_2_double(1.0, 1.0); 657 | self.parallel_hover_animation.set_current_time(0); 658 | if self.exit_animation_group.state() != q_abstract_animation::State::Paused { 659 | return; 660 | } 661 | self.exit_animation_group.resume(); 662 | } 663 | 664 | #[slot(SlotNoArgs)] 665 | unsafe fn on_button_clicked(self: &Rc) { 666 | let notification_id = self.notification_id.borrow().to_owned(); 667 | self.action_signal.emit(notification_id as i32); 668 | self.on_close(); 669 | } 670 | } 671 | } 672 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, Ordering}; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | use cpp_core::CppBox; 5 | use lazy_static::lazy_static; 6 | use qt_core::{qs, QBox, QPtr, QSettings, QVariant}; 7 | use qt_gui::{QGuiApplication, QScreen}; 8 | 9 | lazy_static! { 10 | static ref THEME: Mutex = Mutex::new("default".to_string()); 11 | static ref SCREEN: Mutex = Mutex::new(-1); 12 | static ref DO_NOT_DISTURB: Arc = Arc::new(AtomicBool::new(false)); 13 | } 14 | 15 | static mut QSETTINGS: Option> = None; 16 | 17 | pub static mut SETTINGS: Settings = Settings { 18 | theme: Theme { name: "" }, 19 | screen: Screen { 20 | id: -1, 21 | name: "", 22 | qscreen: None, 23 | }, 24 | do_not_disturb: DoNotDisturb { value: false }, 25 | }; 26 | 27 | pub trait Setting { 28 | fn load(&mut self); 29 | fn set(&mut self, value: CppBox); 30 | fn save(&mut self); 31 | } 32 | 33 | pub struct Settings { 34 | pub theme: Theme, 35 | pub screen: Screen, 36 | pub do_not_disturb: DoNotDisturb, 37 | } 38 | 39 | pub unsafe fn load_settings() { 40 | unsafe { 41 | QSETTINGS = Some(QSettings::new()); 42 | } 43 | 44 | let mut theme = Theme { name: "default" }; 45 | let mut screen = Screen { 46 | name: "", 47 | id: -1, 48 | qscreen: None, 49 | }; 50 | 51 | theme.load(); 52 | screen.load(); 53 | 54 | let do_not_disturb = DoNotDisturb { value: false }; 55 | 56 | let this = Settings { 57 | theme, 58 | screen, 59 | do_not_disturb, 60 | }; 61 | 62 | SETTINGS = this; 63 | } 64 | 65 | pub struct Theme { 66 | pub name: &'static str, 67 | } 68 | 69 | impl Setting for Theme { 70 | fn load(&mut self) { 71 | unsafe { 72 | let theme_setting = QSETTINGS.as_ref().unwrap().value_1a(&qs("theme")); 73 | 74 | self.set(theme_setting); 75 | } 76 | } 77 | 78 | fn set(&mut self, value: CppBox) { 79 | unsafe { 80 | if value.is_null() { 81 | self.name = "default"; 82 | } else { 83 | self.name = Box::leak(value.to_string().to_std_string().into_boxed_str()); 84 | } 85 | 86 | let mut _theme = THEME.lock().expect("Could not lock mutex"); 87 | 88 | *_theme = self.name.to_string(); 89 | } 90 | } 91 | 92 | fn save(&mut self) { 93 | unsafe { 94 | QSETTINGS.as_ref().unwrap().set_value( 95 | &qs("theme"), 96 | &QVariant::from_q_string(&qs(self.name.clone())), 97 | ); 98 | } 99 | } 100 | } 101 | 102 | pub struct Screen { 103 | pub id: i32, 104 | pub name: &'static str, 105 | pub qscreen: Option>, 106 | } 107 | 108 | impl Setting for Screen { 109 | fn load(&mut self) { 110 | unsafe { 111 | let screen_setting = QSETTINGS.as_ref().unwrap().value_1a(&qs("screen")); 112 | 113 | self.set(screen_setting); 114 | } 115 | } 116 | 117 | fn set(&mut self, value: CppBox) { 118 | unsafe { 119 | let screens = QGuiApplication::screens(); 120 | let mut screen_id = -1; 121 | let mut screen_name = String::new(); 122 | let mut qscreen = Some(screens.value_1a(0)); 123 | 124 | if !value.is_null() { 125 | for i in 0..screens.length() { 126 | let screen = screens.value_1a(i); 127 | 128 | if screen.name().compare_q_string(&value.to_string()) == 0 { 129 | screen_id = i; 130 | screen_name = screen.name().to_std_string(); 131 | qscreen = Some(screen); 132 | } 133 | } 134 | } 135 | 136 | self.name = Box::leak(screen_name.into_boxed_str()); 137 | self.id = screen_id; 138 | self.qscreen = qscreen; 139 | 140 | let mut _screen = SCREEN.lock().expect("Could not lock screen mutex"); 141 | 142 | *_screen = self.id.clone(); 143 | } 144 | } 145 | 146 | fn save(&mut self) { 147 | unsafe { 148 | QSETTINGS.as_ref().unwrap().set_value( 149 | &qs("screen"), 150 | &QVariant::from_q_string(&qs(self.name.clone())), 151 | ); 152 | } 153 | } 154 | } 155 | 156 | pub struct DoNotDisturb { 157 | pub value: bool, 158 | } 159 | 160 | impl Setting for DoNotDisturb { 161 | fn load(&mut self) { 162 | self.value = false; 163 | } 164 | 165 | fn set(&mut self, value: CppBox) { 166 | unsafe { 167 | self.value = value.to_bool(); 168 | DO_NOT_DISTURB.store(value.to_bool(), Ordering::Relaxed); 169 | } 170 | } 171 | 172 | fn save(&mut self) { 173 | // Should I actually save the Do Not Disturb? 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/tray_menu.rs: -------------------------------------------------------------------------------- 1 | use cpp_core::CppBox; 2 | use qt_core::q_dir::Filter; 3 | use qt_core::{qs, QBox, QDir, QDirIterator, QString, QVariant}; 4 | use qt_gui::{QGuiApplication, QIcon}; 5 | use qt_widgets::{QActionGroup, QApplication, QMenu, QSystemTrayIcon, SlotOfQAction}; 6 | 7 | use crate::settings::Setting; 8 | use crate::SETTINGS; 9 | 10 | pub struct MenuItem { 11 | label: CppBox, 12 | value: CppBox, 13 | } 14 | 15 | pub fn get_available_themes() -> Vec { 16 | let mut values: Vec = Vec::new(); 17 | unsafe { 18 | let theme_directories = QDirIterator::from_q_string_q_flags_filter( 19 | &qs("./res/themes"), 20 | Filter::AllDirs | Filter::NoDotAndDotDot, 21 | ); 22 | 23 | while theme_directories.has_next() { 24 | let theme = QDir::new_1a(&theme_directories.next()); 25 | 26 | values.push(MenuItem { 27 | label: theme.dir_name(), 28 | value: QVariant::from_q_string(theme.dir_name().as_ref()), 29 | }) 30 | } 31 | } 32 | 33 | values 34 | } 35 | 36 | pub fn get_available_screens() -> Vec { 37 | let mut values: Vec = Vec::new(); 38 | unsafe { 39 | let screens = QGuiApplication::screens(); 40 | 41 | values.push(MenuItem { 42 | label: qs("(Primary screen)"), 43 | value: QVariant::from_int(-1), 44 | }); 45 | 46 | for i in 0..screens.length() { 47 | values.push(MenuItem { 48 | label: screens.value_1a(i).name(), 49 | value: QVariant::from_int(i), 50 | }) 51 | } 52 | } 53 | 54 | values 55 | } 56 | 57 | pub unsafe fn generate_tray() -> (QBox, QBox) { 58 | let tray_icon = QSystemTrayIcon::new(); 59 | 60 | tray_icon.set_icon(&QIcon::from_theme_1a(&qs("notifications"))); 61 | 62 | tray_icon.show(); 63 | 64 | let tray_menu = QMenu::new(); 65 | 66 | let theme_menu = tray_menu.add_menu_q_string(&qs("Themes")); 67 | 68 | let theme_action_group = QActionGroup::new(&theme_menu); 69 | theme_action_group.set_exclusive(true); 70 | 71 | let theme_directories = get_available_themes(); 72 | 73 | for theme in theme_directories { 74 | let theme = theme.label; 75 | 76 | let theme_action = theme_menu.add_action_q_string(&theme); 77 | 78 | theme_action.set_object_name(&qs("set_theme")); 79 | theme_action.set_checkable(true); 80 | theme_action.set_data(&QVariant::from_q_string(&theme)); 81 | 82 | if SETTINGS.theme.name == theme.to_std_string() { 83 | theme_action.set_checked(true); 84 | } 85 | 86 | theme_action_group.add_action_q_action(theme_action.as_ptr()); 87 | } 88 | 89 | let screens_menu = tray_menu.add_menu_q_string(&qs("Screen")); 90 | 91 | let screens_action_group = QActionGroup::new(&screens_menu); 92 | screens_action_group.set_exclusive(true); 93 | 94 | for screen in get_available_screens() { 95 | let screen_action = screens_menu.add_action_q_string(&screen.label); 96 | 97 | screen_action.set_object_name(&qs("set_screen")); 98 | screen_action.set_checkable(true); 99 | screen_action.set_data(&screen.value); 100 | 101 | if SETTINGS.screen.name == screen.label.to_std_string() { 102 | screen_action.set_checked(true); 103 | } 104 | 105 | screens_action_group.add_action_q_action(screen_action.as_ptr()); 106 | } 107 | 108 | if screens_action_group.checked_action().is_null() { 109 | screens_action_group.actions().value_1a(0).set_checked(true); 110 | } 111 | 112 | let do_not_disturb_action = tray_menu.add_action_q_string(&qs("Do not disturb")); 113 | do_not_disturb_action.set_object_name(&qs("do_not_disturb_action")); 114 | do_not_disturb_action.set_checkable(true); 115 | 116 | let quit_action = tray_menu.add_action_q_string(&qs("Quit")); 117 | quit_action.set_object_name(&qs("quit_action")); 118 | 119 | tray_icon.set_context_menu(&tray_menu); 120 | 121 | let settings = &mut SETTINGS; 122 | 123 | let tray_icon_ptr = tray_icon.as_ptr(); 124 | 125 | tray_menu 126 | .triggered() 127 | .connect(&SlotOfQAction::new(&tray_menu, move |action| { 128 | if action.object_name().to_std_string() == "quit_action".to_string() { 129 | QApplication::close_all_windows(); 130 | tray_icon_ptr.hide(); 131 | } 132 | 133 | if action.object_name().to_std_string() == "do_not_disturb_action".to_string() { 134 | settings 135 | .do_not_disturb 136 | .set(QVariant::from_bool(action.is_checked())); 137 | } 138 | 139 | if action.object_name().to_std_string() == "set_theme".to_string() { 140 | settings.theme.set(action.data()); 141 | settings.theme.save(); 142 | } 143 | 144 | if action.object_name().to_std_string() == "set_screen".to_string() { 145 | settings 146 | .screen 147 | .set(QVariant::from_q_string(action.text().as_ref())); 148 | settings.screen.save(); 149 | } 150 | })); 151 | 152 | (tray_icon, tray_menu) 153 | } 154 | --------------------------------------------------------------------------------