diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..f280d87 Binary files /dev/null and b/.DS_Store differ diff --git a/Cargo.lock b/Cargo.lock index 2ef5b11..d396c5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,112 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169" + +[[package]] +name = "accesskit" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b76d84ee70e30a4a7e39ab9018e2b17a6a09e31084176cc7c0b2dec036ba45" + +[[package]] +name = "accesskit_atspi_common" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5393c75d4666f580f4cac0a968bc97c36076bb536a129f28210dac54ee127ed" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "serde", + "thiserror 1.0.69", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a12dc159d52233c43d9fe5415969433cbdd52c3d6e0df51bda7d447427b9986" +dependencies = [ + "accesskit", + "immutable-chunkmap", +] + +[[package]] +name = "accesskit_macos" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc6c1ecd82053d127961ad80a8beaa6004fb851a3a5b96506d7a6bd462403f6" +dependencies = [ + "accesskit", + "accesskit_consumer", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "once_cell", +] + +[[package]] +name = "accesskit_unix" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be7f5cf6165be10a54b2655fa2e0e12b2509f38ed6fc43e11c31fdb7ee6230bb" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974e96c347384d9133427167fb8a58c340cb0496988dacceebdc1ed27071023b" +dependencies = [ + "accesskit", + "accesskit_consumer", + "paste", + "static_assertions", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "accesskit_winit" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aea3522719f1c44564d03e9469a8e2f3a98b3a8a880bd66d0789c6b9c4a669dd" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "actix-codec" version = "0.5.2" @@ -65,7 +171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -181,7 +287,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -199,6 +305,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.8.11" @@ -236,6 +348,33 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.6.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -300,6 +439,180 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "arboard" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" +dependencies = [ + "clipboard-win", + "log", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-foundation 0.3.1", + "parking_lot", + "percent-encoding", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading 0.8.8", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.8", + "slab", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.0.8", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "async-signal" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.8", + "signal-hook-registry", + "slab", + "windows-sys 0.60.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.80" @@ -308,7 +621,30 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", ] [[package]] @@ -317,6 +653,57 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atspi" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" +dependencies = [ + "atspi-common", + "atspi-connection", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-connection" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" +dependencies = [ + "atspi-common", + "atspi-proxies", + "futures-lite", + "zbus", +] + +[[package]] +name = "atspi-proxies" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" +dependencies = [ + "atspi-common", + "serde", + "zbus", + "zvariant", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -333,7 +720,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -350,6 +737,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" + [[package]] name = "bitflags" version = "1.3.2" @@ -361,6 +763,15 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "block-buffer" @@ -371,6 +782,37 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2 0.6.1", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brotli" version = "6.0.0" @@ -398,12 +840,38 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.6.0" @@ -419,6 +887,57 @@ dependencies = [ "bytes", ] +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.6.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cc" version = "1.0.104" @@ -430,12 +949,49 @@ dependencies = [ "once_cell", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + [[package]] name = "chrono" version = "0.4.38" @@ -478,10 +1034,10 @@ version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -490,12 +1046,81 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "colorchoice" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -523,12 +1148,46 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -547,6 +1206,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.6" @@ -557,6 +1231,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "data-encoding" version = "2.9.0" @@ -582,7 +1262,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.87", ] [[package]] @@ -596,6 +1276,216 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.0", + "windows-sys 0.60.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.6.0", + "objc2 0.6.1", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.8", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "ecolor" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac2645a9bf4826eb4e91488b1f17b8eaddeef09396706b2f14066461338e24f" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glow 0.14.2", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winapi", + "windows-sys 0.52.0", + "winit", +] + +[[package]] +name = "egui" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974" +dependencies = [ + "accesskit", + "ahash", + "emath", + "epaint", + "log", + "nohash-hasher", +] + +[[package]] +name = "egui-wgpu" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00fd5d06d8405397e64a928fa0ef3934b3c30273ea7603e3dc4627b1f7a1a82" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "thiserror 1.0.69", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9c430f4f816340e8e8c1b20eec274186b1be6bc4c7dfc467ed50d57abc36c6" +dependencies = [ + "accesskit_winit", + "ahash", + "arboard", + "egui", + "log", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e39bccc683cd43adab530d8f21a13eb91e80de10bcc38c3f1c16601b6f62b26" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow 0.14.2", + "log", + "memoffset", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[package]] +name = "emath" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3" +dependencies = [ + "bytemuck", +] + [[package]] name = "encoding_rs" version = "0.8.34" @@ -605,16 +1495,43 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-as-inner" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.87", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -640,6 +1557,29 @@ dependencies = [ "log", ] +[[package]] +name = "epaint" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea" + [[package]] name = "equivalent" version = "1.0.1" @@ -648,12 +1588,39 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", ] [[package]] @@ -668,6 +1635,37 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "flate2" version = "1.0.30" @@ -675,7 +1673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.7.4", ] [[package]] @@ -684,13 +1682,40 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -699,6 +1724,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -708,6 +1739,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.30" @@ -756,6 +1796,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.30" @@ -764,7 +1817,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -797,6 +1850,64 @@ dependencies = [ "slab", ] +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -807,6 +1918,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -824,6 +1945,301 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.6.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glow" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.6.0", + "cfg_aliases 0.2.1", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading 0.8.8", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases 0.2.1", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.6.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd4240fc91d3433d5e5b0fc5b67672d771850dc19bbee03c1381e19322803d7" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "winapi", + "windows 0.52.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.6.0", + "gpu-descriptor-types", + "hashbrown", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "h2" version = "0.3.26" @@ -864,9 +2280,33 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.6.0", + "com", + "libc", + "libloading 0.8.8", + "thiserror 1.0.69", + "widestring", + "winapi", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" @@ -880,6 +2320,24 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + [[package]] name = "hmac" version = "0.12.1" @@ -1047,7 +2505,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -1080,15 +2538,56 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "2.2.6" +name = "image" +version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "num-traits", + "png", +] + +[[package]] +name = "immutable-chunkmap" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f97096f508d54f8f8ab8957862eee2ccd628847b6217af1a335e1c44dee578" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -1119,6 +2618,28 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.31" @@ -1130,13 +2651,25 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.6.0", + "serde", + "unicode-segmentation", +] + [[package]] name = "khm" version = "0.6.3" @@ -1145,10 +2678,15 @@ dependencies = [ "base64 0.21.7", "chrono", "clap", + "dirs 5.0.1", + "eframe", + "egui", "env_logger", "futures", "hostname", "log", + "notify", + "notify-debouncer-mini", "regex", "reqwest", "rust-embed", @@ -1157,7 +2695,47 @@ dependencies = [ "tokio", "tokio-postgres", "tokio-util", + "tray-icon", "trust-dns-resolver", + "urlencoding", + "winit", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading 0.8.8", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", ] [[package]] @@ -1167,10 +2745,84 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] -name = "libc" -version = "0.2.155" +name = "libappindicator" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.2", +] + +[[package]] +name = "libredox" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall 0.5.2", +] + +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] [[package]] name = "linked-hash-map" @@ -1184,6 +2836,18 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "local-channel" version = "0.1.5" @@ -1226,6 +2890,15 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -1248,6 +2921,39 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.6.0", + "block", + "core-graphics-types", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + [[package]] name = "mime" version = "0.3.17" @@ -1263,6 +2969,16 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "0.8.11" @@ -1275,6 +2991,47 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "muda" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "once_cell", + "png", + "thiserror 1.0.69", + "windows-sys 0.59.0", +] + +[[package]] +name = "naga" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.6.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -1292,6 +3049,94 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.6.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-debouncer-mini" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43" +dependencies = [ + "crossbeam-channel", + "log", + "notify", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1313,10 +3158,313 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] +[[package]] +name = "num_enum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "bitflags 2.6.0", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.6.0", + "dispatch2", + "objc2 0.6.1", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +dependencies = [ + "bitflags 2.6.0", + "dispatch2", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +dependencies = [ + "bitflags 2.6.0", + "block2 0.6.1", + "objc2 0.6.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +dependencies = [ + "bitflags 2.6.0", + "objc2 0.6.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + [[package]] name = "object" version = "0.36.1" @@ -1340,7 +3488,7 @@ checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.6.0", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -1355,7 +3503,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -1376,6 +3524,71 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libredox", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1446,7 +3659,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -1461,12 +3674,50 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.0.8", + "windows-sys 0.60.2", +] + [[package]] name = "postgres-protocol" version = "0.6.6" @@ -1509,6 +3760,64 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit 0.22.27", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -1518,6 +3827,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.36" @@ -1557,6 +3891,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1575,6 +3915,28 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror 2.0.12", +] + [[package]] name = "regex" version = "1.10.5" @@ -1610,6 +3972,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "reqwest" version = "0.12.5" @@ -1694,7 +4062,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn", + "syn 2.0.87", "walkdir", ] @@ -1714,6 +4082,18 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1725,15 +4105,28 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.14", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", ] [[package]] @@ -1776,6 +4169,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" version = "1.0.18" @@ -1800,12 +4199,31 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "security-framework" version = "2.11.0" @@ -1813,7 +4231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "libc", "security-framework-sys", @@ -1852,7 +4270,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -1866,6 +4284,26 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1909,6 +4347,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "siphasher" version = "0.3.11" @@ -1924,12 +4368,66 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.5.7" @@ -1946,6 +4444,27 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "stringprep" version = "0.1.5" @@ -1969,6 +4488,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.87" @@ -1993,7 +4523,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2007,6 +4537,25 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.10.1" @@ -2015,17 +4564,35 @@ checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "rustix", + "rustix 0.38.44", "windows-sys 0.52.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -2036,7 +4603,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -2070,6 +4648,31 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinyvec" version = "1.7.0" @@ -2112,7 +4715,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -2175,6 +4778,62 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.7.12", +] + [[package]] name = "tower" version = "0.4.13" @@ -2222,7 +4881,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -2234,6 +4893,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tray-icon" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd75f5002e2513eaa19b2365f533090cc3e93abd38788452d9ea85cff7b48a" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2 0.6.1", + "objc2-app-kit 0.3.1", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.1", + "once_cell", + "png", + "thiserror 2.0.12", + "windows-sys 0.59.0", +] + [[package]] name = "trust-dns-proto" version = "0.23.2" @@ -2252,7 +4932,7 @@ dependencies = [ "once_cell", "rand", "smallvec", - "thiserror", + "thiserror 1.0.69", "tinyvec", "tokio", "tracing", @@ -2274,7 +4954,7 @@ dependencies = [ "rand", "resolv-conf", "smallvec", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", "trust-dns-proto", @@ -2286,12 +4966,38 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2319,6 +5025,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -2336,6 +5060,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2348,6 +5078,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.4" @@ -2387,46 +5123,48 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.87", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2434,33 +5172,272 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.44", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" +dependencies = [ + "bitflags 2.6.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.6.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182" +dependencies = [ + "rustix 0.38.44", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2 0.6.1", + "objc2-foundation 0.3.1", + "url", + "web-sys", +] + +[[package]] +name = "wgpu" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d1c4ba43f80542cf63a0a6ed3134629ae73e8ab51e4b765a67f3aa062eb433" +dependencies = [ + "arrayvec", + "cfg_aliases 0.1.1", + "document-features", + "js-sys", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348c840d1051b8e86c3bcd31206080c5e71e5933dabd79be1ce732b0b2f089a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.6.0", + "cfg_aliases 0.1.1", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6bbf4b4de8b2a83c0401d9e5ae0080a2792055f25859a02bf9be97952bbed4f" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bitflags 2.6.0", + "cfg_aliases 0.1.1", + "core-graphics-types", + "glow 0.13.1", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.8", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9d91f0e2c4b51434dfa6db77846f2793149d8e73f800fa2e41f52b8eac3c5d" +dependencies = [ + "bitflags 2.6.0", + "js-sys", + "web-sys", +] + [[package]] name = "whoami" version = "1.5.1" @@ -2500,7 +5477,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2509,6 +5486,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -2518,6 +5515,69 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2536,6 +5596,39 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2560,13 +5653,35 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2579,6 +5694,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2591,6 +5718,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2603,12 +5742,30 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2621,6 +5778,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2633,6 +5802,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2645,6 +5826,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2657,6 +5850,82 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winit" +version = "0.30.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.6.0", + "block2 0.5.1", + "bytemuck", + "calloop", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -2677,6 +5946,188 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.8", + "once_cell", + "rustix 0.38.44", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.6.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.87", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f374552b954f6abb4bd6ce979e6c9b38fb9d0cd7cc68a7d796e70c9f3a233" +dependencies = [ + "quick-xml 0.30.0", + "serde", + "static_assertions", + "zbus_names", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -2694,7 +6145,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.87", ] [[package]] @@ -2730,3 +6181,40 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.87", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] diff --git a/Cargo.toml b/Cargo.toml index 503ba3f..f89286d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,12 +3,17 @@ name = "khm" version = "0.6.3" edition = "2021" authors = ["AB "] +description = "KHM - Known Hosts Manager for SSH key management and synchronization" +homepage = "https://github.com/house-of-vanity/khm" +repository = "https://github.com/house-of-vanity/khm" +license = "WTFPL" +keywords = ["ssh", "known-hosts", "security", "system-admin", "automation"] +categories = ["command-line-utilities", "network-programming"] [dependencies] actix-web = "4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -env_logger = "0.11.3" log = "0.4" regex = "1.10.5" base64 = "0.21" @@ -22,3 +27,18 @@ trust-dns-resolver = "0.23" futures = "0.3" hostname = "0.3" rust-embed = "8.0" +tray-icon = { version = "0.19", optional = true } +notify = { version = "6.1", optional = true } +notify-debouncer-mini = { version = "0.4", optional = true } +dirs = "5.0" +eframe = { version = "0.29", optional = true } +egui = { version = "0.29", optional = true } +winit = { version = "0.30", optional = true } +env_logger = "0.11" +urlencoding = "2.1" + +[features] +default = ["gui"] +gui = ["tray-icon", "eframe", "egui", "winit", "notify", "notify-debouncer-mini"] +server = [] + diff --git a/src/client.rs b/src/client.rs index 78d3006..a6a26dd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,14 +8,14 @@ use std::io::{self, BufRead, Write}; use std::path::Path; #[derive(Serialize, Deserialize, Clone, Debug)] -struct SshKey { +pub struct SshKey { server: String, public_key: String, #[serde(default)] deprecated: bool, } -fn read_known_hosts(file_path: &str) -> io::Result> { +pub fn read_known_hosts(file_path: &str) -> io::Result> { let path = Path::new(file_path); let file = File::open(&path)?; let reader = io::BufReader::new(file); diff --git a/src/gui/admin/mod.rs b/src/gui/admin/mod.rs new file mode 100644 index 0000000..7f50756 --- /dev/null +++ b/src/gui/admin/mod.rs @@ -0,0 +1,5 @@ +mod state; +mod ui; + +pub use state::*; +pub use ui::*; diff --git a/src/gui/admin/state.rs b/src/gui/admin/state.rs new file mode 100644 index 0000000..357d019 --- /dev/null +++ b/src/gui/admin/state.rs @@ -0,0 +1,178 @@ +use eframe::egui; +use log::{error, info}; +use std::collections::HashMap; +use std::sync::mpsc; +use crate::gui::api::{SshKey, fetch_keys}; +use crate::gui::common::KhmSettings; + +#[derive(Debug, Clone)] +pub enum AdminOperation { + LoadingKeys, + DeprecatingKey, + RestoringKey, + DeletingKey, + BulkDeprecating, + BulkRestoring, + None, +} + +#[derive(Debug, Clone)] +pub struct AdminState { + pub keys: Vec, + pub filtered_keys: Vec, + pub search_term: String, + pub show_deprecated_only: bool, + pub selected_servers: HashMap, + pub expanded_servers: HashMap, + pub current_operation: AdminOperation, + pub last_load_time: Option, +} + +impl Default for AdminState { + fn default() -> Self { + Self { + keys: Vec::new(), + filtered_keys: Vec::new(), + search_term: String::new(), + show_deprecated_only: false, + selected_servers: HashMap::new(), + expanded_servers: HashMap::new(), + current_operation: AdminOperation::None, + last_load_time: None, + } + } +} + +impl AdminState { + /// Filter keys based on current search term and deprecated filter + pub fn filter_keys(&mut self) { + let mut filtered = self.keys.clone(); + + // Apply deprecated filter + if self.show_deprecated_only { + filtered.retain(|key| key.deprecated); + } + + // Apply search filter + if !self.search_term.is_empty() { + let search_term = self.search_term.to_lowercase(); + filtered.retain(|key| { + key.server.to_lowercase().contains(&search_term) || + key.public_key.to_lowercase().contains(&search_term) + }); + } + + self.filtered_keys = filtered; + } + + /// Load keys from server + pub fn load_keys(&mut self, settings: &KhmSettings, ctx: &egui::Context) -> Option, String>>> { + if settings.host.is_empty() || settings.flow.is_empty() { + return None; + } + + self.current_operation = AdminOperation::LoadingKeys; + + let (tx, rx) = mpsc::channel(); + + let host = settings.host.clone(); + let flow = settings.flow.clone(); + let basic_auth = settings.basic_auth.clone(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + fetch_keys(host, flow, basic_auth).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); + + Some(rx) + } + + /// Handle keys load result + pub fn handle_keys_loaded(&mut self, result: Result, String>) { + match result { + Ok(keys) => { + self.keys = keys; + self.last_load_time = Some(std::time::Instant::now()); + self.filter_keys(); + self.current_operation = AdminOperation::None; + info!("Keys loaded successfully: {} keys", self.keys.len()); + } + Err(error) => { + self.current_operation = AdminOperation::None; + error!("Failed to load keys: {}", error); + } + } + } + + /// Get selected servers list + pub fn get_selected_servers(&self) -> Vec { + self.selected_servers + .iter() + .filter_map(|(server, &selected)| if selected { Some(server.clone()) } else { None }) + .collect() + } + + /// Clear selected servers + pub fn clear_selection(&mut self) { + self.selected_servers.clear(); + } + + /// Get statistics + pub fn get_statistics(&self) -> AdminStatistics { + let total_keys = self.keys.len(); + let active_keys = self.keys.iter().filter(|k| !k.deprecated).count(); + let deprecated_keys = total_keys - active_keys; + let unique_servers = self.keys.iter().map(|k| &k.server).collect::>().len(); + + AdminStatistics { + total_keys, + active_keys, + deprecated_keys, + unique_servers, + } + } +} + +#[derive(Debug, Clone)] +pub struct AdminStatistics { + pub total_keys: usize, + pub active_keys: usize, + pub deprecated_keys: usize, + pub unique_servers: usize, +} + +/// Get SSH key type from public key string +pub fn get_key_type(public_key: &str) -> String { + if public_key.starts_with("ssh-rsa") { + "RSA".to_string() + } else if public_key.starts_with("ssh-ed25519") { + "ED25519".to_string() + } else if public_key.starts_with("ecdsa-sha2-nistp") { + "ECDSA".to_string() + } else if public_key.starts_with("ssh-dss") { + "DSA".to_string() + } else { + "Unknown".to_string() + } +} + +/// Get preview of SSH key (first 12 characters of key part) +pub fn get_key_preview(public_key: &str) -> String { + let parts: Vec<&str> = public_key.split_whitespace().collect(); + if parts.len() >= 2 { + let key_part = parts[1]; + if key_part.len() > 12 { + format!("{}...", &key_part[..12]) + } else { + key_part.to_string() + } + } else { + format!("{}...", &public_key[..std::cmp::min(12, public_key.len())]) + } +} diff --git a/src/gui/admin/ui.rs b/src/gui/admin/ui.rs new file mode 100644 index 0000000..bed66fe --- /dev/null +++ b/src/gui/admin/ui.rs @@ -0,0 +1,451 @@ +use eframe::egui; +use std::collections::BTreeMap; +use super::state::{AdminState, get_key_type, get_key_preview}; +use crate::gui::api::SshKey; + +/// Render statistics cards +pub fn render_statistics(ui: &mut egui::Ui, admin_state: &AdminState) { + let stats = admin_state.get_statistics(); + + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.label(egui::RichText::new("📊 Statistics").size(16.0).strong()); + ui.add_space(8.0); + + ui.horizontal(|ui| { + ui.columns(4, |cols| { + // Total keys + cols[0].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("📊").size(20.0)); + ui.label(egui::RichText::new(stats.total_keys.to_string()).size(24.0).strong()); + ui.label(egui::RichText::new("Total Keys").size(11.0).color(egui::Color32::GRAY)); + }); + + // Active keys + cols[1].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("✅").size(20.0)); + ui.label(egui::RichText::new(stats.active_keys.to_string()).size(24.0).strong().color(egui::Color32::LIGHT_GREEN)); + ui.label(egui::RichText::new("Active").size(11.0).color(egui::Color32::GRAY)); + }); + + // Deprecated keys + cols[2].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("❌").size(20.0)); + ui.label(egui::RichText::new(stats.deprecated_keys.to_string()).size(24.0).strong().color(egui::Color32::LIGHT_RED)); + ui.label(egui::RichText::new("Deprecated").size(11.0).color(egui::Color32::GRAY)); + }); + + // Servers + cols[3].vertical_centered_justified(|ui| { + ui.label(egui::RichText::new("💻").size(20.0)); + ui.label(egui::RichText::new(stats.unique_servers.to_string()).size(24.0).strong().color(egui::Color32::LIGHT_BLUE)); + ui.label(egui::RichText::new("Servers").size(11.0).color(egui::Color32::GRAY)); + }); + }); + }); + }); + }); +} + +/// Render search and filter controls +pub fn render_search_controls(ui: &mut egui::Ui, admin_state: &mut AdminState) -> bool { + let mut changed = false; + + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.label(egui::RichText::new("🔍 Search").size(16.0).strong()); + ui.add_space(8.0); + + // Search field with full width + ui.horizontal(|ui| { + ui.label(egui::RichText::new("🔍").size(14.0)); + let search_response = ui.add_sized( + [ui.available_width() * 0.6, 20.0], + egui::TextEdit::singleline(&mut admin_state.search_term) + .hint_text("Search servers or keys...") + ); + + if admin_state.search_term.is_empty() { + ui.label(egui::RichText::new("Type to search").size(11.0).color(egui::Color32::GRAY)); + } else { + ui.label(egui::RichText::new(format!("{} results", admin_state.filtered_keys.len())).size(11.0)); + if ui.add(egui::Button::new(egui::RichText::new("❌").color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(170, 170, 170)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(89, 89, 89))) + .rounding(egui::Rounding::same(3.0)) + .min_size(egui::vec2(18.0, 18.0)) + ).on_hover_text("Clear search").clicked() { + admin_state.search_term.clear(); + changed = true; + } + } + + // Handle search text changes + if search_response.changed() { + changed = true; + } + }); + + ui.add_space(5.0); + + // Filter controls + ui.horizontal(|ui| { + ui.label("Filter:"); + let show_deprecated = admin_state.show_deprecated_only; + if ui.selectable_label(!show_deprecated, "✅ Active").clicked() { + admin_state.show_deprecated_only = false; + changed = true; + } + if ui.selectable_label(show_deprecated, "❗ Deprecated").clicked() { + admin_state.show_deprecated_only = true; + changed = true; + } + }); + }); + }); + + if changed { + admin_state.filter_keys(); + } + + changed +} + +/// Render bulk actions controls +pub fn render_bulk_actions(ui: &mut egui::Ui, admin_state: &mut AdminState) -> BulkAction { + let selected_count = admin_state.selected_servers.values().filter(|&&v| v).count(); + + if selected_count == 0 { + return BulkAction::None; + } + + let mut action = BulkAction::None; + + ui.group(|ui| { + ui.set_min_width(ui.available_width()); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("📋").size(14.0)); + ui.label(egui::RichText::new(format!("Selected {} servers", selected_count)) + .size(14.0) + .strong() + .color(egui::Color32::LIGHT_BLUE)); + }); + + ui.add_space(5.0); + + ui.horizontal(|ui| { + if ui.add(egui::Button::new(egui::RichText::new("❗ Deprecate Selected").color(egui::Color32::BLACK)) + .fill(egui::Color32::from_rgb(255, 200, 0)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(102, 94, 72))) + .rounding(egui::Rounding::same(6.0)) + .min_size(egui::vec2(130.0, 28.0)) + ).clicked() { + action = BulkAction::DeprecateSelected; + } + + if ui.add(egui::Button::new(egui::RichText::new("✅ Restore Selected").color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(101, 199, 40)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(94, 105, 25))) + .rounding(egui::Rounding::same(6.0)) + .min_size(egui::vec2(120.0, 28.0)) + ).clicked() { + action = BulkAction::RestoreSelected; + } + + if ui.add(egui::Button::new(egui::RichText::new("X Clear Selection").color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(170, 170, 170)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(89, 89, 89))) + .rounding(egui::Rounding::same(6.0)) + .min_size(egui::vec2(110.0, 28.0)) + ).clicked() { + admin_state.clear_selection(); + action = BulkAction::ClearSelection; + } + }); + }); + }); + + action +} + +/// Render keys table grouped by servers +pub fn render_keys_table(ui: &mut egui::Ui, admin_state: &mut AdminState) -> KeyAction { + if admin_state.filtered_keys.is_empty() { + render_empty_state(ui, admin_state); + return KeyAction::None; + } + + let mut action = KeyAction::None; + + // Group keys by server + let mut servers: BTreeMap> = BTreeMap::new(); + for key in &admin_state.filtered_keys { + servers.entry(key.server.clone()).or_insert_with(Vec::new).push(key.clone()); + } + + // Render each server group + for (server_name, server_keys) in servers { + let is_expanded = admin_state.expanded_servers.get(&server_name).copied().unwrap_or(false); + let active_count = server_keys.iter().filter(|k| !k.deprecated).count(); + let deprecated_count = server_keys.len() - active_count; + + // Server header + ui.group(|ui| { + ui.horizontal(|ui| { + // Server selection checkbox + let mut selected = admin_state.selected_servers.get(&server_name).copied().unwrap_or(false); + if ui.add(egui::Checkbox::new(&mut selected, "") + .indeterminate(false) + ).changed() { + admin_state.selected_servers.insert(server_name.clone(), selected); + } + + // Expand/collapse button + let expand_icon = if is_expanded { "▼" } else { "▶" }; + if ui.add(egui::Button::new(expand_icon) + .fill(egui::Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE) + .min_size(egui::vec2(20.0, 20.0)) + ).clicked() { + admin_state.expanded_servers.insert(server_name.clone(), !is_expanded); + } + + // Server icon and name + ui.label(egui::RichText::new("💻").size(16.0)); + ui.label(egui::RichText::new(&server_name) + .size(15.0) + .strong() + .color(egui::Color32::WHITE)); + + // Keys count badge + render_badge(ui, &format!("{} keys", server_keys.len()), egui::Color32::from_rgb(52, 152, 219), egui::Color32::WHITE); + + ui.add_space(5.0); + + // Deprecated count badge + if deprecated_count > 0 { + render_badge(ui, &format!("{} depr", deprecated_count), egui::Color32::from_rgb(231, 76, 60), egui::Color32::WHITE); + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Server action buttons + if deprecated_count > 0 { + if ui.add(egui::Button::new(egui::RichText::new("✅ Restore").color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(101, 199, 40)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(94, 105, 25))) + .rounding(egui::Rounding::same(4.0)) + .min_size(egui::vec2(70.0, 24.0)) + ).clicked() { + action = KeyAction::RestoreServer(server_name.clone()); + } + } + + if active_count > 0 { + if ui.add(egui::Button::new(egui::RichText::new("❗ Deprecate").color(egui::Color32::BLACK)) + .fill(egui::Color32::from_rgb(255, 200, 0)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(102, 94, 72))) + .rounding(egui::Rounding::same(4.0)) + .min_size(egui::vec2(85.0, 24.0)) + ).clicked() { + action = KeyAction::DeprecateServer(server_name.clone()); + } + } + }); + }); + }); + + // Expanded key details + if is_expanded { + ui.indent("server_keys", |ui| { + for key in &server_keys { + if let Some(key_action) = render_key_item(ui, key, &server_name) { + action = key_action; + } + } + }); + } + + ui.add_space(5.0); + } + + action +} + +/// Render empty state when no keys are available +fn render_empty_state(ui: &mut egui::Ui, admin_state: &AdminState) { + ui.vertical_centered(|ui| { + ui.add_space(60.0); + if admin_state.keys.is_empty() { + ui.label(egui::RichText::new("🔑").size(48.0).color(egui::Color32::GRAY)); + ui.label(egui::RichText::new("No SSH keys available") + .size(18.0) + .color(egui::Color32::GRAY)); + ui.label(egui::RichText::new("Keys will appear here once loaded from the server") + .size(14.0) + .color(egui::Color32::DARK_GRAY)); + } else if !admin_state.search_term.is_empty() { + ui.label(egui::RichText::new("🔍").size(48.0).color(egui::Color32::GRAY)); + ui.label(egui::RichText::new("No results found") + .size(18.0) + .color(egui::Color32::GRAY)); + ui.label(egui::RichText::new(format!("Try adjusting your search: '{}'", admin_state.search_term)) + .size(14.0) + .color(egui::Color32::DARK_GRAY)); + } else { + ui.label(egui::RichText::new("❌").size(48.0).color(egui::Color32::GRAY)); + ui.label(egui::RichText::new("No keys match current filters") + .size(18.0) + .color(egui::Color32::GRAY)); + ui.label(egui::RichText::new("Try adjusting your search or filter settings") + .size(14.0) + .color(egui::Color32::DARK_GRAY)); + } + }); +} + +/// Render individual key item +fn render_key_item(ui: &mut egui::Ui, key: &SshKey, server_name: &str) -> Option { + let mut action = None; + + ui.group(|ui| { + ui.horizontal(|ui| { + // Key type badge + let key_type = get_key_type(&key.public_key); + let (badge_color, text_color) = match key_type.as_str() { + "RSA" => (egui::Color32::from_rgb(52, 144, 220), egui::Color32::WHITE), + "ED25519" => (egui::Color32::from_rgb(46, 204, 113), egui::Color32::WHITE), + "ECDSA" => (egui::Color32::from_rgb(241, 196, 15), egui::Color32::BLACK), + "DSA" => (egui::Color32::from_rgb(230, 126, 34), egui::Color32::WHITE), + _ => (egui::Color32::GRAY, egui::Color32::WHITE), + }; + + render_small_badge(ui, &key_type, badge_color, text_color); + ui.add_space(5.0); + + // Status badge + if key.deprecated { + ui.label(egui::RichText::new("❗ DEPR") + .size(10.0) + .color(egui::Color32::from_rgb(231, 76, 60)) + .strong()); + } else { + ui.label(egui::RichText::new("[OK] ACTIVE") + .size(10.0) + .color(egui::Color32::from_rgb(46, 204, 113)) + .strong()); + } + + ui.add_space(5.0); + + // Key preview + ui.label(egui::RichText::new(get_key_preview(&key.public_key)) + .font(egui::FontId::monospace(10.0)) + .color(egui::Color32::LIGHT_GRAY)); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Key action buttons + if key.deprecated { + if ui.add(egui::Button::new(egui::RichText::new("[R]").color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(101, 199, 40)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(94, 105, 25))) + .rounding(egui::Rounding::same(3.0)) + .min_size(egui::vec2(22.0, 18.0)) + ).on_hover_text("Restore key").clicked() { + action = Some(KeyAction::RestoreKey(server_name.to_string())); + } + if ui.add(egui::Button::new(egui::RichText::new("Del").color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(246, 36, 71)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(129, 18, 17))) + .rounding(egui::Rounding::same(3.0)) + .min_size(egui::vec2(26.0, 18.0)) + ).on_hover_text("Delete key").clicked() { + action = Some(KeyAction::DeleteKey(server_name.to_string())); + } + } else { + if ui.add(egui::Button::new(egui::RichText::new("❗").color(egui::Color32::BLACK)) + .fill(egui::Color32::from_rgb(255, 200, 0)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(102, 94, 72))) + .rounding(egui::Rounding::same(3.0)) + .min_size(egui::vec2(22.0, 18.0)) + ).on_hover_text("Deprecate key").clicked() { + action = Some(KeyAction::DeprecateKey(server_name.to_string())); + } + } + + if ui.add(egui::Button::new(egui::RichText::new("Copy").color(egui::Color32::WHITE)) + .fill(egui::Color32::from_rgb(0, 111, 230)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(35, 84, 97))) + .rounding(egui::Rounding::same(3.0)) + .min_size(egui::vec2(30.0, 18.0)) + ).on_hover_text("Copy to clipboard").clicked() { + ui.output_mut(|o| o.copied_text = key.public_key.clone()); + } + }); + }); + }); + + action +} + +/// Render a badge with text +fn render_badge(ui: &mut egui::Ui, text: &str, bg_color: egui::Color32, text_color: egui::Color32) { + let (rect, _) = ui.allocate_exact_size( + egui::vec2(50.0, 18.0), + egui::Sense::hover() + ); + ui.painter().rect_filled( + rect, + egui::Rounding::same(8.0), + bg_color + ); + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + text, + egui::FontId::proportional(10.0), + text_color, + ); +} + +/// Render a small badge with text +fn render_small_badge(ui: &mut egui::Ui, text: &str, bg_color: egui::Color32, text_color: egui::Color32) { + let (rect, _) = ui.allocate_exact_size( + egui::vec2(40.0, 16.0), + egui::Sense::hover() + ); + ui.painter().rect_filled( + rect, + egui::Rounding::same(3.0), + bg_color + ); + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + text, + egui::FontId::proportional(9.0), + text_color, + ); +} + +/// Actions that can be performed on keys +#[derive(Debug, Clone)] +pub enum KeyAction { + None, + DeprecateKey(String), + RestoreKey(String), + DeleteKey(String), + DeprecateServer(String), + RestoreServer(String), +} + +/// Bulk actions that can be performed +#[derive(Debug, Clone)] +pub enum BulkAction { + None, + DeprecateSelected, + RestoreSelected, + ClearSelection, +} diff --git a/src/gui/api/client.rs b/src/gui/api/client.rs new file mode 100644 index 0000000..ea0ed50 --- /dev/null +++ b/src/gui/api/client.rs @@ -0,0 +1,254 @@ +use reqwest::Client; +use log::info; +use serde::{Deserialize, Serialize}; +use crate::gui::common::{KhmSettings, perform_sync}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SshKey { + pub server: String, + pub public_key: String, + #[serde(default)] + pub deprecated: bool, +} + +/// Test connection to KHM server +pub async fn test_connection(host: String, flow: String, basic_auth: String) -> Result { + if host.is_empty() || flow.is_empty() { + return Err("Host and flow must be specified".to_string()); + } + + let url = format!("{}/{}/keys", host.trim_end_matches('/'), flow); + info!("Testing connection to: {}", url); + + let client = create_http_client()?; + let mut request = client.get(&url); + + request = add_auth_if_needed(request, &basic_auth)?; + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + check_html_response(&body)?; + + let keys: Vec = serde_json::from_str(&body) + .map_err(|e| format!("Failed to parse response: {}", e))?; + + let message = format!("Found {} SSH keys from flow '{}'", keys.len(), flow); + info!("Connection test successful: {}", message); + Ok(message) +} + +/// Fetch all SSH keys including deprecated ones +pub async fn fetch_keys(host: String, flow: String, basic_auth: String) -> Result, String> { + if host.is_empty() || flow.is_empty() { + return Err("Host and flow must be specified".to_string()); + } + + let url = format!("{}/{}/keys?include_deprecated=true", host.trim_end_matches('/'), flow); + info!("Fetching keys from: {}", url); + + let client = create_http_client()?; + let mut request = client.get(&url); + + request = add_auth_if_needed(request, &basic_auth)?; + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + check_html_response(&body)?; + + let keys: Vec = serde_json::from_str(&body) + .map_err(|e| format!("Failed to parse response: {}", e))?; + + info!("Fetched {} SSH keys", keys.len()); + Ok(keys) +} + +/// Deprecate a key for a specific server +pub async fn deprecate_key(host: String, flow: String, basic_auth: String, server: String) -> Result { + let url = format!("{}/{}/keys/{}", host.trim_end_matches('/'), flow, urlencoding::encode(&server)); + info!("Deprecating key for server '{}' at: {}", server, url); + + let client = create_http_client()?; + let mut request = client.delete(&url); + + request = add_auth_if_needed(request, &basic_auth)?; + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + parse_api_response(&body, &format!("Successfully deprecated key for server '{}'", server)) +} + +/// Restore a key for a specific server +pub async fn restore_key(host: String, flow: String, basic_auth: String, server: String) -> Result { + let url = format!("{}/{}/keys/{}/restore", host.trim_end_matches('/'), flow, urlencoding::encode(&server)); + info!("Restoring key for server '{}' at: {}", server, url); + + let client = create_http_client()?; + let mut request = client.post(&url); + + request = add_auth_if_needed(request, &basic_auth)?; + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + parse_api_response(&body, &format!("Successfully restored key for server '{}'", server)) +} + +/// Delete a key permanently for a specific server +pub async fn delete_key(host: String, flow: String, basic_auth: String, server: String) -> Result { + let url = format!("{}/{}/keys/{}/delete", host.trim_end_matches('/'), flow, urlencoding::encode(&server)); + info!("Permanently deleting key for server '{}' at: {}", server, url); + + let client = create_http_client()?; + let mut request = client.delete(&url); + + request = add_auth_if_needed(request, &basic_auth)?; + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + parse_api_response(&body, &format!("Successfully deleted key for server '{}'", server)) +} + +/// Bulk deprecate multiple servers +pub async fn bulk_deprecate_servers(host: String, flow: String, basic_auth: String, servers: Vec) -> Result { + let url = format!("{}/{}/bulk-deprecate", host.trim_end_matches('/'), flow); + info!("Bulk deprecating {} servers at: {}", servers.len(), url); + + let client = create_http_client()?; + let mut request = client.post(&url) + .json(&serde_json::json!({ + "servers": servers + })); + + request = add_auth_if_needed(request, &basic_auth)?; + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + parse_api_response(&body, "Successfully deprecated servers") +} + +/// Bulk restore multiple servers +pub async fn bulk_restore_servers(host: String, flow: String, basic_auth: String, servers: Vec) -> Result { + let url = format!("{}/{}/bulk-restore", host.trim_end_matches('/'), flow); + info!("Bulk restoring {} servers at: {}", servers.len(), url); + + let client = create_http_client()?; + let mut request = client.post(&url) + .json(&serde_json::json!({ + "servers": servers + })); + + request = add_auth_if_needed(request, &basic_auth)?; + + let response = request.send().await + .map_err(|e| format!("Request failed: {}", e))?; + + check_response_status(&response)?; + + let body = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + parse_api_response(&body, "Successfully restored servers") +} + +/// Perform manual sync operation +pub async fn perform_manual_sync(settings: KhmSettings) -> Result { + match perform_sync(&settings).await { + Ok(keys_count) => Ok(format!("Sync completed successfully with {} keys", keys_count)), + Err(e) => Err(e.to_string()), + } +} + +// Helper functions + +fn create_http_client() -> Result { + Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|e| format!("Failed to create HTTP client: {}", e)) +} + +fn add_auth_if_needed(request: reqwest::RequestBuilder, basic_auth: &str) -> Result { + if basic_auth.is_empty() { + return Ok(request); + } + + let auth_parts: Vec<&str> = basic_auth.splitn(2, ':').collect(); + if auth_parts.len() == 2 { + Ok(request.basic_auth(auth_parts[0], Some(auth_parts[1]))) + } else { + Err("Basic auth format should be 'username:password'".to_string()) + } +} + +fn check_response_status(response: &reqwest::Response) -> Result<(), String> { + let status = response.status().as_u16(); + + if status == 401 { + return Err("Authentication required. Please provide valid basic auth credentials.".to_string()); + } + + if status >= 300 && status < 400 { + return Err("Server redirects to login page. Authentication may be required.".to_string()); + } + + if !response.status().is_success() { + return Err(format!("Server returned error: {} {}", status, response.status().canonical_reason().unwrap_or("Unknown"))); + } + + Ok(()) +} + +fn check_html_response(body: &str) -> Result<(), String> { + if body.trim_start().starts_with(" Result { + if let Ok(json_response) = serde_json::from_str::(body) { + if let Some(message) = json_response.get("message").and_then(|v| v.as_str()) { + Ok(message.to_string()) + } else { + Ok(default_message.to_string()) + } + } else { + Ok(default_message.to_string()) + } +} diff --git a/src/gui/api/mod.rs b/src/gui/api/mod.rs new file mode 100644 index 0000000..be50984 --- /dev/null +++ b/src/gui/api/mod.rs @@ -0,0 +1,3 @@ +mod client; + +pub use client::*; diff --git a/src/gui/common/mod.rs b/src/gui/common/mod.rs new file mode 100644 index 0000000..a10a586 --- /dev/null +++ b/src/gui/common/mod.rs @@ -0,0 +1,3 @@ +mod settings; + +pub use settings::*; diff --git a/src/gui/common/settings.rs b/src/gui/common/settings.rs new file mode 100644 index 0000000..c30f34e --- /dev/null +++ b/src/gui/common/settings.rs @@ -0,0 +1,143 @@ +use dirs::home_dir; +use log::{debug, error, info}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KhmSettings { + pub host: String, + pub flow: String, + pub known_hosts: String, + pub basic_auth: String, + pub in_place: bool, + pub auto_sync_interval_minutes: u32, +} + +impl Default for KhmSettings { + fn default() -> Self { + Self { + host: String::new(), + flow: String::new(), + known_hosts: get_default_known_hosts_path(), + basic_auth: String::new(), + in_place: true, + auto_sync_interval_minutes: 60, + } + } +} + +/// Get default known_hosts file path based on OS +fn get_default_known_hosts_path() -> String { + #[cfg(target_os = "windows")] + { + if let Ok(user_profile) = std::env::var("USERPROFILE") { + format!("{}/.ssh/known_hosts", user_profile) + } else { + "~/.ssh/known_hosts".to_string() + } + } + #[cfg(not(target_os = "windows"))] + { + "~/.ssh/known_hosts".to_string() + } +} + +/// Get configuration file path +pub fn get_config_path() -> PathBuf { + let mut path = home_dir().expect("Could not find home directory"); + path.push(".khm"); + fs::create_dir_all(&path).ok(); + path.push("khm_config.json"); + path +} + +/// Load settings from configuration file +pub fn load_settings() -> KhmSettings { + let path = get_config_path(); + match fs::read_to_string(&path) { + Ok(contents) => { + let mut settings: KhmSettings = serde_json::from_str(&contents).unwrap_or_else(|e| { + error!("Failed to parse KHM config: {}", e); + KhmSettings::default() + }); + + // Fill in default known_hosts path if empty + if settings.known_hosts.is_empty() { + settings.known_hosts = get_default_known_hosts_path(); + } + + settings + } + Err(_) => { + debug!("KHM config file not found, using defaults"); + KhmSettings::default() + } + } +} + +/// Save settings to configuration file +pub fn save_settings(settings: &KhmSettings) -> Result<(), std::io::Error> { + let path = get_config_path(); + let json = serde_json::to_string_pretty(settings)?; + fs::write(&path, json)?; + info!("KHM settings saved"); + Ok(()) +} + +/// Expand path with ~ substitution +pub fn expand_path(path: &str) -> String { + if path.starts_with("~/") { + if let Some(home) = home_dir() { + return home.join(&path[2..]).to_string_lossy().to_string(); + } + } + path.to_string() +} + +/// Perform sync operation using KHM client logic +pub async fn perform_sync(settings: &KhmSettings) -> Result { + use crate::Args; + + info!("Starting sync with settings: host={}, flow={}, known_hosts={}, in_place={}", + settings.host, settings.flow, settings.known_hosts, settings.in_place); + + // Convert KhmSettings to Args for client module + let args = Args { + server: false, + gui: false, + settings_ui: false, + in_place: settings.in_place, + flows: vec!["default".to_string()], // Not used in client mode + ip: "127.0.0.1".to_string(), // Not used in client mode + port: 8080, // Not used in client mode + db_host: "127.0.0.1".to_string(), // Not used in client mode + db_name: "khm".to_string(), // Not used in client mode + db_user: None, // Not used in client mode + db_password: None, // Not used in client mode + host: Some(settings.host.clone()), + flow: Some(settings.flow.clone()), + known_hosts: expand_path(&settings.known_hosts), + basic_auth: settings.basic_auth.clone(), + }; + + info!("Expanded known_hosts path: {}", args.known_hosts); + + // Get keys count before and after sync + let keys_before = crate::client::read_known_hosts(&args.known_hosts) + .unwrap_or_else(|_| Vec::new()) + .len(); + + crate::client::run_client(args.clone()).await?; + + let keys_after = if args.in_place { + crate::client::read_known_hosts(&args.known_hosts) + .unwrap_or_else(|_| Vec::new()) + .len() + } else { + keys_before + }; + + info!("Sync completed: {} keys before, {} keys after", keys_before, keys_after); + Ok(keys_after) +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs new file mode 100644 index 0000000..da9ba4c --- /dev/null +++ b/src/gui/mod.rs @@ -0,0 +1,43 @@ +use log::info; + +// Modules +mod api; +mod admin; +mod common; + +#[cfg(feature = "gui")] +mod settings; +#[cfg(feature = "gui")] +mod tray; + +// Re-exports for backward compatibility and external usage +#[cfg(feature = "gui")] +pub use settings::run_settings_window; +#[cfg(feature = "gui")] +pub use tray::run_tray_app; + +// User events for GUI communication +#[cfg(feature = "gui")] +#[derive(Debug)] +pub enum UserEvent { + TrayIconEvent, + MenuEvent(tray_icon::menu::MenuEvent), + ConfigFileChanged, + UpdateMenu, +} + +/// Run GUI application in tray mode +#[cfg(feature = "gui")] +pub async fn run_gui() -> std::io::Result<()> { + info!("Starting KHM tray application"); + run_tray_app().await +} + +/// Stub function when GUI is disabled +#[cfg(not(feature = "gui"))] +pub async fn run_gui() -> std::io::Result<()> { + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "GUI features not compiled. Install system dependencies and rebuild with --features gui" + )); +} diff --git a/src/gui/settings/connection.rs b/src/gui/settings/connection.rs new file mode 100644 index 0000000..83fc6be --- /dev/null +++ b/src/gui/settings/connection.rs @@ -0,0 +1,202 @@ +use eframe::egui; +use log::{error, info}; +use std::sync::mpsc; +use crate::gui::api::{test_connection, perform_manual_sync}; +use crate::gui::common::{KhmSettings, save_settings}; + +#[derive(Debug, Clone)] +pub enum ConnectionStatus { + Unknown, + Connected { keys_count: usize, flow: String }, + Error(String), +} + +#[derive(Debug, Clone)] +pub enum SyncStatus { + Unknown, + Success { keys_count: usize }, + Error(String), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum SettingsTab { + Connection, + Admin, +} + +pub struct ConnectionTab { + pub connection_status: ConnectionStatus, + pub is_testing_connection: bool, + pub test_result_receiver: Option>>, + pub is_syncing: bool, + pub sync_result_receiver: Option>>, + pub sync_status: SyncStatus, +} + +impl Default for ConnectionTab { + fn default() -> Self { + Self { + connection_status: ConnectionStatus::Unknown, + is_testing_connection: false, + test_result_receiver: None, + is_syncing: false, + sync_result_receiver: None, + sync_status: SyncStatus::Unknown, + } + } +} + +impl ConnectionTab { + /// Start connection test + pub fn start_test(&mut self, settings: &KhmSettings, ctx: &egui::Context) { + if self.is_testing_connection { + return; + } + + self.is_testing_connection = true; + self.connection_status = ConnectionStatus::Unknown; + + let (tx, rx) = mpsc::channel(); + self.test_result_receiver = Some(rx); + + let host = settings.host.clone(); + let flow = settings.flow.clone(); + let basic_auth = settings.basic_auth.clone(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + test_connection(host, flow, basic_auth).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); + } + + /// Start manual sync + pub fn start_sync(&mut self, settings: &KhmSettings, ctx: &egui::Context) { + if self.is_syncing { + return; + } + + self.is_syncing = true; + self.sync_status = SyncStatus::Unknown; + + let (tx, rx) = mpsc::channel(); + self.sync_result_receiver = Some(rx); + + let settings = settings.clone(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + perform_manual_sync(settings).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); + } + + /// Check for test/sync results + pub fn check_results(&mut self, ctx: &egui::Context, settings: &KhmSettings, operation_log: &mut Vec) { + // Check for test connection result + if let Some(receiver) = &self.test_result_receiver { + if let Ok(result) = receiver.try_recv() { + self.is_testing_connection = false; + match result { + Ok(message) => { + // Parse keys count from message + let keys_count = if let Some(start) = message.find("Found ") { + if let Some(end) = message[start + 6..].find(" SSH keys") { + message[start + 6..start + 6 + end].parse::().unwrap_or(0) + } else { 0 } + } else { 0 }; + + self.connection_status = ConnectionStatus::Connected { + keys_count, + flow: settings.flow.clone() + }; + info!("Connection test successful: {}", message); + + // Add to UI log + super::ui::add_log_entry(operation_log, format!("✅ Connection test successful: {}", message)); + } + Err(error) => { + self.connection_status = ConnectionStatus::Error(error.clone()); + error!("Connection test failed"); + + // Add to UI log + super::ui::add_log_entry(operation_log, format!("❌ Connection test failed: {}", error)); + } + } + self.test_result_receiver = None; + ctx.request_repaint(); + } + } + + // Check for sync result + if let Some(receiver) = &self.sync_result_receiver { + if let Ok(result) = receiver.try_recv() { + self.is_syncing = false; + match result { + Ok(message) => { + // Parse keys count from message + let keys_count = parse_keys_count(&message); + self.sync_status = SyncStatus::Success { keys_count }; + info!("Sync successful: {}", message); + + // Add to UI log + super::ui::add_log_entry(operation_log, format!("✅ Sync completed: {}", message)); + } + Err(error) => { + self.sync_status = SyncStatus::Error(error.clone()); + error!("Sync failed"); + + // Add to UI log + super::ui::add_log_entry(operation_log, format!("❌ Sync failed: {}", error)); + } + } + self.sync_result_receiver = None; + ctx.request_repaint(); + } + } + } +} + +/// Parse keys count from sync result message +fn parse_keys_count(message: &str) -> usize { + if let Some(start) = message.find("updated with ") { + let search_start = start + "updated with ".len(); + if let Some(end) = message[search_start..].find(" keys") { + let number_str = &message[search_start..search_start + end]; + return number_str.parse::().unwrap_or(0); + } + } else if let Some(start) = message.find("Retrieved ") { + let search_start = start + "Retrieved ".len(); + if let Some(end) = message[search_start..].find(" keys") { + let number_str = &message[search_start..search_start + end]; + return number_str.parse::().unwrap_or(0); + } + } else if let Some(keys_pos) = message.find(" keys") { + let before_keys = &message[..keys_pos]; + if let Some(space_pos) = before_keys.rfind(' ') { + let number_str = &before_keys[space_pos + 1..]; + return number_str.parse::().unwrap_or(0); + } + } + + 0 +} + +/// Save settings with validation +pub fn save_settings_validated(settings: &KhmSettings) -> Result<(), String> { + if settings.host.is_empty() || settings.flow.is_empty() { + return Err("Host URL and Flow Name are required".to_string()); + } + + save_settings(settings).map_err(|e| format!("Failed to save settings: {}", e)) +} diff --git a/src/gui/settings/mod.rs b/src/gui/settings/mod.rs new file mode 100644 index 0000000..32c7ddd --- /dev/null +++ b/src/gui/settings/mod.rs @@ -0,0 +1,5 @@ +mod connection; +mod ui; +mod window; + +pub use window::*; diff --git a/src/gui/settings/ui.rs b/src/gui/settings/ui.rs new file mode 100644 index 0000000..58c897d --- /dev/null +++ b/src/gui/settings/ui.rs @@ -0,0 +1,549 @@ +use eframe::egui; +use crate::gui::common::{KhmSettings, get_config_path}; +use super::connection::{ConnectionTab, ConnectionStatus, SyncStatus, save_settings_validated}; + +/// Render connection settings tab with modern horizontal UI design +pub fn render_connection_tab( + ui: &mut egui::Ui, + ctx: &egui::Context, + settings: &mut KhmSettings, + auto_sync_interval_str: &mut String, + connection_tab: &mut ConnectionTab, + operation_log: &mut Vec +) { + // Check for connection test and sync results + connection_tab.check_results(ctx, settings, operation_log); + + // Use scrollable area for the entire content + egui::ScrollArea::vertical() + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.spacing_mut().item_spacing = egui::vec2(6.0, 8.0); + ui.spacing_mut().button_padding = egui::vec2(12.0, 6.0); + ui.spacing_mut().indent = 16.0; + + // Connection Status Card at top (full width) + render_connection_status_card(ui, connection_tab); + + // Main configuration area - horizontal layout + ui.horizontal_top(|ui| { + let available_width = ui.available_width(); + let left_panel_width = available_width * 0.6; + let right_panel_width = available_width * 0.38; + + // Left panel - Connection and Local config + ui.allocate_ui_with_layout( + [left_panel_width, ui.available_height()].into(), + egui::Layout::top_down(egui::Align::Min), + |ui| { + // Connection Configuration Card + render_connection_config_card(ui, settings); + + // Local Configuration Card + render_local_config_card(ui, settings); + } + ); + + ui.add_space(8.0); + + // Right panel - Auto-sync and System info + ui.allocate_ui_with_layout( + [right_panel_width, ui.available_height()].into(), + egui::Layout::top_down(egui::Align::Min), + |ui| { + // Auto-sync Configuration Card + render_auto_sync_card(ui, settings, auto_sync_interval_str); + + // System Information Card + render_system_info_card(ui); + } + ); + }); + + ui.add_space(12.0); + + // Action buttons at bottom + render_action_section(ui, ctx, settings, connection_tab, operation_log); + }); +} + +/// Connection status card with modern visual design +fn render_connection_status_card(ui: &mut egui::Ui, connection_tab: &ConnectionTab) { + let frame = egui::Frame::group(ui.style()) + .fill(ui.visuals().faint_bg_color) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)) + .rounding(6.0) + .inner_margin(egui::Margin::same(12.0)); + + frame.show(ui, |ui| { + // Header with status indicator + ui.horizontal(|ui| { + let (status_icon, status_text, status_color) = match &connection_tab.connection_status { + ConnectionStatus::Connected { keys_count, flow } => { + let text = if flow.is_empty() { + format!("Connected • {} keys", keys_count) + } else { + format!("Connected to '{}' • {} keys", flow, keys_count) + }; + ("🟢", text, egui::Color32::GREEN) + } + ConnectionStatus::Error(error_msg) => { + ("🔴", format!("Connection Error: {}", error_msg), egui::Color32::RED) + } + ConnectionStatus::Unknown => { + ("⚫", "Not Connected".to_string(), ui.visuals().text_color()) + } + }; + + ui.label(egui::RichText::new(status_icon).size(14.0)); + ui.label(egui::RichText::new("Connection Status").size(14.0).strong()); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if connection_tab.is_testing_connection { + ui.spinner(); + ui.label(egui::RichText::new("Testing...").italics().color(ui.visuals().weak_text_color())); + } else { + ui.label(egui::RichText::new(&status_text).size(13.0).color(status_color)); + } + }); + }); + + // Sync status - always visible + ui.add_space(6.0); + ui.separator(); + ui.add_space(6.0); + + ui.horizontal(|ui| { + ui.label("🔄"); + ui.label("Last Sync:"); + + match &connection_tab.sync_status { + SyncStatus::Success { keys_count } => { + ui.label(egui::RichText::new(format!("✅ {} keys synced", keys_count)) + .size(13.0).color(egui::Color32::GREEN)); + } + SyncStatus::Error(error_msg) => { + ui.label(egui::RichText::new("❌ Failed") + .size(13.0).color(egui::Color32::RED)) + .on_hover_text(error_msg); + } + SyncStatus::Unknown => { + ui.label(egui::RichText::new("No sync performed yet") + .size(13.0).color(ui.visuals().weak_text_color())); + } + } + }); + }); + + ui.add_space(8.0); +} + +/// Connection configuration card with input fields +fn render_connection_config_card(ui: &mut egui::Ui, settings: &mut KhmSettings) { + let frame = egui::Frame::group(ui.style()) + .fill(ui.visuals().faint_bg_color) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)) + .rounding(6.0) + .inner_margin(egui::Margin::same(12.0)); + + frame.show(ui, |ui| { + // Header + ui.horizontal(|ui| { + ui.label("🌐"); + ui.label(egui::RichText::new("Server Configuration").size(14.0).strong()); + }); + + ui.add_space(8.0); + + // Input fields with better spacing + ui.vertical(|ui| { + ui.spacing_mut().item_spacing.y = 8.0; + + // Host URL + ui.vertical(|ui| { + ui.label(egui::RichText::new("Host URL").size(13.0).strong()); + ui.add_space(3.0); + ui.add_sized( + [ui.available_width(), 28.0], // Smaller height for better centering + egui::TextEdit::singleline(&mut settings.host) + .hint_text("https://your-khm-server.com") + .font(egui::FontId::new(14.0, egui::FontFamily::Monospace)) + .margin(egui::Margin::symmetric(8.0, 6.0)) // Better vertical centering + ); + }); + + // Flow Name + ui.vertical(|ui| { + ui.label(egui::RichText::new("Flow Name").size(13.0).strong()); + ui.add_space(3.0); + ui.add_sized( + [ui.available_width(), 28.0], + egui::TextEdit::singleline(&mut settings.flow) + .hint_text("production, staging, development") + .font(egui::FontId::new(14.0, egui::FontFamily::Proportional)) + .margin(egui::Margin::symmetric(8.0, 6.0)) + ); + }); + + // Basic Auth (optional) + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Basic Authentication").size(13.0).strong()); + ui.label(egui::RichText::new("(optional)").size(12.0).weak().italics()); + }); + ui.add_space(3.0); + ui.add_sized( + [ui.available_width(), 28.0], + egui::TextEdit::singleline(&mut settings.basic_auth) + .hint_text("username:password") + .password(true) + .font(egui::FontId::new(14.0, egui::FontFamily::Monospace)) + .margin(egui::Margin::symmetric(8.0, 6.0)) + ); + }); + }); + }); + + ui.add_space(8.0); +} + +/// Local configuration card +fn render_local_config_card(ui: &mut egui::Ui, settings: &mut KhmSettings) { + let frame = egui::Frame::group(ui.style()) + .fill(ui.visuals().faint_bg_color) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)) + .rounding(6.0) + .inner_margin(egui::Margin::same(12.0)); + + frame.show(ui, |ui| { + // Header + ui.horizontal(|ui| { + ui.label("📁"); + ui.label(egui::RichText::new("Local Configuration").size(14.0).strong()); + }); + + ui.add_space(8.0); + + // Known hosts file + ui.vertical(|ui| { + ui.label(egui::RichText::new("Known Hosts File Path").size(13.0).strong()); + ui.add_space(3.0); + ui.add_sized( + [ui.available_width(), 28.0], + egui::TextEdit::singleline(&mut settings.known_hosts) + .hint_text("~/.ssh/known_hosts") + .font(egui::FontId::new(14.0, egui::FontFamily::Monospace)) + .margin(egui::Margin::symmetric(8.0, 6.0)) + ); + + ui.add_space(8.0); + + // In-place update option with better styling + ui.horizontal(|ui| { + ui.checkbox(&mut settings.in_place, ""); + ui.vertical(|ui| { + ui.label(egui::RichText::new("Update file in-place after sync").size(13.0).strong()); + ui.label(egui::RichText::new("Automatically modify the known_hosts file when synchronizing").size(12.0).weak().italics()); + }); + }); + }); + }); + + ui.add_space(8.0); +} + +/// Auto-sync configuration card +fn render_auto_sync_card(ui: &mut egui::Ui, settings: &mut KhmSettings, auto_sync_interval_str: &mut String) { + let frame = egui::Frame::group(ui.style()) + .fill(ui.visuals().faint_bg_color) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)) + .rounding(6.0) + .inner_margin(egui::Margin::same(12.0)); + + frame.show(ui, |ui| { + let is_auto_sync_enabled = !settings.host.is_empty() + && !settings.flow.is_empty() + && settings.in_place; + + // Header with status + ui.horizontal(|ui| { + ui.label("🔄"); + ui.label(egui::RichText::new("Auto Sync").size(14.0).strong()); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let (status_text, status_color) = if is_auto_sync_enabled { + ("● Active", egui::Color32::GREEN) + } else { + ("○ Inactive", egui::Color32::from_gray(128)) + }; + + ui.label(egui::RichText::new(status_text).size(12.0).color(status_color)); + }); + }); + + ui.add_space(8.0); + + // Interval setting + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Interval").size(13.0).strong()); + ui.add_space(6.0); + ui.add_sized( + [80.0, 26.0], // Smaller height + egui::TextEdit::singleline(auto_sync_interval_str) + .font(egui::FontId::new(14.0, egui::FontFamily::Monospace)) + .margin(egui::Margin::symmetric(6.0, 5.0)) + ); + ui.label("min"); + + // Update the actual setting + if let Ok(value) = auto_sync_interval_str.parse::() { + if value > 0 { + settings.auto_sync_interval_minutes = value; + } + } + }); + + // Requirements - always visible + ui.add_space(8.0); + ui.separator(); + ui.add_space(8.0); + + ui.vertical(|ui| { + ui.label(egui::RichText::new("Requirements:").size(12.0).strong()); + ui.add_space(3.0); + + let host_ok = !settings.host.is_empty(); + let flow_ok = !settings.flow.is_empty(); + let in_place_ok = settings.in_place; + + ui.horizontal(|ui| { + let (icon, color) = if host_ok { ("✅", egui::Color32::GREEN) } else { ("❌", egui::Color32::RED) }; + ui.label(egui::RichText::new(icon).color(color)); + ui.label(egui::RichText::new("Host URL").size(11.0)); + }); + + ui.horizontal(|ui| { + let (icon, color) = if flow_ok { ("✅", egui::Color32::GREEN) } else { ("❌", egui::Color32::RED) }; + ui.label(egui::RichText::new(icon).color(color)); + ui.label(egui::RichText::new("Flow name").size(11.0)); + }); + + ui.horizontal(|ui| { + let (icon, color) = if in_place_ok { ("✅", egui::Color32::GREEN) } else { ("❌", egui::Color32::RED) }; + ui.label(egui::RichText::new(icon).color(color)); + ui.label(egui::RichText::new("In-place update").size(11.0)); + }); + }); + }); + + ui.add_space(8.0); +} + +/// System information card +fn render_system_info_card(ui: &mut egui::Ui) { + let frame = egui::Frame::group(ui.style()) + .fill(ui.visuals().extreme_bg_color) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)) + .rounding(6.0) + .inner_margin(egui::Margin::same(12.0)); + + frame.show(ui, |ui| { + // Header + ui.horizontal(|ui| { + ui.label("⚙️"); + ui.label(egui::RichText::new("System Info").size(14.0).strong()); + }); + + ui.add_space(8.0); + + // Config file location + ui.vertical(|ui| { + ui.label(egui::RichText::new("Config File").size(13.0).strong()); + ui.add_space(3.0); + + let config_path = get_config_path(); + let path_str = config_path.display().to_string(); + + ui.vertical(|ui| { + ui.add_sized( + [ui.available_width(), 26.0], // Smaller height + egui::TextEdit::singleline(&mut path_str.clone()) + .interactive(false) + .font(egui::FontId::new(12.0, egui::FontFamily::Monospace)) + .margin(egui::Margin::symmetric(8.0, 5.0)) + ); + + ui.add_space(4.0); + + if ui.small_button("📋 Copy Path").clicked() { + ui.output_mut(|o| o.copied_text = path_str); + } + }); + }); + }); + + ui.add_space(8.0); +} + +/// Action section with buttons only (Activity Log moved to bottom panel) +fn render_action_section( + ui: &mut egui::Ui, + ctx: &egui::Context, + settings: &KhmSettings, + connection_tab: &mut ConnectionTab, + operation_log: &mut Vec +) { + ui.add_space(8.0); + + // Validation message + let save_enabled = !settings.host.is_empty() && !settings.flow.is_empty(); + if !save_enabled { + ui.horizontal(|ui| { + ui.label("⚠️"); + ui.label(egui::RichText::new("Complete server configuration to enable saving") + .size(12.0) + .color(egui::Color32::LIGHT_YELLOW) + .italics()); + }); + ui.add_space(8.0); + } + + // Action buttons with modern styling + render_modern_action_buttons(ui, ctx, settings, connection_tab, save_enabled, operation_log); +} + +/// Modern action buttons with improved styling and layout +fn render_modern_action_buttons( + ui: &mut egui::Ui, + ctx: &egui::Context, + settings: &KhmSettings, + connection_tab: &mut ConnectionTab, + save_enabled: bool, + operation_log: &mut Vec +) { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 8.0; + + // Primary actions (left side) + if ui.add_enabled( + save_enabled, + egui::Button::new( + egui::RichText::new("💾 Save & Close") + .size(13.0) + .color(egui::Color32::WHITE) + ) + .fill(if save_enabled { + egui::Color32::from_rgb(0, 120, 212) + } else { + ui.visuals().widgets.inactive.bg_fill + }) + .min_size(egui::vec2(120.0, 32.0)) + .rounding(6.0) + ).clicked() { + match save_settings_validated(settings) { + Ok(()) => { + add_log_entry(operation_log, "✅ Settings saved successfully".to_string()); + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + Err(e) => { + add_log_entry(operation_log, format!("❌ Failed to save settings: {}", e)); + } + } + } + + if ui.add( + egui::Button::new( + egui::RichText::new("✖ Cancel") + .size(13.0) + .color(ui.visuals().text_color()) + ) + .stroke(egui::Stroke::new(1.0, ui.visuals().text_color())) + .fill(egui::Color32::TRANSPARENT) + .min_size(egui::vec2(80.0, 32.0)) + .rounding(6.0) + ).clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + + // Spacer + ui.add_space(ui.available_width() - 220.0); + + // Secondary actions (right side) + let can_test = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_testing_connection; + let can_sync = !settings.host.is_empty() && !settings.flow.is_empty() && !connection_tab.is_syncing; + + if ui.add_enabled( + can_test, + egui::Button::new( + egui::RichText::new( + if connection_tab.is_testing_connection { + "🔄 Testing..." + } else { + "🔍 Test" + } + ) + .size(13.0) + .color(egui::Color32::WHITE) + ) + .fill(if can_test { + egui::Color32::from_rgb(16, 124, 16) + } else { + ui.visuals().widgets.inactive.bg_fill + }) + .min_size(egui::vec2(80.0, 32.0)) + .rounding(6.0) + ).on_hover_text("Test server connection").clicked() { + add_log_entry(operation_log, "🔍 Testing connection...".to_string()); + connection_tab.start_test(settings, ctx); + } + + if ui.add_enabled( + can_sync, + egui::Button::new( + egui::RichText::new( + if connection_tab.is_syncing { + "🔄 Syncing..." + } else { + "🔄 Sync" + } + ) + .size(13.0) + .color(egui::Color32::WHITE) + ) + .fill(if can_sync { + egui::Color32::from_rgb(255, 140, 0) + } else { + ui.visuals().widgets.inactive.bg_fill + }) + .min_size(egui::vec2(80.0, 32.0)) + .rounding(6.0) + ).on_hover_text("Synchronize SSH keys now").clicked() { + add_log_entry(operation_log, "🔄 Starting sync...".to_string()); + connection_tab.start_sync(settings, ctx); + } + }); +} + +/// Add entry to operation log with timestamp +pub fn add_log_entry(operation_log: &mut Vec, message: String) { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap(); + let secs = now.as_secs(); + let millis = now.subsec_millis(); + + // Format as HH:MM:SS.mmm + let hours = (secs / 3600) % 24; + let minutes = (secs / 60) % 60; + let seconds = secs % 60; + let timestamp = format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis); + + let log_entry = format!("{} {}", timestamp, message); + + operation_log.push(log_entry); + + // Keep only last 20 entries to prevent memory growth + if operation_log.len() > 20 { + operation_log.remove(0); + } +} diff --git a/src/gui/settings/window.rs b/src/gui/settings/window.rs new file mode 100644 index 0000000..c69eafc --- /dev/null +++ b/src/gui/settings/window.rs @@ -0,0 +1,584 @@ +use eframe::egui; +use log::info; +use std::sync::mpsc; +use crate::gui::common::{load_settings, KhmSettings}; +use crate::gui::admin::{AdminState, AdminOperation, render_statistics, render_search_controls, + render_bulk_actions, render_keys_table, KeyAction, BulkAction}; +use crate::gui::api::{SshKey, bulk_deprecate_servers, bulk_restore_servers, + deprecate_key, restore_key, delete_key}; + +use super::connection::{ConnectionTab, SettingsTab}; +use super::ui::{render_connection_tab, add_log_entry}; + +pub struct SettingsWindow { + settings: KhmSettings, + auto_sync_interval_str: String, + current_tab: SettingsTab, + connection_tab: ConnectionTab, + admin_state: AdminState, + admin_receiver: Option, String>>>, + operation_receiver: Option>>, + operation_log: Vec, +} + +impl SettingsWindow { + pub fn new() -> Self { + let settings = load_settings(); + let auto_sync_interval_str = settings.auto_sync_interval_minutes.to_string(); + + Self { + settings, + auto_sync_interval_str, + current_tab: SettingsTab::Connection, + connection_tab: ConnectionTab::default(), + admin_state: AdminState::default(), + admin_receiver: None, + operation_receiver: None, + operation_log: Vec::new(), + } + } +} + +impl eframe::App for SettingsWindow { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Check for admin operation results + self.check_admin_results(ctx); + + // Apply enhanced modern dark theme + apply_modern_theme(ctx); + + // Bottom panel for Activity Log (fixed at bottom) + egui::TopBottomPanel::bottom("activity_log_panel") + .resizable(false) + .min_height(140.0) + .max_height(140.0) + .frame(egui::Frame::none() + .fill(egui::Color32::from_gray(12)) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(60))) + ) + .show(ctx, |ui| { + render_bottom_activity_log(ui, &mut self.operation_log); + }); + + egui::CentralPanel::default() + .frame(egui::Frame::none() + .fill(egui::Color32::from_gray(18)) + .inner_margin(egui::Margin::same(20.0)) + ) + .show(ctx, |ui| { + // Modern header with gradient-like styling + let header_frame = egui::Frame::none() + .fill(ui.visuals().panel_fill) + .rounding(egui::Rounding::same(8.0)) + .inner_margin(egui::Margin::same(12.0)) + .stroke(egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color)); + + header_frame.show(ui, |ui| { + ui.horizontal(|ui| { + ui.add_space(4.0); + ui.label("🔑"); + ui.heading(egui::RichText::new("KHM Settings").size(20.0).strong()); + ui.label(egui::RichText::new( + "(Known Hosts Manager for SSH key management and synchronization)" + ).size(11.0).weak().italics()); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // Version from Cargo.toml + let version = env!("CARGO_PKG_VERSION"); + if ui.small_button(format!("v{}", version)) + .on_hover_text(format!( + "{}\n{}\nRepository: {}\nLicense: {}", + env!("CARGO_PKG_DESCRIPTION"), + env!("CARGO_PKG_AUTHORS"), + env!("CARGO_PKG_REPOSITORY"), + "WTFPL" + )) + .clicked() + { + // Open repository URL + if let Err(_) = std::process::Command::new("open") + .arg(env!("CARGO_PKG_REPOSITORY")) + .spawn() + { + // Fallback for non-macOS systems + let _ = std::process::Command::new("xdg-open") + .arg(env!("CARGO_PKG_REPOSITORY")) + .spawn(); + } + } + }); + }); + }); + + ui.add_space(12.0); + + // Modern tab selector with card styling + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 6.0; + + // Connection/Settings Tab + let connection_selected = matches!(self.current_tab, SettingsTab::Connection); + let connection_button = egui::Button::new( + egui::RichText::new("🌐 Connection").size(13.0) + ) + .fill(if connection_selected { + egui::Color32::from_rgb(0, 120, 212) + } else { + ui.visuals().widgets.inactive.bg_fill + }) + .stroke(if connection_selected { + egui::Stroke::new(1.0, egui::Color32::from_rgb(0, 120, 212)) + } else { + egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color) + }) + .rounding(6.0) + .min_size(egui::vec2(110.0, 32.0)); + + if ui.add(connection_button).clicked() { + self.current_tab = SettingsTab::Connection; + } + + // Admin Tab + let admin_selected = matches!(self.current_tab, SettingsTab::Admin); + let admin_button = egui::Button::new( + egui::RichText::new("🔧 Admin Panel").size(13.0) + ) + .fill(if admin_selected { + egui::Color32::from_rgb(120, 80, 0) + } else { + ui.visuals().widgets.inactive.bg_fill + }) + .stroke(if admin_selected { + egui::Stroke::new(1.0, egui::Color32::from_rgb(120, 80, 0)) + } else { + egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color) + }) + .rounding(6.0) + .min_size(egui::vec2(110.0, 32.0)); + + if ui.add(admin_button).clicked() { + self.current_tab = SettingsTab::Admin; + } + }); + + ui.add_space(16.0); + + // Content area with proper spacing + match self.current_tab { + SettingsTab::Connection => { + render_connection_tab( + ui, + ctx, + &mut self.settings, + &mut self.auto_sync_interval_str, + &mut self.connection_tab, + &mut self.operation_log + ); + } + SettingsTab::Admin => { + self.render_admin_tab(ui, ctx); + } + } + }); + } +} + +impl SettingsWindow { + fn check_admin_results(&mut self, ctx: &egui::Context) { + // Check for admin keys loading result + if let Some(receiver) = &self.admin_receiver { + if let Ok(result) = receiver.try_recv() { + self.admin_state.handle_keys_loaded(result); + self.admin_receiver = None; + ctx.request_repaint(); + } + } + + // Check for operation results + if let Some(receiver) = &self.operation_receiver { + if let Ok(result) = receiver.try_recv() { + match result { + Ok(message) => { + info!("Operation completed: {}", message); + add_log_entry(&mut self.operation_log, format!("✅ {}", message)); + // Reload keys after operation + self.load_admin_keys(ctx); + } + Err(error) => { + add_log_entry(&mut self.operation_log, format!("❌ Operation failed: {}", error)); + } + } + self.admin_state.current_operation = AdminOperation::None; + self.operation_receiver = None; + ctx.request_repaint(); + } + } + } + + fn render_admin_tab(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { + // Admin tab header + ui.horizontal(|ui| { + ui.label(egui::RichText::new("🔧 Admin Panel").size(18.0).strong()); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("🔁 Refresh").clicked() { + self.load_admin_keys(ctx); + } + + if let Some(last_load) = self.admin_state.last_load_time { + let elapsed = last_load.elapsed().as_secs(); + ui.label(format!("Updated {}s ago", elapsed)); + } + }); + }); + + ui.separator(); + ui.add_space(10.0); + + // Check if connection is configured + if self.settings.host.is_empty() || self.settings.flow.is_empty() { + ui.vertical_centered(|ui| { + ui.label(egui::RichText::new("❗ Please configure connection settings first") + .size(16.0) + .color(egui::Color32::YELLOW)); + ui.add_space(10.0); + if ui.button("Go to Connection Settings").clicked() { + self.current_tab = SettingsTab::Connection; + } + }); + return; + } + + // Load keys automatically on first view + if self.admin_state.keys.is_empty() && !matches!(self.admin_state.current_operation, AdminOperation::LoadingKeys) { + self.load_admin_keys(ctx); + } + + // Show loading state + if matches!(self.admin_state.current_operation, AdminOperation::LoadingKeys) { + ui.vertical_centered(|ui| { + ui.spinner(); + ui.label("Loading keys..."); + }); + return; + } + + // Statistics section + render_statistics(ui, &self.admin_state); + ui.add_space(10.0); + + // Search and filters + render_search_controls(ui, &mut self.admin_state); + ui.add_space(10.0); + + // Bulk actions + let bulk_action = render_bulk_actions(ui, &mut self.admin_state); + self.handle_bulk_action(bulk_action, ctx); + + if self.admin_state.selected_servers.values().any(|&v| v) { + ui.add_space(8.0); + } + + // Keys table + egui::ScrollArea::vertical() + .max_height(450.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + let key_action = render_keys_table(ui, &mut self.admin_state); + self.handle_key_action(key_action, ctx); + }); + } + + fn load_admin_keys(&mut self, ctx: &egui::Context) { + if let Some(receiver) = self.admin_state.load_keys(&self.settings, ctx) { + self.admin_receiver = Some(receiver); + } + } + + fn handle_bulk_action(&mut self, action: BulkAction, ctx: &egui::Context) { + match action { + BulkAction::DeprecateSelected => { + let selected = self.admin_state.get_selected_servers(); + if !selected.is_empty() { + self.start_bulk_deprecate(selected, ctx); + } + } + BulkAction::RestoreSelected => { + let selected = self.admin_state.get_selected_servers(); + if !selected.is_empty() { + self.start_bulk_restore(selected, ctx); + } + } + BulkAction::ClearSelection => { + // Selection already cleared in UI + } + BulkAction::None => {} + } + } + + fn handle_key_action(&mut self, action: KeyAction, ctx: &egui::Context) { + match action { + KeyAction::DeprecateKey(server) | KeyAction::DeprecateServer(server) => { + self.start_deprecate_key(&server, ctx); + } + KeyAction::RestoreKey(server) | KeyAction::RestoreServer(server) => { + self.start_restore_key(&server, ctx); + } + KeyAction::DeleteKey(server) => { + self.start_delete_key(&server, ctx); + } + KeyAction::None => {} + } + } + + fn start_bulk_deprecate(&mut self, servers: Vec, ctx: &egui::Context) { + self.admin_state.current_operation = AdminOperation::BulkDeprecating; + add_log_entry(&mut self.operation_log, format!("Deprecating {} servers...", servers.len())); + + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + let host = self.settings.host.clone(); + let flow = self.settings.flow.clone(); + let basic_auth = self.settings.basic_auth.clone(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + bulk_deprecate_servers(host, flow, basic_auth, servers).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); + } + + fn start_bulk_restore(&mut self, servers: Vec, ctx: &egui::Context) { + self.admin_state.current_operation = AdminOperation::BulkRestoring; + add_log_entry(&mut self.operation_log, format!("Restoring {} servers...", servers.len())); + + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + let host = self.settings.host.clone(); + let flow = self.settings.flow.clone(); + let basic_auth = self.settings.basic_auth.clone(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + bulk_restore_servers(host, flow, basic_auth, servers).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); + } + + fn start_deprecate_key(&mut self, server: &str, ctx: &egui::Context) { + self.admin_state.current_operation = AdminOperation::DeprecatingKey; + add_log_entry(&mut self.operation_log, format!("Deprecating key for server: {}", server)); + + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + let host = self.settings.host.clone(); + let flow = self.settings.flow.clone(); + let basic_auth = self.settings.basic_auth.clone(); + let server_name = server.to_string(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + deprecate_key(host, flow, basic_auth, server_name).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); + } + + fn start_restore_key(&mut self, server: &str, ctx: &egui::Context) { + self.admin_state.current_operation = AdminOperation::RestoringKey; + add_log_entry(&mut self.operation_log, format!("Restoring key for server: {}", server)); + + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + let host = self.settings.host.clone(); + let flow = self.settings.flow.clone(); + let basic_auth = self.settings.basic_auth.clone(); + let server_name = server.to_string(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + restore_key(host, flow, basic_auth, server_name).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); + } + + fn start_delete_key(&mut self, server: &str, ctx: &egui::Context) { + self.admin_state.current_operation = AdminOperation::DeletingKey; + add_log_entry(&mut self.operation_log, format!("Deleting key for server: {}", server)); + + let (tx, rx) = mpsc::channel(); + self.operation_receiver = Some(rx); + + let host = self.settings.host.clone(); + let flow = self.settings.flow.clone(); + let basic_auth = self.settings.basic_auth.clone(); + let server_name = server.to_string(); + let ctx_clone = ctx.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(async { + delete_key(host, flow, basic_auth, server_name).await + }); + + let _ = tx.send(result); + ctx_clone.request_repaint(); + }); + } +} + +/// Apply modern dark theme for the settings window with enhanced styling +fn apply_modern_theme(ctx: &egui::Context) { + let mut visuals = egui::Visuals::dark(); + + // Modern color palette + visuals.window_fill = egui::Color32::from_gray(18); // Darker background + visuals.panel_fill = egui::Color32::from_gray(24); // Panel background + visuals.faint_bg_color = egui::Color32::from_gray(32); // Card background + visuals.extreme_bg_color = egui::Color32::from_gray(12); // Darkest areas + + // Enhanced widget styling + visuals.button_frame = true; + visuals.collapsing_header_frame = true; + visuals.indent_has_left_vline = true; + visuals.striped = true; + + // Modern rounded corners + let rounding = egui::Rounding::same(8.0); + visuals.menu_rounding = rounding; + visuals.window_rounding = egui::Rounding::same(16.0); + visuals.widgets.noninteractive.rounding = rounding; + visuals.widgets.inactive.rounding = rounding; + visuals.widgets.hovered.rounding = rounding; + visuals.widgets.active.rounding = rounding; + + // Better widget colors + visuals.widgets.noninteractive.bg_fill = egui::Color32::from_gray(40); + visuals.widgets.inactive.bg_fill = egui::Color32::from_gray(45); + visuals.widgets.hovered.bg_fill = egui::Color32::from_gray(55); + visuals.widgets.active.bg_fill = egui::Color32::from_gray(60); + + // Subtle borders + let border_color = egui::Color32::from_gray(60); + visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, border_color); + visuals.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, border_color); + visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.5, egui::Color32::from_gray(80)); + visuals.widgets.active.bg_stroke = egui::Stroke::new(1.5, egui::Color32::from_gray(100)); + + ctx.set_visuals(visuals); +} + +/// Render bottom activity log panel +fn render_bottom_activity_log(ui: &mut egui::Ui, operation_log: &mut Vec) { + ui.add_space(18.0); // Larger top padding + + ui.horizontal(|ui| { + ui.add_space(8.0); + ui.label("📋"); + ui.label(egui::RichText::new("Activity Log").size(13.0).strong()); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { + ui.add_space(8.0); + if ui.small_button("🗑 Clear").clicked() { + operation_log.clear(); + } + }); + }); + + ui.add_space(8.0); + + // Add horizontal margin for the text area + ui.horizontal(|ui| { + ui.add_space(8.0); // Left margin + + // Show last 5 log entries in multiline text + let log_text = if operation_log.is_empty() { + "No recent activity".to_string() + } else { + let start_idx = if operation_log.len() > 5 { + operation_log.len() - 5 + } else { + 0 + }; + operation_log[start_idx..].join("\n") + }; + + ui.add_sized( + [ui.available_width() - 8.0, 80.0], // Account for right margin + egui::TextEdit::multiline(&mut log_text.clone()) + .font(egui::FontId::new(11.0, egui::FontFamily::Monospace)) + .interactive(false) + ); + + ui.add_space(8.0); // Right margin + }); +} + +/// Create window icon for settings window +pub fn create_window_icon() -> egui::IconData { + // Create a simple programmatic icon (blue square with white border) + let icon_size = 32; + let icon_data: Vec = (0..icon_size * icon_size) + .flat_map(|i| { + let y = i / icon_size; + let x = i % icon_size; + if x < 2 || x >= 30 || y < 2 || y >= 30 { + [255, 255, 255, 255] // White border + } else { + [64, 128, 255, 255] // Blue center + } + }) + .collect(); + + egui::IconData { + rgba: icon_data, + width: icon_size as u32, + height: icon_size as u32, + } +} + +/// Run the settings window application with modern horizontal styling +pub fn run_settings_window() { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_title("KHM Settings") + .with_inner_size([900.0, 905.0]) // Decreased height by another 15px + .with_min_inner_size([900.0, 905.0]) // Fixed size + .with_max_inner_size([900.0, 905.0]) // Same as min - fixed size + .with_resizable(false) // Disable resizing since window is fixed size + .with_icon(create_window_icon()) + .with_decorations(true) + .with_transparent(false), + centered: true, + ..Default::default() + }; + + let _ = eframe::run_native( + "KHM Settings", + options, + Box::new(|_cc| Ok(Box::new(SettingsWindow::new()))), + ); +} diff --git a/src/gui/tray/app.rs b/src/gui/tray/app.rs new file mode 100644 index 0000000..b02d738 --- /dev/null +++ b/src/gui/tray/app.rs @@ -0,0 +1,275 @@ +use log::{error, info}; + +#[cfg(feature = "gui")] +use notify::RecursiveMode; +#[cfg(feature = "gui")] +use notify_debouncer_mini::{new_debouncer, DebounceEventResult}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tray_icon::{ + menu::MenuEvent, + TrayIcon, +}; +use winit::{ + application::ApplicationHandler, + event_loop::{EventLoop, EventLoopProxy}, +}; + +#[cfg(target_os = "macos")] +use winit::platform::macos::EventLoopBuilderExtMacOS; + +use super::{SyncStatus, TrayMenuIds, create_tray_icon, update_tray_menu, + create_tooltip, start_auto_sync_task, update_sync_status}; +use crate::gui::common::{load_settings, get_config_path, perform_sync, KhmSettings}; + +pub struct TrayApplication { + tray_icon: Option, + menu_ids: Option, + settings: Arc>, + sync_status: Arc>, + #[cfg(feature = "gui")] + _debouncer: Option>, + proxy: EventLoopProxy, + auto_sync_handle: Option>, +} + +impl TrayApplication { + pub fn new(proxy: EventLoopProxy) -> Self { + Self { + tray_icon: None, + menu_ids: None, + settings: Arc::new(Mutex::new(load_settings())), + sync_status: Arc::new(Mutex::new(SyncStatus::default())), + #[cfg(feature = "gui")] + _debouncer: None, + proxy, + auto_sync_handle: None, + } + } + + #[cfg(feature = "gui")] + fn setup_file_watcher(&mut self) { + let config_path = get_config_path(); + let (tx, rx) = std::sync::mpsc::channel::(); + let proxy = self.proxy.clone(); + + std::thread::spawn(move || { + while let Ok(result) = rx.recv() { + if let Ok(events) = result { + if events.iter().any(|e| e.path.to_string_lossy().contains("khm_config.json")) { + let _ = proxy.send_event(crate::gui::UserEvent::ConfigFileChanged); + } + } + } + }); + + if let Ok(mut debouncer) = new_debouncer(Duration::from_millis(500), tx) { + if let Some(config_dir) = config_path.parent() { + if debouncer.watcher().watch(config_dir, RecursiveMode::NonRecursive).is_ok() { + info!("File watcher started"); + self._debouncer = Some(debouncer); + } else { + error!("Failed to start file watcher"); + } + } + } + } + + fn handle_config_change(&mut self) { + info!("Config file changed"); + let new_settings = load_settings(); + let old_interval = self.settings.lock().unwrap().auto_sync_interval_minutes; + let new_interval = new_settings.auto_sync_interval_minutes; + + *self.settings.lock().unwrap() = new_settings; + + // Update menu + if let Some(tray_icon) = &self.tray_icon { + let settings = self.settings.lock().unwrap(); + let new_menu_ids = update_tray_menu(tray_icon, &settings); + self.menu_ids = Some(new_menu_ids); + } + + // Update tooltip + self.update_tooltip(); + + // Restart auto sync if interval changed + if old_interval != new_interval { + info!("Auto sync interval changed from {} to {} minutes, restarting auto sync", old_interval, new_interval); + self.start_auto_sync(); + } + } + + fn start_auto_sync(&mut self) { + if let Some(handle) = self.auto_sync_handle.take() { + // Note: In a real implementation, you'd want to properly signal the thread to stop + drop(handle); + } + + self.auto_sync_handle = start_auto_sync_task( + Arc::clone(&self.settings), + Arc::clone(&self.sync_status), + self.proxy.clone() + ); + } + + fn update_tooltip(&self) { + if let Some(tray_icon) = &self.tray_icon { + let settings = self.settings.lock().unwrap(); + let sync_status = self.sync_status.lock().unwrap(); + let tooltip = create_tooltip(&settings, &sync_status); + let _ = tray_icon.set_tooltip(Some(&tooltip)); + } + } + + fn handle_menu_event(&mut self, event: MenuEvent, event_loop: &winit::event_loop::ActiveEventLoop) { + if let Some(menu_ids) = &self.menu_ids { + if event.id == menu_ids.settings_id { + info!("Settings menu clicked"); + self.launch_settings_window(); + } else if event.id == menu_ids.quit_id { + info!("Quitting KHM application"); + event_loop.exit(); + } else if event.id == menu_ids.sync_id { + info!("Starting manual sync operation"); + self.start_manual_sync(); + } + } + } + + fn launch_settings_window(&self) { + if let Ok(exe_path) = std::env::current_exe() { + std::thread::spawn(move || { + if let Err(e) = std::process::Command::new(&exe_path) + .arg("--gui") + .arg("--settings-ui") + .spawn() + { + error!("Failed to launch settings window: {}", e); + } + }); + } + } + + fn start_manual_sync(&self) { + let settings = self.settings.lock().unwrap().clone(); + let sync_status_clone: Arc> = Arc::clone(&self.sync_status); + let proxy_clone = self.proxy.clone(); + + // Check if settings are valid + if settings.host.is_empty() || settings.flow.is_empty() { + error!("Cannot sync: host or flow not configured"); + return; + } + + info!("Syncing with host: {}, flow: {}", settings.host, settings.flow); + + // Run sync in separate thread with its own tokio runtime + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + match perform_sync(&settings).await { + Ok(keys_count) => { + info!("Sync completed successfully with {} keys", keys_count); + let mut status = sync_status_clone.lock().unwrap(); + status.last_sync_time = Some(std::time::Instant::now()); + status.last_sync_keys = Some(keys_count); + let _ = proxy_clone.send_event(crate::gui::UserEvent::UpdateMenu); + } + Err(e) => { + error!("Sync failed: {}", e); + } + } + }); + }); + } + + fn handle_update_menu(&mut self) { + let settings = self.settings.lock().unwrap(); + if !settings.host.is_empty() && !settings.flow.is_empty() && settings.in_place { + let mut sync_status = self.sync_status.lock().unwrap(); + update_sync_status(&settings, &mut sync_status); + } + drop(settings); + + self.update_tooltip(); + } +} + +impl ApplicationHandler for TrayApplication { + fn window_event( + &mut self, + _event_loop: &winit::event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + _event: winit::event::WindowEvent, + ) {} + + fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) { + if self.tray_icon.is_none() { + info!("Creating tray icon"); + let settings = self.settings.lock().unwrap(); + let sync_status = self.sync_status.lock().unwrap(); + let (tray_icon, menu_ids) = create_tray_icon(&settings, &sync_status); + drop(settings); + drop(sync_status); + + self.tray_icon = Some(tray_icon); + self.menu_ids = Some(menu_ids); + + self.setup_file_watcher(); + self.start_auto_sync(); + info!("KHM tray application ready"); + } + } + + fn user_event(&mut self, event_loop: &winit::event_loop::ActiveEventLoop, event: crate::gui::UserEvent) { + match event { + crate::gui::UserEvent::TrayIconEvent => {} + crate::gui::UserEvent::UpdateMenu => { + self.handle_update_menu(); + } + crate::gui::UserEvent::MenuEvent(event) => { + self.handle_menu_event(event, event_loop); + } + crate::gui::UserEvent::ConfigFileChanged => { + self.handle_config_change(); + } + } + } +} + +/// Run tray application +pub async fn run_tray_app() -> std::io::Result<()> { + #[cfg(target_os = "macos")] + let event_loop = { + use winit::platform::macos::ActivationPolicy; + EventLoop::::with_user_event() + .with_activation_policy(ActivationPolicy::Accessory) + .build() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to create event loop: {}", e)))? + }; + + #[cfg(not(target_os = "macos"))] + let event_loop = EventLoop::::with_user_event().build() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to create event loop: {}", e)))?; + + let proxy = event_loop.create_proxy(); + + // Setup event handlers + let proxy_clone = proxy.clone(); + tray_icon::TrayIconEvent::set_event_handler(Some(move |_event| { + let _ = proxy_clone.send_event(crate::gui::UserEvent::TrayIconEvent); + })); + + let proxy_clone = proxy.clone(); + MenuEvent::set_event_handler(Some(move |event: MenuEvent| { + let _ = proxy_clone.send_event(crate::gui::UserEvent::MenuEvent(event)); + })); + + let mut app = TrayApplication::new(proxy); + + event_loop.run_app(&mut app) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Event loop error: {:?}", e)))?; + + Ok(()) +} diff --git a/src/gui/tray/icon.rs b/src/gui/tray/icon.rs new file mode 100644 index 0000000..3d1c8de --- /dev/null +++ b/src/gui/tray/icon.rs @@ -0,0 +1,281 @@ +use log::{error, info}; +use std::sync::{Arc, Mutex}; +use tray_icon::{ + menu::{Menu, MenuItem, MenuId}, + TrayIcon, TrayIconBuilder, +}; +use crate::gui::common::{KhmSettings, perform_sync}; + +#[derive(Debug, Clone)] +pub struct SyncStatus { + pub last_sync_time: Option, + pub last_sync_keys: Option, + pub next_sync_in_seconds: Option, +} + +impl Default for SyncStatus { + fn default() -> Self { + Self { + last_sync_time: None, + last_sync_keys: None, + next_sync_in_seconds: None, + } + } +} + +pub struct TrayMenuIds { + pub settings_id: MenuId, + pub quit_id: MenuId, + pub sync_id: MenuId, +} + +/// Create tray icon with menu +pub fn create_tray_icon(settings: &KhmSettings, sync_status: &SyncStatus) -> (TrayIcon, TrayMenuIds) { + // Create simple blue icon + let icon_data: Vec = (0..32*32).flat_map(|i| { + let y = i / 32; + let x = i % 32; + if x < 2 || x >= 30 || y < 2 || y >= 30 { + [255, 255, 255, 255] // White border + } else { + [64, 128, 255, 255] // Blue center + } + }).collect(); + + let icon = tray_icon::Icon::from_rgba(icon_data, 32, 32).unwrap(); + let menu = Menu::new(); + + // Show current configuration status (static) + let host_text = if settings.host.is_empty() { + "Host: Not configured" + } else { + &format!("Host: {}", settings.host) + }; + menu.append(&MenuItem::new(host_text, false, None)).unwrap(); + + let flow_text = if settings.flow.is_empty() { + "Flow: Not configured" + } else { + &format!("Flow: {}", settings.flow) + }; + menu.append(&MenuItem::new(flow_text, false, None)).unwrap(); + + let is_auto_sync_enabled = !settings.host.is_empty() && !settings.flow.is_empty() && settings.in_place; + let sync_text = format!("Auto sync: {} ({}min)", + if is_auto_sync_enabled { "On" } else { "Off" }, + settings.auto_sync_interval_minutes); + menu.append(&MenuItem::new(&sync_text, false, None)).unwrap(); + + menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap(); + + // Sync Now menu item + let sync_item = MenuItem::new("Sync Now", !settings.host.is_empty() && !settings.flow.is_empty(), None); + let sync_id = sync_item.id().clone(); + menu.append(&sync_item).unwrap(); + + menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap(); + + // Settings menu item + let settings_item = MenuItem::new("Settings", true, None); + let settings_id = settings_item.id().clone(); + menu.append(&settings_item).unwrap(); + + menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap(); + + // Quit menu item + let quit_item = MenuItem::new("Quit", true, None); + let quit_id = quit_item.id().clone(); + menu.append(&quit_item).unwrap(); + + // Create initial tooltip + let tooltip = create_tooltip(settings, sync_status); + + let tray_icon = TrayIconBuilder::new() + .with_tooltip(&tooltip) + .with_icon(icon) + .with_menu(Box::new(menu)) + .build() + .unwrap(); + + let menu_ids = TrayMenuIds { + settings_id, + quit_id, + sync_id, + }; + + (tray_icon, menu_ids) +} + +/// Update tray menu with new settings +pub fn update_tray_menu(tray_icon: &TrayIcon, settings: &KhmSettings) -> TrayMenuIds { + let menu = Menu::new(); + + // Show current configuration status (static) + let host_text = if settings.host.is_empty() { + "Host: Not configured" + } else { + &format!("Host: {}", settings.host) + }; + menu.append(&MenuItem::new(host_text, false, None)).unwrap(); + + let flow_text = if settings.flow.is_empty() { + "Flow: Not configured" + } else { + &format!("Flow: {}", settings.flow) + }; + menu.append(&MenuItem::new(flow_text, false, None)).unwrap(); + + let is_auto_sync_enabled = !settings.host.is_empty() && !settings.flow.is_empty() && settings.in_place; + let sync_text = format!("Auto sync: {} ({}min)", + if is_auto_sync_enabled { "On" } else { "Off" }, + settings.auto_sync_interval_minutes); + menu.append(&MenuItem::new(&sync_text, false, None)).unwrap(); + + menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap(); + + // Sync Now menu item + let sync_item = MenuItem::new("Sync Now", !settings.host.is_empty() && !settings.flow.is_empty(), None); + let sync_id = sync_item.id().clone(); + menu.append(&sync_item).unwrap(); + + menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap(); + + // Settings menu item + let settings_item = MenuItem::new("Settings", true, None); + let settings_id = settings_item.id().clone(); + menu.append(&settings_item).unwrap(); + + menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap(); + + // Quit menu item + let quit_item = MenuItem::new("Quit", true, None); + let quit_id = quit_item.id().clone(); + menu.append(&quit_item).unwrap(); + + tray_icon.set_menu(Some(Box::new(menu))); + + TrayMenuIds { + settings_id, + quit_id, + sync_id, + } +} + +/// Create tooltip text for tray icon +pub fn create_tooltip(settings: &KhmSettings, sync_status: &SyncStatus) -> String { + let mut tooltip = format!("KHM - SSH Key Manager\nHost: {}\nFlow: {}", settings.host, settings.flow); + + if let Some(keys_count) = sync_status.last_sync_keys { + tooltip.push_str(&format!("\nLast sync: {} keys", keys_count)); + } else { + tooltip.push_str("\nLast sync: Never"); + } + + if let Some(seconds) = sync_status.next_sync_in_seconds { + if seconds > 60 { + tooltip.push_str(&format!("\nNext sync: {}m {}s", seconds / 60, seconds % 60)); + } else { + tooltip.push_str(&format!("\nNext sync: {}s", seconds)); + } + } + + tooltip +} + +/// Start auto sync background task +pub fn start_auto_sync_task( + settings: Arc>, + sync_status: Arc>, + event_sender: winit::event_loop::EventLoopProxy +) -> Option> { + let initial_settings = settings.lock().unwrap().clone(); + + // Only start auto sync if settings are valid and in_place is enabled + if initial_settings.host.is_empty() || initial_settings.flow.is_empty() || !initial_settings.in_place { + info!("Auto sync disabled or settings invalid"); + return None; + } + + info!("Starting auto sync with interval {} minutes", initial_settings.auto_sync_interval_minutes); + + let handle = std::thread::spawn(move || { + // Initial sync on startup + info!("Performing initial sync on startup"); + let current_settings = settings.lock().unwrap().clone(); + if !current_settings.host.is_empty() && !current_settings.flow.is_empty() { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + match perform_sync(¤t_settings).await { + Ok(keys_count) => { + info!("Initial sync completed successfully with {} keys", keys_count); + let mut status = sync_status.lock().unwrap(); + status.last_sync_time = Some(std::time::Instant::now()); + status.last_sync_keys = Some(keys_count); + let _ = event_sender.send_event(crate::gui::UserEvent::UpdateMenu); + } + Err(e) => { + error!("Initial sync failed: {}", e); + } + } + }); + } + + // Start menu update timer + let timer_sender = event_sender.clone(); + std::thread::spawn(move || { + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + let _ = timer_sender.send_event(crate::gui::UserEvent::UpdateMenu); + } + }); + + // Periodic sync + loop { + let interval_minutes = current_settings.auto_sync_interval_minutes; + std::thread::sleep(std::time::Duration::from_secs(interval_minutes as u64 * 60)); + + let current_settings = settings.lock().unwrap().clone(); + if current_settings.host.is_empty() || current_settings.flow.is_empty() || !current_settings.in_place { + info!("Auto sync stopped due to invalid settings or disabled in_place"); + break; + } + + info!("Performing scheduled auto sync"); + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + match perform_sync(¤t_settings).await { + Ok(keys_count) => { + info!("Auto sync completed successfully with {} keys", keys_count); + let mut status = sync_status.lock().unwrap(); + status.last_sync_time = Some(std::time::Instant::now()); + status.last_sync_keys = Some(keys_count); + let _ = event_sender.send_event(crate::gui::UserEvent::UpdateMenu); + } + Err(e) => { + error!("Auto sync failed: {}", e); + } + } + }); + } + }); + + Some(handle) +} + +/// Update sync status for tooltip +pub fn update_sync_status(settings: &KhmSettings, sync_status: &mut SyncStatus) { + if !settings.host.is_empty() && !settings.flow.is_empty() && settings.in_place { + if let Some(last_sync) = sync_status.last_sync_time { + let elapsed = last_sync.elapsed().as_secs(); + let interval_seconds = settings.auto_sync_interval_minutes as u64 * 60; + + if elapsed < interval_seconds { + sync_status.next_sync_in_seconds = Some(interval_seconds - elapsed); + } else { + sync_status.next_sync_in_seconds = Some(0); + } + } else { + sync_status.next_sync_in_seconds = None; + } + } +} diff --git a/src/gui/tray/mod.rs b/src/gui/tray/mod.rs new file mode 100644 index 0000000..661c6bf --- /dev/null +++ b/src/gui/tray/mod.rs @@ -0,0 +1,6 @@ +mod app; +mod icon; + +pub use app::*; +pub use icon::{SyncStatus, TrayMenuIds, create_tray_icon, update_tray_menu, + create_tooltip, start_auto_sync_task, update_sync_status}; diff --git a/src/main.rs b/src/main.rs index a0857da..7488926 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod client; mod db; mod server; mod web; +mod gui; use clap::Parser; use env_logger; @@ -10,7 +11,7 @@ use log::{error, info}; /// This application manages SSH keys and flows, either as a server or client. /// In server mode, it stores keys and flows in a PostgreSQL database. /// In client mode, it sends keys to the server and can update the known_hosts file with keys from the server. -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Clone)] #[command( author = env!("CARGO_PKG_AUTHORS"), version = env!("CARGO_PKG_VERSION"), @@ -26,21 +27,29 @@ use log::{error, info}; \n\ " )] -struct Args { +pub struct Args { /// Run in server mode (default: false) #[arg(long, help = "Run in server mode")] - server: bool, + pub server: bool, + + /// Run with GUI tray interface (default: false) + #[arg(long, help = "Run with GUI tray interface")] + pub gui: bool, + + /// Run settings UI window (used with --gui) + #[arg(long, help = "Run settings UI window (used with --gui)")] + pub settings_ui: bool, /// Update the known_hosts file with keys from the server after sending keys (default: false) #[arg( long, help = "Server mode: Sync the known_hosts file with keys from the server" )] - in_place: bool, + pub in_place: bool, /// Comma-separated list of flows to manage (default: default) #[arg(long, default_value = "default", value_parser, num_args = 1.., value_delimiter = ',', help = "Server mode: Comma-separated list of flows to manage")] - flows: Vec, + pub flows: Vec, /// IP address to bind the server or client to (default: 127.0.0.1) #[arg( @@ -49,7 +58,7 @@ struct Args { default_value = "127.0.0.1", help = "Server mode: IP address to bind the server to" )] - ip: String, + pub ip: String, /// Port to bind the server or client to (default: 8080) #[arg( @@ -58,7 +67,7 @@ struct Args { default_value = "8080", help = "Server mode: Port to bind the server to" )] - port: u16, + pub port: u16, /// Hostname or IP address of the PostgreSQL database (default: 127.0.0.1) #[arg( @@ -66,7 +75,7 @@ struct Args { default_value = "127.0.0.1", help = "Server mode: Hostname or IP address of the PostgreSQL database" )] - db_host: String, + pub db_host: String, /// Name of the PostgreSQL database (default: khm) #[arg( @@ -74,7 +83,7 @@ struct Args { default_value = "khm", help = "Server mode: Name of the PostgreSQL database" )] - db_name: String, + pub db_name: String, /// Username for the PostgreSQL database (required in server mode) #[arg( @@ -82,7 +91,7 @@ struct Args { required_if_eq("server", "true"), help = "Server mode: Username for the PostgreSQL database" )] - db_user: Option, + pub db_user: Option, /// Password for the PostgreSQL database (required in server mode) #[arg( @@ -90,7 +99,7 @@ struct Args { required_if_eq("server", "true"), help = "Server mode: Password for the PostgreSQL database" )] - db_password: Option, + pub db_password: Option, /// Host address of the server to connect to in client mode (required in client mode) #[arg( @@ -98,7 +107,7 @@ struct Args { required_if_eq("server", "false"), help = "Client mode: Full host address of the server to connect to. Like https://khm.example.com" )] - host: Option, + pub host: Option, /// Flow name to use on the server #[arg( @@ -106,7 +115,7 @@ struct Args { required_if_eq("server", "false"), help = "Client mode: Flow name to use on the server" )] - flow: Option, + pub flow: Option, /// Path to the known_hosts file (default: ~/.ssh/known_hosts) #[arg( @@ -114,24 +123,66 @@ struct Args { default_value = "~/.ssh/known_hosts", help = "Client mode: Path to the known_hosts file" )] - known_hosts: String, + pub known_hosts: String, /// Basic auth string for client mode. Format: user:pass #[arg(long, default_value = "", help = "Client mode: Basic Auth credentials")] - basic_auth: String, + pub basic_auth: String, } #[actix_web::main] async fn main() -> std::io::Result<()> { - env_logger::init(); + // Configure logging to show only khm logs, filtering out noisy library logs + env_logger::Builder::from_default_env() + .filter_level(log::LevelFilter::Warn) // Default level for all modules + .filter_module("khm", log::LevelFilter::Debug) // Our app logs + .filter_module("actix_web", log::LevelFilter::Info) // Server logs + .filter_module("reqwest", log::LevelFilter::Warn) // HTTP client + .filter_module("winit", log::LevelFilter::Error) // Window management + .filter_module("egui", log::LevelFilter::Error) // GUI framework + .filter_module("eframe", log::LevelFilter::Error) // GUI framework + .filter_module("tray_icon", log::LevelFilter::Error) // Tray icon + .filter_module("wgpu", log::LevelFilter::Error) // Graphics + .filter_module("naga", log::LevelFilter::Error) // Graphics + .filter_module("glow", log::LevelFilter::Error) // Graphics + .filter_module("tracing", log::LevelFilter::Error) // Tracing spans + .init(); + info!("Starting SSH Key Manager"); let args = Args::parse(); - // Check if we have the minimum required arguments - if !args.server && (args.host.is_none() || args.flow.is_none()) { - // Neither server mode nor client mode properly configured - eprintln!("Error: You must specify either server mode (--server) or client mode (--host and --flow)"); + // Settings UI mode - just show settings window and exit + if args.settings_ui { + #[cfg(feature = "gui")] + { + info!("Running settings UI window"); + gui::run_settings_window(); + return Ok(()); + } + #[cfg(not(feature = "gui"))] + { + error!("GUI features not compiled. Install system dependencies and rebuild with --features gui"); + return Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "GUI features not compiled" + )); + } + } + + // GUI mode has priority + if args.gui { + info!("Running in GUI mode"); + if let Err(e) = gui::run_gui().await { + error!("Failed to run GUI: {}", e); + } + return Ok(()); + } + + // Check if we have the minimum required arguments for server/client mode + if !args.server && !args.gui && (args.host.is_none() || args.flow.is_none()) { + // Neither server mode nor client mode nor GUI mode properly configured + eprintln!("Error: You must specify either server mode (--server), client mode (--host and --flow), or GUI mode (--gui)"); eprintln!(); eprintln!("Examples:"); eprintln!( @@ -142,6 +193,14 @@ async fn main() -> std::io::Result<()> { " Client mode: {} --host https://khm.example.com --flow work", env!("CARGO_PKG_NAME") ); + eprintln!( + " GUI mode: {} --gui", + env!("CARGO_PKG_NAME") + ); + eprintln!( + " Settings window: {} --gui --settings-ui", + env!("CARGO_PKG_NAME") + ); eprintln!(); eprintln!("Use --help for more information."); std::process::exit(1); diff --git a/static/.DS_Store b/static/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/static/.DS_Store differ diff --git a/static/khm-icon.svg b/static/khm-icon.svg new file mode 100644 index 0000000..5713aa3 --- /dev/null +++ b/static/khm-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + K +