From efd6539a43b31f349ec5e8369244abaae951cb72 Mon Sep 17 00:00:00 2001 From: htngr <124245785+htngr@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:20:01 +0100 Subject: [PATCH 01/49] egui init --- build/build-rust-package.nix | 7 + codchi/Cargo.lock | 2987 ++++++++++++++++++++++++++++------ codchi/Cargo.toml | 7 +- codchi/default.nix | 1 + codchi/shell.nix | 13 + codchi/src/cli.rs | 8 +- codchi/src/gui/mod.rs | 53 + codchi/src/main.rs | 3 + 8 files changed, 2623 insertions(+), 456 deletions(-) create mode 100644 codchi/src/gui/mod.rs diff --git a/build/build-rust-package.nix b/build/build-rust-package.nix index a91d2bed..6d344c52 100644 --- a/build/build-rust-package.nix +++ b/build/build-rust-package.nix @@ -19,6 +19,10 @@ , pkg-config , gtk3 , libayatana-appindicator +, xorg +, libxkbcommon +, libGL +, libGLU , llvmPackages , cargo-xwin @@ -194,6 +198,9 @@ let buildInputs = [ gtk3 libayatana-appindicator.out + libxkbcommon.out + libGL.out + libGLU.out ]; outputs = [ "out" "docs" ]; diff --git a/codchi/Cargo.lock b/codchi/Cargo.lock index 3a7985fb..bb804e7a 100644 --- a/codchi/Cargo.lock +++ b/codchi/Cargo.lock @@ -2,6 +2,114 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "accesskit" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d3b8f9bae46a948369bc4a03e815d4ed6d616bd00de4051133a5019dc31c5a" + +[[package]] +name = "accesskit_atspi_common" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5dd55e6e94949498698daf4d48fb5659e824d7abec0d394089656ceaf99d4f" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "serde", + "thiserror", + "zvariant 4.2.0", +] + +[[package]] +name = "accesskit_consumer" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47983a1084940ba9a39c077a8c63e55c619388be5476ac04c804cfbd1e63459" +dependencies = [ + "accesskit", + "hashbrown 0.15.2", + "immutable-chunkmap", +] + +[[package]] +name = "accesskit_macos" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7329821f3bd1101e03a7d2e03bd339e3ac0dc64c70b4c9f9ae1949e3ba8dece1" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.15.2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "accesskit_unix" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcee751cc20d88678c33edaf9c07e8b693cd02819fe89053776f5313492273f5" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus 4.4.0", +] + +[[package]] +name = "accesskit_windows" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24fcd5d23d70670992b823e735e859374d694a3d12bfd8dd32bd3bd8bedb5d81" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.15.2", + "paste", + "static_assertions", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "accesskit_winit" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6a48dad5530b6deb9fc7a52cc6c3bf72cdd9eb8157ac9d32d69f2427a5e879" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -17,6 +125,19 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -26,6 +147,33 @@ dependencies = [ "memchr", ] +[[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.8.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror", +] + +[[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" @@ -43,9 +191,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -58,52 +206,95 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" dependencies = [ "backtrace", ] +[[package]] +name = "arboard" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" +dependencies = [ + "clipboard-win", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "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.6", +] + [[package]] name = "async-broadcast" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", @@ -149,9 +340,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", @@ -204,7 +395,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -233,20 +424,20 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] name = "atk" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" dependencies = [ "atk-sys", "glib", @@ -255,9 +446,9 @@ dependencies = [ [[package]] name = "atk-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" dependencies = [ "glib-sys", "gobject-sys", @@ -271,6 +462,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 4.4.0", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[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 4.4.0", +] + +[[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 4.4.0", + "zvariant 4.2.0", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -298,6 +540,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -306,9 +563,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] @@ -362,15 +619,29 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] [[package]] name = "byteorder" @@ -386,9 +657,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.7.2" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "cairo-rs" @@ -396,7 +667,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cairo-sys-rs", "glib", "libc", @@ -415,6 +686,32 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.8.0", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + [[package]] name = "carapace_spec_clap" version = "1.1.0" @@ -423,17 +720,19 @@ checksum = "09a6810b1aada7fb10830104d1d5dd8019fbdefd5e51bae3961cc5b58f458023" dependencies = [ "clap", "clap_complete", - "indexmap 2.6.0", + "indexmap 2.7.1", "serde", "serde_yaml_ng", ] [[package]] name = "cc" -version = "1.1.28" +version = "1.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" +checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -459,17 +758,32 @@ 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" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -480,9 +794,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" dependencies = [ "clap_builder", "clap_derive", @@ -490,9 +804,9 @@ dependencies = [ [[package]] name = "clap-verbosity-flag" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e099138e1807662ff75e2cebe4ae2287add879245574489f9b1588eb5e5564ed" +checksum = "34c77f67047557f62582784fd7482884697731b2932c7d37ced54bce2312e1e2" dependencies = [ "clap", "log", @@ -500,9 +814,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" dependencies = [ "anstream", "anstyle", @@ -512,9 +826,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.32" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a01f4f9ee6c066d42a1c8dedf0dcddad16c72a8981a309d6398de3a75b0c39" +checksum = "1e3040c8291884ddf39445dc033c70abc2bc44a42f0a3a00571a0f483a83f0cd" dependencies = [ "clap", ] @@ -544,9 +858,9 @@ dependencies = [ [[package]] name = "clap_complete_nushell" -version = "4.5.3" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe32110e006bccf720f8c9af3fee1ba7db290c724eab61544e1d3295be3a40e" +checksum = "c6a8b1593457dfc2fe539002b795710d022dc62a65bf15023f039f9760c7b18a" dependencies = [ "clap", "clap_complete", @@ -554,43 +868,52 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clap_mangen" -version = "0.2.23" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" +checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" dependencies = [ "clap", "roff", ] +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + [[package]] name = "cocoa" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block", "cocoa-foundation", - "core-foundation", - "core-graphics", + "core-foundation 0.10.0", + "core-graphics 0.24.0", "foreign-types", "libc", "objc", @@ -602,10 +925,10 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block", - "core-foundation", - "core-graphics-types", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", "libc", "objc", ] @@ -628,6 +951,9 @@ dependencies = [ "ctrlc", "directories", "duct", + "eframe", + "egui", + "egui_extras", "embed-manifest", "env_logger", "freedesktop_entry_parser", @@ -639,7 +965,7 @@ dependencies = [ "indicatif-log-bridge", "indoc", "inquire", - "itertools", + "itertools 0.13.0", "known-folders", "lazy-regex", "log", @@ -658,7 +984,7 @@ dependencies = [ "tao", "thiserror", "throttle", - "toml_edit 0.22.22", + "toml_edit 0.22.24", "tray-icon", "uuid", "version-compare", @@ -667,11 +993,21 @@ dependencies = [ "wslapi", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width 0.1.14", +] + [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" @@ -685,14 +1021,13 @@ dependencies = [ [[package]] name = "comfy-table" -version = "7.1.1" +version = "7.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" +checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" dependencies = [ - "crossterm 0.27.0", - "strum", - "strum_macros", - "unicode-width", + "crossterm 0.28.1", + "unicode-segmentation", + "unicode-width 0.2.0", ] [[package]] @@ -706,15 +1041,25 @@ dependencies = [ [[package]] name = "console" -version = "0.15.8" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "unicode-width", - "windows-sys 0.52.0", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -733,35 +1078,59 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[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 0.1.3", + "foreign-types", + "libc", +] + [[package]] name = "core-graphics" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.6.0", - "core-foundation", - "core-graphics-types", + "bitflags 2.8.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", "foreign-types", "libc", ] [[package]] name = "core-graphics-types" -version = "0.2.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ - "bitflags 2.6.0", - "core-foundation", + "bitflags 1.3.2", + "core-foundation 0.9.4", "libc", ] [[package]] -name = "cpufeatures" -version = "0.2.14" +name = "core-graphics-types" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -777,18 +1146,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -805,9 +1174,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" @@ -827,14 +1196,14 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "crossterm_winapi", - "libc", "parking_lot", + "rustix", "winapi", ] @@ -867,6 +1236,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + [[package]] name = "darling" version = "0.20.10" @@ -888,7 +1263,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -899,7 +1274,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -979,6 +1354,26 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.7.4", +] + [[package]] name = "dlopen2" version = "0.7.0" @@ -999,9 +1394,24 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", +] + +[[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.1" @@ -1022,9 +1432,141 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.17" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" + +[[package]] +name = "ecolor" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d72e9c39f6e11a2e922d04a34ec5e7ef522ea3f5a1acfca7a19d16ad5fe50f5" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d9e7ea2d11ec9e98a8683b6eb99f9d7d0448394ef6e0d6d91bd4eb817220" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glow 0.16.0", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "profiling", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winapi", + "windows-sys 0.59.0", + "winit", +] + +[[package]] +name = "egui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252d52224d35be1535d7fd1d6139ce071fb42c9097773e79f7665604f5596b5e" +dependencies = [ + "accesskit", + "ahash", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", +] + +[[package]] +name = "egui-wgpu" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c1e821d2d8921ef6ce98b258c7e24d9d6aab2ca1f9cdf374eca997e7f67f59" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e84c2919cd9f3a38a91e8f84ac6a245c19251fd95226ed9fae61d5ea564fce3" +dependencies = [ + "accesskit_winit", + "ahash", + "arboard", + "egui", + "log", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_extras" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7a8198c088b1007108cb2d403bc99a5e370999b200db4f14559610d7330126" +dependencies = [ + "ahash", + "egui", + "enum-map", + "log", + "mime_guess2", + "profiling", +] + +[[package]] +name = "egui_glow" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +checksum = "3eaf6264cc7608e3e69a7d57a6175f438275f1b3889c1a551b418277721c95e6" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow 0.16.0", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", + "winit", +] [[package]] name = "either" @@ -1032,6 +1574,15 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "emath" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4fe73c1207b864ee40aa0b0c038d6092af1030744678c60188a05c28553515d" +dependencies = [ + "bytemuck", +] + [[package]] name = "embed-manifest" version = "1.4.0" @@ -1040,9 +1591,9 @@ checksum = "41cd446c890d6bed1d8b53acef5f240069ebef91d6fae7c5f52efe61fe8b5eae" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "endi" @@ -1050,11 +1601,32 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "enumflags2" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" dependencies = [ "enumflags2_derive", "serde", @@ -1062,20 +1634,20 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -1083,9 +1655,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", @@ -1094,27 +1666,57 @@ dependencies = [ "log", ] +[[package]] +name = "epaint" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5666f8d25236293c966fbb3635eac18b04ad1914e3bab55bc7d44b9980cafcac" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66f6ddac3e6ac6fd4c3d48bb8b1943472f8da0f43a4303bcd8a18aa594401c80" + [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] +[[package]] +name = "error-code" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" + [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -1123,9 +1725,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener", "pin-project-lite", @@ -1133,15 +1735,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -1158,9 +1760,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1172,6 +1774,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "foreign-types" version = "0.5.0" @@ -1190,7 +1798,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -1262,9 +1870,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", @@ -1281,7 +1889,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -1333,9 +1941,9 @@ dependencies = [ [[package]] name = "gdk" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -1374,9 +1982,9 @@ dependencies = [ [[package]] name = "gdk-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1391,9 +1999,9 @@ dependencies = [ [[package]] name = "gdkwayland-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90fbf5c033c65d93792192a49a8efb5bb1e640c419682a58bb96f5ae77f3d4a" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" dependencies = [ "gdk-sys", "glib-sys", @@ -1405,9 +2013,9 @@ dependencies = [ [[package]] name = "gdkx11-sys" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee8f00f4ee46cad2939b8990f5c70c94ff882c3028f3cc5abf950fa4ab53043" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" dependencies = [ "gdk-sys", "glib-sys", @@ -1426,6 +2034,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" @@ -1434,7 +2052,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] [[package]] @@ -1478,21 +2108,31 @@ dependencies = [ [[package]] name = "git-url-parse" version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d7ff03a34ea818a59cf30c0d7aa55354925484fa30bcc4cb96d784ff07578f" +source = "git+https://github.com/tjtelan/git-url-parse-rs#23031bfa3e7d0cbba17a2ccbe26f348c35f5c524" dependencies = [ "strum", "thiserror", "url", ] +[[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", + "bitflags 2.8.0", "futures-channel", "futures-core", "futures-executor", @@ -1520,7 +2160,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -1534,86 +2174,218 @@ dependencies = [ ] [[package]] -name = "gobject-sys" -version = "0.18.0" +name = "glow" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" dependencies = [ - "glib-sys", - "libc", - "system-deps", + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "gtk" -version = "0.18.1" +name = "glow" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "gtk-sys" -version = "0.18.0" +name = "glutin" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +checksum = "03642b8b0cce622392deb0ee3e88511f75df2daac806102597905c3ea1974848" dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", + "bitflags 2.8.0", + "cfg_aliases 0.2.1", + "cgl", + "core-foundation 0.9.4", + "dispatch", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading 0.8.6", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", ] [[package]] -name = "gtk3-macros" -version = "0.18.0" +name = "glutin-winit" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.79", + "cfg_aliases 0.2.1", + "glutin", + "raw-window-handle", + "winit", ] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "glutin_egl_sys" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] [[package]] -name = "hashbrown" -version = "0.15.0" +name = "glutin_glx_sys" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] [[package]] -name = "heck" -version = "0.4.1" +name = "glutin_wgl_sys" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - +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.8.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.8.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" +dependencies = [ + "bitflags 2.8.0", + "gpu-descriptor-types", + "hashbrown 0.15.2", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.8.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.98", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1632,13 +2404,19 @@ 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 = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1686,6 +2464,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1694,19 +2590,30 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", ] [[package]] name = "image" -version = "0.25.2" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -1714,6 +2621,15 @@ dependencies = [ "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 = "1.9.3" @@ -1727,26 +2643,26 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.2", "serde", ] [[package]] name = "indicatif" -version = "0.17.8" +version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ "console", - "instant", "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.0", + "web-time", ] [[package]] @@ -1771,7 +2687,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "crossterm 0.25.0", "dyn-clone", "fuzzy-matcher", @@ -1779,7 +2695,7 @@ dependencies = [ "newline-converter", "once_cell", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1806,11 +2722,20 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jni" @@ -1834,12 +2759,22 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1849,11 +2784,28 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "serde", "unicode-segmentation", ] +[[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.6", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "known-folders" version = "1.2.0" @@ -1865,9 +2817,9 @@ dependencies = [ [[package]] name = "lazy-regex" -version = "3.3.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d8e41c97e6bc7ecb552016274b99fbb5d035e8de288c582d9b933af6677bfda" +checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" dependencies = [ "lazy-regex-proc_macros", "once_cell", @@ -1876,14 +2828,14 @@ dependencies = [ [[package]] name = "lazy-regex-proc_macros" -version = "3.3.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e1d8b05d672c53cb9c7b920bbba8783845ae4f0b076e02a3db1d02c81b4163" +checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -1912,15 +2864,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] [[package]] name = "libc" -version = "0.2.159" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -1932,21 +2884,44 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", + "redox_syscall 0.5.8", ] [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" @@ -1960,9 +2935,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "mac-notification-sys" @@ -1992,6 +2967,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2001,6 +2985,37 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.8.0", + "block", + "core-graphics-types 0.1.3", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a3333bb1609500601edc766a39b4c1772874a4ce26022f4d866854dc020c41" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minidl" version = "0.1.6" @@ -2015,9 +3030,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" dependencies = [ "adler2", "simd-adler32", @@ -2031,7 +3046,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -2048,9 +3063,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.15.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8123dfd4996055ac9b15a60ad263b44b01e539007523ad7a4a533a3d93b0591" +checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" dependencies = [ "crossbeam-channel", "dpi", @@ -2065,16 +3080,37 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "naga" +version = "23.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.8.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "hexf-parse", + "indexmap 2.7.1", + "log", + "rustc-hash", + "spirv", + "termcolor", + "thiserror", + "unicode-xid", +] + [[package]] name = "ndk" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "jni-sys", "log", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", "thiserror", @@ -2086,6 +3122,15 @@ 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" @@ -2110,13 +3155,19 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", - "cfg_aliases", + "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 = "nom" version = "7.1.3" @@ -2129,15 +3180,16 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.11.3" +version = "4.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5134a72dc570b178bff81b01e81ab14a6fcc015391ed4b3b14853090658cd3a3" +checksum = "7fa3b9f2364a09bd359aa0206702882e208437450866a374d5372d64aece4029" dependencies = [ + "futures-lite", "log", "mac-notification-sys", "serde", "tauri-winrt-notification", - "zbus", + "zbus 5.5.0", ] [[package]] @@ -2182,7 +3234,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -2233,7 +3285,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "libc", "objc2", @@ -2243,13 +3295,37 @@ dependencies = [ "objc2-quartz-core", ] +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + [[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", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -2267,11 +3343,23 @@ dependencies = [ "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", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" @@ -2279,19 +3367,32 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", + "dispatch", "libc", "objc2", ] +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + [[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", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -2303,13 +3404,68 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", "objc2-metal", ] +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "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", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -2321,18 +3477,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "option-ext" @@ -2340,6 +3496,15 @@ 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" @@ -2352,9 +3517,9 @@ dependencies = [ [[package]] name = "os_info" -version = "3.9.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ca711d8b83edbb00b44d504503cd247c9c0bd8b0fa2694f2a1a3d8165379ce" +checksum = "2a604e53c24761286860eba4e2c8b23a0161526476b1de520139d69cdb85a6b5" dependencies = [ "log", "serde", @@ -2371,6 +3536,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[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" @@ -2420,11 +3594,17 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.8", "smallvec", "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2438,17 +3618,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "rand", ] +[[package]] +name = "pin-project" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2475,9 +3675,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "png" -version = "0.17.14" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -2488,9 +3688,9 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", @@ -2503,9 +3703,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "powerfmt" @@ -2547,7 +3747,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.22.22", + "toml_edit 0.22.24", ] [[package]] @@ -2582,13 +3782,29 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" + +[[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.31.0" @@ -2598,11 +3814,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +dependencies = [ + "memchr", +] + [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2634,7 +3859,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -2665,11 +3890,20 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags 2.6.0", + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.8.0", ] [[package]] @@ -2678,16 +3912,16 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2697,9 +3931,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2712,6 +3946,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + [[package]] name = "roff" version = "0.2.2" @@ -2724,6 +3964,12 @@ 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_version" version = "0.4.1" @@ -2735,28 +3981,28 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "safe-proc-macro2" @@ -2814,43 +4060,62 @@ dependencies = [ "winapi-util", ] +[[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 = "semver" -version = "1.0.23" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" dependencies = [ "itoa", "memchr", @@ -2866,7 +4131,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -2880,15 +4145,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.6.0", + "indexmap 2.7.1", "serde", "serde_derive", "serde_json", @@ -2898,14 +4163,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -2914,7 +4179,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.1", "itoa", "ryu", "serde", @@ -2949,61 +4214,130 @@ dependencies = [ ] [[package]] -name = "shlex" -version = "1.3.0" +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +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.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] -name = "signal-hook" -version = "0.3.17" +name = "smithay-client-toolkit" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ + "bitflags 2.8.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", "libc", - "signal-hook-registry", + "log", + "memmap2", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", ] [[package]] -name = "signal-hook-mio" -version = "0.2.4" +name = "smithay-clipboard" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" dependencies = [ "libc", - "mio", - "signal-hook", + "smithay-client-toolkit", + "wayland-backend", ] [[package]] -name = "signal-hook-registry" -version = "1.4.2" +name = "smol_str" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" dependencies = [ - "libc", + "serde", ] [[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "slab" -version = "0.4.9" +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "autocfg", + "bitflags 2.8.0", ] [[package]] -name = "smallvec" -version = "1.13.2" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" @@ -3011,6 +4345,12 @@ 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 = "strsim" version = "0.11.1" @@ -3036,7 +4376,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -3052,20 +4392,31 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "sysinfo" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" dependencies = [ "core-foundation-sys", "libc", @@ -3090,14 +4441,14 @@ dependencies = [ [[package]] name = "tao" -version = "0.30.3" +version = "0.30.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0dbbebe82d02044dfa481adca1550d6dd7bd16e086bc34fa0fbecceb5a63751" +checksum = "6682a07cf5bab0b8a2bd20d0a542917ab928b5edb75ebd4eda6b05cbaab872da" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cocoa", - "core-foundation", - "core-graphics", + "core-foundation 0.10.0", + "core-graphics 0.24.0", "crossbeam-channel", "dispatch", "dlopen2", @@ -3112,7 +4463,7 @@ dependencies = [ "log", "ndk", "ndk-context", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "objc", "once_cell", "parking_lot", @@ -3134,7 +4485,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -3149,42 +4500,52 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f89f5fb70d6f62381f5d9b2ba9008196150b40b75f3068eb24faeddf1c686871" dependencies = [ - "quick-xml", + "quick-xml 0.31.0", "windows 0.56.0", "windows-version", ] [[package]] name = "tempfile" -version = "3.13.0" +version = "3.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" dependencies = [ "cfg-if", "fastrand", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.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.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -3205,9 +4566,9 @@ checksum = "0b7d2d6a110f96a589b87d0f6c46d4048c84a01a7b81a2dcc97656694ace28b1" [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -3226,39 +4587,59 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", ] [[package]] -name = "tinyvec" -version = "1.8.0" +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 = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" dependencies = [ - "tinyvec_macros", + "arrayref", + "bytemuck", + "strict-num", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] [[package]] name = "toml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.22", + "toml_edit 0.22.24", ] [[package]] @@ -3276,7 +4657,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.1", "toml_datetime", "winnow 0.5.40", ] @@ -3287,29 +4668,29 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.1", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.20", + "winnow 0.7.3", ] [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3318,31 +4699,31 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] [[package]] name = "tray-icon" -version = "0.19.0" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533fc2d4105e0e3d96ce1c71f2d308c9fbbe2ef9c587cab63dd627ab5bde218f" +checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b" dependencies = [ - "core-graphics", + "core-graphics 0.24.0", "crossbeam-channel", "dirs", "libappindicator", @@ -3356,11 +4737,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash", +] + [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "uds_windows" @@ -3374,25 +4770,16 @@ dependencies = [ ] [[package]] -name = "unicode-bidi" -version = "0.3.17" +name = "unicase" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "unicode-normalization" -version = "0.1.24" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] name = "unicode-segmentation" @@ -3406,6 +4793,18 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3414,15 +4813,27 @@ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -3431,11 +4842,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1" dependencies = [ - "getrandom", + "getrandom 0.3.1", "sha1_smol", ] @@ -3467,37 +4878,59 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3505,22 +4938,134 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" +dependencies = [ + "bitflags 2.8.0", + "rustix", + "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.8.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" +dependencies = [ + "bitflags 2.8.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3" +dependencies = [ + "bitflags 2.8.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" +dependencies = [ + "bitflags 2.8.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.2", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] [[package]] name = "wchar" @@ -3533,15 +5078,153 @@ dependencies = [ ] [[package]] -name = "wchar-impl" -version = "0.6.0" +name = "wchar-impl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f135922b9303f899bfa446fce1eb149f43462f1e9ac7f50e24ea6b913416dd84" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9fe1ebb156110ff855242c1101df158b822487e4957b0556d9ffce9db0f535" +dependencies = [ + "block2", + "core-foundation 0.10.0", + "home", + "jni", + "log", + "ndk-context", + "objc2", + "objc2-foundation", + "url", + "web-sys", +] + +[[package]] +name = "wgpu" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a" +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 = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.8.0", + "cfg_aliases 0.1.1", + "document-features", + "indexmap 2.7.1", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash", + "smallvec", + "thiserror", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bitflags 2.8.0", + "bytemuck", + "cfg_aliases 0.1.1", + "core-graphics-types 0.1.3", + "glow 0.14.2", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.6", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash", + "smallvec", + "thiserror", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows 0.58.0", +] + +[[package]] +name = "wgpu-types" +version = "23.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f135922b9303f899bfa446fce1eb149f43462f1e9ac7f50e24ea6b913416dd84" +checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068" dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", + "bitflags 2.8.0", + "js-sys", + "web-sys", ] [[package]] @@ -3671,7 +5354,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -3682,7 +5365,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -3693,7 +5376,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -3704,7 +5387,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -3715,7 +5398,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -3726,7 +5409,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", ] [[package]] @@ -3832,20 +5515,36 @@ 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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +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-version" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" +checksum = "c12476c23a74725c539b24eae8bfc0dac4029c39cdb561d9f23616accd4ae26d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.0", ] [[package]] @@ -3866,6 +5565,12 @@ 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" @@ -3884,6 +5589,12 @@ 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" @@ -3902,12 +5613,24 @@ 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" @@ -3926,6 +5649,12 @@ 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" @@ -3944,6 +5673,12 @@ 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" @@ -3962,6 +5697,12 @@ 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" @@ -3980,6 +5721,64 @@ 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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.8.0", + "block2", + "bytemuck", + "calloop", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix", + "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" @@ -3991,9 +5790,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" dependencies = [ "memchr", ] @@ -4004,6 +5803,27 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wslapi" version = "0.1.3" @@ -4036,6 +5856,33 @@ dependencies = [ "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.6", + "once_cell", + "rustix", + "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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" + [[package]] name = "xdg-home" version = "1.3.0" @@ -4046,6 +5893,55 @@ dependencies = [ "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.8.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.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + [[package]] name = "zbus" version = "4.4.0" @@ -4079,9 +5975,69 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" +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-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.7.3", + "xdg-home", + "zbus_macros 5.5.0", + "zbus_names 4.2.0", + "zvariant 5.4.0", +] + +[[package]] +name = "zbus-lockstep" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" +dependencies = [ + "zbus_xml", + "zvariant 4.2.0", +] + +[[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.98", + "zbus-lockstep", + "zbus_xml", + "zvariant 4.2.0", ] [[package]] @@ -4093,8 +6049,23 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.79", - "zvariant_utils", + "syn 2.0.98", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.98", + "zbus_names 4.2.0", + "zvariant 5.4.0", + "zvariant_utils 3.2.0", ] [[package]] @@ -4105,7 +6076,32 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.3", + "zvariant 5.4.0", +] + +[[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 3.0.0", + "zvariant 4.2.0", ] [[package]] @@ -4126,7 +6122,50 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] @@ -4139,7 +6178,22 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "winnow 0.7.3", + "zvariant_derive 5.4.0", + "zvariant_utils 3.2.0", ] [[package]] @@ -4151,8 +6205,21 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.79", - "zvariant_utils", + "syn 2.0.98", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.98", + "zvariant_utils 3.2.0", ] [[package]] @@ -4163,5 +6230,19 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.98", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.98", + "winnow 0.7.3", ] diff --git a/codchi/Cargo.toml b/codchi/Cargo.toml index 084bdb5f..643cf347 100644 --- a/codchi/Cargo.toml +++ b/codchi/Cargo.toml @@ -52,7 +52,7 @@ tray-icon = { version = "0.19", default-features = false } image = { version = "0.25", features = ["png"], default-features = false } tao = { version = "0.30", default-features = false } fs4 = { version = "0.9", features = ["sync"] } -git-url-parse = "0.4.5" +git-url-parse = { git = "https://github.com/tjtelan/git-url-parse-rs" } toml_edit = { version = "0.22.22", features = ["serde"] } which = "6.0.3" notify-rust = "4.11.3" @@ -61,6 +61,9 @@ human-panic = "2.0.2" # termimad = "0.30.0" rand = "0.8.5" ctrlc = { version = "3.4.5", features = ["termination"] } +egui = "0.30.0" +eframe = "0.30.0" +egui_extras = "0.30.0" [target.'cfg(unix)'.dependencies] indoc = "2.0.5" @@ -93,7 +96,7 @@ clap_mangen = "0" embed-manifest = "1.4.0" log = "0.4" build-data = "0" -git-url-parse = "0" +git-url-parse = { git = "https://github.com/tjtelan/git-url-parse-rs" } lazy-regex = "3.3.0" duct = "0.13.7" diff --git a/codchi/default.nix b/codchi/default.nix index 0034aebc..e4232b59 100644 --- a/codchi/default.nix +++ b/codchi/default.nix @@ -22,5 +22,6 @@ callPackage ../build/build-rust-package.nix cargoLock.outputHashes = { # "tray-icon-0.16.0" = "sha256-LxkEP31myIiWh6FDOzr9rZ8KAWISbja0jmEx0E2lM44="; # "clap-4.5.19" = "sha256-YRuZlp7jk05QLI551shgcVftcqKytTkxHlKbVejT1eE="; + "git-url-parse-0.4.5" = ""; }; } diff --git a/codchi/shell.nix b/codchi/shell.nix index 1e4979a9..3b8aa8c1 100644 --- a/codchi/shell.nix +++ b/codchi/shell.nix @@ -24,6 +24,9 @@ , cargo-flamegraph , graphviz +, vscode-with-extensions +, vscodium +, vscode-extensions , ... }: let @@ -96,6 +99,16 @@ mkShell (lib.recursiveUpdate runScript = "zed"; }) + (vscode-with-extensions.override { + vscode = vscodium; + vscodeExtensions = with vscode-extensions; [ + rust-lang.rust-analyzer + jnoortheen.nix-ide + mkhl.direnv + asvetliakov.vscode-neovim + ]; + }) + ] ++ (codchi.nativeBuildInputs or [ ]); shellHook = '' diff --git a/codchi/src/cli.rs b/codchi/src/cli.rs index 496e686d..6416a186 100644 --- a/codchi/src/cli.rs +++ b/codchi/src/cli.rs @@ -486,11 +486,17 @@ See the following docs on how to register the completions with your shell: #[arg(value_enum)] shell: clap_complete_command::Shell, }, + /// - /// Start the codchi tray if not running. + /// Start the Codchi tray if not running. #[clap(hide = true)] Tray {}, + /// + /// Start the Codchi GUI. + #[clap(hide = true)] + GUI {}, + #[clap( about = "Export the file system of a code machine including NixOS configuration and codchi secrets." )] diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs new file mode 100644 index 00000000..f9a3dcaa --- /dev/null +++ b/codchi/src/gui/mod.rs @@ -0,0 +1,53 @@ +use eframe::egui; + +pub fn run() -> anyhow::Result<()> { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]), + ..Default::default() + }; + eframe::run_native( + "My egui App", + options, + Box::new(|cc| { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Ok(Box::::default()) + }), + ).unwrap(); + Ok(()) +} + +struct MyApp { + name: String, + age: u32, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("My egui Application"); + ui.horizontal(|ui| { + let name_label = ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name) + .labelled_by(name_label.id); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + + ui.image(egui::include_image!("../../assets/logo.png")); + }); + } +} diff --git a/codchi/src/main.rs b/codchi/src/main.rs index e1e7f8b2..46456766 100644 --- a/codchi/src/main.rs +++ b/codchi/src/main.rs @@ -28,6 +28,7 @@ use util::{ResultExt, UtilExt}; pub mod cli; pub mod config; pub mod consts; +pub mod gui; pub mod logging; pub mod module; pub mod platform; @@ -356,6 +357,8 @@ secret. Is this OK? [y/n]", Cmd::Tray {} => tray::run()?, + Cmd::GUI {} => gui::run()?, + Cmd::Completion { .. } => unreachable!(), Cmd::Tar { .. } => unreachable!(), From a15131edb1861dcea9348a6ca23bf5e0ca78ab1c Mon Sep 17 00:00:00 2001 From: htngr <124245785+htngr@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:18:56 +0100 Subject: [PATCH 02/49] added image support --- codchi/Cargo.lock | 1 + codchi/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/codchi/Cargo.lock b/codchi/Cargo.lock index bb804e7a..8b3c148c 100644 --- a/codchi/Cargo.lock +++ b/codchi/Cargo.lock @@ -1545,6 +1545,7 @@ dependencies = [ "ahash", "egui", "enum-map", + "image", "log", "mime_guess2", "profiling", diff --git a/codchi/Cargo.toml b/codchi/Cargo.toml index 643cf347..1812a5fa 100644 --- a/codchi/Cargo.toml +++ b/codchi/Cargo.toml @@ -63,7 +63,7 @@ rand = "0.8.5" ctrlc = { version = "3.4.5", features = ["termination"] } egui = "0.30.0" eframe = "0.30.0" -egui_extras = "0.30.0" +egui_extras = { version = "0.30.0", features = ["default", "image"] } [target.'cfg(unix)'.dependencies] indoc = "2.0.5" From 09ab8014ff137305e6d27f1d5f4d5581277ae341 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:33:59 +0000 Subject: [PATCH 03/49] implemented gui-structure --- codchi/src/gui/bug_report.rs | 55 +++++++ codchi/src/gui/machine_creation.rs | 45 ++++++ codchi/src/gui/machine_inspection.rs | 45 ++++++ codchi/src/gui/mod.rs | 215 +++++++++++++++++++++++---- 4 files changed, 333 insertions(+), 27 deletions(-) create mode 100644 codchi/src/gui/bug_report.rs create mode 100644 codchi/src/gui/machine_creation.rs create mode 100644 codchi/src/gui/machine_inspection.rs diff --git a/codchi/src/gui/bug_report.rs b/codchi/src/gui/bug_report.rs new file mode 100644 index 00000000..98cd82a5 --- /dev/null +++ b/codchi/src/gui/bug_report.rs @@ -0,0 +1,55 @@ +use crate::gui::create_modal; +use crate::gui::MainPanel; +use crate::gui::MainPanelType; +use crate::platform::Machine; + +pub struct BugReportMainPanel { + show_modal: bool, + bug_report: String, + next_panel_type: Option, +} + +impl Default for BugReportMainPanel { + fn default() -> Self { + BugReportMainPanel { + show_modal: false, + bug_report: String::from(""), + next_panel_type: None, + } + } +} + +impl MainPanel for BugReportMainPanel { + fn update(&mut self, ui: &mut egui::Ui) { + ui.add_sized( + [ui.available_width(), 100.0], + egui::TextEdit::multiline(&mut self.bug_report).hint_text("Bug Description"), + ); + ui.separator(); + let send_button = egui::Button::new("Submit").fill(egui::Color32::GRAY); + ui.horizontal(|ui| { + if ui.add(send_button).clicked() { + self.bug_report = String::from(""); + + self.show_modal = true; + self.next_panel_type = Some(MainPanelType::MachineInspection); + } + if ui.button("Cancel").clicked() { + self.next_panel_type = Some(MainPanelType::MachineInspection); + } + }); + } + + fn modal_update(&mut self, ctx: &egui::Context) { + create_modal(ctx, "bug_report_modal", &mut self.show_modal, |ui| { + ui.strong("Bug Report was submitted."); + ui.label("Thank you for your feedback."); + }); + } + + fn next_panel(&mut self) -> Option { + self.next_panel_type.take() + } + + fn pass_machine(&mut self, machine: Machine) {} +} diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs new file mode 100644 index 00000000..6036df0f --- /dev/null +++ b/codchi/src/gui/machine_creation.rs @@ -0,0 +1,45 @@ +use crate::gui::MainPanel; +use crate::gui::MainPanelType; +use crate::platform::Machine; + +pub struct MachineCreationMainPanel { + machine_form: MachineForm, + next_panel_type: Option, +} + +struct MachineForm { + name: String, +} + +impl Default for MachineCreationMainPanel { + fn default() -> Self { + MachineCreationMainPanel { + machine_form: MachineForm::default(), + next_panel_type: None, + } + } +} + +impl MainPanel for MachineCreationMainPanel { + fn update(&mut self, ui: &mut egui::Ui) { + if ui.button("Finish").clicked() { + self.next_panel_type = Some(MainPanelType::MachineInspection); + } + } + + fn modal_update(&mut self, ctx: &egui::Context) {} + + fn next_panel(&mut self) -> Option { + self.next_panel_type.take() + } + + fn pass_machine(&mut self, machine: Machine) {} +} + +impl Default for MachineForm { + fn default() -> Self { + MachineForm { + name: String::from(""), + } + } +} diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs new file mode 100644 index 00000000..fd475005 --- /dev/null +++ b/codchi/src/gui/machine_inspection.rs @@ -0,0 +1,45 @@ +use crate::gui::MainPanel; +use crate::gui::MainPanelType; +use crate::platform::Machine; +use crate::platform::PlatformStatus; + +pub struct MachineInspectionMainPanel { + machine: Option, + next_panel_type: Option, +} + +impl Default for MachineInspectionMainPanel { + fn default() -> Self { + MachineInspectionMainPanel { + machine: None, + next_panel_type: None, + } + } +} + +impl MainPanel for MachineInspectionMainPanel { + fn update(&mut self, ui: &mut egui::Ui) { + if let Some(machine) = &self.machine { + ui.heading(format!( + "Machine - '{}'", + machine.config.name + )); + ui.label(match machine.platform_status { + PlatformStatus::NotInstalled => "not installed", + PlatformStatus::Stopped => "not running", + PlatformStatus::Running => "running", + }); + ui.separator(); + } + } + + fn modal_update(&mut self, ctx: &egui::Context) {} + + fn next_panel(&mut self) -> Option { + self.next_panel_type.take() + } + + fn pass_machine(&mut self, machine: Machine) { + self.machine = Some(machine); + } +} diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index f9a3dcaa..bf520970 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -1,53 +1,214 @@ -use eframe::egui; +mod bug_report; +mod machine_creation; +mod machine_inspection; + +use crate::platform::Machine; +use bug_report::BugReportMainPanel; +use egui; +use machine_creation::MachineCreationMainPanel; +use machine_inspection::MachineInspectionMainPanel; +use std::any::Any; pub fn run() -> anyhow::Result<()> { let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]), + viewport: egui::ViewportBuilder::default().with_inner_size((640.0, 480.0)), ..Default::default() }; eframe::run_native( - "My egui App", + "Codchi", options, Box::new(|cc| { // This gives us image support: egui_extras::install_image_loaders(&cc.egui_ctx); - Ok(Box::::default()) + // Zoom in a bit initially + let ppp = cc.egui_ctx.pixels_per_point(); + cc.egui_ctx.set_pixels_per_point(1.25 * ppp); + + Ok(Box::::new(Gui::new())) }), - ).unwrap(); + ) + .unwrap(); Ok(()) } -struct MyApp { - name: String, - age: u32, +struct Gui { + main_panels: Vec>, + current_main_panel_index: usize, + show_bug_report_modal: bool, + machines: Vec, } -impl Default for MyApp { - fn default() -> Self { +#[derive(Clone)] +enum MainPanelType { + MachineInspection, + MachineCreation, + BugReport, +} + +impl eframe::App for Gui { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + self.menu_bar_panel(ctx); + self.status_bar_panel(ctx); + self.side_panel(ctx); + self.main_panel(ctx); + } +} + +impl Gui { + fn new() -> Self { Self { - name: "Arthur".to_owned(), - age: 42, + main_panels: vec![ + Box::new(MachineInspectionMainPanel::default()), + Box::new(MachineCreationMainPanel::default()), + Box::new(BugReportMainPanel::default()), + ], + current_main_panel_index: 0, + show_bug_report_modal: false, + machines: Machine::list(true).expect("Machines could not be listed"), } } -} -impl eframe::App for MyApp { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("My egui Application"); - ui.horizontal(|ui| { - let name_label = ui.label("Your name: "); - ui.text_edit_singleline(&mut self.name) - .labelled_by(name_label.id); + fn menu_bar_panel(&mut self, ctx: &egui::Context) { + let height = 50.0; + egui::TopBottomPanel::top("menubar_panel") + .resizable(false) + .exact_height(height) + .show(ctx, |ui| { + ui.horizontal_centered(|ui| { + let codchi_button = + egui::Button::image(egui::include_image!("../../assets/logo.png")); + if ui.add(codchi_button).clicked() { + self.current_main_panel_index = + Self::get_main_panel_index(MainPanelType::MachineInspection); + } + ui.separator(); + ui.menu_button("Settings", |ui| { + if ui.button("Zoom In").clicked() { + egui::gui_zoom::zoom_in(ctx); + ui.close_menu(); + } + if ui.button("Zoom Out").clicked() { + egui::gui_zoom::zoom_out(ctx); + ui.close_menu(); + } + }); + if ui.button("BugReport").clicked() { + self.current_main_panel_index = + Self::get_main_panel_index(MainPanelType::BugReport); + } + if ui.button("Github").clicked() { + ui.ctx().open_url(egui::OpenUrl::new_tab( + "https://github.com/aformatik/codchi/", + )); + } + }); + }); + } + + fn status_bar_panel(&self, ctx: &egui::Context) {} + + fn side_panel(&mut self, ctx: &egui::Context) { + let width = 200.0; + egui::SidePanel::left("side_panel") + .exact_width(width) + .resizable(false) + .show(ctx, |ui| { + egui::ScrollArea::vertical() + .auto_shrink(false) + .show(ui, |ui| { + ui.with_layout( + egui::Layout::with_cross_justify( + egui::Layout::top_down(egui::Align::Center), + true, + ), + |ui| { + let new_machine_button = + egui::Button::new(egui::RichText::new("New").heading()); + if ui.add(new_machine_button).clicked() { + self.current_main_panel_index = + Self::get_main_panel_index(MainPanelType::MachineCreation); + } + ui.separator(); + for machine in &self.machines { + let machine_button = egui::Button::new(machine.config.name.clone()); + let button_handle = ui.add(machine_button); + if button_handle.clicked() { + let machine_inspection_panel_index = + Self::get_main_panel_index( + MainPanelType::MachineInspection, + ); + self.current_main_panel_index = + machine_inspection_panel_index; + let machine = Machine::by_name(&machine.config.name.clone(), true) + .expect("machine doesn't exist"); + self.main_panels[machine_inspection_panel_index].pass_machine(machine); + } + } + }, + ); + }); }); - ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); - if ui.button("Increment").clicked() { - self.age += 1; - } - ui.label(format!("Hello '{}', age {}", self.name, self.age)); + } + + fn main_panel(&mut self, ctx: &egui::Context) { + egui::CentralPanel::default().show(ctx, |ui| { + egui::ScrollArea::both() + .id_salt("machine_info_scroll") + .auto_shrink(false) + .show(ui, |ui| { + ui.spacing_mut().scroll = egui::style::ScrollStyle::solid(); + + let next_panel_type_option = + self.main_panels[self.current_main_panel_index].next_panel(); + if let Some(next_panel_type) = next_panel_type_option { + self.current_main_panel_index = Self::get_main_panel_index(next_panel_type); + } - ui.image(egui::include_image!("../../assets/logo.png")); + self.main_panels[self.current_main_panel_index].update(ui); + }) }); + for mut main_panel in &mut self.main_panels { + main_panel.modal_update(ctx); + } + } + + fn get_main_panel_index(main_panel_type: MainPanelType) -> usize { + match main_panel_type { + MainPanelType::MachineInspection => 0, + MainPanelType::MachineCreation => 1, + MainPanelType::BugReport => 2, + } + } +} + +pub trait MainPanel: Any { + fn update(&mut self, ui: &mut egui::Ui); + + fn modal_update(&mut self, ctx: &egui::Context); + + fn next_panel(&mut self) -> Option; + + fn pass_machine(&mut self, machine: Machine); +} + +pub fn create_modal( + ctx: &egui::Context, + id: &str, + show_modal_bool: &mut bool, + add_contents: impl FnOnce(&mut egui::Ui) -> R, +) { + if *show_modal_bool { + let modal = egui::Modal::new(egui::Id::new(id)).show(ctx, |ui| { + add_contents(ui); + ui.vertical_centered(|ui| { + if ui.button("Ok").clicked() { + *show_modal_bool = false; + } + }); + }); + if modal.should_close() { + *show_modal_bool = false; + } } } From 458e02f85b321ea059c0dd750e96e5ab75681a7b Mon Sep 17 00:00:00 2001 From: htngr <124245785+htngr@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:55:45 +0100 Subject: [PATCH 04/49] Updated flake to nixos-24.11 and Cargo.toml --- build/build-rust-package.nix | 6 +- codchi/Cargo.lock | 861 +++++++++++++++++++------------- codchi/Cargo.toml | 39 +- codchi/src/cli.rs | 4 +- codchi/src/logging/nix.rs | 40 +- codchi/src/logging/progress.rs | 14 +- codchi/src/main.rs | 6 +- flake.lock | 156 +++++- flake.nix | 3 +- nix/container/store/default.nix | 3 +- 10 files changed, 726 insertions(+), 406 deletions(-) diff --git a/build/build-rust-package.nix b/build/build-rust-package.nix index 6d344c52..6a774b75 100644 --- a/build/build-rust-package.nix +++ b/build/build-rust-package.nix @@ -19,7 +19,6 @@ , pkg-config , gtk3 , libayatana-appindicator -, xorg , libxkbcommon , libGL , libGLU @@ -48,7 +47,7 @@ let ]; }; # rustOrig = rust-bin.stable.latest.default.override rustConfig; - rustOrig = rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override rustConfig); + rustOrig =rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override rustConfig); rustPlatformOrig = makeRustPlatform { cargo = rustOrig; rustc = rustOrig; }; xwin = rustPlatformOrig.buildRustPackage rec { name = "xwin"; @@ -121,8 +120,7 @@ let EOF chmod +x $out/bin/cargo '') - // - { inherit (rustOrig) meta; }; + // { inherit (rustOrig) meta targetPlatforms badTargetPlatforms; }; rustPlatform = makeRustPlatform { cargo = passthru.rust; rustc = passthru.rust; }; setupXWin = topDir: /* bash */ '' diff --git a/codchi/Cargo.lock b/codchi/Cargo.lock index 8b3c148c..4da9b9a2 100644 --- a/codchi/Cargo.lock +++ b/codchi/Cargo.lock @@ -34,7 +34,7 @@ dependencies = [ "accesskit_consumer", "atspi-common", "serde", - "thiserror", + "thiserror 1.0.69", "zvariant 4.2.0", ] @@ -58,9 +58,9 @@ dependencies = [ "accesskit", "accesskit_consumer", "hashbrown 0.15.2", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -135,7 +135,7 @@ dependencies = [ "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -165,7 +165,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -255,11 +255,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" dependencies = [ "clipboard-win", + "core-graphics 0.23.2", + "image", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "parking_lot", + "windows-sys 0.48.0", "x11rb", ] @@ -591,7 +594,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037" +dependencies = [ + "objc2 0.6.0", ] [[package]] @@ -672,7 +684,7 @@ dependencies = [ "glib", "libc", "once_cell", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -697,7 +709,7 @@ dependencies = [ "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -758,12 +770,6 @@ 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" @@ -781,22 +787,22 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.6", + "windows-link", ] [[package]] name = "clap" -version = "4.5.30" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" dependencies = [ "clap_builder", "clap_derive", @@ -804,9 +810,9 @@ dependencies = [ [[package]] name = "clap-verbosity-flag" -version = "2.2.3" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34c77f67047557f62582784fd7482884697731b2932c7d37ced54bce2312e1e2" +checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" dependencies = [ "clap", "log", @@ -814,9 +820,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.30" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" dependencies = [ "anstream", "anstyle", @@ -826,9 +832,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.45" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e3040c8291884ddf39445dc033c70abc2bc44a42f0a3a00571a0f483a83f0cd" +checksum = "f5c5508ea23c5366f77e53f5a0070e5a84e51687ec3ef9e0464c86dc8d13ce98" dependencies = [ "clap", ] @@ -903,36 +909,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "cocoa" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" -dependencies = [ - "bitflags 2.8.0", - "block", - "cocoa-foundation", - "core-foundation 0.10.0", - "core-graphics 0.24.0", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" -dependencies = [ - "bitflags 2.8.0", - "block", - "core-foundation 0.10.0", - "core-graphics-types 0.2.0", - "libc", - "objc", -] - [[package]] name = "codchi" version = "0.3.1" @@ -965,7 +941,7 @@ dependencies = [ "indicatif-log-bridge", "indoc", "inquire", - "itertools 0.13.0", + "itertools", "known-folders", "lazy-regex", "log", @@ -975,21 +951,21 @@ dependencies = [ "num_enum", "number_prefix", "petname", - "rand", + "rand 0.9.0", "serde", "serde_json", "serde_with", - "strum", + "strum 0.27.1", "sysinfo", "tao", - "thiserror", + "thiserror 2.0.11", "throttle", "toml_edit 0.22.24", "tray-icon", "uuid", "version-compare", "which", - "windows 0.58.0", + "windows 0.60.0", "wslapi", ] @@ -1299,18 +1275,18 @@ dependencies = [ [[package]] name = "directories" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ "dirs-sys", ] [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] @@ -1327,14 +1303,14 @@ dependencies = [ [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", - "windows-sys 0.48.0", + "redox_users 0.5.0", + "windows-sys 0.59.0", ] [[package]] @@ -1344,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -1371,7 +1347,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.6", ] [[package]] @@ -1438,9 +1414,9 @@ checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" [[package]] name = "ecolor" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d72e9c39f6e11a2e922d04a34ec5e7ef522ea3f5a1acfca7a19d16ad5fe50f5" +checksum = "878e9005799dd739e5d5d89ff7480491c12d0af571d44399bcaefa1ee172dd76" dependencies = [ "bytemuck", "emath", @@ -1448,9 +1424,9 @@ dependencies = [ [[package]] name = "eframe" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f2d9e7ea2d11ec9e98a8683b6eb99f9d7d0448394ef6e0d6d91bd4eb817220" +checksum = "eba4c50d905804fe9ec4e159fde06b9d38f9440228617ab64a03d7a2091ece63" dependencies = [ "ahash", "bytemuck", @@ -1459,15 +1435,15 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_glow", - "glow 0.16.0", + "glow", "glutin", "glutin-winit", "image", "js-sys", "log", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "parking_lot", "percent-encoding", "profiling", @@ -1484,12 +1460,13 @@ dependencies = [ [[package]] name = "egui" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252d52224d35be1535d7fd1d6139ce071fb42c9097773e79f7665604f5596b5e" +checksum = "7d2768eaa6d5c80a6e2a008da1f0e062dff3c83eb2b28605ea2d0732d46e74d6" dependencies = [ "accesskit", "ahash", + "bitflags 2.8.0", "emath", "epaint", "log", @@ -1499,9 +1476,9 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c1e821d2d8921ef6ce98b258c7e24d9d6aab2ca1f9cdf374eca997e7f67f59" +checksum = "6d8151704bcef6271bec1806c51544d70e79ef20e8616e5eac01facfd9c8c54a" dependencies = [ "ahash", "bytemuck", @@ -1510,7 +1487,7 @@ dependencies = [ "epaint", "log", "profiling", - "thiserror", + "thiserror 1.0.69", "type-map", "web-time", "wgpu", @@ -1519,13 +1496,14 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e84c2919cd9f3a38a91e8f84ac6a245c19251fd95226ed9fae61d5ea564fce3" +checksum = "ace791b367c1f63e6044aef2f3834904509d1d1a6912fd23ebf3f6a9af92cd84" dependencies = [ "accesskit_winit", "ahash", "arboard", + "bytemuck", "egui", "log", "profiling", @@ -1538,9 +1516,9 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7a8198c088b1007108cb2d403bc99a5e370999b200db4f14559610d7330126" +checksum = "b5b5cf69510eb3d19211fc0c062fb90524f43fe8e2c012967dcf0e2d81cb040f" dependencies = [ "ahash", "egui", @@ -1553,14 +1531,14 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf6264cc7608e3e69a7d57a6175f438275f1b3889c1a551b418277721c95e6" +checksum = "9a53e2374a964c3c793cb0b8ead81bca631f24974bc0b747d1a5622f4e39fdd0" dependencies = [ "ahash", "bytemuck", "egui", - "glow 0.16.0", + "glow", "log", "memoffset", "profiling", @@ -1571,15 +1549,15 @@ dependencies = [ [[package]] name = "either" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" [[package]] name = "emath" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe73c1207b864ee40aa0b0c038d6092af1030744678c60188a05c28553515d" +checksum = "55b7b6be5ad1d247f11738b0e4699d9c20005ed366f2c29f5ec1f8e1de180bc2" dependencies = [ "bytemuck", ] @@ -1654,6 +1632,12 @@ dependencies = [ "regex", ] +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "env_logger" version = "0.11.6" @@ -1669,9 +1653,9 @@ dependencies = [ [[package]] name = "epaint" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5666f8d25236293c966fbb3635eac18b04ad1914e3bab55bc7d44b9980cafcac" +checksum = "275b665a7b9611d8317485187e5458750850f9e64604d3c58434bb3fc1d22915" dependencies = [ "ab_glyph", "ahash", @@ -1687,9 +1671,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66f6ddac3e6ac6fd4c3d48bb8b1943472f8da0f43a4303bcd8a18aa594401c80" +checksum = "9343d356d7cac894dacafc161b4654e0881301097bdf32a122ed503d97cb94b6" [[package]] name = "equivalent" @@ -1761,9 +1745,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", "miniz_oxide", @@ -1824,17 +1808,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" dependencies = [ "nom", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "fs4" -version = "0.9.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c6b3bd49c37d2aa3f3f2220233b29a7cd23f79d1fe70e5337d25fb390793de" +checksum = "be058769cf1633370c3d0dac6bb9b223b8f18900cf808abadf7843192e706238" dependencies = [ "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2090,7 +2074,7 @@ dependencies = [ "once_cell", "pin-project-lite", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2111,8 +2095,8 @@ name = "git-url-parse" version = "0.4.5" source = "git+https://github.com/tjtelan/git-url-parse-rs#23031bfa3e7d0cbba17a2ccbe26f348c35f5c524" dependencies = [ - "strum", - "thiserror", + "strum 0.26.3", + "thiserror 1.0.69", "url", ] @@ -2147,7 +2131,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2174,18 +2158,6 @@ dependencies = [ "system-deps", ] -[[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 = "glow" version = "0.16.0" @@ -2205,7 +2177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03642b8b0cce622392deb0ee3e88511f75df2daac806102597905c3ea1974848" dependencies = [ "bitflags 2.8.0", - "cfg_aliases 0.2.1", + "cfg_aliases", "cgl", "core-foundation 0.9.4", "dispatch", @@ -2213,9 +2185,9 @@ dependencies = [ "glutin_glx_sys", "glutin_wgl_sys", "libloading 0.8.6", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "once_cell", "raw-window-handle", "wayland-sys", @@ -2229,7 +2201,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases", "glutin", "raw-window-handle", "winit", @@ -2620,6 +2592,7 @@ dependencies = [ "byteorder-lite", "num-traits", "png", + "tiff", ] [[package]] @@ -2699,30 +2672,12 @@ dependencies = [ "unicode-width 0.1.14", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -2749,7 +2704,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -2769,6 +2724,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.77" @@ -2871,9 +2832,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libloading" @@ -2903,7 +2864,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.8.0", "libc", - "redox_syscall 0.5.8", + "redox_syscall 0.5.9", ] [[package]] @@ -2988,9 +2949,9 @@ dependencies = [ [[package]] name = "metal" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ "bitflags 2.8.0", "block", @@ -3031,9 +2992,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", "simd-adler32", @@ -3064,41 +3025,43 @@ dependencies = [ [[package]] name = "muda" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" +checksum = "89fed9ce3e5c01700e3a129d3d74619bbf468645b58274b420885107e496ecff" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-foundation 0.3.0", "once_cell", "png", - "thiserror", + "thiserror 2.0.11", "windows-sys 0.59.0", ] [[package]] name = "naga" -version = "23.1.0" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" dependencies = [ "arrayvec", "bit-set", "bitflags 2.8.0", - "cfg_aliases 0.1.1", + "cfg_aliases", "codespan-reporting", "hexf-parse", "indexmap 2.7.1", "log", "rustc-hash", "spirv", + "strum 0.26.3", "termcolor", - "thiserror", + "thiserror 2.0.11", "unicode-xid", ] @@ -3114,7 +3077,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3158,7 +3121,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.8.0", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "memoffset", ] @@ -3280,6 +3243,15 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -3287,15 +3259,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.8.0", - "block2", + "block2 0.5.1", "libc", - "objc2", + "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-quartz-core", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" +dependencies = [ + "bitflags 2.8.0", + "objc2 0.6.0", + "objc2-core-foundation", + "objc2-foundation 0.3.0", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -3303,10 +3287,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -3315,9 +3299,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3327,9 +3311,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +dependencies = [ + "bitflags 2.8.0", + "objc2 0.6.0", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" +dependencies = [ + "bitflags 2.8.0", + "objc2-core-foundation", ] [[package]] @@ -3338,9 +3342,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -3350,10 +3354,10 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-contacts", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -3369,10 +3373,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.8.0", - "block2", + "block2 0.5.1", "dispatch", "libc", - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" +dependencies = [ + "bitflags 2.8.0", + "block2 0.6.0", + "objc2 0.6.0", + "objc2-core-foundation", ] [[package]] @@ -3381,10 +3397,10 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3394,9 +3410,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3406,9 +3422,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -3418,8 +3434,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3429,13 +3445,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -3449,9 +3465,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3461,10 +3477,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -3506,6 +3522,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -3595,7 +3620,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.8", + "redox_syscall 0.5.9", "smallvec", "windows-targets 0.52.6", ] @@ -3619,10 +3644,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools", "proc-macro2", "quote", - "rand", + "rand 0.8.5", ] [[package]] @@ -3704,9 +3729,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "powerfmt" @@ -3720,7 +3745,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -3840,8 +3865,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.2", + "zerocopy 0.8.20", ] [[package]] @@ -3851,7 +3887,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.2", ] [[package]] @@ -3863,6 +3909,16 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a509b1a2ffbe92afab0e55c8fd99dea1c280e8171bd2d88682bb20bc41cbc2c" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.20", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3900,9 +3956,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" dependencies = [ "bitflags 2.8.0", ] @@ -3915,7 +3971,18 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "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 0.2.15", + "libredox", + "thiserror 2.0.11", ] [[package]] @@ -4294,7 +4361,7 @@ dependencies = [ "log", "memmap2", "rustix", - "thiserror", + "thiserror 1.0.69", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -4364,7 +4431,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros 0.27.1", ] [[package]] @@ -4380,6 +4456,19 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.98", +] + [[package]] name = "syn" version = "1.0.109" @@ -4415,9 +4504,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.32.1" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" dependencies = [ "core-foundation-sys", "libc", @@ -4442,12 +4531,11 @@ dependencies = [ [[package]] name = "tao" -version = "0.30.8" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6682a07cf5bab0b8a2bd20d0a542917ab928b5edb75ebd4eda6b05cbaab872da" +checksum = "9899a4cb2590e669dd9d32ea6360ac67f29843fbf0424e06f9fc0e9e1ec9579a" dependencies = [ "bitflags 2.8.0", - "cocoa", "core-foundation 0.10.0", "core-graphics 0.24.0", "crossbeam-channel", @@ -4457,7 +4545,6 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "instant", "jni", "lazy_static", "libc", @@ -4465,15 +4552,17 @@ dependencies = [ "ndk", "ndk-context", "ndk-sys 0.6.0+11769913", - "objc", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-foundation 0.3.0", "once_cell", "parking_lot", "scopeguard", "tao-macros", "unicode-segmentation", "url", - "windows 0.58.0", - "windows-core 0.58.0", + "windows 0.60.0", + "windows-core 0.60.1", "windows-version", "x11-dl", ] @@ -4535,7 +4624,16 @@ 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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -4549,6 +4647,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -4565,6 +4674,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7d2d6a110f96a589b87d0f6c46d4048c84a01a7b81a2dcc97656694ace28b1" +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.37" @@ -4720,21 +4840,22 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.19.2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b" +checksum = "d433764348e7084bad2c5ea22c96c71b61b17afe3a11645710f533bd72b6a2b5" dependencies = [ - "core-graphics 0.24.0", "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.0", "once_cell", "png", - "thiserror", + "thiserror 2.0.11", "windows-sys 0.59.0", ] @@ -4843,9 +4964,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1" +checksum = "bd8dcafa1ca14750d8d7a05aa05988c17aab20886e1f3ae33a40223c58d92ef7" dependencies = [ "getrandom 0.3.1", "sha1_smol", @@ -5116,26 +5237,33 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea9fe1ebb156110ff855242c1101df158b822487e4957b0556d9ffce9db0f535" dependencies = [ - "block2", + "block2 0.5.1", "core-foundation 0.10.0", "home", "jni", "log", "ndk-context", - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "url", "web-sys", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "wgpu" -version = "23.0.1" +version = "24.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a" +checksum = "47f55718f85c2fa756edffa0e7f0e0a60aba463d1362b57e23123c58f035e4b6" dependencies = [ "arrayvec", - "cfg_aliases 0.1.1", + "bitflags 2.8.0", + "cfg_aliases", "document-features", "js-sys", "log", @@ -5154,14 +5282,14 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "23.0.1" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a" +checksum = "82a39b8842dc9ffcbe34346e3ab6d496b32a47f6497e119d762c97fcaae3cb37" dependencies = [ "arrayvec", "bit-vec", "bitflags 2.8.0", - "cfg_aliases 0.1.1", + "cfg_aliases", "document-features", "indexmap 2.7.1", "log", @@ -5172,25 +5300,25 @@ dependencies = [ "raw-window-handle", "rustc-hash", "smallvec", - "thiserror", + "thiserror 2.0.11", "wgpu-hal", "wgpu-types", ] [[package]] name = "wgpu-hal" -version = "23.0.1" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821" +checksum = "5a782e5056b060b0b4010881d1decddd059e44f2ecd01e2db2971b48ad3627e5" dependencies = [ "android_system_properties", "arrayvec", "ash", "bitflags 2.8.0", "bytemuck", - "cfg_aliases 0.1.1", + "cfg_aliases", "core-graphics-types 0.1.3", - "glow 0.14.2", + "glow", "glutin_wgl_sys", "gpu-alloc", "gpu-descriptor", @@ -5204,13 +5332,14 @@ dependencies = [ "ndk-sys 0.5.0+25.2.9519653", "objc", "once_cell", + "ordered-float", "parking_lot", "profiling", "raw-window-handle", "renderdoc-sys", "rustc-hash", "smallvec", - "thiserror", + "thiserror 2.0.11", "wasm-bindgen", "web-sys", "wgpu-types", @@ -5219,23 +5348,24 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "23.0.0" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" dependencies = [ "bitflags 2.8.0", "js-sys", + "log", "web-sys", ] [[package]] name = "which" -version = "6.0.3" +version = "7.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283" dependencies = [ "either", - "home", + "env_home", "rustix", "winsafe", ] @@ -5301,6 +5431,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" +dependencies = [ + "windows-collections", + "windows-core 0.60.1", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" +dependencies = [ + "windows-core 0.60.1", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -5343,10 +5495,33 @@ dependencies = [ "windows-implement 0.58.0", "windows-interface 0.58.0", "windows-result 0.2.0", - "windows-strings", + "windows-strings 0.1.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" +dependencies = [ + "windows-implement 0.59.0", + "windows-interface 0.59.0", + "windows-link", + "windows-result 0.3.1", + "windows-strings 0.3.1", +] + +[[package]] +name = "windows-future" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" +dependencies = [ + "windows-core 0.60.1", + "windows-link", +] + [[package]] name = "windows-implement" version = "0.56.0" @@ -5380,6 +5555,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "windows-implement" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "windows-interface" version = "0.56.0" @@ -5413,6 +5599,33 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "windows-interface" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + +[[package]] +name = "windows-numerics" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" +dependencies = [ + "windows-core 0.60.1", + "windows-link", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -5431,6 +5644,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -5441,6 +5663,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -5516,36 +5747,20 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -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-version" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12476c23a74725c539b24eae8bfc0dac4029c39cdb561d9f23616accd4ae26d" +checksum = "7bfbcc4996dd183ff1376a20ade1242da0d2dcaff83cc76710a588d24fd4c5db" dependencies = [ - "windows-targets 0.53.0", + "windows-link", ] [[package]] @@ -5566,12 +5781,6 @@ 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" @@ -5590,12 +5799,6 @@ 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" @@ -5614,24 +5817,12 @@ 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" @@ -5650,12 +5841,6 @@ 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" @@ -5674,12 +5859,6 @@ 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" @@ -5698,12 +5877,6 @@ 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" @@ -5722,12 +5895,6 @@ 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.9" @@ -5738,10 +5905,10 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.8.0", - "block2", + "block2 0.5.1", "bytemuck", "calloop", - "cfg_aliases 0.2.1", + "cfg_aliases", "concurrent-queue", "core-foundation 0.9.4", "core-graphics 0.23.2", @@ -5751,9 +5918,9 @@ dependencies = [ "libc", "memmap2", "ndk", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", "percent-encoding", @@ -5967,7 +6134,7 @@ dependencies = [ "hex", "nix", "ordered-stream", - "rand", + "rand 0.8.5", "serde", "serde_repr", "sha1", @@ -6112,7 +6279,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c" +dependencies = [ + "zerocopy-derive 0.8.20", ] [[package]] @@ -6126,6 +6302,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "zerofrom" version = "0.1.5" diff --git a/codchi/Cargo.toml b/codchi/Cargo.toml index 1812a5fa..ff53de19 100644 --- a/codchi/Cargo.toml +++ b/codchi/Cargo.toml @@ -20,14 +20,14 @@ log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_with = "3.11" -strum = { version = "0.26", features = ["derive"] } -directories = "5" -itertools = "0.13" +strum = { version = "0.27", features = ["derive"] } +directories = "6" +itertools = "0.14" lazy-regex = "3.3.0" # base64 = "0.22.1" clap = { version = "4", features = ["derive", "cargo", "string"] } clap_complete_command = { version = "0.6.1", features = ["fig", "carapace"] } -clap-verbosity-flag = "2.2.2" +clap-verbosity-flag = "3.0.2" comfy-table = "7.1.1" console = { version = "0.15.8", default-features = false, features = [ "windows-console-colors", @@ -45,25 +45,25 @@ petname = { version = "2.0.2", default-features = false, features = [ "default-words", ] } # rustls-native-certs = "0.8.0" -sysinfo = "0.32.0" -thiserror = "1.0" +sysinfo = "0.33.1" +thiserror = "2.0" throttle = "0.1.0" -tray-icon = { version = "0.19", default-features = false } +tray-icon = { version = "0.20", default-features = false } image = { version = "0.25", features = ["png"], default-features = false } -tao = { version = "0.30", default-features = false } -fs4 = { version = "0.9", features = ["sync"] } +tao = { version = "0.32", default-features = false } +fs4 = { version = "0.13", features = ["sync"] } git-url-parse = { git = "https://github.com/tjtelan/git-url-parse-rs" } toml_edit = { version = "0.22.22", features = ["serde"] } -which = "6.0.3" +which = "7.0.2" notify-rust = "4.11.3" human-panic = "2.0.2" # clap-help = "1.3.0" # termimad = "0.30.0" -rand = "0.8.5" +rand = "0.9.0" ctrlc = { version = "3.4.5", features = ["termination"] } -egui = "0.30.0" -eframe = "0.30.0" -egui_extras = { version = "0.30.0", features = ["default", "image"] } +egui = "0.31.0" +eframe = "0.31.0" +egui_extras = { version = "0.31.0", features = ["default", "image"] } [target.'cfg(unix)'.dependencies] indoc = "2.0.5" @@ -73,21 +73,14 @@ nix = { version = "0.29.0", features = ["user", "hostname"] } known-folders = "1.2.0" mslnk = "0.1.8" version-compare = "0.2" -windows = { version = "0.58", features = [ - "Win32_System_Console", - "Win32_UI_WindowsAndMessaging", - "Win32_System_Diagnostics_Debug", - "Win32_Storage_FileSystem", - "Win32_Security", - "Win32_System_Threading", -] } +windows = { version = "0.60", features = ["Win32_System_Console", "Win32_UI_WindowsAndMessaging", "Win32_System_Diagnostics_Debug", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_Threading"] } wslapi = "0.1.3" clap_complete_command = { version = "0.6.1", default-features = false } uuid = { version = "1.11.0", features = ["v5"] } [build-dependencies] clap = { version = "4", features = ["derive", "cargo", "string"] } -clap-verbosity-flag = "2.2.2" +clap-verbosity-flag = "3.0.2" clap_complete = "4" clap_complete_command = { version = "0.6.1", features = ["fig", "carapace"] } clap_complete_fig = "4" diff --git a/codchi/src/cli.rs b/codchi/src/cli.rs index 6416a186..a8e797b7 100644 --- a/codchi/src/cli.rs +++ b/codchi/src/cli.rs @@ -20,7 +20,7 @@ pub static DEBUG: LazyLock = LazyLock::new(|| { CLI_ARGS .get() .and_then(|cli| cli.verbose.log_level()) - .or(::default()) + .or(::default_filter().into()) .unwrap() >= Level::Debug }); @@ -486,7 +486,7 @@ See the following docs on how to register the completions with your shell: #[arg(value_enum)] shell: clap_complete_command::Shell, }, - + /// /// Start the Codchi tray if not running. #[clap(hide = true)] diff --git a/codchi/src/logging/nix.rs b/codchi/src/logging/nix.rs index 79d1dd4c..a6174b7c 100644 --- a/codchi/src/logging/nix.rs +++ b/codchi/src/logging/nix.rs @@ -70,26 +70,26 @@ pub enum Activity { FetchTree, } -impl Activity { - pub fn to_type(&self) -> ActivityType { - match self { - Activity::Unknown => ActivityType::Unknown, - Activity::CopyPath { .. } => ActivityType::CopyPath, - Activity::FileTransfer { .. } => ActivityType::FileTransfer, - Activity::Realise => ActivityType::Realise, - Activity::CopyPaths => ActivityType::CopyPaths, - Activity::Builds => ActivityType::Builds, - Activity::Build { .. } => ActivityType::Build, - Activity::OptimiseStore => ActivityType::OptimiseStore, - Activity::VerifyPaths => ActivityType::VerifyPaths, - Activity::Substitute { .. } => ActivityType::Substitute, - Activity::QueryPathInfo { .. } => ActivityType::QueryPathInfo, - Activity::PostBuildHook { .. } => ActivityType::PostBuildHook, - Activity::BuildWaiting { .. } => ActivityType::BuildWaiting, - Activity::FetchTree { .. } => ActivityType::FetchTree, - } - } -} +// impl Activity { +// pub fn to_type(&self) -> ActivityType { +// match self { +// Activity::Unknown => ActivityType::Unknown, +// Activity::CopyPath { .. } => ActivityType::CopyPath, +// Activity::FileTransfer { .. } => ActivityType::FileTransfer, +// Activity::Realise => ActivityType::Realise, +// Activity::CopyPaths => ActivityType::CopyPaths, +// Activity::Builds => ActivityType::Builds, +// Activity::Build { .. } => ActivityType::Build, +// Activity::OptimiseStore => ActivityType::OptimiseStore, +// Activity::VerifyPaths => ActivityType::VerifyPaths, +// Activity::Substitute { .. } => ActivityType::Substitute, +// Activity::QueryPathInfo { .. } => ActivityType::QueryPathInfo, +// Activity::PostBuildHook { .. } => ActivityType::PostBuildHook, +// Activity::BuildWaiting { .. } => ActivityType::BuildWaiting, +// Activity::FetchTree { .. } => ActivityType::FetchTree, +// } +// } +// } #[derive(Clone, Copy, Debug, TryFromPrimitive, IntoPrimitive, PartialEq, Eq)] #[repr(i64)] diff --git a/codchi/src/logging/progress.rs b/codchi/src/logging/progress.rs index 56ed49f7..42d100de 100644 --- a/codchi/src/logging/progress.rs +++ b/codchi/src/logging/progress.rs @@ -74,13 +74,13 @@ impl Progress { self.status_bar.set_message(msg); } - pub fn with_status(self, msg: M) -> Self - where - M: Into>, - { - self.status_bar.set_message(msg); - self - } + // pub fn with_status(self, msg: M) -> Self + // where + // M: Into>, + // { + // self.status_bar.set_message(msg); + // self + // } pub fn log(&mut self, fallback_target: &str, fallback_level: Level, msg: &str) { let result = nix::parse_line(msg); diff --git a/codchi/src/main.rs b/codchi/src/main.rs index 46456766..1013d6d6 100644 --- a/codchi/src/main.rs +++ b/codchi/src/main.rs @@ -17,7 +17,7 @@ use secrets::MachineSecrets; use std::{ env, io::IsTerminal, - panic::{self, PanicInfo}, + panic::{self, PanicHookInfo}, process::exit, sync::{mpsc::channel, OnceLock}, thread, @@ -37,7 +37,7 @@ pub mod tray; pub mod util; fn main() -> anyhow::Result<()> { - panic::set_hook(Box::new(move |info: &PanicInfo<'_>| { + panic::set_hook(Box::new(move |info: &PanicHookInfo<'_>| { let meta = human_panic::Metadata::new( env!("CARGO_PKG_NAME"), format!( @@ -405,7 +405,7 @@ fn alert_dirty(machine: Machine) { } } -fn get_panic_cause(panic_info: &PanicInfo) -> String { +fn get_panic_cause(panic_info: &PanicHookInfo) -> String { #[cfg(feature = "nightly")] let message = panic_info.message().map(|m| format!("{}", m)); diff --git a/flake.lock b/flake.lock index f237fb62..e89f0250 100644 --- a/flake.lock +++ b/flake.lock @@ -1,35 +1,175 @@ { "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "git-hooks-nix": { + "inputs": { + "flake-compat": [ + "nix" + ], + "gitignore": [ + "nix" + ], + "nixpkgs": [ + "nix", + "nixpkgs" + ], + "nixpkgs-stable": [ + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1734279981, + "narHash": "sha256-NdaCraHPp8iYMWzdXAt5Nv6sA3MUzlCiGiR586TCwo0=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "aa9f40c906904ebd83da78e7f328cd8aeaeae785", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "nix": { + "inputs": { + "flake-compat": "flake-compat", + "flake-parts": "flake-parts", + "git-hooks-nix": "git-hooks-nix", + "nixpkgs": "nixpkgs", + "nixpkgs-23-11": "nixpkgs-23-11", + "nixpkgs-regression": "nixpkgs-regression" + }, + "locked": { + "lastModified": 1740510906, + "narHash": "sha256-xA+qZSUOELEQeoI5wTqoqq/qht2B11ZvusxdoTcoSjQ=", + "owner": "NixOS", + "repo": "nix", + "rev": "81834e7f0076912196560644f7e6c373f252f483", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nix", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1720553833, - "narHash": "sha256-IXMiHQMtdShDXcBW95ctA+m5Oq2kLxnBt7WlMxvDQXA=", + "lastModified": 1734359947, + "narHash": "sha256-1Noao/H+N8nFB4Beoy8fgwrcOQLVm9o4zKW1ODaqK9E=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "48d12d5e70ee91fe8481378e540433a7303dbf6a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-23-11": { + "locked": { + "lastModified": 1717159533, + "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", + "type": "github" + } + }, + "nixpkgs-regression": { + "locked": { + "lastModified": 1643052045, + "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1740463929, + "narHash": "sha256-4Xhu/3aUdCKeLfdteEHMegx5ooKQvwPHNkOgNCXQrvc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "249fbde2a178a2ea2638b65b9ecebd531b338cf9", + "rev": "5d7db4668d7a0c6cc5fc8cf6ef33b008b2b1ed8b", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-24.05", + "ref": "nixos-24.11", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "nixpkgs": "nixpkgs", + "nix": "nix", + "nixpkgs": "nixpkgs_2", "rust-overlay": "rust-overlay" } }, "rust-overlay": { "flake": false, "locked": { - "lastModified": 1720664424, - "narHash": "sha256-+odiMNHRYdvzL1ewl41UVFxsjmdoXfH+maQ8xvUoR4g=", + "lastModified": 1740536993, + "narHash": "sha256-3YI+1ONZ28chM19Hep9Z+TSyiybYf/1VC/gwImVZKUw=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "fec97e65fcbaab0decccba740ac8688f61dadd70", + "rev": "9f05c0655de9dc2c7b60b689447c48abb9190bf8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e5fe3565..52840e65 100644 --- a/flake.nix +++ b/flake.nix @@ -7,11 +7,12 @@ }; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; rust-overlay = { url = "github:oxalica/rust-overlay"; flake = false; # prevent fetching transitive inputs TODO }; + nix.url = "github:NixOS/nix"; # nixvim = { # url = "github:nix-community/nixvim"; # inputs.nixpkgs.follows = "nixpkgs"; diff --git a/nix/container/store/default.nix b/nix/container/store/default.nix index feef9ed0..d76b68e8 100644 --- a/nix/container/store/default.nix +++ b/nix/container/store/default.nix @@ -113,7 +113,8 @@ in binPackages = with pkgs.pkgsStatic; [ busybox bashInteractive - nix + inputs.nix.packages.${pkgs.system}.nix-everything-static + pkgs.codchi-utils # ndd (pkgs.writeShellScriptBinStatic "run" /* bash */ '' From 332493e450bc54a1b8ebe902d915d8959374c66f Mon Sep 17 00:00:00 2001 From: htngr <124245785+htngr@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:04:06 +0100 Subject: [PATCH 05/49] Execute nix-cache on pull request --- .github/workflows/nix-cache.yml | 1 + nix/container/store/default.nix | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/nix-cache.yml b/.github/workflows/nix-cache.yml index 23175d7f..f394837a 100644 --- a/.github/workflows/nix-cache.yml +++ b/.github/workflows/nix-cache.yml @@ -6,6 +6,7 @@ on: # - "v*.*.*" branches: - "master" + pull_request: permissions: contents: read diff --git a/nix/container/store/default.nix b/nix/container/store/default.nix index d76b68e8..23f7e4ca 100644 --- a/nix/container/store/default.nix +++ b/nix/container/store/default.nix @@ -82,10 +82,16 @@ in flakes = [{ exact = true; from = { type = "indirect"; id = "nixpkgs"; }; - to = { type = "path"; path = inputs.nixpkgs.outPath; } - // lib.filterAttrs - (n: _: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash") - inputs.nixpkgs; + # to = { type = "path"; path = inputs.nixpkgs.outPath; } + # // lib.filterAttrs + # (n: _: n == "lastModified" || n == "rev" || n == "revCount" || n == "narHash") + # inputs.nixpkgs; + to = { + type = "github"; + owner = "NixOS"; + repo = "nixpkgs"; + inherit (inputs.nixpkgs) rev; + }; }]; }); # nix runs as root and needs to access user repositories From 45258f777a228860cbf1119895843f2455e9a423 Mon Sep 17 00:00:00 2001 From: htngr <124245785+htngr@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:09:12 +0100 Subject: [PATCH 06/49] Added git-url-parse hash --- codchi/default.nix | 2 +- flake.lock | 7 ++++--- flake.nix | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/codchi/default.nix b/codchi/default.nix index e4232b59..d0752028 100644 --- a/codchi/default.nix +++ b/codchi/default.nix @@ -22,6 +22,6 @@ callPackage ../build/build-rust-package.nix cargoLock.outputHashes = { # "tray-icon-0.16.0" = "sha256-LxkEP31myIiWh6FDOzr9rZ8KAWISbja0jmEx0E2lM44="; # "clap-4.5.19" = "sha256-YRuZlp7jk05QLI551shgcVftcqKytTkxHlKbVejT1eE="; - "git-url-parse-0.4.5" = ""; + "git-url-parse-0.4.5" = "sha256-q3lrdWE+WpAI0FSbpzUabk9aPCjzoqIHvNoDmqRl2BY="; }; } diff --git a/flake.lock b/flake.lock index e89f0250..a6c51ce7 100644 --- a/flake.lock +++ b/flake.lock @@ -78,15 +78,16 @@ "nixpkgs-regression": "nixpkgs-regression" }, "locked": { - "lastModified": 1740510906, - "narHash": "sha256-xA+qZSUOELEQeoI5wTqoqq/qht2B11ZvusxdoTcoSjQ=", + "lastModified": 1739376598, + "narHash": "sha256-EOnBPe+ydQ0/P5ZyWnFekvpyUxMcmh2rnP9yNFi/EqU=", "owner": "NixOS", "repo": "nix", - "rev": "81834e7f0076912196560644f7e6c373f252f483", + "rev": "b3e92048335d88553c1d6bbcf280e95b9a1b5a75", "type": "github" }, "original": { "owner": "NixOS", + "ref": "2.26.2", "repo": "nix", "type": "github" } diff --git a/flake.nix b/flake.nix index 52840e65..06bcefe6 100644 --- a/flake.nix +++ b/flake.nix @@ -12,7 +12,7 @@ url = "github:oxalica/rust-overlay"; flake = false; # prevent fetching transitive inputs TODO }; - nix.url = "github:NixOS/nix"; + nix.url = "github:NixOS/nix/2.26.2"; # nixvim = { # url = "github:nix-community/nixvim"; # inputs.nixpkgs.follows = "nixpkgs"; From c168d197b4e5f44f96bb34873c8939c73293e428 Mon Sep 17 00:00:00 2001 From: htngr <124245785+htngr@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:45:58 +0100 Subject: [PATCH 07/49] Nix 2.26 --- codchi/src/platform/windows/mod.rs | 33 +++++++++++++++++++++++++----- nix/container/store/default.nix | 4 ++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/codchi/src/platform/windows/mod.rs b/codchi/src/platform/windows/mod.rs index 9ea35a71..55da34a9 100644 --- a/codchi/src/platform/windows/mod.rs +++ b/codchi/src/platform/windows/mod.rs @@ -368,13 +368,36 @@ tail -f "{log_file}" } fn create_exec_cmd(&self, cmd: &[&str]) -> super::LinuxCommandBuilder { - let cmd = match cmd.split_first() { - Some((cmd, args)) => self.cmd().run(cmd, args), - None => self.cmd().run("bash", &["-l"]), + // let cmd = match cmd.split_first() { + // Some((cmd, args)) => self.cmd().run(cmd, args), + // None => self.cmd().run("bash", &["-l"]), + // }; + let cmd = if cmd.is_empty() { + self.cmd().raw( + "/run/current-system/sw/bin/machinectl", + &[ + &["shell", "-q", &format!("{}@", consts::user::DEFAULT_NAME)], + cmd, + ] + .concat(), + ) + } else { + self.cmd().raw( + "/run/current-system/sw/bin/machinectl", + &[ + "shell", + "-q", + &format!("{}@", consts::user::DEFAULT_NAME), + "/bin/bash", + "-lc", + &cmd.join(" "), + ], + ) }; - cmd.with_cwd(consts::user::DEFAULT_HOME.clone()) - .with_user(LinuxUser::Default) + // cmd.with_cwd(consts::user::DEFAULT_HOME.clone()) + // .with_user(LinuxUser::Default) + cmd.with_user(LinuxUser::Root) } fn tar(&self, target_file: &std::path::Path) -> Result<()> { diff --git a/nix/container/store/default.nix b/nix/container/store/default.nix index 23f7e4ca..3525772f 100644 --- a/nix/container/store/default.nix +++ b/nix/container/store/default.nix @@ -203,8 +203,8 @@ in nix $NIX_VERBOSITY profile wipe-history else logE "Updating store..." - nix flake update $NIX_VERBOSITY "${consts.store.DIR_CONFIG_STORE}" - ndd $NIX_VERBOSITY profile upgrade --profile "${consts.store.PROFILE_STORE}" '.*' + nix flake update $NIX_VERBOSITY --flake "${consts.store.DIR_CONFIG_STORE}" + ndd $NIX_VERBOSITY profile upgrade --profile "${consts.store.PROFILE_STORE}" --all fi # kill $NIX_DAEMON_PID From f8a0a1e3b26f40876ce42c73afd3c16aa2d1d28c Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:17:12 +0000 Subject: [PATCH 08/49] fleshed out machine inspection --- codchi/Cargo.toml | 2 +- codchi/assets/bug_icon.png | Bin 0 -> 51083 bytes codchi/assets/github_logo.png | Bin 0 -> 17356 bytes codchi/assets/settings.png | Bin 0 -> 26925 bytes codchi/src/gui/bug_report.rs | 55 ----- codchi/src/gui/machine_creation.rs | 16 +- codchi/src/gui/machine_inspection.rs | 356 +++++++++++++++++++++++++-- codchi/src/gui/mod.rs | 312 +++++++++++++++++------ codchi/src/platform/host.rs | 12 +- codchi/src/platform/linux/host.rs | 12 + codchi/src/platform/mod.rs | 2 +- codchi/src/platform/windows/host.rs | 29 ++- 12 files changed, 638 insertions(+), 158 deletions(-) create mode 100755 codchi/assets/bug_icon.png create mode 100755 codchi/assets/github_logo.png create mode 100755 codchi/assets/settings.png delete mode 100644 codchi/src/gui/bug_report.rs diff --git a/codchi/Cargo.toml b/codchi/Cargo.toml index ff53de19..ccb52ddf 100644 --- a/codchi/Cargo.toml +++ b/codchi/Cargo.toml @@ -49,7 +49,7 @@ sysinfo = "0.33.1" thiserror = "2.0" throttle = "0.1.0" tray-icon = { version = "0.20", default-features = false } -image = { version = "0.25", features = ["png"], default-features = false } +image = { version = "0.25", features = ["png", "ico"], default-features = false } tao = { version = "0.32", default-features = false } fs4 = { version = "0.13", features = ["sync"] } git-url-parse = { git = "https://github.com/tjtelan/git-url-parse-rs" } diff --git a/codchi/assets/bug_icon.png b/codchi/assets/bug_icon.png new file mode 100755 index 0000000000000000000000000000000000000000..6c845dd434a71662aa874d772ee9d111e6665d78 GIT binary patch literal 51083 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 HJ4F8%kbWgX literal 0 HcmV?d00001 diff --git a/codchi/assets/github_logo.png b/codchi/assets/github_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..3325d7bfe6d41f4999ad4fc173d5ef394e7f5378 GIT binary patch literal 17356 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 literal 0 HcmV?d00001 diff --git a/codchi/assets/settings.png b/codchi/assets/settings.png new file mode 100755 index 0000000000000000000000000000000000000000..2507242dfbf5a49e5792ac3c55e494bc583718fd GIT binary patch literal 26925 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 z z2Ny@UE&^_@E{89o&ROLJkX2ttxE!sISqH5s+6yjE%-UKO^Z|nsq_y z%Z5fq%0e;!FfMTs!f-f(+UW(kpl5$+$bSud@THdC(qZBPuqb6s;gQwR`cMa%JM}x? zm)a91a_$Y%or?4wu~}*n8GS3v^K)b00D0PgPBSS)mTVV;oy#*;V(e=3J7bG%q_c?5 zWvk>xoC5Wn>N3&21Hi=XKpDoUcCWHCFa4jeQ^JC+J`oTP!4-ON;1)o-Hk&ZA2A?{? zaF6h==JFx9%*Y%_RUZMNM}3&NaR78S@NyQ*3o~u6YIeuIYHU_0Ew%L9H9+?o<d=5~<+bWuoYUEE#}+ybr$I3S*A9(;Fuu9pP1Bq4QiLlY_{+l7$8 z!ATN6O831eZa_uP4D}oDcrP*%Q2ar<%S-gkZXC-7rej^u{5*={N|})r08DlOoe!X_ zDc{}jEbgtmObqae`9u&hH=_C#|IvIv7Y>ndOlSV3&xiKYCGgIqo7}l2JV+tk?#{JP zVQTj$vc!ve5qH5JgJA^dpzc~y6s7S^4}zTMg~WPz(!#l30n&XRG<>gT#t7CGZ&}nD zy$!?fPcY)>3g@!x@8~QGQ$BbE68(id^X52N;%2=N!;K`2F9AgwbaE!oGwc=oc^|9d z-mh=yd%Fq1=mg#!QhiHc^CLcOnHNZNl*R4!r|2lWsyQ;TxBd&!8Sm8upY8eM{vE}} z9bb_6OkN|J3iiN4t+veJiyS4N+O^3Kbk0TWpCno~?9m;v@kLB|;7=Kn?9>|nBo5$1 zao)iNieVu5z&cP6@OyCC$9&9A_Z5*}l{`n&1|v#|AMrY0e0*7f@XgaXPa~?Y73)i zU-U$|G{PfsP4V)%fMTUJ+J^GvxBW4kzVEtePwGb-v_Fli#O6w3C{5&-on$95$_-L9 ziNrPN_BW!+-pSu#asHsK5u=- zzGkMnR$52kHYu2=#=E_ucBPJUDt{%qLm}*X*jimH-FPm1udc#+%osAUE_(25_aU?4 zBlQ~3CfO#{yMC(X&UBGE1|7Zu2fOa;uR#cb!wRhaskmXXlMtYroQmiR(lue$LnENd zcF&Y(DIA54wTwEAY6?I6b5eI?D$ak;*$fyh9P=$NZ1kQ;e140X&S_5fmn^xON&94> zCnxH4?669zYqKz1rWjih@61W!!ATG1dhtO45VmAH z<_a&h9fp9MHJV?;e3R@(yucptxs;#G-W+OP&#>b}MB&6*G3un@J@7C*-whd-8BEmf zaC{oI9hyP#Wnj3g6pY`+djACbVI-Ba1DsA9Gv;Opk<#RZbKxYfjlO0-IIlOj#EDL5 zjpqx|2?>}6v0(@@9D#Hhhv@M2tq@J8^T@IvtD}S;2dcyMh!%XV=~QAoa1IN4+!}8h zl5al=O6QzvfMb(^QPYt0o;Qopb7QsmzOB6Vbv;ry{!8{$-A~|*euFCq!sf^Fu8L9v zh;s=Tu2;Xl*hT%_+&aLwu!nSo7<+WZ7$PK@B7-lH+EJ(R&+t-3wGdDzH+h|z|BLYh z^wQsOKiG#iGu8e^_~D4+@1G!!#D5WfjP$n~^7nVE6;3z(jRN%l{sXMmX0}5LnMB=; bWD9xox0Zb?EOY{4kRbnB+aIs8^hx|5`Ce<8 literal 0 HcmV?d00001 diff --git a/codchi/src/gui/bug_report.rs b/codchi/src/gui/bug_report.rs deleted file mode 100644 index 98cd82a5..00000000 --- a/codchi/src/gui/bug_report.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::gui::create_modal; -use crate::gui::MainPanel; -use crate::gui::MainPanelType; -use crate::platform::Machine; - -pub struct BugReportMainPanel { - show_modal: bool, - bug_report: String, - next_panel_type: Option, -} - -impl Default for BugReportMainPanel { - fn default() -> Self { - BugReportMainPanel { - show_modal: false, - bug_report: String::from(""), - next_panel_type: None, - } - } -} - -impl MainPanel for BugReportMainPanel { - fn update(&mut self, ui: &mut egui::Ui) { - ui.add_sized( - [ui.available_width(), 100.0], - egui::TextEdit::multiline(&mut self.bug_report).hint_text("Bug Description"), - ); - ui.separator(); - let send_button = egui::Button::new("Submit").fill(egui::Color32::GRAY); - ui.horizontal(|ui| { - if ui.add(send_button).clicked() { - self.bug_report = String::from(""); - - self.show_modal = true; - self.next_panel_type = Some(MainPanelType::MachineInspection); - } - if ui.button("Cancel").clicked() { - self.next_panel_type = Some(MainPanelType::MachineInspection); - } - }); - } - - fn modal_update(&mut self, ctx: &egui::Context) { - create_modal(ctx, "bug_report_modal", &mut self.show_modal, |ui| { - ui.strong("Bug Report was submitted."); - ui.label("Thank you for your feedback."); - }); - } - - fn next_panel(&mut self) -> Option { - self.next_panel_type.take() - } - - fn pass_machine(&mut self, machine: Machine) {} -} diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs index 6036df0f..d9bef504 100644 --- a/codchi/src/gui/machine_creation.rs +++ b/codchi/src/gui/machine_creation.rs @@ -3,6 +3,8 @@ use crate::gui::MainPanelType; use crate::platform::Machine; pub struct MachineCreationMainPanel { + status_text: Option, + machine_form: MachineForm, next_panel_type: Option, } @@ -14,6 +16,8 @@ struct MachineForm { impl Default for MachineCreationMainPanel { fn default() -> Self { MachineCreationMainPanel { + status_text: None, + machine_form: MachineForm::default(), next_panel_type: None, } @@ -27,13 +31,21 @@ impl MainPanel for MachineCreationMainPanel { } } - fn modal_update(&mut self, ctx: &egui::Context) {} + fn modal_update(&mut self, _ctx: &egui::Context) {} fn next_panel(&mut self) -> Option { self.next_panel_type.take() } - fn pass_machine(&mut self, machine: Machine) {} + fn pass_machine(&mut self, _machine: Machine) {} + + fn get_status_text(&self) -> &Option { + &self.status_text + } + + fn renew(&mut self) { + self.machine_form = MachineForm::default(); + } } impl Default for MachineForm { diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index fd475005..d23247b4 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -1,45 +1,365 @@ -use crate::gui::MainPanel; -use crate::gui::MainPanelType; -use crate::platform::Machine; -use crate::platform::PlatformStatus; +use crate::config::Mod; +use crate::gui::{create_password_field, MainPanel, MainPanelType}; +use crate::logging::CodchiOutput; +use crate::platform::{platform::HostImpl, DesktopEntry, Host, Machine, MachineDriver}; +use crate::secrets::{EnvSecret, MachineSecrets}; +use egui::*; +use itertools::Itertools; +use std::{ + collections::HashMap, + sync::mpsc::{channel, Receiver, Sender}, + thread, +}; + +use super::create_modal; pub struct MachineInspectionMainPanel { - machine: Option, + status_text: Option, + + machine_data_map: HashMap, + current_machine: String, next_panel_type: Option, + + pending_msgs: usize, + sender: Sender<(String, ChannelDataType)>, + receiver: Receiver<(String, ChannelDataType)>, + + show_delete_confirmation_modal: bool, + + textures: HashMap, +} + +struct MachineData { + machine: Machine, + applications: Option>, + modules: Option>, + secrets: Option>, + initialized: bool, +} + +enum ChannelDataType { + Applications(Vec), + Modules(Vec), + Secrets(Vec), +} + +impl MachineData { + fn new(machine: Machine) -> Self { + Self { + machine, + applications: None, + modules: None, + secrets: None, + initialized: false, + } + } } impl Default for MachineInspectionMainPanel { fn default() -> Self { + let (sender, receiver) = channel(); MachineInspectionMainPanel { - machine: None, + status_text: None, + + machine_data_map: HashMap::new(), + current_machine: String::from(""), next_panel_type: None, + + pending_msgs: 0, + sender, + receiver, + + show_delete_confirmation_modal: false, + + textures: HashMap::new(), } } } impl MainPanel for MachineInspectionMainPanel { - fn update(&mut self, ui: &mut egui::Ui) { - if let Some(machine) = &self.machine { - ui.heading(format!( - "Machine - '{}'", - machine.config.name - )); - ui.label(match machine.platform_status { - PlatformStatus::NotInstalled => "not installed", - PlatformStatus::Stopped => "not running", - PlatformStatus::Running => "running", + fn update(&mut self, ui: &mut Ui) { + if self.pending_msgs != 0 { + let received_answer: Option<(String, ChannelDataType)> = self.receiver.try_recv().ok(); + if let Some((machine_name, data_type)) = received_answer { + self.pending_msgs -= 1; + if self.pending_msgs == 0 { + self.status_text = None; + self.machine_data_map + .entry(machine_name.clone()) + .and_modify(|machine_data| { + machine_data.initialized = true; + }); + // attempt to load currently inspecting machine + if !self.current_machine.is_empty() { + self.pass_machine( + Machine::by_name(&self.current_machine, true).ok().unwrap(), + ); + } + } + match data_type { + ChannelDataType::Modules(modules) => { + self.machine_data_map + .entry(machine_name) + .and_modify(|machine_data| { + machine_data.modules = Some(modules); + }); + } + ChannelDataType::Secrets(secrets) => { + self.machine_data_map + .entry(machine_name) + .and_modify(|machine_data| { + machine_data.secrets = Some(secrets); + }); + } + ChannelDataType::Applications(applications) => { + self.machine_data_map + .entry(machine_name) + .and_modify(|machine_data| { + machine_data.applications = Some(applications); + }); + } + } + } + } + + if !self.current_machine.is_empty() { + ui.horizontal(|ui| { + let title_text = format!("Machine: {}", &self.current_machine); + ui.add(widgets::Label::new( + RichText::from(title_text).heading().strong(), + )); + ui.with_layout(Layout::right_to_left(Align::BOTTOM), |ui| { + let delete_button = Button::new("Delete").fill(Color32::DARK_RED); + if ui.add(delete_button).clicked() { + self.show_delete_confirmation_modal = true; + } + if ui.button("Stop").clicked() { + let _ = self + .machine_data_map + .get(&self.current_machine) + .unwrap() + .machine + .stop(false); + } + if ui.button("Reload").clicked() { + self.machine_data_map.remove(&self.current_machine); + self.pass_machine( + Machine::by_name(&self.current_machine, true).ok().unwrap(), + ); + } + }); }); + let machine_data = self.machine_data_map.get(&self.current_machine).unwrap(); + let status_text = match machine_data.machine.platform_status { + crate::platform::PlatformStatus::NotInstalled => "Not Installed", + crate::platform::PlatformStatus::Stopped => "Stopped", + crate::platform::PlatformStatus::Running => "Running", + }; + ui.label(status_text); ui.separator(); + + ui.heading("Applications"); + ui.add_space(3.0); + if let Some(desktop_entries) = &machine_data.applications { + for desktop_entry in desktop_entries { + let icon = if let Some(icon_path) = &desktop_entry.icon { + if !self.textures.contains_key(&desktop_entry.app_name) { + let image = image::open(icon_path) + .expect("Failed to open image icon") + .to_rgba8(); + let size = [image.width() as usize, image.height() as usize]; + let texture = ui.ctx().load_texture( + "icon_texture", + ColorImage::from_rgba_unmultiplied(size, &image), + Default::default(), + ); + self.textures + .insert(desktop_entry.app_name.clone(), texture); + } + let texture = self.textures.get(&desktop_entry.app_name).unwrap(); + let image = + Image::from_texture(texture).max_size(Vec2 { x: 12.0, y: 12.0 }); + Some(image) + } else { + None + }; + let text = WidgetText::RichText(RichText::new(&desktop_entry.app_name)); + let button = Button::opt_image_and_text(icon, Some(text)); + let button_handle = ui.add(button); + if button_handle.clicked() { + let _ = + HostImpl::execute(&machine_data.machine.config.name, &desktop_entry); + } + } + } else { + ui.horizontal(|ui| { + ui.add_space(20.0); + ui.label("Loading ..."); + }); + } + ui.separator(); + + ui.heading("Modules"); + if let Some(modules) = &machine_data.modules { + if !modules.is_empty() { + Grid::new("modules_grid").show(ui, |ui| { + ui.strong("Name\t"); + ui.strong("URL\t"); + ui.strong("Path\t"); + ui.end_row(); + + for module in modules { + ui.label(format!("{}\t", module.name)); + ui.label(format!("{}\t", module.url)); + ui.label(format!("{}\t", module.flake_module)); + ui.end_row(); + } + }); + } else { + ui.horizontal(|ui| { + ui.label("No modules"); + }); + } + } else { + ui.horizontal(|ui| { + ui.add_space(20.0); + ui.label("Loading ..."); + }); + } + ui.separator(); + + ui.heading("Secrets"); + if let Some(secrets) = &machine_data.secrets { + if !secrets.is_empty() { + Grid::new("secrets_grid").show(ui, |ui| { + ui.strong("Name\t"); + ui.strong("Description\t"); + ui.strong("Value\t"); + ui.end_row(); + + for secret in secrets { + let val = secret.value.clone().unwrap(); + ui.label(format!("{}\t", secret.name)); + ui.label(format!("{}\t", secret.description)); + ui.add(create_password_field(&format!("{}\t", val))); + ui.end_row(); + } + }); + } else { + ui.horizontal(|ui| { + ui.label("No secrets"); + }); + } + } else { + ui.horizontal(|ui| { + ui.add_space(20.0); + ui.label("Loading ..."); + }); + } } } - fn modal_update(&mut self, ctx: &egui::Context) {} + fn modal_update(&mut self, ctx: &Context) { + if self.show_delete_confirmation_modal { + let modal = Modal::new(Id::new("delete_machine_confirmation_modal")).show(ctx, |ui| { + ui.heading(format!("Delete machine '{}'?", &self.current_machine)); + ui.horizontal(|ui| { + let delete_button = Button::new("Delete").fill(Color32::DARK_RED); + if ui.add(delete_button).clicked() { + let machine = self + .machine_data_map + .remove(&self.current_machine) + .unwrap() + .machine; + self.current_machine = String::from(""); + thread::spawn(move || { + let _ = machine.delete(true); + }); + self.show_delete_confirmation_modal = false; + } + if ui.button("Cancel").clicked() { + self.show_delete_confirmation_modal = false; + } + }); + }); + if modal.should_close() { + self.show_delete_confirmation_modal = false; + } + } + } fn next_panel(&mut self) -> Option { self.next_panel_type.take() } fn pass_machine(&mut self, machine: Machine) { - self.machine = Some(machine); + let machine_name = machine.config.name.clone(); + if !self.machine_data_map.contains_key(&machine_name) { + let machine_data = MachineData::new(machine.clone()); + self.machine_data_map + .insert(machine_name.clone(), machine_data); + } + + if self.pending_msgs == 0 + && !self + .machine_data_map + .get(&machine_name) + .unwrap() + .initialized + { + self.pending_msgs = 3; + self.status_text = Some(String::from(format!( + "Loading machine {}...", + &machine_name + ))); + + let applications_sender = self.sender.clone(); + let machine_name_clone = machine_name.clone(); + let machine_clone = machine.clone(); + thread::spawn(move || { + let applications = HostImpl::list_desktop_entries(&machine_clone).ok().unwrap(); + + applications_sender + .send(( + machine_name_clone, + ChannelDataType::Applications(applications), + )) + .expect("modules could not be sent"); + }); + + let modules_sender = self.sender.clone(); + let modules_clone = machine.config.modules.clone(); + let machine_name_clone = machine_name.clone(); + thread::spawn(move || { + let modules = modules_clone.to_output(); + modules_sender + .send((machine_name_clone, ChannelDataType::Modules(modules))) + .expect("modules could not be sent"); + }); + + let secrets_sender = self.sender.clone(); + let machine_name_clone = machine_name.clone(); + thread::spawn(move || { + let secrets = machine + .eval_env_secrets() + .expect("failed to load machine secrets") + .into_values() + .collect_vec() + .to_output(); + secrets_sender + .send((machine_name_clone, ChannelDataType::Secrets(secrets))) + .expect("modules could not be sent"); + }); + } + self.current_machine = machine_name; + } + + fn get_status_text(&self) -> &Option { + &self.status_text + } + + fn renew(&mut self) { + self.machine_data_map.clear(); + self.current_machine = String::from(""); } } diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index bf520970..7fa3be99 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -1,23 +1,32 @@ -mod bug_report; mod machine_creation; mod machine_inspection; -use crate::platform::Machine; -use bug_report::BugReportMainPanel; -use egui; +use crate::{ + config::CodchiConfig, + platform::{Machine, PlatformStatus}, +}; +use egui::*; use machine_creation::MachineCreationMainPanel; use machine_inspection::MachineInspectionMainPanel; -use std::any::Any; +use std::{ + any::Any, + collections::HashMap, + sync::mpsc::{channel, Receiver, Sender}, + thread, +}; pub fn run() -> anyhow::Result<()> { let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default().with_inner_size((640.0, 480.0)), + viewport: ViewportBuilder::default().with_inner_size((960.0, 540.0)), ..Default::default() }; eframe::run_native( "Codchi", options, Box::new(|cc| { + // set custom theme + cc.egui_ctx.set_visuals(get_visuals()); + // This gives us image support: egui_extras::install_image_loaders(&cc.egui_ctx); @@ -25,29 +34,74 @@ pub fn run() -> anyhow::Result<()> { let ppp = cc.egui_ctx.pixels_per_point(); cc.egui_ctx.set_pixels_per_point(1.25 * ppp); - Ok(Box::::new(Gui::new())) + Ok(Box::::new(Gui::new(load_textures(&cc.egui_ctx)))) }), ) .unwrap(); Ok(()) } +fn load_textures(ctx: &Context) -> HashMap { + let green_square = ColorImage::new([10, 10], Color32::GREEN); + let yellow_square = ColorImage::new([10, 10], Color32::YELLOW); + let red_square = ColorImage::new([10, 10], Color32::RED); + + let green_handle = ctx.load_texture("green_texture", green_square, TextureOptions::default()); + let yellow_handle = + ctx.load_texture("yellow_texture", yellow_square, TextureOptions::default()); + let red_handle = ctx.load_texture("red_texture", red_square, TextureOptions::default()); + + let mut textures = HashMap::new(); + textures.insert("green".to_string(), green_handle); + textures.insert("yellow".to_string(), yellow_handle); + textures.insert("red".to_string(), red_handle); + + textures +} + struct Gui { main_panels: Vec>, current_main_panel_index: usize, - show_bug_report_modal: bool, machines: Vec, + textures: HashMap, + + status_text: Option, + + pending_msgs: usize, + sender: Sender, + receiver: Receiver, + + show_tray: bool, } #[derive(Clone)] -enum MainPanelType { +pub enum MainPanelType { MachineInspection, MachineCreation, BugReport, } +enum ChannelDataType { + Machines(Vec), +} + impl eframe::App for Gui { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { + if self.pending_msgs != 0 { + let received_answer: Option = self.receiver.try_recv().ok(); + if let Some(data_type) = received_answer { + self.pending_msgs -= 1; + if self.pending_msgs == 0 { + self.status_text = None; + } + match data_type { + ChannelDataType::Machines(machines) => { + self.machines = machines; + } + } + } + } + self.menu_bar_panel(ctx); self.status_bar_panel(ctx); self.side_panel(ctx); @@ -56,108 +110,170 @@ impl eframe::App for Gui { } impl Gui { - fn new() -> Self { + fn new(textures: HashMap) -> Self { + let (sender, receiver) = channel(); Self { main_panels: vec![ Box::new(MachineInspectionMainPanel::default()), Box::new(MachineCreationMainPanel::default()), - Box::new(BugReportMainPanel::default()), ], current_main_panel_index: 0, - show_bug_report_modal: false, machines: Machine::list(true).expect("Machines could not be listed"), + textures, + + status_text: None, + + pending_msgs: 0, + sender, + receiver, + + show_tray: false, } } - fn menu_bar_panel(&mut self, ctx: &egui::Context) { + fn menu_bar_panel(&mut self, ctx: &Context) { let height = 50.0; - egui::TopBottomPanel::top("menubar_panel") + TopBottomPanel::top("menubar_panel") .resizable(false) .exact_height(height) .show(ctx, |ui| { ui.horizontal_centered(|ui| { - let codchi_button = - egui::Button::image(egui::include_image!("../../assets/logo.png")); + let codchi_button = Button::image(include_image!("../../assets/logo.png")); if ui.add(codchi_button).clicked() { self.current_main_panel_index = Self::get_main_panel_index(MainPanelType::MachineInspection); } ui.separator(); - ui.menu_button("Settings", |ui| { - if ui.button("Zoom In").clicked() { - egui::gui_zoom::zoom_in(ctx); - ui.close_menu(); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.set_height(25.0); + let github_button = + Button::image(include_image!("../../assets/github_logo.png")); + let bug_report_button = + Button::image(include_image!("../../assets/bug_icon.png")); + if ui.add(github_button).clicked() { + ui.ctx() + .open_url(OpenUrl::new_tab("https://github.com/aformatik/codchi/")); } - if ui.button("Zoom Out").clicked() { - egui::gui_zoom::zoom_out(ctx); - ui.close_menu(); + if ui.add(bug_report_button).clicked() { + ui.ctx().open_url(OpenUrl::new_tab( + "https://github.com/aformatik/codchi/issues", + )); } + ui.menu_image_button(include_image!("../../assets/settings.png"), |ui| { + if ui.button("Zoom In").clicked() { + gui_zoom::zoom_in(ctx); + ui.close_menu(); + } + if ui.button("Zoom Out").clicked() { + gui_zoom::zoom_out(ctx); + ui.close_menu(); + } + ui.separator(); + if ui.button("Recover store").clicked() { + let _ = crate::platform::platform::store_recover(); + ui.close_menu(); + } + }); }); - if ui.button("BugReport").clicked() { - self.current_main_panel_index = - Self::get_main_panel_index(MainPanelType::BugReport); - } - if ui.button("Github").clicked() { - ui.ctx().open_url(egui::OpenUrl::new_tab( - "https://github.com/aformatik/codchi/", - )); - } }); }); } - fn status_bar_panel(&self, ctx: &egui::Context) {} + fn status_bar_panel(&self, ctx: &Context) { + TopBottomPanel::bottom("statusbar_panel") + .resizable(false) + .show(ctx, |ui| { + if let Some(text) = &self.status_text { + ui.label(text); + } else if let Some(text) = + self.main_panels[self.current_main_panel_index].get_status_text() + { + ui.label(text); + } + }); + } - fn side_panel(&mut self, ctx: &egui::Context) { + fn side_panel(&mut self, ctx: &Context) { let width = 200.0; - egui::SidePanel::left("side_panel") + SidePanel::left("side_panel") .exact_width(width) .resizable(false) .show(ctx, |ui| { - egui::ScrollArea::vertical() - .auto_shrink(false) - .show(ui, |ui| { - ui.with_layout( - egui::Layout::with_cross_justify( - egui::Layout::top_down(egui::Align::Center), - true, - ), - |ui| { - let new_machine_button = - egui::Button::new(egui::RichText::new("New").heading()); - if ui.add(new_machine_button).clicked() { - self.current_main_panel_index = - Self::get_main_panel_index(MainPanelType::MachineCreation); - } - ui.separator(); - for machine in &self.machines { - let machine_button = egui::Button::new(machine.config.name.clone()); - let button_handle = ui.add(machine_button); - if button_handle.clicked() { - let machine_inspection_panel_index = - Self::get_main_panel_index( - MainPanelType::MachineInspection, - ); - self.current_main_panel_index = - machine_inspection_panel_index; - let machine = Machine::by_name(&machine.config.name.clone(), true) - .expect("machine doesn't exist"); - self.main_panels[machine_inspection_panel_index].pass_machine(machine); + ScrollArea::vertical().auto_shrink(false).show(ui, |ui| { + let new_machine_button = Button::new(RichText::new("New").heading()); + let new_machin_button_handle = + ui.add_sized([ui.available_width(), 0.0], new_machine_button); + if new_machin_button_handle.clicked() { + self.current_main_panel_index = + Self::get_main_panel_index(MainPanelType::MachineCreation); + } + let reload_button = Button::new(RichText::new("Refresh").heading()); + let reload_button_handle = + ui.add_sized([ui.available_width(), 0.0], reload_button); + if reload_button_handle.clicked() { + self.pending_msgs += 1; + self.status_text = Some(String::from(format!("Reloading machines..."))); + self.machines = Machine::list(false).expect("Machines could not be reset"); + self.main_panels + [Self::get_main_panel_index(MainPanelType::MachineInspection)] + .renew(); + + let machines_sender = self.sender.clone(); + thread::spawn(move || { + let machines = + Machine::list(true).expect("Machines could not be listed"); + + machines_sender + .send(ChannelDataType::Machines(machines)) + .expect("modules could not be sent"); + }); + } + ui.separator(); + + ui.horizontal_top(|ui| { + ui.separator(); + ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { + for machine in &self.machines { + let icon = match machine.platform_status { + PlatformStatus::NotInstalled => None, + PlatformStatus::Stopped => { + let texture = self.textures.get("red").unwrap(); + Some(Image::from_texture(texture)) + } + PlatformStatus::Running => { + let texture = self.textures.get("green").unwrap(); + Some(Image::from_texture(texture)) } + }; + let button_text = + RichText::strong(format!("{}", machine.config.name).into()); + let machine_button = Button::opt_image_and_text( + icon, + Some(WidgetText::RichText(button_text)), + ); + let button_handle = ui.add(machine_button); + if button_handle.clicked() { + let machine_inspection_panel_index = Self::get_main_panel_index( + MainPanelType::MachineInspection, + ); + self.current_main_panel_index = machine_inspection_panel_index; + self.main_panels[machine_inspection_panel_index] + .pass_machine(machine.clone()); } - }, - ); + } + }) }); + }); }); } - fn main_panel(&mut self, ctx: &egui::Context) { - egui::CentralPanel::default().show(ctx, |ui| { - egui::ScrollArea::both() + fn main_panel(&mut self, ctx: &Context) { + CentralPanel::default().show(ctx, |ui| { + ScrollArea::both() .id_salt("machine_info_scroll") .auto_shrink(false) .show(ui, |ui| { - ui.spacing_mut().scroll = egui::style::ScrollStyle::solid(); + ui.spacing_mut().scroll = style::ScrollStyle::solid(); let next_panel_type_option = self.main_panels[self.current_main_panel_index].next_panel(); @@ -168,7 +284,7 @@ impl Gui { self.main_panels[self.current_main_panel_index].update(ui); }) }); - for mut main_panel in &mut self.main_panels { + for main_panel in &mut self.main_panels { main_panel.modal_update(ctx); } } @@ -183,23 +299,27 @@ impl Gui { } pub trait MainPanel: Any { - fn update(&mut self, ui: &mut egui::Ui); + fn update(&mut self, ui: &mut Ui); - fn modal_update(&mut self, ctx: &egui::Context); + fn modal_update(&mut self, ctx: &Context); fn next_panel(&mut self) -> Option; fn pass_machine(&mut self, machine: Machine); + + fn get_status_text(&self) -> &Option; + + fn renew(&mut self); } pub fn create_modal( - ctx: &egui::Context, + ctx: &Context, id: &str, show_modal_bool: &mut bool, - add_contents: impl FnOnce(&mut egui::Ui) -> R, + add_contents: impl FnOnce(&mut Ui) -> R, ) { if *show_modal_bool { - let modal = egui::Modal::new(egui::Id::new(id)).show(ctx, |ui| { + let modal = Modal::new(Id::new(id)).show(ctx, |ui| { add_contents(ui); ui.vertical_centered(|ui| { if ui.button("Ok").clicked() { @@ -212,3 +332,41 @@ pub fn create_modal( } } } + +pub fn create_password_field(password: &String) -> impl Widget + '_ { + move |ui: &mut Ui| create_password_field_ui(ui, password) +} + +pub fn create_password_field_ui(ui: &mut Ui, password: &str) -> Response { + let state_id = ui.id().with("show_plaintext"); + let mut show_plaintext = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(false)); + + let result = ui.horizontal(|ui| { + let response = ui + .add(SelectableLabel::new(show_plaintext, "👁")) + .on_hover_text("Show/hide password"); + + if response.clicked() { + show_plaintext = !show_plaintext; + } + + let mut password = String::from(password); + + ui.add_sized( + [200.0, ui.available_height()], + TextEdit::singleline(&mut password) + .interactive(false) + .password(!show_plaintext), + ); + }); + ui.data_mut(|d| d.insert_temp(state_id, show_plaintext)); + + result.response +} + +fn get_visuals() -> Visuals { + let mut visuals = Visuals::dark(); + visuals.widgets.active.fg_stroke.color = Color32::WHITE; + visuals.override_text_color = Some(Color32::LIGHT_GRAY); + visuals +} diff --git a/codchi/src/platform/host.rs b/codchi/src/platform/host.rs index 8f2d0e4b..ca302490 100644 --- a/codchi/src/platform/host.rs +++ b/codchi/src/platform/host.rs @@ -25,6 +25,12 @@ pub trait Host: Sized { fn delete_shortcuts(name: &str) -> Result<()>; fn write_machine_shortcuts(machine: &Machine) -> Result<()> { + let desktop_entries = Self::list_desktop_entries(machine)?; + Self::write_shortcuts(&machine.config.name, desktop_entries.iter())?; + Ok(()) + } + + fn list_desktop_entries(machine: &Machine) -> Result> { let nix_path = Driver::store().cmd().realpath( &consts::store::DIR_CONFIG .join_machine(&machine.config.name) @@ -92,8 +98,7 @@ pub trait Host: Sized { }); } - Self::write_shortcuts(&machine.config.name, desktop_entries.iter())?; - Ok(()) + Ok(desktop_entries) } fn open_terminal(&self, cmd: &[&str]) -> Result<()>; @@ -109,7 +114,6 @@ pub trait Host: Sized { p.exe().is_some_and(|p| p == exe) && p.cmd().get(1).is_some_and(|arg| arg == "tray") }) { - log::trace!("Kill running: {kill_running}. Process: {:?}", p.cmd()); if kill_running { log::debug!("Killing running tray"); @@ -150,6 +154,8 @@ pub trait Host: Sized { fn post_delete(_machine_name: &str) -> Result<()> { Ok(()) } + + fn execute(machine_name: &str, desktop_entry: &DesktopEntry) -> Result<()>; } #[derive(Clone, Debug)] diff --git a/codchi/src/platform/linux/host.rs b/codchi/src/platform/linux/host.rs index 57cc62c7..324dd89f 100644 --- a/codchi/src/platform/linux/host.rs +++ b/codchi/src/platform/linux/host.rs @@ -165,4 +165,16 @@ impl Host for HostImpl { } bail!("Could not find a terminal."); } + + fn execute(machine_name: &str, desktop_entry: &DesktopEntry) -> Result<()> { + let exe = env::current_exe()?.to_string_lossy().to_string(); + let mut cmd = Command::new(&exe); + cmd.args(["exec", machine_name]); + for arg in desktop_entry.exec.split(" ") { + cmd.arg(arg); + } + cmd.spawn()?; + + Ok(()) + } } diff --git a/codchi/src/platform/mod.rs b/codchi/src/platform/mod.rs index a8d27640..ecb495e3 100644 --- a/codchi/src/platform/mod.rs +++ b/codchi/src/platform/mod.rs @@ -6,7 +6,7 @@ mod store; #[allow(clippy::module_inception)] #[cfg_attr(target_os = "linux", path = "linux/mod.rs")] #[cfg_attr(target_os = "windows", path = "windows/mod.rs")] -mod platform; +pub mod platform; pub use self::cmd::*; pub use self::host::*; diff --git a/codchi/src/platform/windows/host.rs b/codchi/src/platform/windows/host.rs index e5615ef3..609a2308 100644 --- a/codchi/src/platform/windows/host.rs +++ b/codchi/src/platform/windows/host.rs @@ -11,7 +11,9 @@ use known_folders::{get_known_folder_path, KnownFolder}; use mslnk::{FileAttributeFlags, LinkFlags, MSLinkError, ShellLink}; use std::{env, fs, os::windows::process::CommandExt, path::Path, process::Command}; use sysinfo::System; -use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW}; +use windows::Win32::System::Threading::{ + CREATE_NEW_CONSOLE, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, +}; pub struct HostImpl; impl HostImpl {} @@ -274,4 +276,29 @@ impl Host for HostImpl { Ok(()) } + + fn execute(machine_name: &str, desktop_entry: &DesktopEntry) -> Result<()> { + let codchi_exe = get_known_folder_path(KnownFolder::LocalAppData) + .expect("FOLDERID_LocalAppData missing") + .join("Microsoft") + .join("WindowsApps") + .join("codchi.exe"); + let exe = if desktop_entry.is_terminal { + codchi_exe + } else { + codchi_exe + .parent() + .with_context(|| format!("Missing parent of {codchi_exe:?}"))? + .join("codchiw.exe") + }; + + let mut cmd = Command::new(&exe); + cmd.args(["exec", machine_name]); + for arg in desktop_entry.exec.split(" ") { + cmd.arg(arg); + } + cmd.creation_flags(CREATE_NEW_CONSOLE.0).spawn()?; + + Ok(()) + } } From c9c08f7b71dbd468eb7c915e52cd6f60245c7f0e Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 4 Mar 2025 15:37:08 +0000 Subject: [PATCH 09/49] Implemented codchi tray button --- codchi/src/gui/machine_inspection.rs | 2 -- codchi/src/gui/mod.rs | 18 ++++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index d23247b4..8435c66a 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -11,8 +11,6 @@ use std::{ thread, }; -use super::create_modal; - pub struct MachineInspectionMainPanel { status_text: Option, diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 7fa3be99..4a494d00 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -71,7 +71,7 @@ struct Gui { sender: Sender, receiver: Receiver, - show_tray: bool, + tray_autostart: bool, } #[derive(Clone)] @@ -127,7 +127,7 @@ impl Gui { sender, receiver, - show_tray: false, + tray_autostart: CodchiConfig::get().tray.autostart, } } @@ -173,6 +173,20 @@ impl Gui { let _ = crate::platform::platform::store_recover(); ui.close_menu(); } + ui.separator(); + let tray_button = Button::new(if self.tray_autostart { + "Hide tray icon" + } else { + "Show tray icon" + }); + if ui.add(tray_button).clicked() { + self.tray_autostart = !self.tray_autostart; + let mut doc = + CodchiConfig::open_mut().expect("Failed to open config"); + doc.tray_autostart(!self.tray_autostart); + doc.write().expect("Failed to write config"); + ui.close_menu(); + } }); }); }); From 1ff6ad63688c5dc4a1f44bdbcbbb84ddee937ea7 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 4 Mar 2025 15:38:17 +0000 Subject: [PATCH 10/49] Made store recover asynchronous --- codchi/src/gui/mod.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 4a494d00..1a04ef1e 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -83,6 +83,7 @@ pub enum MainPanelType { enum ChannelDataType { Machines(Vec), + StoreRecovered, } impl eframe::App for Gui { @@ -98,6 +99,9 @@ impl eframe::App for Gui { ChannelDataType::Machines(machines) => { self.machines = machines; } + ChannelDataType::StoreRecovered => { + self.status_text = Some(String::from("")); + } } } } @@ -170,7 +174,12 @@ impl Gui { } ui.separator(); if ui.button("Recover store").clicked() { - let _ = crate::platform::platform::store_recover(); + self.status_text = Some(String::from("Recovering Codchi store...")); + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let _ = crate::platform::platform::store_recover(); + sender_clone.send(ChannelDataType::StoreRecovered).unwrap(); + }); ui.close_menu(); } ui.separator(); From 07ceb898c25b669428a5b381ee529a8e1a62bc9d Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:30:23 +0000 Subject: [PATCH 11/49] Added remaining tray options --- codchi/src/gui/mod.rs | 95 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 1a04ef1e..81c013a4 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -2,7 +2,7 @@ mod machine_creation; mod machine_inspection; use crate::{ - config::CodchiConfig, + config::{CodchiConfig, ConfigMut}, platform::{Machine, PlatformStatus}, }; use egui::*; @@ -70,8 +70,6 @@ struct Gui { pending_msgs: usize, sender: Sender, receiver: Receiver, - - tray_autostart: bool, } #[derive(Clone)] @@ -130,8 +128,6 @@ impl Gui { pending_msgs: 0, sender, receiver, - - tray_autostart: CodchiConfig::get().tray.autostart, } } @@ -183,18 +179,54 @@ impl Gui { ui.close_menu(); } ui.separator(); - let tray_button = Button::new(if self.tray_autostart { - "Hide tray icon" - } else { - "Show tray icon" - }); - if ui.add(tray_button).clicked() { - self.tray_autostart = !self.tray_autostart; - let mut doc = - CodchiConfig::open_mut().expect("Failed to open config"); - doc.tray_autostart(!self.tray_autostart); - doc.write().expect("Failed to write config"); - ui.close_menu(); + ui.add(create_advanced_checkbox( + "codchi_tray_checkbox", + CodchiConfig::get().tray.autostart, + |checked| { + let mut doc = + CodchiConfig::open_mut().expect("Failed to open config"); + doc.tray_autostart(checked); + doc.write().expect("Failed to write config"); + }, + "Show tray icon", + )); + #[cfg(target_os = "windows")] + { + ui.menu_button("VcXsrv", |ui| { + ui.add(create_advanced_checkbox( + "vcxsrv_enable_checkbox", + CodchiConfig::get().vcxsrv.enable, + |checked| { + let mut doc = CodchiConfig::open_mut() + .expect("Failed to open config"); + doc.vcxsrv_enable(checked); + doc.write().expect("Failed to write config"); + }, + "Enable", + )); + ui.add(create_advanced_checkbox( + "vcxsrv_tray_checkbox", + CodchiConfig::get().vcxsrv.tray, + |checked| { + let mut doc = CodchiConfig::open_mut() + .expect("Failed to open config"); + doc.vcxsrv_tray(checked); + doc.write().expect("Failed to write config"); + }, + "Show tray icon", + )); + }); + ui.add(create_advanced_checkbox( + "wsl_vpnkit_enable_checkbox", + CodchiConfig::get().enable_wsl_vpnkit, + |checked| { + let mut doc = CodchiConfig::open_mut() + .expect("Failed to open config"); + doc.enable_wsl_vpnkit(checked); + doc.write().expect("Failed to write config"); + }, + "Enable wsl-vpnkit", + )); } }); }); @@ -387,6 +419,35 @@ pub fn create_password_field_ui(ui: &mut Ui, password: &str) -> Response { result.response } +pub fn create_advanced_checkbox<'a>( + id: &'a str, + initial: bool, + set: fn(enabled: bool), + text: &'a str, +) -> impl Widget + 'a { + move |ui: &mut Ui| create_advanced_checkbox_ui(ui, id, initial, set, text) +} + +pub fn create_advanced_checkbox_ui( + ui: &mut Ui, + id: &str, + initial: bool, + write_closure: impl FnOnce(bool), + text: &str, +) -> Response { + let state_id = ui.id().with(id); + let mut checked = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(initial)); + + let result = ui.checkbox(&mut checked, text); + + if result.clicked() { + write_closure(checked); + } + ui.data_mut(|d| d.insert_temp(state_id, checked)); + + result +} + fn get_visuals() -> Visuals { let mut visuals = Visuals::dark(); visuals.widgets.active.fg_stroke.color = Color32::WHITE; From 42bbf927c2d49182f12ea611b6447e21091e73ed Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:42:58 +0000 Subject: [PATCH 12/49] Made store recover windows-only --- codchi/src/gui/mod.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 81c013a4..a8e52f7b 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -2,7 +2,7 @@ mod machine_creation; mod machine_inspection; use crate::{ - config::{CodchiConfig, ConfigMut}, + config::CodchiConfig, platform::{Machine, PlatformStatus}, }; use egui::*; @@ -169,16 +169,20 @@ impl Gui { ui.close_menu(); } ui.separator(); - if ui.button("Recover store").clicked() { - self.status_text = Some(String::from("Recovering Codchi store...")); - let sender_clone = self.sender.clone(); - thread::spawn(move || { - let _ = crate::platform::platform::store_recover(); - sender_clone.send(ChannelDataType::StoreRecovered).unwrap(); - }); - ui.close_menu(); + #[cfg(target_os = "windows")] + { + if ui.button("Recover store").clicked() { + self.status_text = + Some(String::from("Recovering Codchi store...")); + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let _ = crate::platform::platform::store_recover(); + sender_clone.send(ChannelDataType::StoreRecovered).unwrap(); + }); + ui.close_menu(); + } + ui.separator(); } - ui.separator(); ui.add(create_advanced_checkbox( "codchi_tray_checkbox", CodchiConfig::get().tray.autostart, From d70e1d82dbfec599c61f646c63940b7c61c5fa5c Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Wed, 5 Mar 2025 12:18:33 +0000 Subject: [PATCH 13/49] Machines reload periodically --- codchi/src/gui/mod.rs | 146 ++++++++++++++++++++++++++---------------- 1 file changed, 92 insertions(+), 54 deletions(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index a8e52f7b..5933ced5 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -13,6 +13,7 @@ use std::{ collections::HashMap, sync::mpsc::{channel, Receiver, Sender}, thread, + time::Instant, }; pub fn run() -> anyhow::Result<()> { @@ -41,24 +42,6 @@ pub fn run() -> anyhow::Result<()> { Ok(()) } -fn load_textures(ctx: &Context) -> HashMap { - let green_square = ColorImage::new([10, 10], Color32::GREEN); - let yellow_square = ColorImage::new([10, 10], Color32::YELLOW); - let red_square = ColorImage::new([10, 10], Color32::RED); - - let green_handle = ctx.load_texture("green_texture", green_square, TextureOptions::default()); - let yellow_handle = - ctx.load_texture("yellow_texture", yellow_square, TextureOptions::default()); - let red_handle = ctx.load_texture("red_texture", red_square, TextureOptions::default()); - - let mut textures = HashMap::new(); - textures.insert("green".to_string(), green_handle); - textures.insert("yellow".to_string(), yellow_handle); - textures.insert("red".to_string(), red_handle); - - textures -} - struct Gui { main_panels: Vec>, current_main_panel_index: usize, @@ -70,6 +53,9 @@ struct Gui { pending_msgs: usize, sender: Sender, receiver: Receiver, + + reloading_machine_index: Option, + last_reload: Instant, } #[derive(Clone)] @@ -80,12 +66,19 @@ pub enum MainPanelType { } enum ChannelDataType { - Machines(Vec), + Machine(Machine, usize), StoreRecovered, } impl eframe::App for Gui { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { + if self.reloading_machine_index.is_none() { + let now = Instant::now(); + if now.duration_since(self.last_reload).as_secs() >= 10 { + self.reloading_machine_index = Some(0); + self.reload_machine(); + } + } if self.pending_msgs != 0 { let received_answer: Option = self.receiver.try_recv().ok(); if let Some(data_type) = received_answer { @@ -94,8 +87,9 @@ impl eframe::App for Gui { self.status_text = None; } match data_type { - ChannelDataType::Machines(machines) => { - self.machines = machines; + ChannelDataType::Machine(machine, machine_index) => { + self.machines[machine_index] = machine; + self.reloading_machine_index = self.reload_machine(); } ChannelDataType::StoreRecovered => { self.status_text = Some(String::from("")); @@ -128,6 +122,9 @@ impl Gui { pending_msgs: 0, sender, receiver, + + reloading_machine_index: None, + last_reload: Instant::now(), } } @@ -266,44 +263,34 @@ impl Gui { self.current_main_panel_index = Self::get_main_panel_index(MainPanelType::MachineCreation); } - let reload_button = Button::new(RichText::new("Refresh").heading()); - let reload_button_handle = - ui.add_sized([ui.available_width(), 0.0], reload_button); - if reload_button_handle.clicked() { - self.pending_msgs += 1; - self.status_text = Some(String::from(format!("Reloading machines..."))); - self.machines = Machine::list(false).expect("Machines could not be reset"); - self.main_panels - [Self::get_main_panel_index(MainPanelType::MachineInspection)] - .renew(); - - let machines_sender = self.sender.clone(); - thread::spawn(move || { - let machines = - Machine::list(true).expect("Machines could not be listed"); - - machines_sender - .send(ChannelDataType::Machines(machines)) - .expect("modules could not be sent"); - }); - } ui.separator(); ui.horizontal_top(|ui| { ui.separator(); ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { - for machine in &self.machines { - let icon = match machine.platform_status { - PlatformStatus::NotInstalled => None, - PlatformStatus::Stopped => { - let texture = self.textures.get("red").unwrap(); + for i in 0..self.machines.len() { + let machine = &self.machines[i]; + let icon = + if self.reloading_machine_index.is_some_and(|index| index == i) + { + let texture = &self.textures["gray"]; Some(Image::from_texture(texture)) - } - PlatformStatus::Running => { - let texture = self.textures.get("green").unwrap(); - Some(Image::from_texture(texture)) - } - }; + } else { + match machine.platform_status { + PlatformStatus::NotInstalled => { + let texture = &self.textures["yellow"]; + Some(Image::from_texture(texture)) + } + PlatformStatus::Stopped => { + let texture = &self.textures["red"]; + Some(Image::from_texture(texture)) + } + PlatformStatus::Running => { + let texture = &self.textures["green"]; + Some(Image::from_texture(texture)) + } + } + }; let button_text = RichText::strong(format!("{}", machine.config.name).into()); let machine_button = Button::opt_image_and_text( @@ -355,6 +342,33 @@ impl Gui { MainPanelType::BugReport => 2, } } + + fn reload_machine(&mut self) -> Option { + if let Some(machine_index) = self.reloading_machine_index { + if let Some(machine) = self.machines.get_mut(machine_index) { + self.pending_msgs += 1; + self.status_text = Some(String::from(format!( + "Updating status for machine '{}'", + machine.config.name + ))); + + let machine_config = machine.config.clone(); + let machine_sender = self.sender.clone(); + thread::spawn(move || { + let machine = + Machine::read(machine_config, true).expect("Machine could not be read"); + + machine_sender + .send(ChannelDataType::Machine(machine, machine_index)) + .expect("machine could not be sent"); + }); + } + Some(machine_index + 1) + } else { + self.last_reload = Instant::now(); + None + } + } } pub trait MainPanel: Any { @@ -452,9 +466,33 @@ pub fn create_advanced_checkbox_ui( result } +fn load_textures(ctx: &Context) -> HashMap { + let green_square = ColorImage::new([10, 10], Color32::GREEN); + let yellow_square = ColorImage::new([10, 10], Color32::YELLOW); + let red_square = ColorImage::new([10, 10], Color32::RED); + let gray_square = ColorImage::new( + [10, 10], + get_visuals().widgets.noninteractive.fg_stroke.color, + ); + + let green_handle = ctx.load_texture("green_texture", green_square, TextureOptions::default()); + let yellow_handle = + ctx.load_texture("yellow_texture", yellow_square, TextureOptions::default()); + let red_handle = ctx.load_texture("red_texture", red_square, TextureOptions::default()); + let gray_handle = ctx.load_texture("gray_texture", gray_square, TextureOptions::default()); + + let mut textures = HashMap::new(); + textures.insert("green".to_string(), green_handle); + textures.insert("yellow".to_string(), yellow_handle); + textures.insert("red".to_string(), red_handle); + textures.insert("gray".to_string(), gray_handle); + + textures +} + fn get_visuals() -> Visuals { let mut visuals = Visuals::dark(); visuals.widgets.active.fg_stroke.color = Color32::WHITE; - visuals.override_text_color = Some(Color32::LIGHT_GRAY); + visuals.widgets.noninteractive.fg_stroke.color = Color32::LIGHT_GRAY; visuals } From f51706575929d6314fca98da0e58fa71d0626c36 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Wed, 5 Mar 2025 13:26:53 +0000 Subject: [PATCH 14/49] Reload strategy now works as intended --- codchi/src/gui/mod.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 5933ced5..3b3b6b9c 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -89,7 +89,13 @@ impl eframe::App for Gui { match data_type { ChannelDataType::Machine(machine, machine_index) => { self.machines[machine_index] = machine; - self.reloading_machine_index = self.reload_machine(); + if machine_index + 1 < self.machines.len() { + self.reloading_machine_index = Some(machine_index + 1); + } else { + self.reloading_machine_index = None; + self.last_reload = Instant::now(); + } + self.reload_machine(); } ChannelDataType::StoreRecovered => { self.status_text = Some(String::from("")); @@ -343,7 +349,7 @@ impl Gui { } } - fn reload_machine(&mut self) -> Option { + fn reload_machine(&mut self) { if let Some(machine_index) = self.reloading_machine_index { if let Some(machine) = self.machines.get_mut(machine_index) { self.pending_msgs += 1; @@ -363,10 +369,6 @@ impl Gui { .expect("machine could not be sent"); }); } - Some(machine_index + 1) - } else { - self.last_reload = Instant::now(); - None } } } From e45d02898caba7f6bb5dadcc51cc708b97330bce Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:39:47 +0000 Subject: [PATCH 15/49] Added Rebuild, Duplicate and Tar buttons --- codchi/src/gui/machine_inspection.rs | 191 +++++++++++++++++++++++---- 1 file changed, 163 insertions(+), 28 deletions(-) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 8435c66a..3227b840 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -5,6 +5,7 @@ use crate::platform::{platform::HostImpl, DesktopEntry, Host, Machine, MachineDr use crate::secrets::{EnvSecret, MachineSecrets}; use egui::*; use itertools::Itertools; +use std::path::PathBuf; use std::{ collections::HashMap, sync::mpsc::{channel, Receiver, Sender}, @@ -22,6 +23,9 @@ pub struct MachineInspectionMainPanel { sender: Sender<(String, ChannelDataType)>, receiver: Receiver<(String, ChannelDataType)>, + show_rebuild_spec_modal: bool, + show_duplicate_spec_modal: bool, + show_tar_spec_modal: bool, show_delete_confirmation_modal: bool, textures: HashMap, @@ -39,6 +43,7 @@ enum ChannelDataType { Applications(Vec), Modules(Vec), Secrets(Vec), + ClearStatus, } impl MachineData { @@ -67,6 +72,9 @@ impl Default for MachineInspectionMainPanel { sender, receiver, + show_rebuild_spec_modal: false, + show_duplicate_spec_modal: false, + show_tar_spec_modal: false, show_delete_confirmation_modal: false, textures: HashMap::new(), @@ -116,6 +124,7 @@ impl MainPanel for MachineInspectionMainPanel { machine_data.applications = Some(applications); }); } + ChannelDataType::ClearStatus => {} } } } @@ -127,27 +136,35 @@ impl MainPanel for MachineInspectionMainPanel { RichText::from(title_text).heading().strong(), )); ui.with_layout(Layout::right_to_left(Align::BOTTOM), |ui| { - let delete_button = Button::new("Delete").fill(Color32::DARK_RED); - if ui.add(delete_button).clicked() { - self.show_delete_confirmation_modal = true; - } - if ui.button("Stop").clicked() { - let _ = self - .machine_data_map - .get(&self.current_machine) - .unwrap() - .machine - .stop(false); - } - if ui.button("Reload").clicked() { - self.machine_data_map.remove(&self.current_machine); - self.pass_machine( - Machine::by_name(&self.current_machine, true).ok().unwrap(), - ); - } + ui.menu_button("Actions", |ui| { + if ui.button("Reload").clicked() { + self.machine_data_map.remove(&self.current_machine); + self.pass_machine( + Machine::by_name(&self.current_machine, true).ok().unwrap(), + ); + } + if ui.button("Rebuild").clicked() { + self.show_rebuild_spec_modal = true; + } + if ui.button("Duplicate").clicked() { + self.show_duplicate_spec_modal = true; + } + if ui.button("Tar").clicked() { + self.show_tar_spec_modal = true; + } + if ui.button("Stop").clicked() { + let _ = self.machine_data_map[&self.current_machine] + .machine + .stop(false); + } + let delete_button = Button::new("Delete").fill(Color32::DARK_RED); + if ui.add(delete_button).clicked() { + self.show_delete_confirmation_modal = true; + } + }); }); }); - let machine_data = self.machine_data_map.get(&self.current_machine).unwrap(); + let machine_data = &self.machine_data_map[&self.current_machine]; let status_text = match machine_data.machine.platform_status { crate::platform::PlatformStatus::NotInstalled => "Not Installed", crate::platform::PlatformStatus::Stopped => "Stopped", @@ -174,7 +191,7 @@ impl MainPanel for MachineInspectionMainPanel { self.textures .insert(desktop_entry.app_name.clone(), texture); } - let texture = self.textures.get(&desktop_entry.app_name).unwrap(); + let texture = &self.textures[&desktop_entry.app_name]; let image = Image::from_texture(texture).max_size(Vec2 { x: 12.0, y: 12.0 }); Some(image) @@ -258,20 +275,144 @@ impl MainPanel for MachineInspectionMainPanel { } fn modal_update(&mut self, ctx: &Context) { + if self.show_rebuild_spec_modal { + let modal = Modal::new(Id::new("rebuild_machine_spec_modal")).show(ctx, |ui| { + let state_id = ui.id().with("rebuild_machine_spec_modal_checkbox"); + let mut checked = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(true)); + ui.heading(format!("Rebuild machine '{}'", &self.current_machine)); + ui.checkbox(&mut checked, "update modules"); + ui.horizontal(|ui| { + let rebuild_button = Button::new("Rebuild").fill(Color32::DARK_BLUE); + if ui.add(rebuild_button).clicked() { + self.status_text = Some(String::from(format!( + "Building machine '{}'...", + self.current_machine + ))); + + self.pending_msgs += 1; + let mut machine = + self.machine_data_map[&self.current_machine].machine.clone(); + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let _ = machine.build(!checked); + sender_clone + .send((machine.config.name, ChannelDataType::ClearStatus)) + .unwrap(); + }); + self.show_rebuild_spec_modal = false; + } + if ui.button("Cancel").clicked() { + self.show_rebuild_spec_modal = false; + } + }); + ui.data_mut(|d| d.insert_temp(state_id, checked)); + }); + if modal.should_close() { + self.show_delete_confirmation_modal = false; + } + } + if self.show_duplicate_spec_modal { + let modal = Modal::new(Id::new("duplicate_machine_spec_modal")).show(ctx, |ui| { + let state_id = ui.id().with("duplicate_machine_spec_modal_name"); + let mut new_machine_name = + ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(String::from(""))); + ui.heading(format!("Duplicate machine '{}'", &self.current_machine)); + let name_editor = + TextEdit::singleline(&mut new_machine_name).hint_text("New Machine Name"); + ui.add(name_editor); + ui.horizontal(|ui| { + let duplicate_button = Button::new("Duplicate").fill(Color32::DARK_GREEN); + if ui.add(duplicate_button).clicked() { + self.status_text = Some(String::from(format!( + "Duplicating machine '{}' as '{}'...", + self.current_machine, new_machine_name + ))); + + self.pending_msgs += 1; + let machine = self.machine_data_map[&self.current_machine].machine.clone(); + let new_machine_name_clone = new_machine_name.clone(); + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let _ = machine.duplicate(&new_machine_name_clone); + sender_clone + .send((machine.config.name, ChannelDataType::ClearStatus)) + .unwrap(); + }); + self.show_duplicate_spec_modal = false; + } + if ui.button("Cancel").clicked() { + self.show_duplicate_spec_modal = false; + } + }); + ui.data_mut(|d| d.insert_temp(state_id, new_machine_name)); + }); + if modal.should_close() { + self.show_delete_confirmation_modal = false; + } + } + if self.show_tar_spec_modal { + let modal = Modal::new(Id::new("tar_machine_spec_modal")).show(ctx, |ui| { + let state_id = ui.id().with("tar_machine_spec_modal_path"); + let mut tar_path = + ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(String::from(""))); + ui.heading(format!("Export machine '{}'", &self.current_machine)); + let path_editor = TextEdit::singleline(&mut tar_path).hint_text(".tar Path"); + ui.add(path_editor); + ui.horizontal(|ui| { + let tar_button = Button::new("Tar").fill(Color32::DARK_GREEN); + if ui.add(tar_button).clicked() { + let path = PathBuf::try_from(&tar_path).unwrap(); + self.status_text = Some(String::from(format!( + "Exporting files of {} to {path:?}...", + self.current_machine + ))); + + self.pending_msgs += 1; + let machine = self.machine_data_map[&self.current_machine].machine.clone(); + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let _ = machine.tar(&path); + sender_clone + .send((machine.config.name, ChannelDataType::ClearStatus)) + .unwrap(); + }); + self.show_tar_spec_modal = false; + } + if ui.button("Cancel").clicked() { + self.show_tar_spec_modal = false; + } + }); + ui.data_mut(|d| d.insert_temp(state_id, tar_path)); + }); + if modal.should_close() { + self.show_delete_confirmation_modal = false; + } + } if self.show_delete_confirmation_modal { let modal = Modal::new(Id::new("delete_machine_confirmation_modal")).show(ctx, |ui| { ui.heading(format!("Delete machine '{}'?", &self.current_machine)); ui.horizontal(|ui| { let delete_button = Button::new("Delete").fill(Color32::DARK_RED); if ui.add(delete_button).clicked() { + self.status_text = Some(String::from(format!( + "Deleting machine '{}'...", + self.current_machine + ))); + + self.pending_msgs += 1; let machine = self .machine_data_map .remove(&self.current_machine) .unwrap() .machine; - self.current_machine = String::from(""); + let mut machine_name = String::from(""); + std::mem::swap(&mut self.current_machine, &mut machine_name); + let sender_clone = self.sender.clone(); thread::spawn(move || { let _ = machine.delete(true); + sender_clone + .send((machine_name, ChannelDataType::ClearStatus)) + .unwrap(); }); self.show_delete_confirmation_modal = false; } @@ -298,13 +439,7 @@ impl MainPanel for MachineInspectionMainPanel { .insert(machine_name.clone(), machine_data); } - if self.pending_msgs == 0 - && !self - .machine_data_map - .get(&machine_name) - .unwrap() - .initialized - { + if self.pending_msgs == 0 && !self.machine_data_map[&machine_name].initialized { self.pending_msgs = 3; self.status_text = Some(String::from(format!( "Loading machine {}...", From 028c8f3a8f7381d780b0a9cd54622763e81c9a03 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:53:56 +0000 Subject: [PATCH 16/49] Added Spinner to statusbar --- codchi/src/gui/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 3b3b6b9c..cfc1ddd7 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -245,12 +245,19 @@ impl Gui { TopBottomPanel::bottom("statusbar_panel") .resizable(false) .show(ctx, |ui| { + let mut final_text = None; if let Some(text) = &self.status_text { - ui.label(text); + final_text = Some(text); } else if let Some(text) = self.main_panels[self.current_main_panel_index].get_status_text() { - ui.label(text); + final_text = Some(text); + } + if let Some(text) = final_text { + ui.horizontal(|ui| { + ui.add(egui::Spinner::new()); + ui.label(text); + }); } }); } From 6491c07922b6f9501fb14b60b89f7772af564f11 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:50:04 +0000 Subject: [PATCH 17/49] Added open Shell button for machines --- codchi/src/gui/machine_inspection.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 3227b840..5f1c2cf6 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -206,6 +206,13 @@ impl MainPanel for MachineInspectionMainPanel { HostImpl::execute(&machine_data.machine.config.name, &desktop_entry); } } + if ui.button("Shell").clicked() { + let _ = crate::platform::Driver::host().open_terminal(&[ + &std::env::current_exe().unwrap().display().to_string(), + "exec", + &machine_data.machine.config.name, + ]); + } } else { ui.horizontal(|ui| { ui.add_space(20.0); From 057784f66104fc7f320c75e8f7bd9d799872d6e5 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:19:29 +0000 Subject: [PATCH 18/49] updated machine reload button --- codchi/assets/reload.png | Bin 0 -> 134019 bytes codchi/src/gui/machine_inspection.rs | 14 ++++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100755 codchi/assets/reload.png diff --git a/codchi/assets/reload.png b/codchi/assets/reload.png new file mode 100755 index 0000000000000000000000000000000000000000..0d4d3d6811e95d63966ee7032082b42a94d578a5 GIT binary patch literal 134019 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 LI}5iT{_p<*LJ~Vh literal 0 HcmV?d00001 diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 5f1c2cf6..bc9291ba 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -137,12 +137,6 @@ impl MainPanel for MachineInspectionMainPanel { )); ui.with_layout(Layout::right_to_left(Align::BOTTOM), |ui| { ui.menu_button("Actions", |ui| { - if ui.button("Reload").clicked() { - self.machine_data_map.remove(&self.current_machine); - self.pass_machine( - Machine::by_name(&self.current_machine, true).ok().unwrap(), - ); - } if ui.button("Rebuild").clicked() { self.show_rebuild_spec_modal = true; } @@ -162,6 +156,14 @@ impl MainPanel for MachineInspectionMainPanel { self.show_delete_confirmation_modal = true; } }); + let reload_image = Image::new(include_image!("../../assets/reload.png")); + let reload_button = Button::image(reload_image); + if ui.add(reload_button).clicked() { + self.machine_data_map.remove(&self.current_machine); + self.pass_machine( + Machine::by_name(&self.current_machine, true).ok().unwrap(), + ); + } }); }); let machine_data = &self.machine_data_map[&self.current_machine]; From 8c02c954f3a5dcf269c6e445347dc0e2ce07d2c2 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:16:05 +0000 Subject: [PATCH 19/49] Reload strategy can now deal with adding to/removing from machines --- codchi/src/gui/mod.rs | 44 +++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index cfc1ddd7..1c9c11c1 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -2,7 +2,7 @@ mod machine_creation; mod machine_inspection; use crate::{ - config::CodchiConfig, + config::{CodchiConfig, MachineConfig}, platform::{Machine, PlatformStatus}, }; use egui::*; @@ -45,6 +45,7 @@ pub fn run() -> anyhow::Result<()> { struct Gui { main_panels: Vec>, current_main_panel_index: usize, + machine_configs: Vec, machines: Vec, textures: HashMap, @@ -76,6 +77,8 @@ impl eframe::App for Gui { let now = Instant::now(); if now.duration_since(self.last_reload).as_secs() >= 10 { self.reloading_machine_index = Some(0); + self.machine_configs = + MachineConfig::list().expect("Machine-configs could not be listed"); self.reload_machine(); } } @@ -88,10 +91,22 @@ impl eframe::App for Gui { } match data_type { ChannelDataType::Machine(machine, machine_index) => { - self.machines[machine_index] = machine; - if machine_index + 1 < self.machines.len() { - self.reloading_machine_index = Some(machine_index + 1); + // reload happens for MachineConfig::list(), which can diverge from Machine::list() + if machine_index < self.machines.len() + && self.machines[machine_index].config.name == machine.config.name + { + self.machines[machine_index] = machine; } else { + self.machines.insert(machine_index, machine); + } + let next_machine_index = machine_index + 1; + if next_machine_index < self.machine_configs.len() { + self.reloading_machine_index = Some(next_machine_index); + } else { + // remove remaining machines that were not listed by MachineConfig::list() + while self.machines.get(next_machine_index).is_some() { + self.machines.remove(next_machine_index); + } self.reloading_machine_index = None; self.last_reload = Instant::now(); } @@ -120,6 +135,7 @@ impl Gui { Box::new(MachineCreationMainPanel::default()), ], current_main_panel_index: 0, + machine_configs: MachineConfig::list().expect("Machine-configs could not be listed"), machines: Machine::list(true).expect("Machines could not be listed"), textures, @@ -358,20 +374,20 @@ impl Gui { fn reload_machine(&mut self) { if let Some(machine_index) = self.reloading_machine_index { - if let Some(machine) = self.machines.get_mut(machine_index) { + if let Some(machine_config) = self.machine_configs.get_mut(machine_index) { self.pending_msgs += 1; self.status_text = Some(String::from(format!( "Updating status for machine '{}'", - machine.config.name + machine_config.name ))); - let machine_config = machine.config.clone(); - let machine_sender = self.sender.clone(); + let machine_config_clone = machine_config.clone(); + let machine_sender_clone = self.sender.clone(); thread::spawn(move || { - let machine = - Machine::read(machine_config, true).expect("Machine could not be read"); + let machine = Machine::read(machine_config_clone, true) + .expect("Machine could not be read"); - machine_sender + machine_sender_clone .send(ChannelDataType::Machine(machine, machine_index)) .expect("machine could not be sent"); }); @@ -415,7 +431,7 @@ pub fn create_modal( } } -pub fn create_password_field(password: &String) -> impl Widget + '_ { +pub fn create_password_field(password: &str) -> impl Widget + '_ { move |ui: &mut Ui| create_password_field_ui(ui, password) } @@ -449,10 +465,10 @@ pub fn create_password_field_ui(ui: &mut Ui, password: &str) -> Response { pub fn create_advanced_checkbox<'a>( id: &'a str, initial: bool, - set: fn(enabled: bool), + write_closure: impl FnOnce(bool) + 'a, text: &'a str, ) -> impl Widget + 'a { - move |ui: &mut Ui| create_advanced_checkbox_ui(ui, id, initial, set, text) + move |ui: &mut Ui| create_advanced_checkbox_ui(ui, id, initial, write_closure, text) } pub fn create_advanced_checkbox_ui( From 3a95aabe8fbffd31a660aa952a615fe9f2db91e8 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Fri, 7 Mar 2025 15:03:40 +0000 Subject: [PATCH 20/49] made secrets writable --- codchi/src/gui/machine_inspection.rs | 16 ++++++++++++-- codchi/src/gui/mod.rs | 33 +++++++++++++++++++++------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index bc9291ba..44258386 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -262,10 +262,22 @@ impl MainPanel for MachineInspectionMainPanel { ui.end_row(); for secret in secrets { - let val = secret.value.clone().unwrap(); ui.label(format!("{}\t", secret.name)); ui.label(format!("{}\t", secret.description)); - ui.add(create_password_field(&format!("{}\t", val))); + ui.add(create_password_field( + &secret.name, + |new_password| { + let (lock, mut cfg) = + crate::config::MachineConfig::open_existing( + &self.current_machine, + true, + ) + .unwrap(); + cfg.secrets.insert(secret.name.clone(), new_password); + cfg.write(lock).unwrap(); + }, + &secret.value.clone().unwrap(), + )); ui.end_row(); } }); diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 1c9c11c1..ef670b3d 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -431,13 +431,27 @@ pub fn create_modal( } } -pub fn create_password_field(password: &str) -> impl Widget + '_ { - move |ui: &mut Ui| create_password_field_ui(ui, password) +pub fn create_password_field<'a>( + id: &'a str, + write_closure: impl FnOnce(String) + 'a, + password: &'a str, +) -> impl Widget + 'a { + move |ui: &mut Ui| create_password_field_ui(ui, id, write_closure, password) } -pub fn create_password_field_ui(ui: &mut Ui, password: &str) -> Response { +pub fn create_password_field_ui( + ui: &mut Ui, + id: &str, + write_closure: impl FnOnce(String), + password: &str, +) -> Response { let state_id = ui.id().with("show_plaintext"); + let text_id = ui.id().with(id); let mut show_plaintext = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(false)); + let mut text = ui.data_mut(|d| { + d.get_temp::(text_id) + .unwrap_or(String::from(password)) + }); let result = ui.horizontal(|ui| { let response = ui @@ -448,16 +462,19 @@ pub fn create_password_field_ui(ui: &mut Ui, password: &str) -> Response { show_plaintext = !show_plaintext; } - let mut password = String::from(password); - ui.add_sized( [200.0, ui.available_height()], - TextEdit::singleline(&mut password) - .interactive(false) - .password(!show_plaintext), + TextEdit::singleline(&mut text).password(!show_plaintext), ); + + if password != text { + if ui.button("Write").clicked() { + write_closure(text.clone()); + } + } }); ui.data_mut(|d| d.insert_temp(state_id, show_plaintext)); + ui.data_mut(|d| d.insert_temp(text_id, text)); result.response } From 5cb7d474c4a6bdc63f5229e898c295d583332be4 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:59:04 +0000 Subject: [PATCH 21/49] multiple status texts enabled --- codchi/src/gui/machine_creation.rs | 8 +- codchi/src/gui/machine_inspection.rs | 148 +++++++++++------------ codchi/src/gui/mod.rs | 174 +++++++++++++++++---------- 3 files changed, 190 insertions(+), 140 deletions(-) diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs index d9bef504..7303b7f4 100644 --- a/codchi/src/gui/machine_creation.rs +++ b/codchi/src/gui/machine_creation.rs @@ -2,8 +2,10 @@ use crate::gui::MainPanel; use crate::gui::MainPanelType; use crate::platform::Machine; +use super::StatusEntries; + pub struct MachineCreationMainPanel { - status_text: Option, + status_text: StatusEntries, machine_form: MachineForm, next_panel_type: Option, @@ -16,7 +18,7 @@ struct MachineForm { impl Default for MachineCreationMainPanel { fn default() -> Self { MachineCreationMainPanel { - status_text: None, + status_text: StatusEntries::new(), machine_form: MachineForm::default(), next_panel_type: None, @@ -39,7 +41,7 @@ impl MainPanel for MachineCreationMainPanel { fn pass_machine(&mut self, _machine: Machine) {} - fn get_status_text(&self) -> &Option { + fn get_status_text(&self) -> &StatusEntries { &self.status_text } diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 44258386..01de6d77 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -12,16 +12,17 @@ use std::{ thread, }; +use super::StatusEntries; + pub struct MachineInspectionMainPanel { - status_text: Option, + status_text: StatusEntries, machine_data_map: HashMap, current_machine: String, next_panel_type: Option, - pending_msgs: usize, - sender: Sender<(String, ChannelDataType)>, - receiver: Receiver<(String, ChannelDataType)>, + sender: Sender<(usize, String, ChannelDataType)>, + receiver: Receiver<(usize, String, ChannelDataType)>, show_rebuild_spec_modal: bool, show_duplicate_spec_modal: bool, @@ -62,13 +63,12 @@ impl Default for MachineInspectionMainPanel { fn default() -> Self { let (sender, receiver) = channel(); MachineInspectionMainPanel { - status_text: None, + status_text: StatusEntries::new(), machine_data_map: HashMap::new(), current_machine: String::from(""), next_panel_type: None, - pending_msgs: 0, sender, receiver, @@ -84,48 +84,42 @@ impl Default for MachineInspectionMainPanel { impl MainPanel for MachineInspectionMainPanel { fn update(&mut self, ui: &mut Ui) { - if self.pending_msgs != 0 { - let received_answer: Option<(String, ChannelDataType)> = self.receiver.try_recv().ok(); - if let Some((machine_name, data_type)) = received_answer { - self.pending_msgs -= 1; - if self.pending_msgs == 0 { - self.status_text = None; + let received_answer = self.receiver.try_recv().ok(); + if let Some((index, machine_name, data_type)) = received_answer { + if self.status_text.decrease(index) { + self.machine_data_map + .entry(machine_name.clone()) + .and_modify(|machine_data| { + machine_data.initialized = true; + }); + // attempt to load currently inspecting machine + if !self.current_machine.is_empty() { + self.pass_machine(Machine::by_name(&self.current_machine, true).ok().unwrap()); + } + } + match data_type { + ChannelDataType::Modules(modules) => { self.machine_data_map - .entry(machine_name.clone()) + .entry(machine_name) .and_modify(|machine_data| { - machine_data.initialized = true; + machine_data.modules = Some(modules); }); - // attempt to load currently inspecting machine - if !self.current_machine.is_empty() { - self.pass_machine( - Machine::by_name(&self.current_machine, true).ok().unwrap(), - ); - } } - match data_type { - ChannelDataType::Modules(modules) => { - self.machine_data_map - .entry(machine_name) - .and_modify(|machine_data| { - machine_data.modules = Some(modules); - }); - } - ChannelDataType::Secrets(secrets) => { - self.machine_data_map - .entry(machine_name) - .and_modify(|machine_data| { - machine_data.secrets = Some(secrets); - }); - } - ChannelDataType::Applications(applications) => { - self.machine_data_map - .entry(machine_name) - .and_modify(|machine_data| { - machine_data.applications = Some(applications); - }); - } - ChannelDataType::ClearStatus => {} + ChannelDataType::Secrets(secrets) => { + self.machine_data_map + .entry(machine_name) + .and_modify(|machine_data| { + machine_data.secrets = Some(secrets); + }); } + ChannelDataType::Applications(applications) => { + self.machine_data_map + .entry(machine_name) + .and_modify(|machine_data| { + machine_data.applications = Some(applications); + }); + } + ChannelDataType::ClearStatus => {} } } @@ -305,19 +299,18 @@ impl MainPanel for MachineInspectionMainPanel { ui.horizontal(|ui| { let rebuild_button = Button::new("Rebuild").fill(Color32::DARK_BLUE); if ui.add(rebuild_button).clicked() { - self.status_text = Some(String::from(format!( - "Building machine '{}'...", - self.current_machine - ))); + let index = self.status_text.insert( + 1, + String::from(format!("Building machine '{}'...", self.current_machine)), + ); - self.pending_msgs += 1; let mut machine = self.machine_data_map[&self.current_machine].machine.clone(); let sender_clone = self.sender.clone(); thread::spawn(move || { let _ = machine.build(!checked); sender_clone - .send((machine.config.name, ChannelDataType::ClearStatus)) + .send((index, machine.config.name, ChannelDataType::ClearStatus)) .unwrap(); }); self.show_rebuild_spec_modal = false; @@ -344,19 +337,21 @@ impl MainPanel for MachineInspectionMainPanel { ui.horizontal(|ui| { let duplicate_button = Button::new("Duplicate").fill(Color32::DARK_GREEN); if ui.add(duplicate_button).clicked() { - self.status_text = Some(String::from(format!( - "Duplicating machine '{}' as '{}'...", - self.current_machine, new_machine_name - ))); + let index = self.status_text.insert( + 1, + String::from(format!( + "Duplicating machine '{}' as '{}'...", + self.current_machine, new_machine_name + )), + ); - self.pending_msgs += 1; let machine = self.machine_data_map[&self.current_machine].machine.clone(); let new_machine_name_clone = new_machine_name.clone(); let sender_clone = self.sender.clone(); thread::spawn(move || { let _ = machine.duplicate(&new_machine_name_clone); sender_clone - .send((machine.config.name, ChannelDataType::ClearStatus)) + .send((index, machine.config.name, ChannelDataType::ClearStatus)) .unwrap(); }); self.show_duplicate_spec_modal = false; @@ -383,18 +378,20 @@ impl MainPanel for MachineInspectionMainPanel { let tar_button = Button::new("Tar").fill(Color32::DARK_GREEN); if ui.add(tar_button).clicked() { let path = PathBuf::try_from(&tar_path).unwrap(); - self.status_text = Some(String::from(format!( - "Exporting files of {} to {path:?}...", - self.current_machine - ))); + let index = self.status_text.insert( + 1, + String::from(format!( + "Exporting files of {} to {path:?}...", + self.current_machine + )), + ); - self.pending_msgs += 1; let machine = self.machine_data_map[&self.current_machine].machine.clone(); let sender_clone = self.sender.clone(); thread::spawn(move || { let _ = machine.tar(&path); sender_clone - .send((machine.config.name, ChannelDataType::ClearStatus)) + .send((index, machine.config.name, ChannelDataType::ClearStatus)) .unwrap(); }); self.show_tar_spec_modal = false; @@ -415,12 +412,11 @@ impl MainPanel for MachineInspectionMainPanel { ui.horizontal(|ui| { let delete_button = Button::new("Delete").fill(Color32::DARK_RED); if ui.add(delete_button).clicked() { - self.status_text = Some(String::from(format!( - "Deleting machine '{}'...", - self.current_machine - ))); + let index = self.status_text.insert( + 1, + String::from(format!("Deleting machine '{}'...", self.current_machine)), + ); - self.pending_msgs += 1; let machine = self .machine_data_map .remove(&self.current_machine) @@ -432,7 +428,7 @@ impl MainPanel for MachineInspectionMainPanel { thread::spawn(move || { let _ = machine.delete(true); sender_clone - .send((machine_name, ChannelDataType::ClearStatus)) + .send((index, machine_name, ChannelDataType::ClearStatus)) .unwrap(); }); self.show_delete_confirmation_modal = false; @@ -460,12 +456,11 @@ impl MainPanel for MachineInspectionMainPanel { .insert(machine_name.clone(), machine_data); } - if self.pending_msgs == 0 && !self.machine_data_map[&machine_name].initialized { - self.pending_msgs = 3; - self.status_text = Some(String::from(format!( - "Loading machine {}...", - &machine_name - ))); + if !self.machine_data_map[&machine_name].initialized { + let index = self.status_text.insert( + 3, + String::from(format!("Loading machine {}...", &machine_name)), + ); let applications_sender = self.sender.clone(); let machine_name_clone = machine_name.clone(); @@ -475,6 +470,7 @@ impl MainPanel for MachineInspectionMainPanel { applications_sender .send(( + index, machine_name_clone, ChannelDataType::Applications(applications), )) @@ -487,7 +483,7 @@ impl MainPanel for MachineInspectionMainPanel { thread::spawn(move || { let modules = modules_clone.to_output(); modules_sender - .send((machine_name_clone, ChannelDataType::Modules(modules))) + .send((index, machine_name_clone, ChannelDataType::Modules(modules))) .expect("modules could not be sent"); }); @@ -501,14 +497,14 @@ impl MainPanel for MachineInspectionMainPanel { .collect_vec() .to_output(); secrets_sender - .send((machine_name_clone, ChannelDataType::Secrets(secrets))) + .send((index, machine_name_clone, ChannelDataType::Secrets(secrets))) .expect("modules could not be sent"); }); } self.current_machine = machine_name; } - fn get_status_text(&self) -> &Option { + fn get_status_text(&self) -> &StatusEntries { &self.status_text } diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index ef670b3d..e3bca243 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -49,11 +49,10 @@ struct Gui { machines: Vec, textures: HashMap, - status_text: Option, + status_text: StatusEntries, - pending_msgs: usize, - sender: Sender, - receiver: Receiver, + sender: Sender<(usize, ChannelDataType)>, + receiver: Receiver<(usize, ChannelDataType)>, reloading_machine_index: Option, last_reload: Instant, @@ -82,40 +81,33 @@ impl eframe::App for Gui { self.reload_machine(); } } - if self.pending_msgs != 0 { - let received_answer: Option = self.receiver.try_recv().ok(); - if let Some(data_type) = received_answer { - self.pending_msgs -= 1; - if self.pending_msgs == 0 { - self.status_text = None; - } - match data_type { - ChannelDataType::Machine(machine, machine_index) => { - // reload happens for MachineConfig::list(), which can diverge from Machine::list() - if machine_index < self.machines.len() - && self.machines[machine_index].config.name == machine.config.name - { - self.machines[machine_index] = machine; - } else { - self.machines.insert(machine_index, machine); - } - let next_machine_index = machine_index + 1; - if next_machine_index < self.machine_configs.len() { - self.reloading_machine_index = Some(next_machine_index); - } else { - // remove remaining machines that were not listed by MachineConfig::list() - while self.machines.get(next_machine_index).is_some() { - self.machines.remove(next_machine_index); - } - self.reloading_machine_index = None; - self.last_reload = Instant::now(); - } - self.reload_machine(); + let received_answer = self.receiver.try_recv().ok(); + if let Some((status_index, data_type)) = received_answer { + self.status_text.decrease(status_index); + match data_type { + ChannelDataType::Machine(machine, machine_index) => { + // reload happens for MachineConfig::list(), which can diverge from Machine::list() + if machine_index < self.machines.len() + && self.machines[machine_index].config.name == machine.config.name + { + self.machines[machine_index] = machine; + } else { + self.machines.insert(machine_index, machine); } - ChannelDataType::StoreRecovered => { - self.status_text = Some(String::from("")); + let next_machine_index = machine_index + 1; + if next_machine_index < self.machine_configs.len() { + self.reloading_machine_index = Some(next_machine_index); + } else { + // remove remaining machines that were not listed by MachineConfig::list() + while self.machines.get(next_machine_index).is_some() { + self.machines.remove(next_machine_index); + } + self.reloading_machine_index = None; + self.last_reload = Instant::now(); } + self.reload_machine(); } + ChannelDataType::StoreRecovered => {} } } @@ -139,9 +131,8 @@ impl Gui { machines: Machine::list(true).expect("Machines could not be listed"), textures, - status_text: None, + status_text: StatusEntries::new(), - pending_msgs: 0, sender, receiver, @@ -191,12 +182,15 @@ impl Gui { #[cfg(target_os = "windows")] { if ui.button("Recover store").clicked() { - self.status_text = - Some(String::from("Recovering Codchi store...")); + let index = self + .status_text + .insert(1, String::from("Recovering Codchi store...")); let sender_clone = self.sender.clone(); thread::spawn(move || { let _ = crate::platform::platform::store_recover(); - sender_clone.send(ChannelDataType::StoreRecovered).unwrap(); + sender_clone + .send((index, ChannelDataType::StoreRecovered)) + .unwrap(); }); ui.close_menu(); } @@ -261,20 +255,34 @@ impl Gui { TopBottomPanel::bottom("statusbar_panel") .resizable(false) .show(ctx, |ui| { - let mut final_text = None; - if let Some(text) = &self.status_text { - final_text = Some(text); - } else if let Some(text) = - self.main_panels[self.current_main_panel_index].get_status_text() - { - final_text = Some(text); - } - if let Some(text) = final_text { - ui.horizontal(|ui| { - ui.add(egui::Spinner::new()); - ui.label(text); - }); - } + ui.horizontal(|ui| { + let mut first_status = true; + for status_option in self.status_text.get_status() { + if let Some((_pending_msgs, status)) = status_option { + if first_status { + first_status = false; + ui.add(egui::Spinner::new()); + } else { + ui.separator(); + } + ui.label(status); + } + } + for status_option in self.main_panels[self.current_main_panel_index] + .get_status_text() + .get_status() + { + if let Some((_pending_msgs, status)) = status_option { + if first_status { + first_status = false; + ui.add(egui::Spinner::new()); + } else { + ui.separator(); + } + ui.label(status); + } + } + }); }); } @@ -375,11 +383,13 @@ impl Gui { fn reload_machine(&mut self) { if let Some(machine_index) = self.reloading_machine_index { if let Some(machine_config) = self.machine_configs.get_mut(machine_index) { - self.pending_msgs += 1; - self.status_text = Some(String::from(format!( - "Updating status for machine '{}'", - machine_config.name - ))); + let index = self.status_text.insert( + 1, + String::from(format!( + "Updating status for machine '{}'", + machine_config.name + )), + ); let machine_config_clone = machine_config.clone(); let machine_sender_clone = self.sender.clone(); @@ -388,7 +398,7 @@ impl Gui { .expect("Machine could not be read"); machine_sender_clone - .send(ChannelDataType::Machine(machine, machine_index)) + .send((index, ChannelDataType::Machine(machine, machine_index))) .expect("machine could not be sent"); }); } @@ -405,7 +415,7 @@ pub trait MainPanel: Any { fn pass_machine(&mut self, machine: Machine); - fn get_status_text(&self) -> &Option; + fn get_status_text(&self) -> &StatusEntries; fn renew(&mut self); } @@ -538,3 +548,45 @@ fn get_visuals() -> Visuals { visuals.widgets.noninteractive.fg_stroke.color = Color32::LIGHT_GRAY; visuals } + +pub struct StatusEntries { + status: Vec>, + empty_entries: Vec, +} + +impl StatusEntries { + fn new() -> Self { + Self { + status: Vec::new(), + empty_entries: Vec::new(), + } + } + + fn insert(&mut self, pending_msgs: usize, value: String) -> usize { + if let Some(index) = self.empty_entries.pop() { + self.status[index] = Some((pending_msgs, value)); + index + } else { + self.status.push(Some((pending_msgs, value))); + self.status.len() - 1 + } + } + + fn decrease(&mut self, index: usize) -> bool { + if index < self.status.len() { + if let Some((pending_msgs, _value)) = self.status[index].as_mut() { + *pending_msgs = *pending_msgs - 1; + if *pending_msgs == 0 { + self.status[index] = None; + self.empty_entries.push(index); + return true; + } + } + } + false + } + + fn get_status(&self) -> &Vec> { + &self.status + } +} From 5427a256b66b30f611ce140da4a5af94d479f612 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Mon, 10 Mar 2025 13:09:37 +0000 Subject: [PATCH 22/49] refactored MainPanels --- codchi/src/gui/mod.rs | 168 ++++++++++++++++++++++++++---------------- 1 file changed, 103 insertions(+), 65 deletions(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index e3bca243..4523bbe8 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -15,36 +15,10 @@ use std::{ thread, time::Instant, }; - -pub fn run() -> anyhow::Result<()> { - let options = eframe::NativeOptions { - viewport: ViewportBuilder::default().with_inner_size((960.0, 540.0)), - ..Default::default() - }; - eframe::run_native( - "Codchi", - options, - Box::new(|cc| { - // set custom theme - cc.egui_ctx.set_visuals(get_visuals()); - - // This gives us image support: - egui_extras::install_image_loaders(&cc.egui_ctx); - - // Zoom in a bit initially - let ppp = cc.egui_ctx.pixels_per_point(); - cc.egui_ctx.set_pixels_per_point(1.25 * ppp); - - Ok(Box::::new(Gui::new(load_textures(&cc.egui_ctx)))) - }), - ) - .unwrap(); - Ok(()) -} +use strum::{EnumIter, IntoEnumIterator}; struct Gui { - main_panels: Vec>, - current_main_panel_index: usize, + main_panels: MainPanels, machine_configs: Vec, machines: Vec, textures: HashMap, @@ -58,11 +32,15 @@ struct Gui { last_reload: Instant, } -#[derive(Clone)] +struct MainPanels { + panels: Vec>, + current_panel_index: usize, +} + +#[derive(Clone, EnumIter)] pub enum MainPanelType { MachineInspection, MachineCreation, - BugReport, } enum ChannelDataType { @@ -122,11 +100,7 @@ impl Gui { fn new(textures: HashMap) -> Self { let (sender, receiver) = channel(); Self { - main_panels: vec![ - Box::new(MachineInspectionMainPanel::default()), - Box::new(MachineCreationMainPanel::default()), - ], - current_main_panel_index: 0, + main_panels: MainPanels::default(), machine_configs: MachineConfig::list().expect("Machine-configs could not be listed"), machines: Machine::list(true).expect("Machines could not be listed"), textures, @@ -150,8 +124,7 @@ impl Gui { ui.horizontal_centered(|ui| { let codchi_button = Button::image(include_image!("../../assets/logo.png")); if ui.add(codchi_button).clicked() { - self.current_main_panel_index = - Self::get_main_panel_index(MainPanelType::MachineInspection); + self.main_panels.change(MainPanelType::MachineInspection); } ui.separator(); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { @@ -268,10 +241,7 @@ impl Gui { ui.label(status); } } - for status_option in self.main_panels[self.current_main_panel_index] - .get_status_text() - .get_status() - { + for status_option in self.main_panels.get_status_text().get_status() { if let Some((_pending_msgs, status)) = status_option { if first_status { first_status = false; @@ -297,8 +267,7 @@ impl Gui { let new_machin_button_handle = ui.add_sized([ui.available_width(), 0.0], new_machine_button); if new_machin_button_handle.clicked() { - self.current_main_panel_index = - Self::get_main_panel_index(MainPanelType::MachineCreation); + self.main_panels.change(MainPanelType::MachineCreation); } ui.separator(); @@ -336,12 +305,8 @@ impl Gui { ); let button_handle = ui.add(machine_button); if button_handle.clicked() { - let machine_inspection_panel_index = Self::get_main_panel_index( - MainPanelType::MachineInspection, - ); - self.current_main_panel_index = machine_inspection_panel_index; - self.main_panels[machine_inspection_panel_index] - .pass_machine(machine.clone()); + self.main_panels.change(MainPanelType::MachineInspection); + self.main_panels.pass_machine(machine.clone()); } } }) @@ -358,26 +323,14 @@ impl Gui { .show(ui, |ui| { ui.spacing_mut().scroll = style::ScrollStyle::solid(); - let next_panel_type_option = - self.main_panels[self.current_main_panel_index].next_panel(); - if let Some(next_panel_type) = next_panel_type_option { - self.current_main_panel_index = Self::get_main_panel_index(next_panel_type); + if let Some(next_panel_type) = self.main_panels.next_panel() { + self.main_panels.change(next_panel_type); } - self.main_panels[self.current_main_panel_index].update(ui); + self.main_panels.update(ui); + self.main_panels.modal_update(ctx); }) }); - for main_panel in &mut self.main_panels { - main_panel.modal_update(ctx); - } - } - - fn get_main_panel_index(main_panel_type: MainPanelType) -> usize { - match main_panel_type { - MainPanelType::MachineInspection => 0, - MainPanelType::MachineCreation => 1, - MainPanelType::BugReport => 2, - } } fn reload_machine(&mut self) { @@ -406,6 +359,65 @@ impl Gui { } } +impl Default for MainPanels { + fn default() -> Self { + let mut panels: Vec> = Vec::new(); + for main_panel in MainPanelType::iter() { + let panel: Box = match main_panel { + MainPanelType::MachineInspection => Box::new(MachineInspectionMainPanel::default()), + MainPanelType::MachineCreation => Box::new(MachineCreationMainPanel::default()), + }; + panels.push(panel); + } + Self { + panels, + current_panel_index: 0, + } + } +} + +impl MainPanel for MainPanels { + fn update(&mut self, ui: &mut Ui) { + self.get_current_main_panel_mut().update(ui); + } + + fn modal_update(&mut self, ctx: &Context) { + for main_panel in &mut self.panels { + main_panel.modal_update(ctx); + } + } + + fn next_panel(&mut self) -> Option { + self.get_current_main_panel_mut().next_panel() + } + + fn pass_machine(&mut self, machine: Machine) { + self.get_current_main_panel_mut().pass_machine(machine); + } + + fn get_status_text(&self) -> &StatusEntries { + self.get_current_main_panel().get_status_text() + } + + fn renew(&mut self) { + self.get_current_main_panel_mut().renew(); + } +} + +impl MainPanels { + fn get_current_main_panel(&self) -> &dyn MainPanel { + self.panels[self.current_panel_index].as_ref() + } + + fn get_current_main_panel_mut(&mut self) -> &mut dyn MainPanel { + self.panels[self.current_panel_index].as_mut() + } + + fn change(&mut self, new_main_panel: MainPanelType) { + self.current_panel_index = new_main_panel as usize; + } +} + pub trait MainPanel: Any { fn update(&mut self, ui: &mut Ui); @@ -590,3 +602,29 @@ impl StatusEntries { &self.status } } + +pub fn run() -> anyhow::Result<()> { + let options = eframe::NativeOptions { + viewport: ViewportBuilder::default().with_inner_size((960.0, 540.0)), + ..Default::default() + }; + eframe::run_native( + "Codchi", + options, + Box::new(|cc| { + // set custom theme + cc.egui_ctx.set_visuals(get_visuals()); + + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + // Zoom in a bit initially + let ppp = cc.egui_ctx.pixels_per_point(); + cc.egui_ctx.set_pixels_per_point(1.25 * ppp); + + Ok(Box::::new(Gui::new(load_textures(&cc.egui_ctx)))) + }), + ) + .unwrap(); + Ok(()) +} From 1227d7e2dcda17058a1a5ce314259d5ff4e79775 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Mon, 10 Mar 2025 14:37:40 +0000 Subject: [PATCH 23/49] Added tooltips to icon-buttons --- codchi/src/gui/machine_inspection.rs | 6 ++---- codchi/src/gui/mod.rs | 15 +++++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 01de6d77..3fc3a0ab 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -150,9 +150,8 @@ impl MainPanel for MachineInspectionMainPanel { self.show_delete_confirmation_modal = true; } }); - let reload_image = Image::new(include_image!("../../assets/reload.png")); - let reload_button = Button::image(reload_image); - if ui.add(reload_button).clicked() { + let reload_button = Button::new("\u{21BA}"); + if ui.add(reload_button).on_hover_text("Reload").clicked() { self.machine_data_map.remove(&self.current_machine); self.pass_machine( Machine::by_name(&self.current_machine, true).ok().unwrap(), @@ -509,7 +508,6 @@ impl MainPanel for MachineInspectionMainPanel { } fn renew(&mut self) { - self.machine_data_map.clear(); self.current_machine = String::from(""); } } diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 4523bbe8..0d0df6e0 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -123,8 +123,9 @@ impl Gui { .show(ctx, |ui| { ui.horizontal_centered(|ui| { let codchi_button = Button::image(include_image!("../../assets/logo.png")); - if ui.add(codchi_button).clicked() { + if ui.add(codchi_button).on_hover_text("Home").clicked() { self.main_panels.change(MainPanelType::MachineInspection); + self.main_panels.renew(); } ui.separator(); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { @@ -133,11 +134,15 @@ impl Gui { Button::image(include_image!("../../assets/github_logo.png")); let bug_report_button = Button::image(include_image!("../../assets/bug_icon.png")); - if ui.add(github_button).clicked() { + if ui.add(github_button).on_hover_text("Github").clicked() { ui.ctx() .open_url(OpenUrl::new_tab("https://github.com/aformatik/codchi/")); } - if ui.add(bug_report_button).clicked() { + if ui + .add(bug_report_button) + .on_hover_text("Bug-Report") + .clicked() + { ui.ctx().open_url(OpenUrl::new_tab( "https://github.com/aformatik/codchi/issues", )); @@ -218,7 +223,9 @@ impl Gui { "Enable wsl-vpnkit", )); } - }); + }) + .response + .on_hover_text("Setting"); }); }); }); From 0d68fa1015761bdc570ae29cab5020ce9a69e5fb Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 11 Mar 2025 12:26:53 +0000 Subject: [PATCH 24/49] arc replace channel for async communication --- codchi/src/gui/machine_inspection.rs | 91 ++++++++++++++++------------ codchi/src/gui/mod.rs | 31 +++++----- 2 files changed, 66 insertions(+), 56 deletions(-) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 3fc3a0ab..9ea3a2c1 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -7,8 +7,8 @@ use egui::*; use itertools::Itertools; use std::path::PathBuf; use std::{ - collections::HashMap, - sync::mpsc::{channel, Receiver, Sender}, + collections::{HashMap, VecDeque}, + sync::{Arc, Mutex}, thread, }; @@ -21,8 +21,7 @@ pub struct MachineInspectionMainPanel { current_machine: String, next_panel_type: Option, - sender: Sender<(usize, String, ChannelDataType)>, - receiver: Receiver<(usize, String, ChannelDataType)>, + answer_queue: Arc>>, show_rebuild_spec_modal: bool, show_duplicate_spec_modal: bool, @@ -61,7 +60,6 @@ impl MachineData { impl Default for MachineInspectionMainPanel { fn default() -> Self { - let (sender, receiver) = channel(); MachineInspectionMainPanel { status_text: StatusEntries::new(), @@ -69,8 +67,7 @@ impl Default for MachineInspectionMainPanel { current_machine: String::from(""), next_panel_type: None, - sender, - receiver, + answer_queue: Arc::new(Mutex::new(VecDeque::new())), show_rebuild_spec_modal: false, show_duplicate_spec_modal: false, @@ -84,7 +81,7 @@ impl Default for MachineInspectionMainPanel { impl MainPanel for MachineInspectionMainPanel { fn update(&mut self, ui: &mut Ui) { - let received_answer = self.receiver.try_recv().ok(); + let received_answer = self.answer_queue.try_lock().unwrap().pop_front(); if let Some((index, machine_name, data_type)) = received_answer { if self.status_text.decrease(index) { self.machine_data_map @@ -305,12 +302,14 @@ impl MainPanel for MachineInspectionMainPanel { let mut machine = self.machine_data_map[&self.current_machine].machine.clone(); - let sender_clone = self.sender.clone(); + let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { let _ = machine.build(!checked); - sender_clone - .send((index, machine.config.name, ChannelDataType::ClearStatus)) - .unwrap(); + answer_queue_clone.lock().unwrap().push_back(( + index, + machine.config.name, + ChannelDataType::ClearStatus, + )); }); self.show_rebuild_spec_modal = false; } @@ -346,12 +345,15 @@ impl MainPanel for MachineInspectionMainPanel { let machine = self.machine_data_map[&self.current_machine].machine.clone(); let new_machine_name_clone = new_machine_name.clone(); - let sender_clone = self.sender.clone(); + let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { let _ = machine.duplicate(&new_machine_name_clone); - sender_clone - .send((index, machine.config.name, ChannelDataType::ClearStatus)) - .unwrap(); + + answer_queue_clone.lock().unwrap().push_back(( + index, + machine.config.name, + ChannelDataType::ClearStatus, + )); }); self.show_duplicate_spec_modal = false; } @@ -386,12 +388,15 @@ impl MainPanel for MachineInspectionMainPanel { ); let machine = self.machine_data_map[&self.current_machine].machine.clone(); - let sender_clone = self.sender.clone(); + let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { let _ = machine.tar(&path); - sender_clone - .send((index, machine.config.name, ChannelDataType::ClearStatus)) - .unwrap(); + + answer_queue_clone.lock().unwrap().push_back(( + index, + machine.config.name, + ChannelDataType::ClearStatus, + )); }); self.show_tar_spec_modal = false; } @@ -423,12 +428,15 @@ impl MainPanel for MachineInspectionMainPanel { .machine; let mut machine_name = String::from(""); std::mem::swap(&mut self.current_machine, &mut machine_name); - let sender_clone = self.sender.clone(); + let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { let _ = machine.delete(true); - sender_clone - .send((index, machine_name, ChannelDataType::ClearStatus)) - .unwrap(); + + answer_queue_clone.lock().unwrap().push_back(( + index, + machine_name, + ChannelDataType::ClearStatus, + )); }); self.show_delete_confirmation_modal = false; } @@ -461,32 +469,33 @@ impl MainPanel for MachineInspectionMainPanel { String::from(format!("Loading machine {}...", &machine_name)), ); - let applications_sender = self.sender.clone(); + let answer_queue_clone = self.answer_queue.clone(); let machine_name_clone = machine_name.clone(); let machine_clone = machine.clone(); thread::spawn(move || { let applications = HostImpl::list_desktop_entries(&machine_clone).ok().unwrap(); - applications_sender - .send(( - index, - machine_name_clone, - ChannelDataType::Applications(applications), - )) - .expect("modules could not be sent"); + answer_queue_clone.lock().unwrap().push_back(( + index, + machine_name_clone, + ChannelDataType::Applications(applications), + )); }); - let modules_sender = self.sender.clone(); + let answer_queue_clone = self.answer_queue.clone(); let modules_clone = machine.config.modules.clone(); let machine_name_clone = machine_name.clone(); thread::spawn(move || { let modules = modules_clone.to_output(); - modules_sender - .send((index, machine_name_clone, ChannelDataType::Modules(modules))) - .expect("modules could not be sent"); + + answer_queue_clone.lock().unwrap().push_back(( + index, + machine_name_clone, + ChannelDataType::Modules(modules), + )); }); - let secrets_sender = self.sender.clone(); + let answer_queue_clone = self.answer_queue.clone(); let machine_name_clone = machine_name.clone(); thread::spawn(move || { let secrets = machine @@ -495,9 +504,11 @@ impl MainPanel for MachineInspectionMainPanel { .into_values() .collect_vec() .to_output(); - secrets_sender - .send((index, machine_name_clone, ChannelDataType::Secrets(secrets))) - .expect("modules could not be sent"); + answer_queue_clone.lock().unwrap().push_back(( + index, + machine_name_clone, + ChannelDataType::Secrets(secrets), + )); }); } self.current_machine = machine_name; diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 0d0df6e0..a5d51dcc 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -10,8 +10,8 @@ use machine_creation::MachineCreationMainPanel; use machine_inspection::MachineInspectionMainPanel; use std::{ any::Any, - collections::HashMap, - sync::mpsc::{channel, Receiver, Sender}, + collections::{HashMap, VecDeque}, + sync::{Arc, Mutex}, thread, time::Instant, }; @@ -25,8 +25,7 @@ struct Gui { status_text: StatusEntries, - sender: Sender<(usize, ChannelDataType)>, - receiver: Receiver<(usize, ChannelDataType)>, + answer_queue: Arc>>, reloading_machine_index: Option, last_reload: Instant, @@ -59,7 +58,7 @@ impl eframe::App for Gui { self.reload_machine(); } } - let received_answer = self.receiver.try_recv().ok(); + let received_answer = self.answer_queue.try_lock().unwrap().pop_front(); if let Some((status_index, data_type)) = received_answer { self.status_text.decrease(status_index); match data_type { @@ -98,7 +97,6 @@ impl eframe::App for Gui { impl Gui { fn new(textures: HashMap) -> Self { - let (sender, receiver) = channel(); Self { main_panels: MainPanels::default(), machine_configs: MachineConfig::list().expect("Machine-configs could not be listed"), @@ -107,8 +105,7 @@ impl Gui { status_text: StatusEntries::new(), - sender, - receiver, + answer_queue: Arc::new(Mutex::new(VecDeque::new())), reloading_machine_index: None, last_reload: Instant::now(), @@ -163,12 +160,13 @@ impl Gui { let index = self .status_text .insert(1, String::from("Recovering Codchi store...")); - let sender_clone = self.sender.clone(); + let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { let _ = crate::platform::platform::store_recover(); - sender_clone - .send((index, ChannelDataType::StoreRecovered)) - .unwrap(); + answer_queue_clone + .lock() + .unwrap() + .push_back((index, ChannelDataType::StoreRecovered)); }); ui.close_menu(); } @@ -352,14 +350,15 @@ impl Gui { ); let machine_config_clone = machine_config.clone(); - let machine_sender_clone = self.sender.clone(); + let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { let machine = Machine::read(machine_config_clone, true) .expect("Machine could not be read"); - machine_sender_clone - .send((index, ChannelDataType::Machine(machine, machine_index))) - .expect("machine could not be sent"); + answer_queue_clone + .lock() + .unwrap() + .push_back((index, ChannelDataType::Machine(machine, machine_index))); }); } } From b8ff7c6ab94bbce8837954b4663186092c7a7f36 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:44:13 +0000 Subject: [PATCH 25/49] Made answer-queue lock-acquiring safe --- codchi/src/gui/machine_inspection.rs | 6 +++++- codchi/src/gui/mod.rs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 9ea3a2c1..246ad95f 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -81,7 +81,11 @@ impl Default for MachineInspectionMainPanel { impl MainPanel for MachineInspectionMainPanel { fn update(&mut self, ui: &mut Ui) { - let received_answer = self.answer_queue.try_lock().unwrap().pop_front(); + let received_answer = if let Ok(mut answer_queue) = self.answer_queue.try_lock() { + answer_queue.pop_front() + } else { + None + }; if let Some((index, machine_name, data_type)) = received_answer { if self.status_text.decrease(index) { self.machine_data_map diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index a5d51dcc..bc86500b 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -58,7 +58,11 @@ impl eframe::App for Gui { self.reload_machine(); } } - let received_answer = self.answer_queue.try_lock().unwrap().pop_front(); + let received_answer = if let Ok(mut answer_queue) = self.answer_queue.try_lock() { + answer_queue.pop_front() + } else { + None + }; if let Some((status_index, data_type)) = received_answer { self.status_text.decrease(status_index); match data_type { From 60579f8df67b2039c94c8d01dd2c9f2933d9bf4b Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:49:29 +0000 Subject: [PATCH 26/49] Replaced unsafe unwrap calls --- codchi/src/gui/machine_inspection.rs | 66 ++++++++++++++++------------ 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 246ad95f..6e0b0496 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -95,7 +95,10 @@ impl MainPanel for MachineInspectionMainPanel { }); // attempt to load currently inspecting machine if !self.current_machine.is_empty() { - self.pass_machine(Machine::by_name(&self.current_machine, true).ok().unwrap()); + let current_machine = Machine::by_name(&self.current_machine, true); + if let Ok(machine) = current_machine { + self.pass_machine(machine); + } } } match data_type { @@ -154,9 +157,10 @@ impl MainPanel for MachineInspectionMainPanel { let reload_button = Button::new("\u{21BA}"); if ui.add(reload_button).on_hover_text("Reload").clicked() { self.machine_data_map.remove(&self.current_machine); - self.pass_machine( - Machine::by_name(&self.current_machine, true).ok().unwrap(), - ); + let machine_res = Machine::by_name(&self.current_machine, true); + if let Ok(machine) = machine_res { + self.pass_machine(machine); + } } }); }); @@ -270,7 +274,11 @@ impl MainPanel for MachineInspectionMainPanel { cfg.secrets.insert(secret.name.clone(), new_password); cfg.write(lock).unwrap(); }, - &secret.value.clone().unwrap(), + &if secret.value.is_some() { + secret.value.clone().unwrap() + } else { + String::from("") + }, )); ui.end_row(); } @@ -382,6 +390,7 @@ impl MainPanel for MachineInspectionMainPanel { ui.horizontal(|ui| { let tar_button = Button::new("Tar").fill(Color32::DARK_GREEN); if ui.add(tar_button).clicked() { + // TODO let path = PathBuf::try_from(&tar_path).unwrap(); let index = self.status_text.insert( 1, @@ -420,28 +429,30 @@ impl MainPanel for MachineInspectionMainPanel { ui.horizontal(|ui| { let delete_button = Button::new("Delete").fill(Color32::DARK_RED); if ui.add(delete_button).clicked() { - let index = self.status_text.insert( - 1, - String::from(format!("Deleting machine '{}'...", self.current_machine)), - ); - - let machine = self - .machine_data_map - .remove(&self.current_machine) - .unwrap() - .machine; - let mut machine_name = String::from(""); - std::mem::swap(&mut self.current_machine, &mut machine_name); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let _ = machine.delete(true); + let deleted_machine_data = + self.machine_data_map.remove(&self.current_machine); + if let Some(machine_data) = deleted_machine_data { + let index = self.status_text.insert( + 1, + String::from(format!( + "Deleting machine '{}'...", + self.current_machine + )), + ); - answer_queue_clone.lock().unwrap().push_back(( - index, - machine_name, - ChannelDataType::ClearStatus, - )); - }); + let mut machine_name = String::from(""); + std::mem::swap(&mut self.current_machine, &mut machine_name); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let _ = machine_data.machine.delete(true); + + answer_queue_clone.lock().unwrap().push_back(( + index, + machine_name, + ChannelDataType::ClearStatus, + )); + }); + } self.show_delete_confirmation_modal = false; } if ui.button("Cancel").clicked() { @@ -477,7 +488,8 @@ impl MainPanel for MachineInspectionMainPanel { let machine_name_clone = machine_name.clone(); let machine_clone = machine.clone(); thread::spawn(move || { - let applications = HostImpl::list_desktop_entries(&machine_clone).ok().unwrap(); + let applications = + HostImpl::list_desktop_entries(&machine_clone).unwrap_or(Vec::new()); answer_queue_clone.lock().unwrap().push_back(( index, From b45c2298bcfe4b71c1d2bdc21a91f50905578fb8 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:50:55 +0000 Subject: [PATCH 27/49] All modals now close as they should --- codchi/src/gui/machine_inspection.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 6e0b0496..702656d2 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -332,7 +332,7 @@ impl MainPanel for MachineInspectionMainPanel { ui.data_mut(|d| d.insert_temp(state_id, checked)); }); if modal.should_close() { - self.show_delete_confirmation_modal = false; + self.show_rebuild_spec_modal = false; } } if self.show_duplicate_spec_modal { @@ -376,7 +376,7 @@ impl MainPanel for MachineInspectionMainPanel { ui.data_mut(|d| d.insert_temp(state_id, new_machine_name)); }); if modal.should_close() { - self.show_delete_confirmation_modal = false; + self.show_duplicate_spec_modal = false; } } if self.show_tar_spec_modal { @@ -420,7 +420,7 @@ impl MainPanel for MachineInspectionMainPanel { ui.data_mut(|d| d.insert_temp(state_id, tar_path)); }); if modal.should_close() { - self.show_delete_confirmation_modal = false; + self.show_tar_spec_modal = false; } } if self.show_delete_confirmation_modal { From 2a72e90dc37b689ceb807957d3d3d63532bfa129 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:54:12 +0000 Subject: [PATCH 28/49] machine-creation initial commit --- codchi/src/gui/machine_creation.rs | 682 ++++++++++++++++++++++++++++- codchi/src/module.rs | 2 +- 2 files changed, 676 insertions(+), 8 deletions(-) diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs index 7303b7f4..10c247cd 100644 --- a/codchi/src/gui/machine_creation.rs +++ b/codchi/src/gui/machine_creation.rs @@ -1,39 +1,238 @@ -use crate::gui::MainPanel; -use crate::gui::MainPanelType; -use crate::platform::Machine; +use crate::cli::{InputOptions, ModuleAttrPath, NixpkgsLocation}; +use crate::gui::{MainPanel, MainPanelType}; +use crate::platform::{Machine, NixDriver, Store}; +use crate::util::LinuxPath; +use egui::*; +use git_url_parse::{GitUrl, GitUrlParseError}; +use strum::{EnumIter, IntoEnumIterator}; use super::StatusEntries; +use anyhow::Error; +use std::{ + collections::VecDeque, + fmt::Debug, + process::Command, + sync::{Arc, Mutex}, + thread, +}; pub struct MachineCreationMainPanel { status_text: StatusEntries, + answer_queue: Arc>>, + creation_step: CreationStep, machine_form: MachineForm, next_panel_type: Option, + + url: String, + user: String, + password: String, + use_nixpkgs: bool, + + branches: Option>, + tags: Option>, + + git_ref: GitRef, + branch: String, + tag: String, + commit: String, } +#[derive(PartialEq, Clone, EnumIter)] +enum CreationStep { + SpecifyGenerics, + SpecifyRepository, + SpecifyModules, +} + +#[derive(Debug, PartialEq, Clone)] struct MachineForm { name: String, + git_url: GitUrl, + options: InputOptions, + do_clone: bool, + module_paths: Option, + dont_run_init: bool, +} + +enum ChannelDataType { + Access(String, Option, bool), + Branches(GitUrl, Result, Error>), + Tags(GitUrl, Result, Error>), + Modules(InputOptions, Result, Error>), + NewMachine(MachineForm, Machine), + BuiltMachine(bool, Machine), + ClearStatus, +} + +#[derive(Debug, PartialEq, EnumIter, Clone, Default)] +enum GitRef { + #[default] + Branch, + Tag, + Commit, +} + +#[derive(Debug, Clone, PartialEq)] +struct ModulePaths { + unselected_module_paths: Vec, + selected_module_paths: Vec, } impl Default for MachineCreationMainPanel { fn default() -> Self { MachineCreationMainPanel { status_text: StatusEntries::new(), + answer_queue: Arc::new(Mutex::new(VecDeque::new())), + creation_step: CreationStep::SpecifyGenerics, machine_form: MachineForm::default(), next_panel_type: None, + + url: String::from(""), + user: String::from(""), + password: String::from(""), + use_nixpkgs: false, + + branches: None, + tags: None, + + git_ref: GitRef::default(), + branch: String::from(""), + tag: String::from(""), + commit: String::from(""), } } } impl MainPanel for MachineCreationMainPanel { - fn update(&mut self, ui: &mut egui::Ui) { - if ui.button("Finish").clicked() { - self.next_panel_type = Some(MainPanelType::MachineInspection); + fn update(&mut self, ui: &mut Ui) { + let received_answer = if let Ok(mut answer_queue) = self.answer_queue.try_lock() { + answer_queue.pop_front() + } else { + None + }; + if let Some((index, data_type)) = received_answer { + self.status_text.decrease(index); + match data_type { + ChannelDataType::Access(url, auth, accessible) => { + if accessible && self.url == url && self.get_auth() == auth { + if let Ok(git_url) = Self::get_git_url(&url, &auth) { + self.machine_form.git_url = git_url.clone(); + self.machine_form.options.auth = auth.clone(); + + // load branches for repo + let branches_index = self.status_text.insert( + 1, + String::from(format!("Loading branches for {}...", &self.url)), + ); + let url_clone = url.clone(); + let auth_clone = auth.clone(); + let git_url_clone = git_url.clone(); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let result = Self::load_branches(&url_clone, &auth_clone); + answer_queue_clone.lock().unwrap().push_back(( + branches_index, + ChannelDataType::Branches(git_url_clone, result), + )); + }); + + // load tags for repo + let tags_index = self.status_text.insert( + 1, + String::from(format!("Loading tags for {}...", &self.url)), + ); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let result = Self::load_tags(&url, &auth); + answer_queue_clone.lock().unwrap().push_back(( + tags_index, + ChannelDataType::Tags(git_url, result), + )); + }); + } + } + } + ChannelDataType::Branches(git_url, branches) => { + if branches.is_ok() { + if git_url == self.machine_form.git_url { + self.branches = branches.ok(); + } + } + } + ChannelDataType::Tags(git_url, tags) => { + if tags.is_ok() { + if git_url == self.machine_form.git_url { + self.tags = tags.ok(); + } + } + } + ChannelDataType::Modules(options, result) => { + if let Ok(module_paths) = result { + self.machine_form.options = options; + self.machine_form.module_paths = Some(ModulePaths::new(module_paths)); + } + } + ChannelDataType::NewMachine(machine_form, mut machine) => { + if self.machine_form == machine_form { + self.renew(); + } + if !machine_form.options.no_build { + let index = self.status_text.insert( + 1, + String::from(format!("Building machine '{}'...", machine.config.name)), + ); + + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let answer = if machine.build(false).is_ok() { + ChannelDataType::BuiltMachine(machine_form.dont_run_init, machine) + } else { + ChannelDataType::ClearStatus + }; + answer_queue_clone + .lock() + .unwrap() + .push_back((index, answer)); + }); + } + } + ChannelDataType::BuiltMachine(dont_run_init, machine) => { + if !dont_run_init { + let index = self.status_text.insert( + 1, + String::from(format!( + "Running Init-Script for '{}'...", + machine.config.name + )), + ); + + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let _ = machine.run_init_script(dont_run_init); + answer_queue_clone + .lock() + .unwrap() + .push_back((index, ChannelDataType::ClearStatus)); + }); + } + } + ChannelDataType::ClearStatus => {} + } } + + ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { + ui.separator(); + ui.vertical(|ui| { + self.creation_step_panel(ui); + ui.separator(); + self.dialog_panel(ui); + }); + }); } - fn modal_update(&mut self, _ctx: &egui::Context) {} + fn modal_update(&mut self, _ctx: &Context) {} fn next_panel(&mut self) -> Option { self.next_panel_type.take() @@ -46,14 +245,483 @@ impl MainPanel for MachineCreationMainPanel { } fn renew(&mut self) { + self.creation_step = CreationStep::SpecifyGenerics; self.machine_form = MachineForm::default(); + self.url = String::from(""); + self.user = String::from(""); + self.password = String::from(""); + self.use_nixpkgs = false; + self.branches = None; + self.tags = None; + } +} + +impl MachineCreationMainPanel { + pub fn creation_step_panel(&mut self, ui: &mut Ui) { + ui.columns(CreationStep::iter().count(), |columns| { + for creation_step in CreationStep::iter() { + let i = creation_step.clone() as usize; + columns[i].vertical_centered_justified(|ui| { + let text = RichText::from(format!("Step {}", (i + 1))).strong(); + let button = if &self.creation_step == &creation_step { + Button::new(text).fill(Color32::GRAY) + } else { + Button::new(text) + }; + + let button_enabled = self.is_step_reachable(&creation_step); + let button_handle = ui.add_enabled(button_enabled, button); + if button_handle.clicked() { + self.creation_step = creation_step.clone(); + } + }); + } + }); + } + + pub fn dialog_panel(&mut self, ui: &mut Ui) { + match self.creation_step { + CreationStep::SpecifyGenerics => { + self.specify_generics_panel(ui); + } + CreationStep::SpecifyRepository => { + self.specify_repository_panel(ui); + } + CreationStep::SpecifyModules => { + self.specify_modules_panel(ui); + } + } + ui.with_layout(Layout::bottom_up(Align::Center), |ui| { + ui.separator(); + ui.horizontal(|ui| { + if let Some(previous_step) = self.creation_step.previous() { + if ui.button(RichText::from("Previous").strong()).clicked() { + self.creation_step = previous_step; + } + } + ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { + if let Some(next_step) = self.creation_step.next() { + if self.is_step_reachable(&next_step) { + let next_button = Button::new(RichText::from("Next").strong()); + if ui.add(next_button).clicked() { + self.creation_step = next_step; + } + } + self.load_repo(ui); + } else { + // Last creation step + let text = RichText::from("Finish").strong(); + let finish_button = Button::new(text).fill(Color32::DARK_GREEN); + if ui.add(finish_button).clicked() { + self.create_machine(); + } + } + }) + }) + }); + } + + fn create_machine(&mut self) { + let index = self.status_text.insert( + 1, + String::from(format!( + "Creating new machine '{}'...", + self.machine_form.name + )), + ); + + let machine_form = self.machine_form.clone(); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let machine = crate::module::init( + &machine_form.name, + Some(machine_form.git_url.clone()), + &machine_form.options, + &machine_form + .module_paths + .as_ref() + .unwrap() + .selected_module_paths, + ); + let answer = if let Ok(new_machine) = dbg!(machine) { + ChannelDataType::NewMachine(machine_form, new_machine) + } else { + ChannelDataType::ClearStatus + }; + answer_queue_clone + .lock() + .unwrap() + .push_back((index, answer)); + }); + } + + fn load_repo(&mut self, ui: &mut Ui) { + match self.creation_step { + CreationStep::SpecifyGenerics => { + if ui.button("Load Repository").clicked() { + if !self.url.is_empty() { + self.git_ref = GitRef::default(); + self.branches = None; + self.tags = None; + self.machine_form.options.branch = None; + self.machine_form.options.tag = None; + self.machine_form.options.commit = None; + + let index = self.status_text.insert( + 1, + String::from(format!("Reading repository {}...", &self.url)), + ); + + let url_clone = self.url.clone(); + let auth_clone = self.get_auth(); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let result = Self::is_repo_accessible(&url_clone, &auth_clone); + answer_queue_clone.lock().unwrap().push_back(( + index, + ChannelDataType::Access(url_clone, auth_clone, result.is_ok()), + )); + }); + } + } + } + CreationStep::SpecifyRepository => { + if ui.button("Load Modules").clicked() { + let index = self.status_text.insert( + 1, + String::from(format!("Loading modules for {}...", &self.url)), + ); + let git_url_clone = self.machine_form.git_url.clone(); + let use_nixpkgs = if self.use_nixpkgs { + Some(NixpkgsLocation::Remote) + } else { + Some(NixpkgsLocation::Local) + }; + let auth = self.machine_form.options.auth.clone(); + let (branch, tag, commit) = match self.git_ref { + GitRef::Branch => (Some(self.branch.clone()), None, None), + GitRef::Tag => (None, Some(self.tag.clone()), None), + GitRef::Commit => (None, None, Some(self.commit.clone())), + }; + let answer_queue_clone = self.answer_queue.clone(); + let opts = InputOptions { + dont_prompt: true, + no_build: self.machine_form.options.no_build, + use_nixpkgs, + auth, + branch, + tag, + commit, + }; + thread::spawn(move || { + let modules_result = Self::load_modules(&git_url_clone, &opts); + answer_queue_clone + .lock() + .unwrap() + .push_back((index, ChannelDataType::Modules(opts, modules_result))); + }); + } + } + CreationStep::SpecifyModules => {} + } + } + + fn get_auth(&self) -> Option { + // uniform transformation for auth to prevent lock when calling "nix" commands + Some(format!("{}:{}", &self.user, &self.password)) + } + + fn specify_generics_panel(&mut self, ui: &mut Ui) { + ui.heading("Specify the url of the repository you would like to use"); + ui.separator(); + Grid::new("new_machine_grid_1") + .num_columns(2) + .show(ui, |ui| { + ui.label("URL:\t"); + ui.add_sized( + ui.available_size(), + TextEdit::singleline(&mut self.url) + .hint_text("https://gitlab.example.com/my_repo"), + ); + ui.end_row(); + + ui.label("User:\t"); + ui.add_sized(ui.available_size(), TextEdit::singleline(&mut self.user)); + ui.end_row(); + + ui.label("Password:\t"); + ui.add_sized( + ui.available_size(), + TextEdit::singleline(&mut self.password), + ); + ui.end_row(); + + ui.label("Machine Name:\t"); + ui.add_sized( + ui.available_size(), + TextEdit::singleline(&mut self.machine_form.name).hint_text("My awesome Name"), + ); + ui.end_row(); + }); + ui.separator(); + ui.checkbox( + &mut self.machine_form.options.no_build, + "Don't build machine", + ); + ui.checkbox(&mut self.machine_form.dont_run_init, "Skip init Script"); + if ui + .checkbox(&mut self.use_nixpkgs, "Use provided package version") + .clicked() + { + self.machine_form.options.use_nixpkgs = if self.use_nixpkgs { + Some(NixpkgsLocation::Remote) + } else { + Some(NixpkgsLocation::Local) + }; + } + } + + fn specify_repository_panel(&mut self, ui: &mut Ui) { + ui.heading("Specify the repository-details"); + ui.separator(); + ui.horizontal(|ui| { + ComboBox::from_id_salt("git_ref_combo_box") + .selected_text(self.git_ref.to_text()) + .show_ui(ui, |ui| { + for git_ref in GitRef::iter() { + let label = git_ref.to_text(); + ui.selectable_value(&mut self.git_ref, git_ref, label); + } + }); + match &self.git_ref { + GitRef::Branch => { + if let Some(branches) = &self.branches { + ComboBox::from_id_salt("git_ref_branch_combo_box") + .selected_text(&self.branch) + .show_ui(ui, |ui| { + for branch in branches { + ui.selectable_value(&mut self.branch, branch.clone(), branch); + } + }); + } + } + GitRef::Tag => { + if let Some(tags) = &self.tags { + ComboBox::from_id_salt("git_ref_tag_combo_box") + .selected_text(&self.tag) + .show_ui(ui, |ui| { + for tag in tags { + ui.selectable_value(&mut self.tag, tag.clone(), tag); + } + }); + } + } + GitRef::Commit => { + ui.text_edit_singleline(&mut self.commit); + } + }; + }); + ui.separator(); + ui.checkbox(&mut self.machine_form.do_clone, "Clone Configuration"); + } + + fn specify_modules_panel(&mut self, ui: &mut Ui) { + ui.heading("Specify the modules, your machine should have"); + ui.separator(); + if let Some(module_paths) = self.machine_form.module_paths.as_mut() { + let num_cols = 2; + ui.columns(num_cols, |columns| { + fn add_column( + ui: &mut Ui, + text: &str, + module_path_vec: &mut Vec, + pendant_vec: &mut Vec, + ) { + ui.vertical(|ui| { + ui.strong(text); + let mut clicked_index = None; + for i in 0..module_path_vec.len() { + let module_path = &module_path_vec[i]; + let button_text = + format!("{}.{}", module_path.base, module_path.module); + if ui.button(button_text).clicked() { + clicked_index = Some(i); + } + } + if let Some(index) = clicked_index { + let unselected_module_path = module_path_vec.remove(index); + pendant_vec.push(unselected_module_path); + } + }); + } + add_column( + &mut columns[0], + "Unselected Modules", + module_paths.unselected_module_paths.as_mut(), + module_paths.selected_module_paths.as_mut(), + ); + add_column( + &mut columns[1], + "Selected Modules", + module_paths.selected_module_paths.as_mut(), + module_paths.unselected_module_paths.as_mut(), + ); + }); + } + } + + fn is_step_reachable(&self, creation_step: &CreationStep) -> bool { + match creation_step { + CreationStep::SpecifyGenerics => true, + CreationStep::SpecifyRepository => self.tags.is_some() || self.branches.is_some(), + CreationStep::SpecifyModules => self.machine_form.module_paths.is_some(), + } + } + + fn is_repo_accessible(url: &str, auth: &Option) -> Result, Error> { + let opts = InputOptions { + dont_prompt: true, + no_build: true, + use_nixpkgs: None, + auth: auth.clone().or(Some(String::from(""))), + branch: None, + tag: None, + commit: None, + }; + let git_url = Self::get_git_url(url, auth)?; + let flake_url = crate::module::inquire_module_url(&opts, &git_url, false)?; + let dummy_path = LinuxPath(String::from("")); + let nix_url = flake_url.to_nix_url(dummy_path); + + let modules = crate::platform::Driver::store() + .cmd() + .list_nixos_modules(&nix_url)?; + + Ok(modules) + } + + fn load_branches(url: &str, auth: &Option) -> Result, Error> { + let output = Command::new("git") + .args(["ls-remote", "--heads", &Self::get_auth_url(url, auth)]) + .output()?; + + let branches = String::from_utf8_lossy(&output.stdout) + .to_string() + .lines() + .filter_map(|line| line.split('\t').nth(1)) + .filter_map(|ref_name| ref_name.strip_prefix("refs/heads/")) + .map(String::from) + .collect(); + + Ok(branches) + } + + fn load_tags(url: &str, auth: &Option) -> Result, Error> { + let output = Command::new("git") + .args(["ls-remote", "--tags", &Self::get_auth_url(url, auth)]) + .output()?; + + let tags = String::from_utf8_lossy(&output.stdout) + .to_string() + .lines() + .filter_map(|line| line.split('\t').nth(1)) + .filter_map(|ref_name| ref_name.strip_prefix("refs/tags/")) + .map(String::from) + .collect(); + + Ok(tags) + } + + fn load_modules( + url: &GitUrl, + opts: &crate::cli::InputOptions, + ) -> Result, Error> { + let flake_url = crate::module::inquire_module_url(opts, url, false)?; + let dummy_path = LinuxPath(String::from("")); + let nix_url = flake_url.to_nix_url(dummy_path); + let modules = crate::platform::Driver::store() + .cmd() + .list_nixos_modules(&nix_url)?; + + Ok(modules) + } + + fn get_auth_url(url: &str, auth: &Option) -> String { + if let Some(auth_val) = auth { + let url_parts: Vec<&str> = url.split("://").collect(); + if url_parts.len() == 2 { + return format!("{}://{}@{}", url_parts[0], auth_val, url_parts[1]); + } + } + url.to_string() + } + + fn get_git_url(url: &str, auth: &Option) -> Result { + if let Some(auth_val) = auth { + let url_parts: Vec<&str> = url.split("://").collect(); + if url_parts.len() == 2 { + return GitUrl::parse(&format!("{}://{}@{}", url_parts[0], auth_val, url_parts[1])); + } + } + GitUrl::parse(url) + } +} + +impl CreationStep { + fn next(&self) -> Option { + match self { + CreationStep::SpecifyGenerics => Some(CreationStep::SpecifyRepository), + CreationStep::SpecifyRepository => Some(CreationStep::SpecifyModules), + CreationStep::SpecifyModules => None, + } + } + + fn previous(&self) -> Option { + match self { + CreationStep::SpecifyGenerics => None, + CreationStep::SpecifyRepository => Some(CreationStep::SpecifyGenerics), + CreationStep::SpecifyModules => Some(CreationStep::SpecifyRepository), + } } } impl Default for MachineForm { fn default() -> Self { + let options = InputOptions { + dont_prompt: true, + no_build: false, + use_nixpkgs: None, + auth: None, + branch: None, + tag: None, + commit: None, + }; MachineForm { name: String::from(""), + git_url: GitUrl::default(), + options, + do_clone: false, + module_paths: None, + dont_run_init: false, + } + } +} + +impl GitRef { + fn to_text(&self) -> String { + match self { + GitRef::Branch => String::from("Branch"), + GitRef::Tag => String::from("Tag"), + GitRef::Commit => String::from("Commit"), + } + } +} + +impl ModulePaths { + fn new(unselected_module_paths: Vec) -> Self { + Self { + unselected_module_paths, + selected_module_paths: Vec::new(), } } } diff --git a/codchi/src/module.rs b/codchi/src/module.rs index 002b792a..4079e4b7 100644 --- a/codchi/src/module.rs +++ b/codchi/src/module.rs @@ -422,7 +422,7 @@ pub fn fetch_modules( Ok((modules, use_nixpkgs)) } -fn inquire_module_url( +pub(crate) fn inquire_module_url( opts: &InputOptions, url: &GitUrl, allow_local: bool, From d5ac639ecae5670612cb3d6119184dc0d1e18af3 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:11:14 +0000 Subject: [PATCH 29/49] Increased default window size --- codchi/src/gui/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index bc86500b..370fef57 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -615,7 +615,7 @@ impl StatusEntries { pub fn run() -> anyhow::Result<()> { let options = eframe::NativeOptions { - viewport: ViewportBuilder::default().with_inner_size((960.0, 540.0)), + viewport: ViewportBuilder::default().with_inner_size((1600.0, 900.0)), ..Default::default() }; eframe::run_native( From f02701349d298d76e5b5a7914d1e2cbb3cb04198 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:59:55 +0000 Subject: [PATCH 30/49] Added url-input-line at the top --- codchi/src/gui/machine_creation.rs | 12 ++- codchi/src/gui/machine_inspection.rs | 119 ++++++++++++++------------- codchi/src/gui/mod.rs | 32 +++++-- 3 files changed, 99 insertions(+), 64 deletions(-) diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs index 10c247cd..725be3ce 100644 --- a/codchi/src/gui/machine_creation.rs +++ b/codchi/src/gui/machine_creation.rs @@ -6,7 +6,7 @@ use egui::*; use git_url_parse::{GitUrl, GitUrlParseError}; use strum::{EnumIter, IntoEnumIterator}; -use super::StatusEntries; +use super::{StatusEntries, DTO}; use anyhow::Error; use std::{ collections::VecDeque, @@ -238,7 +238,15 @@ impl MainPanel for MachineCreationMainPanel { self.next_panel_type.take() } - fn pass_machine(&mut self, _machine: Machine) {} + fn transfer_data(&mut self, dto: DTO) { + match dto { + DTO::Machine(_) => todo!(), + DTO::Text(text) => { + self.renew(); + self.url = text; + } + } + } fn get_status_text(&self) -> &StatusEntries { &self.status_text diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 702656d2..423465d1 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -12,7 +12,7 @@ use std::{ thread, }; -use super::StatusEntries; +use super::{StatusEntries, DTO}; pub struct MachineInspectionMainPanel { status_text: StatusEntries, @@ -97,7 +97,7 @@ impl MainPanel for MachineInspectionMainPanel { if !self.current_machine.is_empty() { let current_machine = Machine::by_name(&self.current_machine, true); if let Ok(machine) = current_machine { - self.pass_machine(machine); + self.transfer_data(DTO::Machine(machine)); } } } @@ -159,7 +159,7 @@ impl MainPanel for MachineInspectionMainPanel { self.machine_data_map.remove(&self.current_machine); let machine_res = Machine::by_name(&self.current_machine, true); if let Ok(machine) = machine_res { - self.pass_machine(machine); + self.transfer_data(DTO::Machine(machine)); } } }); @@ -470,64 +470,69 @@ impl MainPanel for MachineInspectionMainPanel { self.next_panel_type.take() } - fn pass_machine(&mut self, machine: Machine) { - let machine_name = machine.config.name.clone(); - if !self.machine_data_map.contains_key(&machine_name) { - let machine_data = MachineData::new(machine.clone()); - self.machine_data_map - .insert(machine_name.clone(), machine_data); - } - - if !self.machine_data_map[&machine_name].initialized { - let index = self.status_text.insert( - 3, - String::from(format!("Loading machine {}...", &machine_name)), - ); - - let answer_queue_clone = self.answer_queue.clone(); - let machine_name_clone = machine_name.clone(); - let machine_clone = machine.clone(); - thread::spawn(move || { - let applications = - HostImpl::list_desktop_entries(&machine_clone).unwrap_or(Vec::new()); - - answer_queue_clone.lock().unwrap().push_back(( - index, - machine_name_clone, - ChannelDataType::Applications(applications), - )); - }); + fn transfer_data(&mut self, dto: DTO) { + match dto { + DTO::Machine(machine) => { + let machine_name = machine.config.name.clone(); + if !self.machine_data_map.contains_key(&machine_name) { + let machine_data = MachineData::new(machine.clone()); + self.machine_data_map + .insert(machine_name.clone(), machine_data); + } - let answer_queue_clone = self.answer_queue.clone(); - let modules_clone = machine.config.modules.clone(); - let machine_name_clone = machine_name.clone(); - thread::spawn(move || { - let modules = modules_clone.to_output(); + if !self.machine_data_map[&machine_name].initialized { + let index = self.status_text.insert( + 3, + String::from(format!("Loading machine {}...", &machine_name)), + ); + + let answer_queue_clone = self.answer_queue.clone(); + let machine_name_clone = machine_name.clone(); + let machine_clone = machine.clone(); + thread::spawn(move || { + let applications = + HostImpl::list_desktop_entries(&machine_clone).unwrap_or(Vec::new()); + + answer_queue_clone.lock().unwrap().push_back(( + index, + machine_name_clone, + ChannelDataType::Applications(applications), + )); + }); - answer_queue_clone.lock().unwrap().push_back(( - index, - machine_name_clone, - ChannelDataType::Modules(modules), - )); - }); + let answer_queue_clone = self.answer_queue.clone(); + let modules_clone = machine.config.modules.clone(); + let machine_name_clone = machine_name.clone(); + thread::spawn(move || { + let modules = modules_clone.to_output(); + + answer_queue_clone.lock().unwrap().push_back(( + index, + machine_name_clone, + ChannelDataType::Modules(modules), + )); + }); - let answer_queue_clone = self.answer_queue.clone(); - let machine_name_clone = machine_name.clone(); - thread::spawn(move || { - let secrets = machine - .eval_env_secrets() - .expect("failed to load machine secrets") - .into_values() - .collect_vec() - .to_output(); - answer_queue_clone.lock().unwrap().push_back(( - index, - machine_name_clone, - ChannelDataType::Secrets(secrets), - )); - }); + let answer_queue_clone = self.answer_queue.clone(); + let machine_name_clone = machine_name.clone(); + thread::spawn(move || { + let secrets = machine + .eval_env_secrets() + .expect("failed to load machine secrets") + .into_values() + .collect_vec() + .to_output(); + answer_queue_clone.lock().unwrap().push_back(( + index, + machine_name_clone, + ChannelDataType::Secrets(secrets), + )); + }); + } + self.current_machine = machine_name; + } + DTO::Text(_) => todo!(), } - self.current_machine = machine_name; } fn get_status_text(&self) -> &StatusEntries { diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 370fef57..d657b8b7 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -29,6 +29,8 @@ struct Gui { reloading_machine_index: Option, last_reload: Instant, + + url_input_line: String, } struct MainPanels { @@ -113,6 +115,8 @@ impl Gui { reloading_machine_index: None, last_reload: Instant::now(), + + url_input_line: String::from(""), } } @@ -228,6 +232,18 @@ impl Gui { }) .response .on_hover_text("Setting"); + + let url_input_line = TextEdit::singleline(&mut self.url_input_line); + let url_input_line_handle = ui.add_sized([400.0, 0.0], url_input_line); + if url_input_line_handle.lost_focus() + && url_input_line_handle + .ctx + .input(|i| i.key_pressed(Key::Enter)) + { + self.main_panels.change(MainPanelType::MachineCreation); + self.main_panels + .transfer_data(DTO::Text(self.url_input_line.take())); + } }); }); }); @@ -315,7 +331,8 @@ impl Gui { let button_handle = ui.add(machine_button); if button_handle.clicked() { self.main_panels.change(MainPanelType::MachineInspection); - self.main_panels.pass_machine(machine.clone()); + self.main_panels + .transfer_data(DTO::Machine(machine.clone())); } } }) @@ -401,8 +418,8 @@ impl MainPanel for MainPanels { self.get_current_main_panel_mut().next_panel() } - fn pass_machine(&mut self, machine: Machine) { - self.get_current_main_panel_mut().pass_machine(machine); + fn transfer_data(&mut self, dto: DTO) { + self.get_current_main_panel_mut().transfer_data(dto); } fn get_status_text(&self) -> &StatusEntries { @@ -428,20 +445,25 @@ impl MainPanels { } } -pub trait MainPanel: Any { +pub trait MainPanel { fn update(&mut self, ui: &mut Ui); fn modal_update(&mut self, ctx: &Context); fn next_panel(&mut self) -> Option; - fn pass_machine(&mut self, machine: Machine); + fn transfer_data(&mut self, dto: DTO); fn get_status_text(&self) -> &StatusEntries; fn renew(&mut self); } +pub enum DTO { + Machine(Machine), + Text(String), +} + pub fn create_modal( ctx: &Context, id: &str, From b280d0d971e58e36f6acbd9de57ef01c26a7056c Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:06:26 +0000 Subject: [PATCH 31/49] Empty Machines can now be created --- codchi/src/gui/machine_creation.rs | 74 +++++++++++++++--------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs index 725be3ce..44c4b960 100644 --- a/codchi/src/gui/machine_creation.rs +++ b/codchi/src/gui/machine_creation.rs @@ -45,10 +45,10 @@ enum CreationStep { SpecifyModules, } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, Default, PartialEq, Clone)] struct MachineForm { name: String, - git_url: GitUrl, + git_url: Option, options: InputOptions, do_clone: bool, module_paths: Option, @@ -118,7 +118,7 @@ impl MainPanel for MachineCreationMainPanel { ChannelDataType::Access(url, auth, accessible) => { if accessible && self.url == url && self.get_auth() == auth { if let Ok(git_url) = Self::get_git_url(&url, &auth) { - self.machine_form.git_url = git_url.clone(); + self.machine_form.git_url = Some(git_url.clone()); self.machine_form.options.auth = auth.clone(); // load branches for repo @@ -156,14 +156,14 @@ impl MainPanel for MachineCreationMainPanel { } ChannelDataType::Branches(git_url, branches) => { if branches.is_ok() { - if git_url == self.machine_form.git_url { + if Some(git_url) == self.machine_form.git_url { self.branches = branches.ok(); } } } ChannelDataType::Tags(git_url, tags) => { if tags.is_ok() { - if git_url == self.machine_form.git_url { + if Some(git_url) == self.machine_form.git_url { self.tags = tags.ok(); } } @@ -308,6 +308,16 @@ impl MachineCreationMainPanel { } } ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { + if self.creation_step == CreationStep::SpecifyGenerics { + if self.url.is_empty() && !self.machine_form.name.is_empty() { + let text = RichText::from("Create").strong(); + let create_empty_machine_button = + Button::new(text).fill(Color32::DARK_GREEN); + if ui.add(create_empty_machine_button).clicked() { + self.create_machine(true); + } + } + } if let Some(next_step) = self.creation_step.next() { if self.is_step_reachable(&next_step) { let next_button = Button::new(RichText::from("Next").strong()); @@ -321,7 +331,7 @@ impl MachineCreationMainPanel { let text = RichText::from("Finish").strong(); let finish_button = Button::new(text).fill(Color32::DARK_GREEN); if ui.add(finish_button).clicked() { - self.create_machine(); + self.create_machine(false); } } }) @@ -329,7 +339,7 @@ impl MachineCreationMainPanel { }); } - fn create_machine(&mut self) { + fn create_machine(&mut self, empty: bool) { let index = self.status_text.insert( 1, String::from(format!( @@ -338,20 +348,32 @@ impl MachineCreationMainPanel { )), ); - let machine_form = self.machine_form.clone(); + let machine_form = if empty { + let mut mf = MachineForm::default(); + mf.name = self.machine_form.name.clone(); + mf + } else { + self.machine_form.clone() + }; let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { - let machine = crate::module::init( - &machine_form.name, - Some(machine_form.git_url.clone()), - &machine_form.options, + let selected_module_paths = if empty { + &Vec::new() + } else { &machine_form .module_paths .as_ref() .unwrap() - .selected_module_paths, + .selected_module_paths + }; + dbg!(&machine_form); + let machine = crate::module::init( + &machine_form.name, + machine_form.git_url.clone(), + &machine_form.options, + &selected_module_paths, ); - let answer = if let Ok(new_machine) = dbg!(machine) { + let answer = if let Ok(new_machine) = machine { ChannelDataType::NewMachine(machine_form, new_machine) } else { ChannelDataType::ClearStatus @@ -422,7 +444,7 @@ impl MachineCreationMainPanel { commit, }; thread::spawn(move || { - let modules_result = Self::load_modules(&git_url_clone, &opts); + let modules_result = Self::load_modules(&git_url_clone.unwrap(), &opts); answer_queue_clone .lock() .unwrap() @@ -693,28 +715,6 @@ impl CreationStep { } } -impl Default for MachineForm { - fn default() -> Self { - let options = InputOptions { - dont_prompt: true, - no_build: false, - use_nixpkgs: None, - auth: None, - branch: None, - tag: None, - commit: None, - }; - MachineForm { - name: String::from(""), - git_url: GitUrl::default(), - options, - do_clone: false, - module_paths: None, - dont_run_init: false, - } - } -} - impl GitRef { fn to_text(&self) -> String { match self { From c9e44d2192e6e63276e1b81c9efa4059c4d4f00b Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:55:29 +0000 Subject: [PATCH 32/49] Side-panel now removes deleted machines --- codchi/src/gui/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index d657b8b7..008050d3 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -9,7 +9,6 @@ use egui::*; use machine_creation::MachineCreationMainPanel; use machine_inspection::MachineInspectionMainPanel; use std::{ - any::Any, collections::{HashMap, VecDeque}, sync::{Arc, Mutex}, thread, @@ -69,14 +68,15 @@ impl eframe::App for Gui { self.status_text.decrease(status_index); match data_type { ChannelDataType::Machine(machine, machine_index) => { - // reload happens for MachineConfig::list(), which can diverge from Machine::list() if machine_index < self.machines.len() && self.machines[machine_index].config.name == machine.config.name { self.machines[machine_index] = machine; } else { + // machine is new self.machines.insert(machine_index, machine); } + let next_machine_index = machine_index + 1; if next_machine_index < self.machine_configs.len() { self.reloading_machine_index = Some(next_machine_index); @@ -370,6 +370,13 @@ impl Gui { )), ); + if let Some(machine) = self.machines.get(machine_index + 1) + && machine.config.name == machine_config.name + { + // machine at machine_index was deleted + self.machines.remove(machine_index); + } + let machine_config_clone = machine_config.clone(); let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { From c0e95033ef93f8365776a0b047bd72d68884127f Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:24:13 +0000 Subject: [PATCH 33/49] Relocated use_nixpkgs checkbox to step2 --- codchi/src/gui/machine_creation.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs index 44c4b960..47ccfa3c 100644 --- a/codchi/src/gui/machine_creation.rs +++ b/codchi/src/gui/machine_creation.rs @@ -325,7 +325,7 @@ impl MachineCreationMainPanel { self.creation_step = next_step; } } - self.load_repo(ui); + self.display_load_button(ui); } else { // Last creation step let text = RichText::from("Finish").strong(); @@ -385,7 +385,7 @@ impl MachineCreationMainPanel { }); } - fn load_repo(&mut self, ui: &mut Ui) { + fn display_load_button(&mut self, ui: &mut Ui) { match self.creation_step { CreationStep::SpecifyGenerics => { if ui.button("Load Repository").clicked() { @@ -499,16 +499,6 @@ impl MachineCreationMainPanel { "Don't build machine", ); ui.checkbox(&mut self.machine_form.dont_run_init, "Skip init Script"); - if ui - .checkbox(&mut self.use_nixpkgs, "Use provided package version") - .clicked() - { - self.machine_form.options.use_nixpkgs = if self.use_nixpkgs { - Some(NixpkgsLocation::Remote) - } else { - Some(NixpkgsLocation::Local) - }; - } } fn specify_repository_panel(&mut self, ui: &mut Ui) { @@ -553,6 +543,16 @@ impl MachineCreationMainPanel { }); ui.separator(); ui.checkbox(&mut self.machine_form.do_clone, "Clone Configuration"); + if ui + .checkbox(&mut self.use_nixpkgs, "Use provided package version") + .clicked() + { + self.machine_form.options.use_nixpkgs = if self.use_nixpkgs { + Some(NixpkgsLocation::Remote) + } else { + Some(NixpkgsLocation::Local) + }; + } } fn specify_modules_panel(&mut self, ui: &mut Ui) { From cf66fe8ca3f65de9820f1bd46edf0e843dfd8d92 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:28:28 +0000 Subject: [PATCH 34/49] machine creation/deletion now displayed properly --- codchi/src/gui/machine_inspection.rs | 472 +++++++++++++++------------ codchi/src/gui/mod.rs | 75 ++++- 2 files changed, 322 insertions(+), 225 deletions(-) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 423465d1..42e0f0b5 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -1,5 +1,5 @@ use crate::config::Mod; -use crate::gui::{create_password_field, MainPanel, MainPanelType}; +use crate::gui::{create_password_field, MainPanel, MainPanelMsgType, MainPanelType}; use crate::logging::CodchiOutput; use crate::platform::{platform::HostImpl, DesktopEntry, Host, Machine, MachineDriver}; use crate::secrets::{EnvSecret, MachineSecrets}; @@ -22,6 +22,7 @@ pub struct MachineInspectionMainPanel { next_panel_type: Option, answer_queue: Arc>>, + frame_msg_callback: Box, show_rebuild_spec_modal: bool, show_duplicate_spec_modal: bool, @@ -39,10 +40,13 @@ struct MachineData { initialized: bool, } -enum ChannelDataType { +pub(crate) enum ChannelDataType { Applications(Vec), Modules(Vec), Secrets(Vec), + RebuiltMachine, + DuplicatedMachine(Machine), + DeletedMachine(String), ClearStatus, } @@ -58,27 +62,6 @@ impl MachineData { } } -impl Default for MachineInspectionMainPanel { - fn default() -> Self { - MachineInspectionMainPanel { - status_text: StatusEntries::new(), - - machine_data_map: HashMap::new(), - current_machine: String::from(""), - next_panel_type: None, - - answer_queue: Arc::new(Mutex::new(VecDeque::new())), - - show_rebuild_spec_modal: false, - show_duplicate_spec_modal: false, - show_tar_spec_modal: false, - show_delete_confirmation_modal: false, - - textures: HashMap::new(), - } - } -} - impl MainPanel for MachineInspectionMainPanel { fn update(&mut self, ui: &mut Ui) { let received_answer = if let Ok(mut answer_queue) = self.answer_queue.try_lock() { @@ -123,178 +106,26 @@ impl MainPanel for MachineInspectionMainPanel { machine_data.applications = Some(applications); }); } - ChannelDataType::ClearStatus => {} - } - } - - if !self.current_machine.is_empty() { - ui.horizontal(|ui| { - let title_text = format!("Machine: {}", &self.current_machine); - ui.add(widgets::Label::new( - RichText::from(title_text).heading().strong(), - )); - ui.with_layout(Layout::right_to_left(Align::BOTTOM), |ui| { - ui.menu_button("Actions", |ui| { - if ui.button("Rebuild").clicked() { - self.show_rebuild_spec_modal = true; - } - if ui.button("Duplicate").clicked() { - self.show_duplicate_spec_modal = true; - } - if ui.button("Tar").clicked() { - self.show_tar_spec_modal = true; - } - if ui.button("Stop").clicked() { - let _ = self.machine_data_map[&self.current_machine] - .machine - .stop(false); - } - let delete_button = Button::new("Delete").fill(Color32::DARK_RED); - if ui.add(delete_button).clicked() { - self.show_delete_confirmation_modal = true; - } - }); - let reload_button = Button::new("\u{21BA}"); - if ui.add(reload_button).on_hover_text("Reload").clicked() { - self.machine_data_map.remove(&self.current_machine); - let machine_res = Machine::by_name(&self.current_machine, true); - if let Ok(machine) = machine_res { - self.transfer_data(DTO::Machine(machine)); - } - } - }); - }); - let machine_data = &self.machine_data_map[&self.current_machine]; - let status_text = match machine_data.machine.platform_status { - crate::platform::PlatformStatus::NotInstalled => "Not Installed", - crate::platform::PlatformStatus::Stopped => "Stopped", - crate::platform::PlatformStatus::Running => "Running", - }; - ui.label(status_text); - ui.separator(); - - ui.heading("Applications"); - ui.add_space(3.0); - if let Some(desktop_entries) = &machine_data.applications { - for desktop_entry in desktop_entries { - let icon = if let Some(icon_path) = &desktop_entry.icon { - if !self.textures.contains_key(&desktop_entry.app_name) { - let image = image::open(icon_path) - .expect("Failed to open image icon") - .to_rgba8(); - let size = [image.width() as usize, image.height() as usize]; - let texture = ui.ctx().load_texture( - "icon_texture", - ColorImage::from_rgba_unmultiplied(size, &image), - Default::default(), - ); - self.textures - .insert(desktop_entry.app_name.clone(), texture); - } - let texture = &self.textures[&desktop_entry.app_name]; - let image = - Image::from_texture(texture).max_size(Vec2 { x: 12.0, y: 12.0 }); - Some(image) - } else { - None - }; - let text = WidgetText::RichText(RichText::new(&desktop_entry.app_name)); - let button = Button::opt_image_and_text(icon, Some(text)); - let button_handle = ui.add(button); - if button_handle.clicked() { - let _ = - HostImpl::execute(&machine_data.machine.config.name, &desktop_entry); - } - } - if ui.button("Shell").clicked() { - let _ = crate::platform::Driver::host().open_terminal(&[ - &std::env::current_exe().unwrap().display().to_string(), - "exec", - &machine_data.machine.config.name, - ]); + ChannelDataType::RebuiltMachine => { + self.reload_machine(Some(&machine_name)); } - } else { - ui.horizontal(|ui| { - ui.add_space(20.0); - ui.label("Loading ..."); - }); - } - ui.separator(); - - ui.heading("Modules"); - if let Some(modules) = &machine_data.modules { - if !modules.is_empty() { - Grid::new("modules_grid").show(ui, |ui| { - ui.strong("Name\t"); - ui.strong("URL\t"); - ui.strong("Path\t"); - ui.end_row(); - - for module in modules { - ui.label(format!("{}\t", module.name)); - ui.label(format!("{}\t", module.url)); - ui.label(format!("{}\t", module.flake_module)); - ui.end_row(); - } - }); - } else { - ui.horizontal(|ui| { - ui.label("No modules"); - }); + ChannelDataType::DuplicatedMachine(machine) => { + (self.frame_msg_callback)(super::MainPanelMsgType::MachineInspection( + ChannelDataType::DuplicatedMachine(machine), + )); } - } else { - ui.horizontal(|ui| { - ui.add_space(20.0); - ui.label("Loading ..."); - }); - } - ui.separator(); - - ui.heading("Secrets"); - if let Some(secrets) = &machine_data.secrets { - if !secrets.is_empty() { - Grid::new("secrets_grid").show(ui, |ui| { - ui.strong("Name\t"); - ui.strong("Description\t"); - ui.strong("Value\t"); - ui.end_row(); - - for secret in secrets { - ui.label(format!("{}\t", secret.name)); - ui.label(format!("{}\t", secret.description)); - ui.add(create_password_field( - &secret.name, - |new_password| { - let (lock, mut cfg) = - crate::config::MachineConfig::open_existing( - &self.current_machine, - true, - ) - .unwrap(); - cfg.secrets.insert(secret.name.clone(), new_password); - cfg.write(lock).unwrap(); - }, - &if secret.value.is_some() { - secret.value.clone().unwrap() - } else { - String::from("") - }, - )); - ui.end_row(); - } - }); - } else { - ui.horizontal(|ui| { - ui.label("No secrets"); - }); + ChannelDataType::DeletedMachine(machine_name) => { + self.current_machine = String::from(""); + self.machine_data_map.remove(&machine_name); + (self.frame_msg_callback)(super::MainPanelMsgType::MachineInspection( + ChannelDataType::DeletedMachine(machine_name), + )); } - } else { - ui.horizontal(|ui| { - ui.add_space(20.0); - ui.label("Loading ..."); - }); + ChannelDataType::ClearStatus => {} } } + + self.display_machine(ui); } fn modal_update(&mut self, ctx: &Context) { @@ -319,8 +150,8 @@ impl MainPanel for MachineInspectionMainPanel { let _ = machine.build(!checked); answer_queue_clone.lock().unwrap().push_back(( index, - machine.config.name, - ChannelDataType::ClearStatus, + machine.config.name.clone(), + ChannelDataType::RebuiltMachine, )); }); self.show_rebuild_spec_modal = false; @@ -360,11 +191,19 @@ impl MainPanel for MachineInspectionMainPanel { let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { let _ = machine.duplicate(&new_machine_name_clone); + let duplicated_machine = + Machine::by_name(&new_machine_name_clone, true); + + let answer = if let Ok(machine) = duplicated_machine { + ChannelDataType::DuplicatedMachine(machine) + } else { + ChannelDataType::ClearStatus + }; answer_queue_clone.lock().unwrap().push_back(( index, machine.config.name, - ChannelDataType::ClearStatus, + answer, )); }); self.show_duplicate_spec_modal = false; @@ -429,30 +268,29 @@ impl MainPanel for MachineInspectionMainPanel { ui.horizontal(|ui| { let delete_button = Button::new("Delete").fill(Color32::DARK_RED); if ui.add(delete_button).clicked() { - let deleted_machine_data = - self.machine_data_map.remove(&self.current_machine); - if let Some(machine_data) = deleted_machine_data { - let index = self.status_text.insert( - 1, - String::from(format!( - "Deleting machine '{}'...", - self.current_machine - )), - ); + let machine_data = &self.machine_data_map[&self.current_machine]; + let index = self.status_text.insert( + 1, + String::from(format!("Deleting machine '{}'...", self.current_machine)), + ); - let mut machine_name = String::from(""); - std::mem::swap(&mut self.current_machine, &mut machine_name); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let _ = machine_data.machine.delete(true); - - answer_queue_clone.lock().unwrap().push_back(( - index, - machine_name, - ChannelDataType::ClearStatus, - )); - }); - } + let machine_clone = machine_data.machine.clone(); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let machine_name = machine_clone.config.name.clone(); + let delete_result = machine_clone.delete(true); + let answer = if dbg!(delete_result.is_ok()) { + ChannelDataType::DeletedMachine(machine_name.clone()) + } else { + ChannelDataType::ClearStatus + }; + + answer_queue_clone.lock().unwrap().push_back(( + index, + machine_name, + answer, + )); + }); self.show_delete_confirmation_modal = false; } if ui.button("Cancel").clicked() { @@ -543,3 +381,205 @@ impl MainPanel for MachineInspectionMainPanel { self.current_machine = String::from(""); } } + +impl MachineInspectionMainPanel { + pub fn new(callback: Box) -> Self { + Self { + status_text: StatusEntries::new(), + + machine_data_map: HashMap::new(), + current_machine: String::from(""), + next_panel_type: None, + + answer_queue: Arc::new(Mutex::new(VecDeque::new())), + frame_msg_callback: callback, + + show_rebuild_spec_modal: false, + show_duplicate_spec_modal: false, + show_tar_spec_modal: false, + show_delete_confirmation_modal: false, + + textures: HashMap::new(), + } + } + + fn display_machine(&mut self, ui: &mut Ui) { + if !self.current_machine.is_empty() { + ui.horizontal(|ui| { + let title_text = format!("Machine: {}", &self.current_machine); + ui.add(widgets::Label::new( + RichText::from(title_text).heading().strong(), + )); + ui.with_layout(Layout::right_to_left(Align::BOTTOM), |ui| { + ui.menu_button("Actions", |ui| { + if ui.button("Rebuild").clicked() { + self.show_rebuild_spec_modal = true; + } + if ui.button("Duplicate").clicked() { + self.show_duplicate_spec_modal = true; + } + if ui.button("Tar").clicked() { + self.show_tar_spec_modal = true; + } + if ui.button("Stop").clicked() { + let _ = self.machine_data_map[&self.current_machine] + .machine + .stop(false); + } + let delete_button = Button::new("Delete").fill(Color32::DARK_RED); + if ui.add(delete_button).clicked() { + self.show_delete_confirmation_modal = true; + } + }); + let reload_button = Button::new("\u{21BA}"); + if ui.add(reload_button).on_hover_text("Reload").clicked() { + self.reload_machine(None); + } + }); + }); + let machine_data = &self.machine_data_map[&self.current_machine]; + let status_text = match machine_data.machine.platform_status { + crate::platform::PlatformStatus::NotInstalled => "Not Installed", + crate::platform::PlatformStatus::Stopped => "Stopped", + crate::platform::PlatformStatus::Running => "Running", + }; + ui.label(status_text); + ui.separator(); + + ui.heading("Applications"); + ui.add_space(3.0); + if let Some(desktop_entries) = &machine_data.applications { + for desktop_entry in desktop_entries { + let icon = if let Some(icon_path) = &desktop_entry.icon { + if !self.textures.contains_key(&desktop_entry.app_name) { + let image = image::open(icon_path) + .expect("Failed to open image icon") + .to_rgba8(); + let size = [image.width() as usize, image.height() as usize]; + let texture = ui.ctx().load_texture( + "icon_texture", + ColorImage::from_rgba_unmultiplied(size, &image), + Default::default(), + ); + self.textures + .insert(desktop_entry.app_name.clone(), texture); + } + let texture = &self.textures[&desktop_entry.app_name]; + let image = + Image::from_texture(texture).max_size(Vec2 { x: 12.0, y: 12.0 }); + Some(image) + } else { + None + }; + let text = WidgetText::RichText(RichText::new(&desktop_entry.app_name)); + let button = Button::opt_image_and_text(icon, Some(text)); + let button_handle = ui.add(button); + if button_handle.clicked() { + let _ = + HostImpl::execute(&machine_data.machine.config.name, &desktop_entry); + } + } + if ui.button("Shell").clicked() { + let _ = crate::platform::Driver::host().open_terminal(&[ + &std::env::current_exe().unwrap().display().to_string(), + "exec", + &machine_data.machine.config.name, + ]); + } + } else { + ui.horizontal(|ui| { + ui.add_space(20.0); + ui.label("Loading ..."); + }); + } + ui.separator(); + + ui.heading("Modules"); + if let Some(modules) = &machine_data.modules { + if !modules.is_empty() { + Grid::new("modules_grid").show(ui, |ui| { + ui.strong("Name\t"); + ui.strong("URL\t"); + ui.strong("Path\t"); + ui.end_row(); + + for module in modules { + ui.label(format!("{}\t", module.name)); + ui.label(format!("{}\t", module.url)); + ui.label(format!("{}\t", module.flake_module)); + ui.end_row(); + } + }); + } else { + ui.horizontal(|ui| { + ui.label("No modules"); + }); + } + } else { + ui.horizontal(|ui| { + ui.add_space(20.0); + ui.label("Loading ..."); + }); + } + ui.separator(); + + ui.heading("Secrets"); + if let Some(secrets) = &machine_data.secrets { + if !secrets.is_empty() { + Grid::new("secrets_grid").show(ui, |ui| { + ui.strong("Name\t"); + ui.strong("Description\t"); + ui.strong("Value\t"); + ui.end_row(); + + for secret in secrets { + ui.label(format!("{}\t", secret.name)); + ui.label(format!("{}\t", secret.description)); + ui.add(create_password_field( + &secret.name, + |new_password| { + let (lock, mut cfg) = + crate::config::MachineConfig::open_existing( + &self.current_machine, + true, + ) + .unwrap(); + cfg.secrets.insert(secret.name.clone(), new_password); + cfg.write(lock).unwrap(); + }, + &if secret.value.is_some() { + secret.value.clone().unwrap() + } else { + String::from("") + }, + )); + ui.end_row(); + } + }); + } else { + ui.horizontal(|ui| { + ui.label("No secrets"); + }); + } + } else { + ui.horizontal(|ui| { + ui.add_space(20.0); + ui.label("Loading ..."); + }); + } + } + } + + fn reload_machine(&mut self, name_option: Option<&str>) { + let machine_name = if let Some(name) = name_option { + name + } else { + &self.current_machine + }; + self.machine_data_map.remove(machine_name); + let machine_res = Machine::by_name(machine_name, true); + if let Ok(machine) = machine_res { + self.transfer_data(DTO::Machine(machine)); + } + } +} diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 008050d3..43067e1a 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -9,7 +9,9 @@ use egui::*; use machine_creation::MachineCreationMainPanel; use machine_inspection::MachineInspectionMainPanel; use std::{ + cell::RefCell, collections::{HashMap, VecDeque}, + rc::Rc, sync::{Arc, Mutex}, thread, time::Instant, @@ -25,6 +27,7 @@ struct Gui { status_text: StatusEntries, answer_queue: Arc>>, + main_panels_msg_queue: Rc>>, reloading_machine_index: Option, last_reload: Instant, @@ -48,6 +51,10 @@ enum ChannelDataType { StoreRecovered, } +pub(crate) enum MainPanelMsgType { + MachineInspection(machine_inspection::ChannelDataType), +} + impl eframe::App for Gui { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { if self.reloading_machine_index.is_none() { @@ -59,6 +66,44 @@ impl eframe::App for Gui { self.reload_machine(); } } + if let Some(main_panel_msg) = self.main_panels_msg_queue.borrow_mut().pop_front() { + match main_panel_msg { + MainPanelMsgType::MachineInspection(msg) => match msg { + machine_inspection::ChannelDataType::Applications(_) => {} + machine_inspection::ChannelDataType::Modules(_) => {} + machine_inspection::ChannelDataType::Secrets(_) => {} + machine_inspection::ChannelDataType::RebuiltMachine => {} + machine_inspection::ChannelDataType::DuplicatedMachine(new_machine) => { + if let Some(machine) = self.machines.last() + && machine.config.name < new_machine.config.name + { + self.machines.push(new_machine); + } else { + for (i, machine) in self.machines.iter().enumerate() { + if new_machine.config.name < machine.config.name { + self.machines.insert(i, new_machine); + break; + } + } + } + } + machine_inspection::ChannelDataType::DeletedMachine(machine_name) => { + let mut i = 0; + for machine_config in &self.machine_configs { + if machine_config.name == machine_name { + break; + } + i += 1; + } + if i < self.machine_configs.len() { + self.machine_configs.remove(i); + } + } + machine_inspection::ChannelDataType::ClearStatus => {} + }, + } + } + let received_answer = if let Ok(mut answer_queue) = self.answer_queue.try_lock() { answer_queue.pop_front() } else { @@ -103,8 +148,9 @@ impl eframe::App for Gui { impl Gui { fn new(textures: HashMap) -> Self { + let mut main_panels_msg_queue = Rc::new(RefCell::new(VecDeque::new())); Self { - main_panels: MainPanels::default(), + main_panels: MainPanels::new(&mut main_panels_msg_queue), machine_configs: MachineConfig::list().expect("Machine-configs could not be listed"), machines: Machine::list(true).expect("Machines could not be listed"), textures, @@ -112,6 +158,7 @@ impl Gui { status_text: StatusEntries::new(), answer_queue: Arc::new(Mutex::new(VecDeque::new())), + main_panels_msg_queue, reloading_machine_index: None, last_reload: Instant::now(), @@ -299,8 +346,7 @@ impl Gui { ui.horizontal_top(|ui| { ui.separator(); ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { - for i in 0..self.machines.len() { - let machine = &self.machines[i]; + for (i, machine) in self.machines.iter().enumerate() { let icon = if self.reloading_machine_index.is_some_and(|index| index == i) { @@ -322,8 +368,7 @@ impl Gui { } } }; - let button_text = - RichText::strong(format!("{}", machine.config.name).into()); + let button_text = RichText::new(&machine.config.name).strong(); let machine_button = Button::opt_image_and_text( icon, Some(WidgetText::RichText(button_text)), @@ -391,14 +436,26 @@ impl Gui { } } } + + fn get_callback( + answer_queue: &mut Rc>>, + ) -> Box { + let answer_queue_clone = answer_queue.clone(); + + Box::new(move |msg: MainPanelMsgType| { + answer_queue_clone.borrow_mut().push_back(msg); + }) + } } -impl Default for MainPanels { - fn default() -> Self { +impl MainPanels { + fn new(answer_queue: &mut Rc>>) -> Self { let mut panels: Vec> = Vec::new(); for main_panel in MainPanelType::iter() { let panel: Box = match main_panel { - MainPanelType::MachineInspection => Box::new(MachineInspectionMainPanel::default()), + MainPanelType::MachineInspection => Box::new(MachineInspectionMainPanel::new( + Gui::get_callback(answer_queue), + )), MainPanelType::MachineCreation => Box::new(MachineCreationMainPanel::default()), }; panels.push(panel); @@ -644,7 +701,7 @@ impl StatusEntries { pub fn run() -> anyhow::Result<()> { let options = eframe::NativeOptions { - viewport: ViewportBuilder::default().with_inner_size((1600.0, 900.0)), + viewport: ViewportBuilder::default().with_inner_size((1280.0, 720.0)), ..Default::default() }; eframe::run_native( From f919ead9416993a34df90090e4c18aef91301abf Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:58:55 +0000 Subject: [PATCH 35/49] removed dbg! and String::from where redundant --- codchi/src/gui/machine_creation.rs | 46 ++++++++++------------------ codchi/src/gui/machine_inspection.rs | 32 ++++++++----------- 2 files changed, 30 insertions(+), 48 deletions(-) diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs index 47ccfa3c..6a4ab17d 100644 --- a/codchi/src/gui/machine_creation.rs +++ b/codchi/src/gui/machine_creation.rs @@ -122,10 +122,9 @@ impl MainPanel for MachineCreationMainPanel { self.machine_form.options.auth = auth.clone(); // load branches for repo - let branches_index = self.status_text.insert( - 1, - String::from(format!("Loading branches for {}...", &self.url)), - ); + let branches_index = self + .status_text + .insert(1, format!("Loading branches for {}...", &self.url)); let url_clone = url.clone(); let auth_clone = auth.clone(); let git_url_clone = git_url.clone(); @@ -139,10 +138,9 @@ impl MainPanel for MachineCreationMainPanel { }); // load tags for repo - let tags_index = self.status_text.insert( - 1, - String::from(format!("Loading tags for {}...", &self.url)), - ); + let tags_index = self + .status_text + .insert(1, format!("Loading tags for {}...", &self.url)); let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { let result = Self::load_tags(&url, &auth); @@ -179,10 +177,9 @@ impl MainPanel for MachineCreationMainPanel { self.renew(); } if !machine_form.options.no_build { - let index = self.status_text.insert( - 1, - String::from(format!("Building machine '{}'...", machine.config.name)), - ); + let index = self + .status_text + .insert(1, format!("Building machine '{}'...", machine.config.name)); let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { @@ -202,10 +199,7 @@ impl MainPanel for MachineCreationMainPanel { if !dont_run_init { let index = self.status_text.insert( 1, - String::from(format!( - "Running Init-Script for '{}'...", - machine.config.name - )), + format!("Running Init-Script for '{}'...", machine.config.name), ); let answer_queue_clone = self.answer_queue.clone(); @@ -342,10 +336,7 @@ impl MachineCreationMainPanel { fn create_machine(&mut self, empty: bool) { let index = self.status_text.insert( 1, - String::from(format!( - "Creating new machine '{}'...", - self.machine_form.name - )), + format!("Creating new machine '{}'...", self.machine_form.name), ); let machine_form = if empty { @@ -366,7 +357,6 @@ impl MachineCreationMainPanel { .unwrap() .selected_module_paths }; - dbg!(&machine_form); let machine = crate::module::init( &machine_form.name, machine_form.git_url.clone(), @@ -397,10 +387,9 @@ impl MachineCreationMainPanel { self.machine_form.options.tag = None; self.machine_form.options.commit = None; - let index = self.status_text.insert( - 1, - String::from(format!("Reading repository {}...", &self.url)), - ); + let index = self + .status_text + .insert(1, format!("Reading repository {}...", &self.url)); let url_clone = self.url.clone(); let auth_clone = self.get_auth(); @@ -417,10 +406,9 @@ impl MachineCreationMainPanel { } CreationStep::SpecifyRepository => { if ui.button("Load Modules").clicked() { - let index = self.status_text.insert( - 1, - String::from(format!("Loading modules for {}...", &self.url)), - ); + let index = self + .status_text + .insert(1, format!("Loading modules for {}...", &self.url)); let git_url_clone = self.machine_form.git_url.clone(); let use_nixpkgs = if self.use_nixpkgs { Some(NixpkgsLocation::Remote) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 42e0f0b5..58771acd 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -138,10 +138,9 @@ impl MainPanel for MachineInspectionMainPanel { ui.horizontal(|ui| { let rebuild_button = Button::new("Rebuild").fill(Color32::DARK_BLUE); if ui.add(rebuild_button).clicked() { - let index = self.status_text.insert( - 1, - String::from(format!("Building machine '{}'...", self.current_machine)), - ); + let index = self + .status_text + .insert(1, format!("Building machine '{}'...", self.current_machine)); let mut machine = self.machine_data_map[&self.current_machine].machine.clone(); @@ -180,10 +179,10 @@ impl MainPanel for MachineInspectionMainPanel { if ui.add(duplicate_button).clicked() { let index = self.status_text.insert( 1, - String::from(format!( + format!( "Duplicating machine '{}' as '{}'...", self.current_machine, new_machine_name - )), + ), ); let machine = self.machine_data_map[&self.current_machine].machine.clone(); @@ -233,10 +232,7 @@ impl MainPanel for MachineInspectionMainPanel { let path = PathBuf::try_from(&tar_path).unwrap(); let index = self.status_text.insert( 1, - String::from(format!( - "Exporting files of {} to {path:?}...", - self.current_machine - )), + format!("Exporting files of {} to {path:?}...", self.current_machine), ); let machine = self.machine_data_map[&self.current_machine].machine.clone(); @@ -269,17 +265,16 @@ impl MainPanel for MachineInspectionMainPanel { let delete_button = Button::new("Delete").fill(Color32::DARK_RED); if ui.add(delete_button).clicked() { let machine_data = &self.machine_data_map[&self.current_machine]; - let index = self.status_text.insert( - 1, - String::from(format!("Deleting machine '{}'...", self.current_machine)), - ); + let index = self + .status_text + .insert(1, format!("Deleting machine '{}'...", self.current_machine)); let machine_clone = machine_data.machine.clone(); let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { let machine_name = machine_clone.config.name.clone(); let delete_result = machine_clone.delete(true); - let answer = if dbg!(delete_result.is_ok()) { + let answer = if delete_result.is_ok() { ChannelDataType::DeletedMachine(machine_name.clone()) } else { ChannelDataType::ClearStatus @@ -319,10 +314,9 @@ impl MainPanel for MachineInspectionMainPanel { } if !self.machine_data_map[&machine_name].initialized { - let index = self.status_text.insert( - 3, - String::from(format!("Loading machine {}...", &machine_name)), - ); + let index = self + .status_text + .insert(3, format!("Loading machine {}...", &machine_name)); let answer_queue_clone = self.answer_queue.clone(); let machine_name_clone = machine_name.clone(); From 23e216b5758d1481e4a1fbf717912e9645c614f1 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:59:59 +0000 Subject: [PATCH 36/49] status "load branches/tags" now as "load repository" --- codchi/src/gui/machine_creation.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs index 6a4ab17d..4f8e00f6 100644 --- a/codchi/src/gui/machine_creation.rs +++ b/codchi/src/gui/machine_creation.rs @@ -121,10 +121,10 @@ impl MainPanel for MachineCreationMainPanel { self.machine_form.git_url = Some(git_url.clone()); self.machine_form.options.auth = auth.clone(); - // load branches for repo - let branches_index = self + let index = self .status_text - .insert(1, format!("Loading branches for {}...", &self.url)); + .insert(2, format!("Loading repository for {}", &self.url)); + let url_clone = url.clone(); let auth_clone = auth.clone(); let git_url_clone = git_url.clone(); @@ -132,22 +132,18 @@ impl MainPanel for MachineCreationMainPanel { thread::spawn(move || { let result = Self::load_branches(&url_clone, &auth_clone); answer_queue_clone.lock().unwrap().push_back(( - branches_index, + index, ChannelDataType::Branches(git_url_clone, result), )); }); - // load tags for repo - let tags_index = self - .status_text - .insert(1, format!("Loading tags for {}...", &self.url)); let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { let result = Self::load_tags(&url, &auth); - answer_queue_clone.lock().unwrap().push_back(( - tags_index, - ChannelDataType::Tags(git_url, result), - )); + answer_queue_clone + .lock() + .unwrap() + .push_back((index, ChannelDataType::Tags(git_url, result))); }); } } From 695b57d05e96b8c38962ca2e8ae3ae15001bb031 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 20 Mar 2025 14:15:05 +0000 Subject: [PATCH 37/49] Deleting machine now instantly removes it from interface, as intended --- codchi/src/gui/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 43067e1a..515fe210 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -95,8 +95,8 @@ impl eframe::App for Gui { } i += 1; } - if i < self.machine_configs.len() { - self.machine_configs.remove(i); + if i < self.machines.len() { + self.machines.remove(i); } } machine_inspection::ChannelDataType::ClearStatus => {} From 67dd51d3a7bb75b64e1cd21c6483e5d05fd19d49 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:15:06 +0000 Subject: [PATCH 38/49] not specifying branch, tag, commit is now possible --- codchi/src/gui/machine_creation.rs | 15 +++++++-------- codchi/src/gui/machine_inspection.rs | 5 +++-- codchi/src/gui/mod.rs | 12 ++++++++++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs index 4f8e00f6..ec7d1f8e 100644 --- a/codchi/src/gui/machine_creation.rs +++ b/codchi/src/gui/machine_creation.rs @@ -1,12 +1,11 @@ use crate::cli::{InputOptions, ModuleAttrPath, NixpkgsLocation}; -use crate::gui::{MainPanel, MainPanelType}; use crate::platform::{Machine, NixDriver, Store}; use crate::util::LinuxPath; use egui::*; use git_url_parse::{GitUrl, GitUrlParseError}; use strum::{EnumIter, IntoEnumIterator}; -use super::{StatusEntries, DTO}; +use super::{content_or_none, MainPanel, MainPanelType, StatusEntries, DTO}; use anyhow::Error; use std::{ collections::VecDeque, @@ -329,13 +328,13 @@ impl MachineCreationMainPanel { }); } - fn create_machine(&mut self, empty: bool) { + fn create_machine(&mut self, without_url: bool) { let index = self.status_text.insert( 1, format!("Creating new machine '{}'...", self.machine_form.name), ); - let machine_form = if empty { + let machine_form = if without_url { let mut mf = MachineForm::default(); mf.name = self.machine_form.name.clone(); mf @@ -344,7 +343,7 @@ impl MachineCreationMainPanel { }; let answer_queue_clone = self.answer_queue.clone(); thread::spawn(move || { - let selected_module_paths = if empty { + let selected_module_paths = if without_url { &Vec::new() } else { &machine_form @@ -413,9 +412,9 @@ impl MachineCreationMainPanel { }; let auth = self.machine_form.options.auth.clone(); let (branch, tag, commit) = match self.git_ref { - GitRef::Branch => (Some(self.branch.clone()), None, None), - GitRef::Tag => (None, Some(self.tag.clone()), None), - GitRef::Commit => (None, None, Some(self.commit.clone())), + GitRef::Branch => (content_or_none(&self.branch), None, None), + GitRef::Tag => (None, content_or_none(&self.tag), None), + GitRef::Commit => (None, None, content_or_none(&self.commit)), }; let answer_queue_clone = self.answer_queue.clone(); let opts = InputOptions { diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index 58771acd..c1f503db 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -1,5 +1,4 @@ use crate::config::Mod; -use crate::gui::{create_password_field, MainPanel, MainPanelMsgType, MainPanelType}; use crate::logging::CodchiOutput; use crate::platform::{platform::HostImpl, DesktopEntry, Host, Machine, MachineDriver}; use crate::secrets::{EnvSecret, MachineSecrets}; @@ -12,7 +11,9 @@ use std::{ thread, }; -use super::{StatusEntries, DTO}; +use super::{ + create_password_field, MainPanel, MainPanelMsgType, MainPanelType, StatusEntries, DTO, +}; pub struct MachineInspectionMainPanel { status_text: StatusEntries, diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 515fe210..f882e45a 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -89,8 +89,8 @@ impl eframe::App for Gui { } machine_inspection::ChannelDataType::DeletedMachine(machine_name) => { let mut i = 0; - for machine_config in &self.machine_configs { - if machine_config.name == machine_name { + for machine in &self.machines { + if machine.config.name == machine_name { break; } i += 1; @@ -724,3 +724,11 @@ pub fn run() -> anyhow::Result<()> { .unwrap(); Ok(()) } + +fn content_or_none(content: &String) -> Option { + if content.is_empty() { + None + } else { + Some(content.to_string()) + } +} From fbe865d62e51692f49217ba883a3efa49d7106cd Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:30:53 +0000 Subject: [PATCH 39/49] adjusted status_bar height to fit spinner --- codchi/src/gui/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index f882e45a..abb7a567 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -297,7 +297,9 @@ impl Gui { } fn status_bar_panel(&self, ctx: &Context) { + let height = 25.0; TopBottomPanel::bottom("statusbar_panel") + .exact_height(height) .resizable(false) .show(ctx, |ui| { ui.horizontal(|ui| { From 2034d029e5eebb0b2378cecc9af4816ea07cf9dd Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:00:02 +0000 Subject: [PATCH 40/49] Updated machine_inspection-modals --- codchi/src/gui/machine_inspection.rs | 311 ++++++++++++++++----------- codchi/src/gui/mod.rs | 12 ++ 2 files changed, 198 insertions(+), 125 deletions(-) diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs index c1f503db..56c74524 100644 --- a/codchi/src/gui/machine_inspection.rs +++ b/codchi/src/gui/machine_inspection.rs @@ -29,6 +29,7 @@ pub struct MachineInspectionMainPanel { show_duplicate_spec_modal: bool, show_tar_spec_modal: bool, show_delete_confirmation_modal: bool, + show_stop_confirmation_modal: bool, textures: HashMap, } @@ -47,6 +48,7 @@ pub(crate) enum ChannelDataType { Secrets(Vec), RebuiltMachine, DuplicatedMachine(Machine), + StoppedMachine(String), DeletedMachine(String), ClearStatus, } @@ -115,6 +117,11 @@ impl MainPanel for MachineInspectionMainPanel { ChannelDataType::DuplicatedMachine(machine), )); } + ChannelDataType::StoppedMachine(machine) => { + (self.frame_msg_callback)(super::MainPanelMsgType::MachineInspection( + ChannelDataType::StoppedMachine(machine), + )); + } ChannelDataType::DeletedMachine(machine_name) => { self.current_machine = String::from(""); self.machine_data_map.remove(&machine_name); @@ -133,34 +140,41 @@ impl MainPanel for MachineInspectionMainPanel { if self.show_rebuild_spec_modal { let modal = Modal::new(Id::new("rebuild_machine_spec_modal")).show(ctx, |ui| { let state_id = ui.id().with("rebuild_machine_spec_modal_checkbox"); - let mut checked = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(true)); + let mut no_update = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(true)); + ui.heading(format!("Rebuild machine '{}'", &self.current_machine)); - ui.checkbox(&mut checked, "update modules"); - ui.horizontal(|ui| { - let rebuild_button = Button::new("Rebuild").fill(Color32::DARK_BLUE); - if ui.add(rebuild_button).clicked() { - let index = self - .status_text - .insert(1, format!("Building machine '{}'...", self.current_machine)); - - let mut machine = - self.machine_data_map[&self.current_machine].machine.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let _ = machine.build(!checked); - answer_queue_clone.lock().unwrap().push_back(( - index, - machine.config.name.clone(), - ChannelDataType::RebuiltMachine, - )); - }); - self.show_rebuild_spec_modal = false; - } - if ui.button("Cancel").clicked() { - self.show_rebuild_spec_modal = false; - } - }); - ui.data_mut(|d| d.insert_temp(state_id, checked)); + ui.checkbox(&mut no_update, "update modules"); + let (rebuild_button, cancel_button) = Sides::new().show( + ui, + |ui_left| ui_left.add(Button::new("Rebuild").fill(Color32::DARK_GREEN)), + |ui_right| ui_right.button("Cancel"), + ); + + if rebuild_button.clicked() { + let index = self + .status_text + .insert(1, format!("Building machine '{}'...", self.current_machine)); + + let mut machine = self.machine_data_map[&self.current_machine].machine.clone(); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let answer = if machine.build(!no_update).is_ok() { + ChannelDataType::RebuiltMachine + } else { + ChannelDataType::ClearStatus + }; + answer_queue_clone.lock().unwrap().push_back(( + index, + machine.config.name.clone(), + answer, + )); + }); + self.show_rebuild_spec_modal = false; + } + if cancel_button.clicked() { + self.show_rebuild_spec_modal = false; + } + ui.data_mut(|d| d.insert_temp(state_id, no_update)); }); if modal.should_close() { self.show_rebuild_spec_modal = false; @@ -171,47 +185,52 @@ impl MainPanel for MachineInspectionMainPanel { let state_id = ui.id().with("duplicate_machine_spec_modal_name"); let mut new_machine_name = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(String::from(""))); + ui.heading(format!("Duplicate machine '{}'", &self.current_machine)); let name_editor = TextEdit::singleline(&mut new_machine_name).hint_text("New Machine Name"); ui.add(name_editor); - ui.horizontal(|ui| { - let duplicate_button = Button::new("Duplicate").fill(Color32::DARK_GREEN); - if ui.add(duplicate_button).clicked() { - let index = self.status_text.insert( - 1, - format!( - "Duplicating machine '{}' as '{}'...", - self.current_machine, new_machine_name - ), - ); - - let machine = self.machine_data_map[&self.current_machine].machine.clone(); - let new_machine_name_clone = new_machine_name.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let _ = machine.duplicate(&new_machine_name_clone); - let duplicated_machine = - Machine::by_name(&new_machine_name_clone, true); - - let answer = if let Ok(machine) = duplicated_machine { - ChannelDataType::DuplicatedMachine(machine) - } else { - ChannelDataType::ClearStatus - }; - - answer_queue_clone.lock().unwrap().push_back(( - index, - machine.config.name, - answer, - )); - }); - self.show_duplicate_spec_modal = false; - } - if ui.button("Cancel").clicked() { - self.show_duplicate_spec_modal = false; - } - }); + let (duplicate_button, cancel_button) = Sides::new().show( + ui, + |ui_left| ui_left.add(Button::new("Duplicate").fill(Color32::DARK_GREEN)), + |ui_right| ui_right.button("Cancel"), + ); + + if duplicate_button.clicked() { + let index = self.status_text.insert( + 1, + format!( + "Duplicating machine '{}' as '{}'...", + self.current_machine, new_machine_name + ), + ); + + let machine = self.machine_data_map[&self.current_machine].machine.clone(); + let new_machine_name_clone = new_machine_name.clone(); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let result = machine.duplicate(&new_machine_name_clone); + let duplicated_machine = Machine::by_name(&new_machine_name_clone, true); + + let answer = if result.is_ok() + && let Ok(machine) = duplicated_machine + { + ChannelDataType::DuplicatedMachine(machine) + } else { + ChannelDataType::ClearStatus + }; + + answer_queue_clone.lock().unwrap().push_back(( + index, + machine.config.name, + answer, + )); + }); + self.show_duplicate_spec_modal = false; + } + if cancel_button.clicked() { + self.show_duplicate_spec_modal = false; + } ui.data_mut(|d| d.insert_temp(state_id, new_machine_name)); }); if modal.should_close() { @@ -226,73 +245,116 @@ impl MainPanel for MachineInspectionMainPanel { ui.heading(format!("Export machine '{}'", &self.current_machine)); let path_editor = TextEdit::singleline(&mut tar_path).hint_text(".tar Path"); ui.add(path_editor); - ui.horizontal(|ui| { - let tar_button = Button::new("Tar").fill(Color32::DARK_GREEN); - if ui.add(tar_button).clicked() { - // TODO - let path = PathBuf::try_from(&tar_path).unwrap(); - let index = self.status_text.insert( - 1, - format!("Exporting files of {} to {path:?}...", self.current_machine), - ); - - let machine = self.machine_data_map[&self.current_machine].machine.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let _ = machine.tar(&path); - - answer_queue_clone.lock().unwrap().push_back(( - index, - machine.config.name, - ChannelDataType::ClearStatus, - )); - }); - self.show_tar_spec_modal = false; - } - if ui.button("Cancel").clicked() { - self.show_tar_spec_modal = false; - } - }); + let (export_button, cancel_button) = Sides::new().show( + ui, + |ui_left| ui_left.add(Button::new("Export").fill(Color32::DARK_GREEN)), + |ui_right| ui_right.button("Cancel"), + ); + + if export_button.clicked() { + let path = PathBuf::try_from(&tar_path).unwrap(); + let index = self.status_text.insert( + 1, + format!("Exporting files of {} to {path:?}...", self.current_machine), + ); + + let machine = self.machine_data_map[&self.current_machine].machine.clone(); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let _ = machine.tar(&path); + + answer_queue_clone.lock().unwrap().push_back(( + index, + machine.config.name, + ChannelDataType::ClearStatus, + )); + }); + self.show_tar_spec_modal = false; + } + if cancel_button.clicked() { + self.show_tar_spec_modal = false; + } ui.data_mut(|d| d.insert_temp(state_id, tar_path)); }); if modal.should_close() { self.show_tar_spec_modal = false; } } + if self.show_stop_confirmation_modal { + let modal = Modal::new(Id::new("stop_machine_confirmation_modal")).show(ctx, |ui| { + ui.heading(format!("Stop machine '{}'?", &self.current_machine)); + let (stop_button, cancel_button) = Sides::new().show( + ui, + |ui_left| ui_left.add(Button::new("Stop").fill(Color32::DARK_RED)), + |ui_right| ui_right.button("Cancel"), + ); + + if stop_button.clicked() { + let index = self + .status_text + .insert(1, format!("Deleting machine '{}'...", self.current_machine)); + + let machine_clone = + self.machine_data_map[&self.current_machine].machine.clone(); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let machine_name = machine_clone.config.name.clone(); + let answer = if machine_clone.stop(false).is_ok() { + ChannelDataType::StoppedMachine(machine_name.clone()) + } else { + ChannelDataType::ClearStatus + }; + answer_queue_clone + .lock() + .unwrap() + .push_back((index, machine_name, answer)); + }); + self.show_stop_confirmation_modal = false; + } + if cancel_button.clicked() { + self.show_stop_confirmation_modal = false; + } + }); + if modal.should_close() { + self.show_stop_confirmation_modal = false; + } + } if self.show_delete_confirmation_modal { let modal = Modal::new(Id::new("delete_machine_confirmation_modal")).show(ctx, |ui| { ui.heading(format!("Delete machine '{}'?", &self.current_machine)); - ui.horizontal(|ui| { - let delete_button = Button::new("Delete").fill(Color32::DARK_RED); - if ui.add(delete_button).clicked() { - let machine_data = &self.machine_data_map[&self.current_machine]; - let index = self - .status_text - .insert(1, format!("Deleting machine '{}'...", self.current_machine)); - - let machine_clone = machine_data.machine.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let machine_name = machine_clone.config.name.clone(); - let delete_result = machine_clone.delete(true); - let answer = if delete_result.is_ok() { - ChannelDataType::DeletedMachine(machine_name.clone()) - } else { - ChannelDataType::ClearStatus - }; - - answer_queue_clone.lock().unwrap().push_back(( - index, - machine_name, - answer, - )); - }); - self.show_delete_confirmation_modal = false; - } - if ui.button("Cancel").clicked() { - self.show_delete_confirmation_modal = false; - } - }); + let (delete_button, cancel_button) = Sides::new().show( + ui, + |ui_left| ui_left.add(Button::new("Delete").fill(Color32::DARK_RED)), + |ui_right| ui_right.button("Cancel"), + ); + + if delete_button.clicked() { + let machine_data = &self.machine_data_map[&self.current_machine]; + let index = self + .status_text + .insert(1, format!("Deleting machine '{}'...", self.current_machine)); + + let machine_clone = machine_data.machine.clone(); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let machine_name = machine_clone.config.name.clone(); + let delete_result = machine_clone.delete(true); + let answer = if delete_result.is_ok() { + ChannelDataType::DeletedMachine(machine_name.clone()) + } else { + ChannelDataType::ClearStatus + }; + + answer_queue_clone + .lock() + .unwrap() + .push_back((index, machine_name, answer)); + }); + self.show_delete_confirmation_modal = false; + } + if cancel_button.clicked() { + self.show_delete_confirmation_modal = false; + } }); if modal.should_close() { self.show_delete_confirmation_modal = false; @@ -393,6 +455,7 @@ impl MachineInspectionMainPanel { show_duplicate_spec_modal: false, show_tar_spec_modal: false, show_delete_confirmation_modal: false, + show_stop_confirmation_modal: false, textures: HashMap::new(), } @@ -417,9 +480,7 @@ impl MachineInspectionMainPanel { self.show_tar_spec_modal = true; } if ui.button("Stop").clicked() { - let _ = self.machine_data_map[&self.current_machine] - .machine - .stop(false); + self.show_stop_confirmation_modal = true; } let delete_button = Button::new("Delete").fill(Color32::DARK_RED); if ui.add(delete_button).clicked() { diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index abb7a567..2da83563 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -87,6 +87,18 @@ impl eframe::App for Gui { } } } + machine_inspection::ChannelDataType::StoppedMachine(machine_name) => { + let mut i = 0; + for machine in &self.machines { + if machine.config.name == machine_name { + break; + } + i += 1; + } + if let Some(machine) = self.machines.get_mut(i) { + machine.platform_status = PlatformStatus::Stopped; + } + } machine_inspection::ChannelDataType::DeletedMachine(machine_name) => { let mut i = 0; for machine in &self.machines { From b22a5d8456d04217280754eb6a085abfe3163ffc Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:08:55 +0000 Subject: [PATCH 41/49] Clicking Home button now renews current panel as intended --- codchi/src/gui/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 2da83563..a42f298c 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -188,8 +188,8 @@ impl Gui { ui.horizontal_centered(|ui| { let codchi_button = Button::image(include_image!("../../assets/logo.png")); if ui.add(codchi_button).on_hover_text("Home").clicked() { - self.main_panels.change(MainPanelType::MachineInspection); self.main_panels.renew(); + self.main_panels.change(MainPanelType::MachineInspection); } ui.separator(); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { From f9a0aee218a38b9e392e98f98a524d432368e363 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:09:29 +0000 Subject: [PATCH 42/49] Error messages introduced to machine creation --- codchi/src/gui/machine_creation.rs | 83 +++++++++++++++++++----------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs index ec7d1f8e..6c67b845 100644 --- a/codchi/src/gui/machine_creation.rs +++ b/codchi/src/gui/machine_creation.rs @@ -35,6 +35,9 @@ pub struct MachineCreationMainPanel { branch: String, tag: String, commit: String, + + repo_error_msg: Option, + module_error_msg: Option, } #[derive(PartialEq, Clone, EnumIter)] @@ -100,6 +103,9 @@ impl Default for MachineCreationMainPanel { branch: String::from(""), tag: String::from(""), commit: String::from(""), + + repo_error_msg: None, + module_error_msg: None, } } } @@ -115,36 +121,41 @@ impl MainPanel for MachineCreationMainPanel { self.status_text.decrease(index); match data_type { ChannelDataType::Access(url, auth, accessible) => { - if accessible && self.url == url && self.get_auth() == auth { - if let Ok(git_url) = Self::get_git_url(&url, &auth) { - self.machine_form.git_url = Some(git_url.clone()); - self.machine_form.options.auth = auth.clone(); - - let index = self - .status_text - .insert(2, format!("Loading repository for {}", &self.url)); - - let url_clone = url.clone(); - let auth_clone = auth.clone(); - let git_url_clone = git_url.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let result = Self::load_branches(&url_clone, &auth_clone); - answer_queue_clone.lock().unwrap().push_back(( - index, - ChannelDataType::Branches(git_url_clone, result), - )); - }); + if accessible + && self.url == url + && self.get_auth() == auth + && let Ok(git_url) = Self::get_git_url(&url, &auth) + { + self.machine_form.git_url = Some(git_url.clone()); + self.machine_form.options.auth = auth.clone(); + self.repo_error_msg = None; - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let result = Self::load_tags(&url, &auth); - answer_queue_clone - .lock() - .unwrap() - .push_back((index, ChannelDataType::Tags(git_url, result))); - }); - } + let index = self + .status_text + .insert(2, format!("Loading repository for {}", &self.url)); + + let url_clone = url.clone(); + let auth_clone = auth.clone(); + let git_url_clone = git_url.clone(); + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let result = Self::load_branches(&url_clone, &auth_clone); + answer_queue_clone.lock().unwrap().push_back(( + index, + ChannelDataType::Branches(git_url_clone, result), + )); + }); + + let answer_queue_clone = self.answer_queue.clone(); + thread::spawn(move || { + let result = Self::load_tags(&url, &auth); + answer_queue_clone + .lock() + .unwrap() + .push_back((index, ChannelDataType::Tags(git_url, result))); + }); + } else { + self.repo_error_msg = Some(String::from("Unable to access Repository")); } } ChannelDataType::Branches(git_url, branches) => { @@ -165,6 +176,9 @@ impl MainPanel for MachineCreationMainPanel { if let Ok(module_paths) = result { self.machine_form.options = options; self.machine_form.module_paths = Some(ModulePaths::new(module_paths)); + self.module_error_msg = None; + } else { + self.module_error_msg = Some(String::from("Unable to load Modules")); } } ChannelDataType::NewMachine(machine_form, mut machine) => { @@ -250,6 +264,8 @@ impl MainPanel for MachineCreationMainPanel { self.use_nixpkgs = false; self.branches = None; self.tags = None; + self.repo_error_msg = None; + self.module_error_msg = None; } } @@ -482,6 +498,11 @@ impl MachineCreationMainPanel { "Don't build machine", ); ui.checkbox(&mut self.machine_form.dont_run_init, "Skip init Script"); + + if let Some(error_msg) = &self.repo_error_msg { + ui.label(String::from("")); + ui.label(RichText::new(error_msg).color(Color32::ORANGE)); + } } fn specify_repository_panel(&mut self, ui: &mut Ui) { @@ -536,6 +557,10 @@ impl MachineCreationMainPanel { Some(NixpkgsLocation::Local) }; } + + if let Some(error_msg) = &self.module_error_msg { + ui.label(RichText::new(error_msg).color(Color32::RED)); + } } fn specify_modules_panel(&mut self, ui: &mut Ui) { From cd983802e812564acd0c49a255ed0880a703a3d7 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:17:52 +0000 Subject: [PATCH 43/49] Major Rearchitecture, temporarily removed Machine Creation --- codchi/Cargo.lock | 82 ++ codchi/Cargo.toml | 1 + codchi/src/gui/machine_creation.rs | 743 --------------- codchi/src/gui/machine_inspection.rs | 641 ------------- .../src/gui/main_panel/machine_inspection.rs | 419 +++++++++ codchi/src/gui/main_panel/mod.rs | 71 ++ codchi/src/gui/menubar.rs | 175 ++++ codchi/src/gui/mod.rs | 884 +++++------------- codchi/src/gui/side_panel.rs | 181 ++++ codchi/src/gui/util/backend_broker.rs | 261 ++++++ codchi/src/gui/util/dialog_manager.rs | 275 ++++++ codchi/src/gui/util/mod.rs | 83 ++ codchi/src/gui/util/status_entries.rs | 68 ++ codchi/src/gui/util/textures_manager.rs | 171 ++++ codchi/src/main.rs | 8 +- codchi/src/module.rs | 2 +- codchi/src/platform/machine.rs | 18 +- 17 files changed, 2055 insertions(+), 2028 deletions(-) delete mode 100644 codchi/src/gui/machine_creation.rs delete mode 100644 codchi/src/gui/machine_inspection.rs create mode 100644 codchi/src/gui/main_panel/machine_inspection.rs create mode 100644 codchi/src/gui/main_panel/mod.rs create mode 100644 codchi/src/gui/menubar.rs create mode 100644 codchi/src/gui/side_panel.rs create mode 100644 codchi/src/gui/util/backend_broker.rs create mode 100644 codchi/src/gui/util/dialog_manager.rs create mode 100644 codchi/src/gui/util/mod.rs create mode 100644 codchi/src/gui/util/status_entries.rs create mode 100644 codchi/src/gui/util/textures_manager.rs diff --git a/codchi/Cargo.lock b/codchi/Cargo.lock index 4da9b9a2..9d106810 100644 --- a/codchi/Cargo.lock +++ b/codchi/Cargo.lock @@ -293,6 +293,25 @@ dependencies = [ "libloading 0.8.6", ] +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.0", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "zbus 5.5.0", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -371,6 +390,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.3.0" @@ -952,6 +982,7 @@ dependencies = [ "number_prefix", "petname", "rand 0.9.0", + "rfd", "serde", "serde_json", "serde_with", @@ -1330,6 +1361,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" +dependencies = [ + "bitflags 2.8.0", + "block2 0.6.0", + "libc", + "objc2 0.6.0", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -3275,6 +3318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" dependencies = [ "bitflags 2.8.0", + "block2 0.6.0", "objc2 0.6.0", "objc2-core-foundation", "objc2-foundation 0.3.0", @@ -3727,6 +3771,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "portable-atomic" version = "1.11.0" @@ -4020,6 +4070,30 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rfd" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d" +dependencies = [ + "ashpd", + "block2 0.6.0", + "dispatch2", + "js-sys", + "log", + "objc2 0.6.0", + "objc2-app-kit 0.3.0", + "objc2-core-foundation", + "objc2-foundation 0.3.0", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "roff" version = "0.2.2" @@ -4942,8 +5016,15 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -6379,6 +6460,7 @@ dependencies = [ "enumflags2", "serde", "static_assertions", + "url", "winnow 0.7.3", "zvariant_derive 5.4.0", "zvariant_utils 3.2.0", diff --git a/codchi/Cargo.toml b/codchi/Cargo.toml index ccb52ddf..4aed9ba3 100644 --- a/codchi/Cargo.toml +++ b/codchi/Cargo.toml @@ -64,6 +64,7 @@ ctrlc = { version = "3.4.5", features = ["termination"] } egui = "0.31.0" eframe = "0.31.0" egui_extras = { version = "0.31.0", features = ["default", "image"] } +rfd = "0.15.3" [target.'cfg(unix)'.dependencies] indoc = "2.0.5" diff --git a/codchi/src/gui/machine_creation.rs b/codchi/src/gui/machine_creation.rs deleted file mode 100644 index 6c67b845..00000000 --- a/codchi/src/gui/machine_creation.rs +++ /dev/null @@ -1,743 +0,0 @@ -use crate::cli::{InputOptions, ModuleAttrPath, NixpkgsLocation}; -use crate::platform::{Machine, NixDriver, Store}; -use crate::util::LinuxPath; -use egui::*; -use git_url_parse::{GitUrl, GitUrlParseError}; -use strum::{EnumIter, IntoEnumIterator}; - -use super::{content_or_none, MainPanel, MainPanelType, StatusEntries, DTO}; -use anyhow::Error; -use std::{ - collections::VecDeque, - fmt::Debug, - process::Command, - sync::{Arc, Mutex}, - thread, -}; - -pub struct MachineCreationMainPanel { - status_text: StatusEntries, - answer_queue: Arc>>, - - creation_step: CreationStep, - machine_form: MachineForm, - next_panel_type: Option, - - url: String, - user: String, - password: String, - use_nixpkgs: bool, - - branches: Option>, - tags: Option>, - - git_ref: GitRef, - branch: String, - tag: String, - commit: String, - - repo_error_msg: Option, - module_error_msg: Option, -} - -#[derive(PartialEq, Clone, EnumIter)] -enum CreationStep { - SpecifyGenerics, - SpecifyRepository, - SpecifyModules, -} - -#[derive(Debug, Default, PartialEq, Clone)] -struct MachineForm { - name: String, - git_url: Option, - options: InputOptions, - do_clone: bool, - module_paths: Option, - dont_run_init: bool, -} - -enum ChannelDataType { - Access(String, Option, bool), - Branches(GitUrl, Result, Error>), - Tags(GitUrl, Result, Error>), - Modules(InputOptions, Result, Error>), - NewMachine(MachineForm, Machine), - BuiltMachine(bool, Machine), - ClearStatus, -} - -#[derive(Debug, PartialEq, EnumIter, Clone, Default)] -enum GitRef { - #[default] - Branch, - Tag, - Commit, -} - -#[derive(Debug, Clone, PartialEq)] -struct ModulePaths { - unselected_module_paths: Vec, - selected_module_paths: Vec, -} - -impl Default for MachineCreationMainPanel { - fn default() -> Self { - MachineCreationMainPanel { - status_text: StatusEntries::new(), - answer_queue: Arc::new(Mutex::new(VecDeque::new())), - - creation_step: CreationStep::SpecifyGenerics, - machine_form: MachineForm::default(), - next_panel_type: None, - - url: String::from(""), - user: String::from(""), - password: String::from(""), - use_nixpkgs: false, - - branches: None, - tags: None, - - git_ref: GitRef::default(), - branch: String::from(""), - tag: String::from(""), - commit: String::from(""), - - repo_error_msg: None, - module_error_msg: None, - } - } -} - -impl MainPanel for MachineCreationMainPanel { - fn update(&mut self, ui: &mut Ui) { - let received_answer = if let Ok(mut answer_queue) = self.answer_queue.try_lock() { - answer_queue.pop_front() - } else { - None - }; - if let Some((index, data_type)) = received_answer { - self.status_text.decrease(index); - match data_type { - ChannelDataType::Access(url, auth, accessible) => { - if accessible - && self.url == url - && self.get_auth() == auth - && let Ok(git_url) = Self::get_git_url(&url, &auth) - { - self.machine_form.git_url = Some(git_url.clone()); - self.machine_form.options.auth = auth.clone(); - self.repo_error_msg = None; - - let index = self - .status_text - .insert(2, format!("Loading repository for {}", &self.url)); - - let url_clone = url.clone(); - let auth_clone = auth.clone(); - let git_url_clone = git_url.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let result = Self::load_branches(&url_clone, &auth_clone); - answer_queue_clone.lock().unwrap().push_back(( - index, - ChannelDataType::Branches(git_url_clone, result), - )); - }); - - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let result = Self::load_tags(&url, &auth); - answer_queue_clone - .lock() - .unwrap() - .push_back((index, ChannelDataType::Tags(git_url, result))); - }); - } else { - self.repo_error_msg = Some(String::from("Unable to access Repository")); - } - } - ChannelDataType::Branches(git_url, branches) => { - if branches.is_ok() { - if Some(git_url) == self.machine_form.git_url { - self.branches = branches.ok(); - } - } - } - ChannelDataType::Tags(git_url, tags) => { - if tags.is_ok() { - if Some(git_url) == self.machine_form.git_url { - self.tags = tags.ok(); - } - } - } - ChannelDataType::Modules(options, result) => { - if let Ok(module_paths) = result { - self.machine_form.options = options; - self.machine_form.module_paths = Some(ModulePaths::new(module_paths)); - self.module_error_msg = None; - } else { - self.module_error_msg = Some(String::from("Unable to load Modules")); - } - } - ChannelDataType::NewMachine(machine_form, mut machine) => { - if self.machine_form == machine_form { - self.renew(); - } - if !machine_form.options.no_build { - let index = self - .status_text - .insert(1, format!("Building machine '{}'...", machine.config.name)); - - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let answer = if machine.build(false).is_ok() { - ChannelDataType::BuiltMachine(machine_form.dont_run_init, machine) - } else { - ChannelDataType::ClearStatus - }; - answer_queue_clone - .lock() - .unwrap() - .push_back((index, answer)); - }); - } - } - ChannelDataType::BuiltMachine(dont_run_init, machine) => { - if !dont_run_init { - let index = self.status_text.insert( - 1, - format!("Running Init-Script for '{}'...", machine.config.name), - ); - - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let _ = machine.run_init_script(dont_run_init); - answer_queue_clone - .lock() - .unwrap() - .push_back((index, ChannelDataType::ClearStatus)); - }); - } - } - ChannelDataType::ClearStatus => {} - } - } - - ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { - ui.separator(); - ui.vertical(|ui| { - self.creation_step_panel(ui); - ui.separator(); - self.dialog_panel(ui); - }); - }); - } - - fn modal_update(&mut self, _ctx: &Context) {} - - fn next_panel(&mut self) -> Option { - self.next_panel_type.take() - } - - fn transfer_data(&mut self, dto: DTO) { - match dto { - DTO::Machine(_) => todo!(), - DTO::Text(text) => { - self.renew(); - self.url = text; - } - } - } - - fn get_status_text(&self) -> &StatusEntries { - &self.status_text - } - - fn renew(&mut self) { - self.creation_step = CreationStep::SpecifyGenerics; - self.machine_form = MachineForm::default(); - self.url = String::from(""); - self.user = String::from(""); - self.password = String::from(""); - self.use_nixpkgs = false; - self.branches = None; - self.tags = None; - self.repo_error_msg = None; - self.module_error_msg = None; - } -} - -impl MachineCreationMainPanel { - pub fn creation_step_panel(&mut self, ui: &mut Ui) { - ui.columns(CreationStep::iter().count(), |columns| { - for creation_step in CreationStep::iter() { - let i = creation_step.clone() as usize; - columns[i].vertical_centered_justified(|ui| { - let text = RichText::from(format!("Step {}", (i + 1))).strong(); - let button = if &self.creation_step == &creation_step { - Button::new(text).fill(Color32::GRAY) - } else { - Button::new(text) - }; - - let button_enabled = self.is_step_reachable(&creation_step); - let button_handle = ui.add_enabled(button_enabled, button); - if button_handle.clicked() { - self.creation_step = creation_step.clone(); - } - }); - } - }); - } - - pub fn dialog_panel(&mut self, ui: &mut Ui) { - match self.creation_step { - CreationStep::SpecifyGenerics => { - self.specify_generics_panel(ui); - } - CreationStep::SpecifyRepository => { - self.specify_repository_panel(ui); - } - CreationStep::SpecifyModules => { - self.specify_modules_panel(ui); - } - } - ui.with_layout(Layout::bottom_up(Align::Center), |ui| { - ui.separator(); - ui.horizontal(|ui| { - if let Some(previous_step) = self.creation_step.previous() { - if ui.button(RichText::from("Previous").strong()).clicked() { - self.creation_step = previous_step; - } - } - ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { - if self.creation_step == CreationStep::SpecifyGenerics { - if self.url.is_empty() && !self.machine_form.name.is_empty() { - let text = RichText::from("Create").strong(); - let create_empty_machine_button = - Button::new(text).fill(Color32::DARK_GREEN); - if ui.add(create_empty_machine_button).clicked() { - self.create_machine(true); - } - } - } - if let Some(next_step) = self.creation_step.next() { - if self.is_step_reachable(&next_step) { - let next_button = Button::new(RichText::from("Next").strong()); - if ui.add(next_button).clicked() { - self.creation_step = next_step; - } - } - self.display_load_button(ui); - } else { - // Last creation step - let text = RichText::from("Finish").strong(); - let finish_button = Button::new(text).fill(Color32::DARK_GREEN); - if ui.add(finish_button).clicked() { - self.create_machine(false); - } - } - }) - }) - }); - } - - fn create_machine(&mut self, without_url: bool) { - let index = self.status_text.insert( - 1, - format!("Creating new machine '{}'...", self.machine_form.name), - ); - - let machine_form = if without_url { - let mut mf = MachineForm::default(); - mf.name = self.machine_form.name.clone(); - mf - } else { - self.machine_form.clone() - }; - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let selected_module_paths = if without_url { - &Vec::new() - } else { - &machine_form - .module_paths - .as_ref() - .unwrap() - .selected_module_paths - }; - let machine = crate::module::init( - &machine_form.name, - machine_form.git_url.clone(), - &machine_form.options, - &selected_module_paths, - ); - let answer = if let Ok(new_machine) = machine { - ChannelDataType::NewMachine(machine_form, new_machine) - } else { - ChannelDataType::ClearStatus - }; - answer_queue_clone - .lock() - .unwrap() - .push_back((index, answer)); - }); - } - - fn display_load_button(&mut self, ui: &mut Ui) { - match self.creation_step { - CreationStep::SpecifyGenerics => { - if ui.button("Load Repository").clicked() { - if !self.url.is_empty() { - self.git_ref = GitRef::default(); - self.branches = None; - self.tags = None; - self.machine_form.options.branch = None; - self.machine_form.options.tag = None; - self.machine_form.options.commit = None; - - let index = self - .status_text - .insert(1, format!("Reading repository {}...", &self.url)); - - let url_clone = self.url.clone(); - let auth_clone = self.get_auth(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let result = Self::is_repo_accessible(&url_clone, &auth_clone); - answer_queue_clone.lock().unwrap().push_back(( - index, - ChannelDataType::Access(url_clone, auth_clone, result.is_ok()), - )); - }); - } - } - } - CreationStep::SpecifyRepository => { - if ui.button("Load Modules").clicked() { - let index = self - .status_text - .insert(1, format!("Loading modules for {}...", &self.url)); - let git_url_clone = self.machine_form.git_url.clone(); - let use_nixpkgs = if self.use_nixpkgs { - Some(NixpkgsLocation::Remote) - } else { - Some(NixpkgsLocation::Local) - }; - let auth = self.machine_form.options.auth.clone(); - let (branch, tag, commit) = match self.git_ref { - GitRef::Branch => (content_or_none(&self.branch), None, None), - GitRef::Tag => (None, content_or_none(&self.tag), None), - GitRef::Commit => (None, None, content_or_none(&self.commit)), - }; - let answer_queue_clone = self.answer_queue.clone(); - let opts = InputOptions { - dont_prompt: true, - no_build: self.machine_form.options.no_build, - use_nixpkgs, - auth, - branch, - tag, - commit, - }; - thread::spawn(move || { - let modules_result = Self::load_modules(&git_url_clone.unwrap(), &opts); - answer_queue_clone - .lock() - .unwrap() - .push_back((index, ChannelDataType::Modules(opts, modules_result))); - }); - } - } - CreationStep::SpecifyModules => {} - } - } - - fn get_auth(&self) -> Option { - // uniform transformation for auth to prevent lock when calling "nix" commands - Some(format!("{}:{}", &self.user, &self.password)) - } - - fn specify_generics_panel(&mut self, ui: &mut Ui) { - ui.heading("Specify the url of the repository you would like to use"); - ui.separator(); - Grid::new("new_machine_grid_1") - .num_columns(2) - .show(ui, |ui| { - ui.label("URL:\t"); - ui.add_sized( - ui.available_size(), - TextEdit::singleline(&mut self.url) - .hint_text("https://gitlab.example.com/my_repo"), - ); - ui.end_row(); - - ui.label("User:\t"); - ui.add_sized(ui.available_size(), TextEdit::singleline(&mut self.user)); - ui.end_row(); - - ui.label("Password:\t"); - ui.add_sized( - ui.available_size(), - TextEdit::singleline(&mut self.password), - ); - ui.end_row(); - - ui.label("Machine Name:\t"); - ui.add_sized( - ui.available_size(), - TextEdit::singleline(&mut self.machine_form.name).hint_text("My awesome Name"), - ); - ui.end_row(); - }); - ui.separator(); - ui.checkbox( - &mut self.machine_form.options.no_build, - "Don't build machine", - ); - ui.checkbox(&mut self.machine_form.dont_run_init, "Skip init Script"); - - if let Some(error_msg) = &self.repo_error_msg { - ui.label(String::from("")); - ui.label(RichText::new(error_msg).color(Color32::ORANGE)); - } - } - - fn specify_repository_panel(&mut self, ui: &mut Ui) { - ui.heading("Specify the repository-details"); - ui.separator(); - ui.horizontal(|ui| { - ComboBox::from_id_salt("git_ref_combo_box") - .selected_text(self.git_ref.to_text()) - .show_ui(ui, |ui| { - for git_ref in GitRef::iter() { - let label = git_ref.to_text(); - ui.selectable_value(&mut self.git_ref, git_ref, label); - } - }); - match &self.git_ref { - GitRef::Branch => { - if let Some(branches) = &self.branches { - ComboBox::from_id_salt("git_ref_branch_combo_box") - .selected_text(&self.branch) - .show_ui(ui, |ui| { - for branch in branches { - ui.selectable_value(&mut self.branch, branch.clone(), branch); - } - }); - } - } - GitRef::Tag => { - if let Some(tags) = &self.tags { - ComboBox::from_id_salt("git_ref_tag_combo_box") - .selected_text(&self.tag) - .show_ui(ui, |ui| { - for tag in tags { - ui.selectable_value(&mut self.tag, tag.clone(), tag); - } - }); - } - } - GitRef::Commit => { - ui.text_edit_singleline(&mut self.commit); - } - }; - }); - ui.separator(); - ui.checkbox(&mut self.machine_form.do_clone, "Clone Configuration"); - if ui - .checkbox(&mut self.use_nixpkgs, "Use provided package version") - .clicked() - { - self.machine_form.options.use_nixpkgs = if self.use_nixpkgs { - Some(NixpkgsLocation::Remote) - } else { - Some(NixpkgsLocation::Local) - }; - } - - if let Some(error_msg) = &self.module_error_msg { - ui.label(RichText::new(error_msg).color(Color32::RED)); - } - } - - fn specify_modules_panel(&mut self, ui: &mut Ui) { - ui.heading("Specify the modules, your machine should have"); - ui.separator(); - if let Some(module_paths) = self.machine_form.module_paths.as_mut() { - let num_cols = 2; - ui.columns(num_cols, |columns| { - fn add_column( - ui: &mut Ui, - text: &str, - module_path_vec: &mut Vec, - pendant_vec: &mut Vec, - ) { - ui.vertical(|ui| { - ui.strong(text); - let mut clicked_index = None; - for i in 0..module_path_vec.len() { - let module_path = &module_path_vec[i]; - let button_text = - format!("{}.{}", module_path.base, module_path.module); - if ui.button(button_text).clicked() { - clicked_index = Some(i); - } - } - if let Some(index) = clicked_index { - let unselected_module_path = module_path_vec.remove(index); - pendant_vec.push(unselected_module_path); - } - }); - } - add_column( - &mut columns[0], - "Unselected Modules", - module_paths.unselected_module_paths.as_mut(), - module_paths.selected_module_paths.as_mut(), - ); - add_column( - &mut columns[1], - "Selected Modules", - module_paths.selected_module_paths.as_mut(), - module_paths.unselected_module_paths.as_mut(), - ); - }); - } - } - - fn is_step_reachable(&self, creation_step: &CreationStep) -> bool { - match creation_step { - CreationStep::SpecifyGenerics => true, - CreationStep::SpecifyRepository => self.tags.is_some() || self.branches.is_some(), - CreationStep::SpecifyModules => self.machine_form.module_paths.is_some(), - } - } - - fn is_repo_accessible(url: &str, auth: &Option) -> Result, Error> { - let opts = InputOptions { - dont_prompt: true, - no_build: true, - use_nixpkgs: None, - auth: auth.clone().or(Some(String::from(""))), - branch: None, - tag: None, - commit: None, - }; - let git_url = Self::get_git_url(url, auth)?; - let flake_url = crate::module::inquire_module_url(&opts, &git_url, false)?; - let dummy_path = LinuxPath(String::from("")); - let nix_url = flake_url.to_nix_url(dummy_path); - - let modules = crate::platform::Driver::store() - .cmd() - .list_nixos_modules(&nix_url)?; - - Ok(modules) - } - - fn load_branches(url: &str, auth: &Option) -> Result, Error> { - let output = Command::new("git") - .args(["ls-remote", "--heads", &Self::get_auth_url(url, auth)]) - .output()?; - - let branches = String::from_utf8_lossy(&output.stdout) - .to_string() - .lines() - .filter_map(|line| line.split('\t').nth(1)) - .filter_map(|ref_name| ref_name.strip_prefix("refs/heads/")) - .map(String::from) - .collect(); - - Ok(branches) - } - - fn load_tags(url: &str, auth: &Option) -> Result, Error> { - let output = Command::new("git") - .args(["ls-remote", "--tags", &Self::get_auth_url(url, auth)]) - .output()?; - - let tags = String::from_utf8_lossy(&output.stdout) - .to_string() - .lines() - .filter_map(|line| line.split('\t').nth(1)) - .filter_map(|ref_name| ref_name.strip_prefix("refs/tags/")) - .map(String::from) - .collect(); - - Ok(tags) - } - - fn load_modules( - url: &GitUrl, - opts: &crate::cli::InputOptions, - ) -> Result, Error> { - let flake_url = crate::module::inquire_module_url(opts, url, false)?; - let dummy_path = LinuxPath(String::from("")); - let nix_url = flake_url.to_nix_url(dummy_path); - let modules = crate::platform::Driver::store() - .cmd() - .list_nixos_modules(&nix_url)?; - - Ok(modules) - } - - fn get_auth_url(url: &str, auth: &Option) -> String { - if let Some(auth_val) = auth { - let url_parts: Vec<&str> = url.split("://").collect(); - if url_parts.len() == 2 { - return format!("{}://{}@{}", url_parts[0], auth_val, url_parts[1]); - } - } - url.to_string() - } - - fn get_git_url(url: &str, auth: &Option) -> Result { - if let Some(auth_val) = auth { - let url_parts: Vec<&str> = url.split("://").collect(); - if url_parts.len() == 2 { - return GitUrl::parse(&format!("{}://{}@{}", url_parts[0], auth_val, url_parts[1])); - } - } - GitUrl::parse(url) - } -} - -impl CreationStep { - fn next(&self) -> Option { - match self { - CreationStep::SpecifyGenerics => Some(CreationStep::SpecifyRepository), - CreationStep::SpecifyRepository => Some(CreationStep::SpecifyModules), - CreationStep::SpecifyModules => None, - } - } - - fn previous(&self) -> Option { - match self { - CreationStep::SpecifyGenerics => None, - CreationStep::SpecifyRepository => Some(CreationStep::SpecifyGenerics), - CreationStep::SpecifyModules => Some(CreationStep::SpecifyRepository), - } - } -} - -impl GitRef { - fn to_text(&self) -> String { - match self { - GitRef::Branch => String::from("Branch"), - GitRef::Tag => String::from("Tag"), - GitRef::Commit => String::from("Commit"), - } - } -} - -impl ModulePaths { - fn new(unselected_module_paths: Vec) -> Self { - Self { - unselected_module_paths, - selected_module_paths: Vec::new(), - } - } -} diff --git a/codchi/src/gui/machine_inspection.rs b/codchi/src/gui/machine_inspection.rs deleted file mode 100644 index 56c74524..00000000 --- a/codchi/src/gui/machine_inspection.rs +++ /dev/null @@ -1,641 +0,0 @@ -use crate::config::Mod; -use crate::logging::CodchiOutput; -use crate::platform::{platform::HostImpl, DesktopEntry, Host, Machine, MachineDriver}; -use crate::secrets::{EnvSecret, MachineSecrets}; -use egui::*; -use itertools::Itertools; -use std::path::PathBuf; -use std::{ - collections::{HashMap, VecDeque}, - sync::{Arc, Mutex}, - thread, -}; - -use super::{ - create_password_field, MainPanel, MainPanelMsgType, MainPanelType, StatusEntries, DTO, -}; - -pub struct MachineInspectionMainPanel { - status_text: StatusEntries, - - machine_data_map: HashMap, - current_machine: String, - next_panel_type: Option, - - answer_queue: Arc>>, - frame_msg_callback: Box, - - show_rebuild_spec_modal: bool, - show_duplicate_spec_modal: bool, - show_tar_spec_modal: bool, - show_delete_confirmation_modal: bool, - show_stop_confirmation_modal: bool, - - textures: HashMap, -} - -struct MachineData { - machine: Machine, - applications: Option>, - modules: Option>, - secrets: Option>, - initialized: bool, -} - -pub(crate) enum ChannelDataType { - Applications(Vec), - Modules(Vec), - Secrets(Vec), - RebuiltMachine, - DuplicatedMachine(Machine), - StoppedMachine(String), - DeletedMachine(String), - ClearStatus, -} - -impl MachineData { - fn new(machine: Machine) -> Self { - Self { - machine, - applications: None, - modules: None, - secrets: None, - initialized: false, - } - } -} - -impl MainPanel for MachineInspectionMainPanel { - fn update(&mut self, ui: &mut Ui) { - let received_answer = if let Ok(mut answer_queue) = self.answer_queue.try_lock() { - answer_queue.pop_front() - } else { - None - }; - if let Some((index, machine_name, data_type)) = received_answer { - if self.status_text.decrease(index) { - self.machine_data_map - .entry(machine_name.clone()) - .and_modify(|machine_data| { - machine_data.initialized = true; - }); - // attempt to load currently inspecting machine - if !self.current_machine.is_empty() { - let current_machine = Machine::by_name(&self.current_machine, true); - if let Ok(machine) = current_machine { - self.transfer_data(DTO::Machine(machine)); - } - } - } - match data_type { - ChannelDataType::Modules(modules) => { - self.machine_data_map - .entry(machine_name) - .and_modify(|machine_data| { - machine_data.modules = Some(modules); - }); - } - ChannelDataType::Secrets(secrets) => { - self.machine_data_map - .entry(machine_name) - .and_modify(|machine_data| { - machine_data.secrets = Some(secrets); - }); - } - ChannelDataType::Applications(applications) => { - self.machine_data_map - .entry(machine_name) - .and_modify(|machine_data| { - machine_data.applications = Some(applications); - }); - } - ChannelDataType::RebuiltMachine => { - self.reload_machine(Some(&machine_name)); - } - ChannelDataType::DuplicatedMachine(machine) => { - (self.frame_msg_callback)(super::MainPanelMsgType::MachineInspection( - ChannelDataType::DuplicatedMachine(machine), - )); - } - ChannelDataType::StoppedMachine(machine) => { - (self.frame_msg_callback)(super::MainPanelMsgType::MachineInspection( - ChannelDataType::StoppedMachine(machine), - )); - } - ChannelDataType::DeletedMachine(machine_name) => { - self.current_machine = String::from(""); - self.machine_data_map.remove(&machine_name); - (self.frame_msg_callback)(super::MainPanelMsgType::MachineInspection( - ChannelDataType::DeletedMachine(machine_name), - )); - } - ChannelDataType::ClearStatus => {} - } - } - - self.display_machine(ui); - } - - fn modal_update(&mut self, ctx: &Context) { - if self.show_rebuild_spec_modal { - let modal = Modal::new(Id::new("rebuild_machine_spec_modal")).show(ctx, |ui| { - let state_id = ui.id().with("rebuild_machine_spec_modal_checkbox"); - let mut no_update = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(true)); - - ui.heading(format!("Rebuild machine '{}'", &self.current_machine)); - ui.checkbox(&mut no_update, "update modules"); - let (rebuild_button, cancel_button) = Sides::new().show( - ui, - |ui_left| ui_left.add(Button::new("Rebuild").fill(Color32::DARK_GREEN)), - |ui_right| ui_right.button("Cancel"), - ); - - if rebuild_button.clicked() { - let index = self - .status_text - .insert(1, format!("Building machine '{}'...", self.current_machine)); - - let mut machine = self.machine_data_map[&self.current_machine].machine.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let answer = if machine.build(!no_update).is_ok() { - ChannelDataType::RebuiltMachine - } else { - ChannelDataType::ClearStatus - }; - answer_queue_clone.lock().unwrap().push_back(( - index, - machine.config.name.clone(), - answer, - )); - }); - self.show_rebuild_spec_modal = false; - } - if cancel_button.clicked() { - self.show_rebuild_spec_modal = false; - } - ui.data_mut(|d| d.insert_temp(state_id, no_update)); - }); - if modal.should_close() { - self.show_rebuild_spec_modal = false; - } - } - if self.show_duplicate_spec_modal { - let modal = Modal::new(Id::new("duplicate_machine_spec_modal")).show(ctx, |ui| { - let state_id = ui.id().with("duplicate_machine_spec_modal_name"); - let mut new_machine_name = - ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(String::from(""))); - - ui.heading(format!("Duplicate machine '{}'", &self.current_machine)); - let name_editor = - TextEdit::singleline(&mut new_machine_name).hint_text("New Machine Name"); - ui.add(name_editor); - let (duplicate_button, cancel_button) = Sides::new().show( - ui, - |ui_left| ui_left.add(Button::new("Duplicate").fill(Color32::DARK_GREEN)), - |ui_right| ui_right.button("Cancel"), - ); - - if duplicate_button.clicked() { - let index = self.status_text.insert( - 1, - format!( - "Duplicating machine '{}' as '{}'...", - self.current_machine, new_machine_name - ), - ); - - let machine = self.machine_data_map[&self.current_machine].machine.clone(); - let new_machine_name_clone = new_machine_name.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let result = machine.duplicate(&new_machine_name_clone); - let duplicated_machine = Machine::by_name(&new_machine_name_clone, true); - - let answer = if result.is_ok() - && let Ok(machine) = duplicated_machine - { - ChannelDataType::DuplicatedMachine(machine) - } else { - ChannelDataType::ClearStatus - }; - - answer_queue_clone.lock().unwrap().push_back(( - index, - machine.config.name, - answer, - )); - }); - self.show_duplicate_spec_modal = false; - } - if cancel_button.clicked() { - self.show_duplicate_spec_modal = false; - } - ui.data_mut(|d| d.insert_temp(state_id, new_machine_name)); - }); - if modal.should_close() { - self.show_duplicate_spec_modal = false; - } - } - if self.show_tar_spec_modal { - let modal = Modal::new(Id::new("tar_machine_spec_modal")).show(ctx, |ui| { - let state_id = ui.id().with("tar_machine_spec_modal_path"); - let mut tar_path = - ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(String::from(""))); - ui.heading(format!("Export machine '{}'", &self.current_machine)); - let path_editor = TextEdit::singleline(&mut tar_path).hint_text(".tar Path"); - ui.add(path_editor); - let (export_button, cancel_button) = Sides::new().show( - ui, - |ui_left| ui_left.add(Button::new("Export").fill(Color32::DARK_GREEN)), - |ui_right| ui_right.button("Cancel"), - ); - - if export_button.clicked() { - let path = PathBuf::try_from(&tar_path).unwrap(); - let index = self.status_text.insert( - 1, - format!("Exporting files of {} to {path:?}...", self.current_machine), - ); - - let machine = self.machine_data_map[&self.current_machine].machine.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let _ = machine.tar(&path); - - answer_queue_clone.lock().unwrap().push_back(( - index, - machine.config.name, - ChannelDataType::ClearStatus, - )); - }); - self.show_tar_spec_modal = false; - } - if cancel_button.clicked() { - self.show_tar_spec_modal = false; - } - ui.data_mut(|d| d.insert_temp(state_id, tar_path)); - }); - if modal.should_close() { - self.show_tar_spec_modal = false; - } - } - if self.show_stop_confirmation_modal { - let modal = Modal::new(Id::new("stop_machine_confirmation_modal")).show(ctx, |ui| { - ui.heading(format!("Stop machine '{}'?", &self.current_machine)); - let (stop_button, cancel_button) = Sides::new().show( - ui, - |ui_left| ui_left.add(Button::new("Stop").fill(Color32::DARK_RED)), - |ui_right| ui_right.button("Cancel"), - ); - - if stop_button.clicked() { - let index = self - .status_text - .insert(1, format!("Deleting machine '{}'...", self.current_machine)); - - let machine_clone = - self.machine_data_map[&self.current_machine].machine.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let machine_name = machine_clone.config.name.clone(); - let answer = if machine_clone.stop(false).is_ok() { - ChannelDataType::StoppedMachine(machine_name.clone()) - } else { - ChannelDataType::ClearStatus - }; - answer_queue_clone - .lock() - .unwrap() - .push_back((index, machine_name, answer)); - }); - self.show_stop_confirmation_modal = false; - } - if cancel_button.clicked() { - self.show_stop_confirmation_modal = false; - } - }); - if modal.should_close() { - self.show_stop_confirmation_modal = false; - } - } - if self.show_delete_confirmation_modal { - let modal = Modal::new(Id::new("delete_machine_confirmation_modal")).show(ctx, |ui| { - ui.heading(format!("Delete machine '{}'?", &self.current_machine)); - let (delete_button, cancel_button) = Sides::new().show( - ui, - |ui_left| ui_left.add(Button::new("Delete").fill(Color32::DARK_RED)), - |ui_right| ui_right.button("Cancel"), - ); - - if delete_button.clicked() { - let machine_data = &self.machine_data_map[&self.current_machine]; - let index = self - .status_text - .insert(1, format!("Deleting machine '{}'...", self.current_machine)); - - let machine_clone = machine_data.machine.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let machine_name = machine_clone.config.name.clone(); - let delete_result = machine_clone.delete(true); - let answer = if delete_result.is_ok() { - ChannelDataType::DeletedMachine(machine_name.clone()) - } else { - ChannelDataType::ClearStatus - }; - - answer_queue_clone - .lock() - .unwrap() - .push_back((index, machine_name, answer)); - }); - self.show_delete_confirmation_modal = false; - } - if cancel_button.clicked() { - self.show_delete_confirmation_modal = false; - } - }); - if modal.should_close() { - self.show_delete_confirmation_modal = false; - } - } - } - - fn next_panel(&mut self) -> Option { - self.next_panel_type.take() - } - - fn transfer_data(&mut self, dto: DTO) { - match dto { - DTO::Machine(machine) => { - let machine_name = machine.config.name.clone(); - if !self.machine_data_map.contains_key(&machine_name) { - let machine_data = MachineData::new(machine.clone()); - self.machine_data_map - .insert(machine_name.clone(), machine_data); - } - - if !self.machine_data_map[&machine_name].initialized { - let index = self - .status_text - .insert(3, format!("Loading machine {}...", &machine_name)); - - let answer_queue_clone = self.answer_queue.clone(); - let machine_name_clone = machine_name.clone(); - let machine_clone = machine.clone(); - thread::spawn(move || { - let applications = - HostImpl::list_desktop_entries(&machine_clone).unwrap_or(Vec::new()); - - answer_queue_clone.lock().unwrap().push_back(( - index, - machine_name_clone, - ChannelDataType::Applications(applications), - )); - }); - - let answer_queue_clone = self.answer_queue.clone(); - let modules_clone = machine.config.modules.clone(); - let machine_name_clone = machine_name.clone(); - thread::spawn(move || { - let modules = modules_clone.to_output(); - - answer_queue_clone.lock().unwrap().push_back(( - index, - machine_name_clone, - ChannelDataType::Modules(modules), - )); - }); - - let answer_queue_clone = self.answer_queue.clone(); - let machine_name_clone = machine_name.clone(); - thread::spawn(move || { - let secrets = machine - .eval_env_secrets() - .expect("failed to load machine secrets") - .into_values() - .collect_vec() - .to_output(); - answer_queue_clone.lock().unwrap().push_back(( - index, - machine_name_clone, - ChannelDataType::Secrets(secrets), - )); - }); - } - self.current_machine = machine_name; - } - DTO::Text(_) => todo!(), - } - } - - fn get_status_text(&self) -> &StatusEntries { - &self.status_text - } - - fn renew(&mut self) { - self.current_machine = String::from(""); - } -} - -impl MachineInspectionMainPanel { - pub fn new(callback: Box) -> Self { - Self { - status_text: StatusEntries::new(), - - machine_data_map: HashMap::new(), - current_machine: String::from(""), - next_panel_type: None, - - answer_queue: Arc::new(Mutex::new(VecDeque::new())), - frame_msg_callback: callback, - - show_rebuild_spec_modal: false, - show_duplicate_spec_modal: false, - show_tar_spec_modal: false, - show_delete_confirmation_modal: false, - show_stop_confirmation_modal: false, - - textures: HashMap::new(), - } - } - - fn display_machine(&mut self, ui: &mut Ui) { - if !self.current_machine.is_empty() { - ui.horizontal(|ui| { - let title_text = format!("Machine: {}", &self.current_machine); - ui.add(widgets::Label::new( - RichText::from(title_text).heading().strong(), - )); - ui.with_layout(Layout::right_to_left(Align::BOTTOM), |ui| { - ui.menu_button("Actions", |ui| { - if ui.button("Rebuild").clicked() { - self.show_rebuild_spec_modal = true; - } - if ui.button("Duplicate").clicked() { - self.show_duplicate_spec_modal = true; - } - if ui.button("Tar").clicked() { - self.show_tar_spec_modal = true; - } - if ui.button("Stop").clicked() { - self.show_stop_confirmation_modal = true; - } - let delete_button = Button::new("Delete").fill(Color32::DARK_RED); - if ui.add(delete_button).clicked() { - self.show_delete_confirmation_modal = true; - } - }); - let reload_button = Button::new("\u{21BA}"); - if ui.add(reload_button).on_hover_text("Reload").clicked() { - self.reload_machine(None); - } - }); - }); - let machine_data = &self.machine_data_map[&self.current_machine]; - let status_text = match machine_data.machine.platform_status { - crate::platform::PlatformStatus::NotInstalled => "Not Installed", - crate::platform::PlatformStatus::Stopped => "Stopped", - crate::platform::PlatformStatus::Running => "Running", - }; - ui.label(status_text); - ui.separator(); - - ui.heading("Applications"); - ui.add_space(3.0); - if let Some(desktop_entries) = &machine_data.applications { - for desktop_entry in desktop_entries { - let icon = if let Some(icon_path) = &desktop_entry.icon { - if !self.textures.contains_key(&desktop_entry.app_name) { - let image = image::open(icon_path) - .expect("Failed to open image icon") - .to_rgba8(); - let size = [image.width() as usize, image.height() as usize]; - let texture = ui.ctx().load_texture( - "icon_texture", - ColorImage::from_rgba_unmultiplied(size, &image), - Default::default(), - ); - self.textures - .insert(desktop_entry.app_name.clone(), texture); - } - let texture = &self.textures[&desktop_entry.app_name]; - let image = - Image::from_texture(texture).max_size(Vec2 { x: 12.0, y: 12.0 }); - Some(image) - } else { - None - }; - let text = WidgetText::RichText(RichText::new(&desktop_entry.app_name)); - let button = Button::opt_image_and_text(icon, Some(text)); - let button_handle = ui.add(button); - if button_handle.clicked() { - let _ = - HostImpl::execute(&machine_data.machine.config.name, &desktop_entry); - } - } - if ui.button("Shell").clicked() { - let _ = crate::platform::Driver::host().open_terminal(&[ - &std::env::current_exe().unwrap().display().to_string(), - "exec", - &machine_data.machine.config.name, - ]); - } - } else { - ui.horizontal(|ui| { - ui.add_space(20.0); - ui.label("Loading ..."); - }); - } - ui.separator(); - - ui.heading("Modules"); - if let Some(modules) = &machine_data.modules { - if !modules.is_empty() { - Grid::new("modules_grid").show(ui, |ui| { - ui.strong("Name\t"); - ui.strong("URL\t"); - ui.strong("Path\t"); - ui.end_row(); - - for module in modules { - ui.label(format!("{}\t", module.name)); - ui.label(format!("{}\t", module.url)); - ui.label(format!("{}\t", module.flake_module)); - ui.end_row(); - } - }); - } else { - ui.horizontal(|ui| { - ui.label("No modules"); - }); - } - } else { - ui.horizontal(|ui| { - ui.add_space(20.0); - ui.label("Loading ..."); - }); - } - ui.separator(); - - ui.heading("Secrets"); - if let Some(secrets) = &machine_data.secrets { - if !secrets.is_empty() { - Grid::new("secrets_grid").show(ui, |ui| { - ui.strong("Name\t"); - ui.strong("Description\t"); - ui.strong("Value\t"); - ui.end_row(); - - for secret in secrets { - ui.label(format!("{}\t", secret.name)); - ui.label(format!("{}\t", secret.description)); - ui.add(create_password_field( - &secret.name, - |new_password| { - let (lock, mut cfg) = - crate::config::MachineConfig::open_existing( - &self.current_machine, - true, - ) - .unwrap(); - cfg.secrets.insert(secret.name.clone(), new_password); - cfg.write(lock).unwrap(); - }, - &if secret.value.is_some() { - secret.value.clone().unwrap() - } else { - String::from("") - }, - )); - ui.end_row(); - } - }); - } else { - ui.horizontal(|ui| { - ui.label("No secrets"); - }); - } - } else { - ui.horizontal(|ui| { - ui.add_space(20.0); - ui.label("Loading ..."); - }); - } - } - } - - fn reload_machine(&mut self, name_option: Option<&str>) { - let machine_name = if let Some(name) = name_option { - name - } else { - &self.current_machine - }; - self.machine_data_map.remove(machine_name); - let machine_res = Machine::by_name(machine_name, true); - if let Ok(machine) = machine_res { - self.transfer_data(DTO::Machine(machine)); - } - } -} diff --git a/codchi/src/gui/main_panel/machine_inspection.rs b/codchi/src/gui/main_panel/machine_inspection.rs new file mode 100644 index 00000000..c3ce166f --- /dev/null +++ b/codchi/src/gui/main_panel/machine_inspection.rs @@ -0,0 +1,419 @@ +use super::{ + super::util::{ + dialog_manager::DialogManager, status_entries::StatusEntries, + textures_manager::TexturesManager, + }, + MainPanelIntent, +}; +use crate::{ + config::Mod, + gui::util::advanced_password_field, + logging::CodchiOutput, + platform::{platform::HostImpl, DesktopEntry, Host, Machine}, + secrets::{EnvSecret, MachineSecrets}, +}; +use anyhow::Result; +use egui::*; +use itertools::Itertools; +use std::collections::HashMap; +use std::{ + sync::mpsc::{channel, Receiver, Sender}, + thread, +}; + +pub struct MachineInspection { + machine_data_map: HashMap, + current_machine: Option, + + sender: Sender<(usize, ChannelDTO)>, + receiver: Receiver<(usize, ChannelDTO)>, +} + +enum ChannelDTO { + Machine(String, Result), + Applications(String, Result>), + Modules(String, Result>), + Secrets(String, Result>), +} + +pub enum MachineInspectionIntent { + Rebuild(Machine), + Duplicate(Machine), + Tar(Machine), + Stop(Machine), + Delete(Machine), +} + +impl MachineInspection { + pub fn update( + &mut self, + ui: &mut Ui, + status_entries: &mut StatusEntries, + _dialog_manager: &mut DialogManager, + textures_manager: &mut TexturesManager, + ) -> Vec { + self.receive_msgs(status_entries); + + self.update_ui(ui, status_entries, textures_manager) + .into_iter() + .map(|machine_inspection_intent| { + MainPanelIntent::MachineInspection(machine_inspection_intent) + }) + .collect_vec() + } + + pub fn reset(&mut self) { + if let Some(current_machine) = &self.current_machine { + self.machine_data_map.remove(current_machine); + } + + self.current_machine = None; + } + + fn receive_msgs(&mut self, status_entries: &mut StatusEntries) { + while let Ok((status_index, channel_dto)) = self.receiver.try_recv() { + status_entries.decrease(status_index); + + match channel_dto { + ChannelDTO::Machine(machine_name, machine_result) => match machine_result { + Ok(machine) => { + let machine_data = MachineData::create(machine.clone()); + self.machine_data_map.insert(machine_name, machine_data); + + self.load_machine_data(machine, status_entries); + } + Err(_error) => self.load_machine(machine_name, status_entries), + }, + ChannelDTO::Applications(machine_name, applications_result) => { + if let Some(machine_data_entry) = self.machine_data_map.get_mut(&machine_name) { + match applications_result { + Ok(applications) => machine_data_entry.set_applications(applications), + Err(_error) => machine_data_entry.set_applications(Vec::new()), + } + } + } + ChannelDTO::Modules(machine_name, modules_result) => { + if let Some(machine_data_entry) = self.machine_data_map.get_mut(&machine_name) { + match modules_result { + Ok(modules) => machine_data_entry.set_modules(modules), + Err(_error) => machine_data_entry.set_modules(Vec::new()), + } + } + } + ChannelDTO::Secrets(machine_name, secrets_result) => { + if let Some(machine_data_entry) = self.machine_data_map.get_mut(&machine_name) { + match secrets_result { + Ok(secrets) => machine_data_entry.set_secrets(secrets), + Err(_error) => machine_data_entry.set_secrets(Vec::new()), + } + } + } + } + } + } + + pub fn new() -> Self { + let (sender, receiver) = channel(); + Self { + machine_data_map: HashMap::new(), + current_machine: None, + + sender, + receiver, + } + } + + pub fn change(&mut self, machine: Machine, status_entries: &mut StatusEntries) { + let machine_name = machine.config.name.clone(); + + if !self.machine_data_map.contains_key(&machine_name) { + self.machine_data_map + .insert(machine_name.clone(), MachineData::create(machine)); + } + + let machine_data_option = self.machine_data_map.get_mut(&machine_name); + if machine_data_option.is_some_and(|machine_data| !machine_data.is_initialized()) { + self.load_machine(machine_name.clone(), status_entries) + } + + self.current_machine = Some(machine_name); + } + + fn load_machine(&self, machine_name: String, status_entries: &mut StatusEntries) { + let status_index = + status_entries.create_entry(format!("Loading Machine '{}'...", &machine_name)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let machine = Machine::by_name(&machine_name, true); + + sender_clone + .send((status_index, ChannelDTO::Machine(machine_name, machine))) + .unwrap(); + }); + } + + fn load_machine_data(&self, machine: Machine, status_entries: &mut StatusEntries) { + let status_index = + status_entries.push(format!("Loading Data for '{}'...", &machine.config.name), 3); + + let machine_clone = machine.clone(); + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let applications = HostImpl::list_desktop_entries(&machine_clone); + + sender_clone + .send(( + status_index, + ChannelDTO::Applications(machine_clone.config.name, applications), + )) + .unwrap(); + }); + + let machine_name_clone = machine.config.name.clone(); + let modules_clone = machine.config.modules.clone(); + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let modules = modules_clone.to_output(); + + sender_clone + .send(( + status_index, + ChannelDTO::Modules(machine_name_clone, Ok(modules)), + )) + .unwrap(); + }); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let secrets = machine + .eval_env_secrets() + .map(|secret_map| secret_map.into_values().collect_vec().to_output()); + + sender_clone + .send(( + status_index, + ChannelDTO::Secrets(machine.config.name, secrets), + )) + .unwrap(); + }); + } + + fn update_ui( + &mut self, + ui: &mut Ui, + status_entries: &mut StatusEntries, + textures_manager: &mut TexturesManager, + ) -> Vec { + let mut intent = Vec::new(); + + let mut machine_menu_intent = self.update_machine_menu(ui, status_entries); + intent.append(&mut machine_menu_intent); + + if let Some(machine_name) = &self.current_machine { + let machine_data = &self.machine_data_map[machine_name]; + let status_text = match machine_data.machine.platform_status { + crate::platform::PlatformStatus::NotInstalled => "Not Installed", + crate::platform::PlatformStatus::Stopped => "Stopped", + crate::platform::PlatformStatus::Running => "Running", + }; + ui.label(status_text); + ui.separator(); + + ui.heading("Applications"); + ui.add_space(3.0); + if let Some(desktop_entries) = &machine_data.applications { + for desktop_entry in desktop_entries { + let app_icon = match &desktop_entry.icon { + Some(icon_path) => textures_manager + .deliver_image(&desktop_entry.app_name, icon_path) + .map(|tex| { + Image::from_texture(tex).max_size(Vec2 { x: 12.0, y: 12.0 }) + }), + None => None, + }; + let app_text = WidgetText::RichText(RichText::new(&desktop_entry.app_name)); + let app_button = Button::opt_image_and_text(app_icon, Some(app_text)); + if ui.add(app_button).clicked() { + let _ = + HostImpl::execute(&machine_data.machine.config.name, &desktop_entry); + } + } + if ui.button("Shell").clicked() { + let _ = crate::platform::Driver::host().open_terminal(&[ + &std::env::current_exe().unwrap().display().to_string(), + "exec", + &machine_data.machine.config.name, + ]); + } + } else { + ui.horizontal(|ui| { + ui.add_space(20.0); + ui.label("Loading ..."); + }); + } + ui.separator(); + + ui.heading("Modules"); + if let Some(modules) = &machine_data.modules { + if !modules.is_empty() { + Grid::new("modules_grid").show(ui, |ui| { + ui.strong("Name\t"); + ui.strong("URL\t"); + ui.strong("Path\t"); + ui.end_row(); + + for module in modules { + ui.label(format!("{}\t", module.name)); + ui.label(format!("{}\t", module.url)); + ui.label(format!("{}\t", module.flake_module)); + ui.end_row(); + } + }); + } else { + ui.horizontal(|ui| { + ui.label("No modules"); + }); + } + } else { + ui.horizontal(|ui| { + ui.add_space(20.0); + ui.label("Loading ..."); + }); + } + ui.separator(); + + ui.heading("Secrets"); + if let Some(secrets) = &machine_data.secrets { + if !secrets.is_empty() { + Grid::new("secrets_grid").show(ui, |ui| { + ui.strong("Name\t"); + ui.strong("Description\t"); + ui.strong("Value\t"); + ui.end_row(); + + for secret in secrets { + ui.label(format!("{}\t", secret.name)); + ui.label(format!("{}\t", secret.description)); + ui.add(advanced_password_field( + &format!("{}_{}", &machine_name, &secret.name), + |new_password| { + let (lock, mut cfg) = + crate::config::MachineConfig::open_existing( + machine_name, + true, + ) + .unwrap(); + cfg.secrets.insert(secret.name.clone(), new_password); + cfg.write(lock).unwrap(); + }, + &if secret.value.is_some() { + secret.value.clone().unwrap() + } else { + String::from("") + }, + )); + ui.end_row(); + } + }); + } else { + ui.horizontal(|ui| { + ui.label("No secrets"); + }); + } + } else { + ui.horizontal(|ui| { + ui.add_space(20.0); + ui.label("Loading ..."); + }); + } + } + + intent + } + + fn update_machine_menu( + &mut self, + ui: &mut Ui, + status_entries: &mut StatusEntries, + ) -> Vec { + let mut intent = Vec::new(); + + if let Some(machine_name) = &self.current_machine { + ui.horizontal(|ui| { + let title_text = format!("Machine: {}", &machine_name); + ui.add(widgets::Label::new( + RichText::from(title_text).heading().strong(), + )); + ui.with_layout(Layout::right_to_left(Align::BOTTOM), |ui| { + ui.menu_button("Actions", |ui| { + if ui.button("Rebuild").clicked() { + let machine_clone = self.machine_data_map[machine_name].machine.clone(); + intent.push(MachineInspectionIntent::Rebuild(machine_clone)); + } + + if ui.button("Duplicate").clicked() { + let machine_clone = self.machine_data_map[machine_name].machine.clone(); + intent.push(MachineInspectionIntent::Duplicate(machine_clone)); + } + + if ui.button("Export").clicked() { + let machine_clone = self.machine_data_map[machine_name].machine.clone(); + intent.push(MachineInspectionIntent::Tar(machine_clone)); + } + + if ui.button("Stop").clicked() { + let machine_clone = self.machine_data_map[machine_name].machine.clone(); + intent.push(MachineInspectionIntent::Stop(machine_clone)); + } + + let delete_button = Button::new("Delete").fill(Color32::DARK_RED); + if ui.add(delete_button).clicked() { + let machine_clone = self.machine_data_map[machine_name].machine.clone(); + intent.push(MachineInspectionIntent::Delete(machine_clone)); + } + }); + if ui.button("\u{21BA}").on_hover_text("Reload").clicked() { + self.load_machine(machine_name.clone(), status_entries); + } + }); + }); + } + + intent + } +} + +struct MachineData { + machine: Machine, + applications: Option>, + modules: Option>, + secrets: Option>, +} + +impl MachineData { + fn create(machine: Machine) -> Self { + Self { + machine, + applications: None, + modules: None, + secrets: None, + } + } + + fn set_applications(&mut self, applications: Vec) { + self.applications = Some(applications); + } + + fn set_modules(&mut self, modules: Vec) { + self.modules = Some(modules); + } + + fn set_secrets(&mut self, secrets: Vec) { + self.secrets = Some(secrets); + } + + fn is_initialized(&self) -> bool { + self.applications.is_some() && self.modules.is_some() && self.secrets.is_some() + } +} diff --git a/codchi/src/gui/main_panel/mod.rs b/codchi/src/gui/main_panel/mod.rs new file mode 100644 index 00000000..a1b12c8d --- /dev/null +++ b/codchi/src/gui/main_panel/mod.rs @@ -0,0 +1,71 @@ +pub mod machine_inspection; + +use super::util::{ + dialog_manager::DialogManager, status_entries::StatusEntries, textures_manager::TexturesManager, +}; +use egui::*; +use machine_inspection::{MachineInspection, MachineInspectionIntent}; + +pub struct MainPanel { + pub panels: MainPanels, + current_main_panel_type: MainPanelType, +} + +impl MainPanel { + pub fn new() -> Self { + Self { + panels: MainPanels::new(), + current_main_panel_type: MainPanelType::MachineInspection, + } + } + + pub fn update( + &mut self, + ui: &mut Ui, + status_entries: &mut StatusEntries, + dialog_manager: &mut DialogManager, + textures_manager: &mut TexturesManager, + ) -> Vec { + match self.current_main_panel_type { + MainPanelType::MachineInspection => self.panels.get_machine_inspection().update( + ui, + status_entries, + dialog_manager, + textures_manager, + ), + } + } + + pub fn reset(&mut self) { + match self.current_main_panel_type { + MainPanelType::MachineInspection => self.panels.get_machine_inspection().reset(), + } + + self.current_main_panel_type = MainPanelType::MachineInspection; + } +} + +pub struct MainPanels { + machine_inspection: Option, +} + +impl MainPanels { + pub fn new() -> Self { + Self { + machine_inspection: None, + } + } + + pub fn get_machine_inspection(&mut self) -> &mut MachineInspection { + self.machine_inspection + .get_or_insert(MachineInspection::new()) + } +} + +pub enum MainPanelType { + MachineInspection, +} + +pub enum MainPanelIntent { + MachineInspection(MachineInspectionIntent), +} diff --git a/codchi/src/gui/menubar.rs b/codchi/src/gui/menubar.rs new file mode 100644 index 00000000..ecc80917 --- /dev/null +++ b/codchi/src/gui/menubar.rs @@ -0,0 +1,175 @@ +use egui::*; + +use crate::config::CodchiConfig; + +use super::util::textures_manager::TexturesManager; + +#[derive(Debug)] +pub enum MenubarIntent { + Home, + OpenGithub, + OpenIssues, + ZoomIn, + ZoomOut, + RecoverStore, + ShowTray(bool), + EnableVcxsrv(bool), + ShowTrayVcxsrv(bool), + EnableWslVpnkit(bool), + InsertUrl(String), +} + +pub fn update(ui: &mut Ui, textures_manager: &mut TexturesManager) -> Vec { + let mut intent = Vec::new(); + + ui.horizontal_centered(|ui| { + let codchi_button = match textures_manager.deliver("logo", "assets/logo.png", 45) { + Some(image) => Button::image(image), + None => Button::new("Home"), + }; + if ui.add(codchi_button).on_hover_text("Home").clicked() { + intent.push(MenubarIntent::Home); + ui.close_menu(); + } + ui.separator(); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + let github_button = + match textures_manager.deliver("github_logo", "assets/github_logo.png", 25) { + Some(image) => Button::image(image), + None => Button::new("Github"), + }; + if ui.add(github_button).on_hover_text("Github").clicked() { + intent.push(MenubarIntent::OpenGithub); + ui.close_menu(); + } + + let bug_report_button = + match textures_manager.deliver("bug_icon", "assets/bug_icon.png", 25) { + Some(image) => Button::image(image), + None => Button::new("Bug-Report"), + }; + if ui + .add(bug_report_button) + .on_hover_text("Bug-Report") + .clicked() + { + intent.push(MenubarIntent::OpenIssues); + ui.close_menu(); + } + + let mut pressed_settings = update_settings_menu(ui, textures_manager); + intent.append(&mut pressed_settings); + + if let Some(url) = singleline_enter(ui, "menubar_url_inputline", "url", 400.0) { + intent.push(MenubarIntent::InsertUrl(url)); + } + }); + }); + + intent +} + +fn update_settings_menu(ui: &mut Ui, textures_manager: &mut TexturesManager) -> Vec { + let mut intent = Vec::new(); + + let settings_button = match textures_manager.deliver("settings", "assets/settings.png", 25) { + Some(image) => Button::image(image), + None => Button::new("Settings"), + }; + menu::menu_custom_button(ui, settings_button, |ui| { + if ui.button("Zoom In").clicked() { + intent.push(MenubarIntent::ZoomIn); + ui.close_menu(); + } + if ui.button("Zoom Out").clicked() { + intent.push(MenubarIntent::ZoomOut); + ui.close_menu(); + } + ui.separator(); + #[cfg(target_os = "windows")] + { + if ui.button("Recover store").clicked() { + intent.push(MenubarIntent::RecoverStore); + ui.close_menu(); + } + ui.separator(); + } + + if let Some(checked) = checkbox_clicked( + ui, + "codchi_tray_checkbox", + CodchiConfig::get().tray.autostart, + "Show tray icon", + ) { + intent.push(MenubarIntent::ShowTray(checked)); + } + + #[cfg(target_os = "windows")] + { + ui.menu_button("VcXsrv", |ui| { + if let Some(checked) = checkbox_clicked( + ui, + "vcxsrv_enable_checkbox", + CodchiConfig::get().vcxsrv.enable, + "Enable", + ) { + intent.push(MenubarIntent::EnableVcxsrv(checked)); + } + + if let Some(checked) = checkbox_clicked( + ui, + "vcxsrv_tray_checkbox", + CodchiConfig::get().vcxsrv.tray, + "Show tray icon", + ) { + intent.push(MenubarIntent::ShowTrayVcxsrv(checked)); + } + }); + if let Some(checked) = checkbox_clicked( + ui, + "wsl_vpnkit_enable_checkbox", + CodchiConfig::get().enable_wsl_vpnkit, + "Enable wsl-vpnkit", + ) { + intent.push(MenubarIntent::EnableWslVpnkit(checked)); + } + } + }) + .response + .on_hover_text("Setting"); + + intent +} + +pub fn checkbox_clicked(ui: &mut Ui, id: &str, initial: bool, text: &str) -> Option { + let state_id = ui.id().with(id); + let mut checked = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(initial)); + + let response = ui.checkbox(&mut checked, text); + ui.data_mut(|d| d.insert_temp(state_id, checked)); + + if response.clicked() { + Some(checked) + } else { + None + } +} + +pub fn singleline_enter(ui: &mut Ui, id: &str, hint_text: &str, width: f32) -> Option { + let state_id = ui.id().with(id); + let mut edit_text = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or("".to_string())); + + let single_line = TextEdit::singleline(&mut edit_text) + .hint_text(hint_text) + .min_size(Vec2 { x: width, y: 0.0 }); + let response = ui.add(single_line); + + if response.lost_focus() && response.ctx.input(|i| i.key_pressed(Key::Enter)) { + ui.data_mut(|d| d.insert_temp(state_id, "".to_string())); + Some(edit_text) + } else { + ui.data_mut(|d| d.insert_temp(state_id, edit_text)); + None + } +} diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index a42f298c..53953b0d 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -1,719 +1,328 @@ -mod machine_creation; -mod machine_inspection; +mod main_panel; +mod menubar; +mod side_panel; +mod util; -use crate::{ - config::{CodchiConfig, MachineConfig}, - platform::{Machine, PlatformStatus}, -}; +use anyhow::Result; use egui::*; -use machine_creation::MachineCreationMainPanel; -use machine_inspection::MachineInspectionMainPanel; +use main_panel::{machine_inspection::MachineInspectionIntent, MainPanel, MainPanelIntent}; +use menubar::MenubarIntent; +use side_panel::{GuiSidePanel, SidePanelIntent}; use std::{ - cell::RefCell, - collections::{HashMap, VecDeque}, - rc::Rc, - sync::{Arc, Mutex}, + path::PathBuf, + sync::mpsc::{channel, Receiver, Sender}, thread, - time::Instant, }; -use strum::{EnumIter, IntoEnumIterator}; +use util::{ + backend_broker::{self, BackendBroker, BackendIntent}, + dialog_manager::{DialogIntent, DialogManager}, + status_entries::StatusEntries, + textures_manager::TexturesManager, +}; -struct Gui { - main_panels: MainPanels, - machine_configs: Vec, - machines: Vec, - textures: HashMap, +use crate::config::CodchiConfig; - status_text: StatusEntries, +struct Gui { + side_panel: GuiSidePanel, + main_panel: MainPanel, - answer_queue: Arc>>, - main_panels_msg_queue: Rc>>, + status_entries: StatusEntries, + dialog_manager: DialogManager, + textures_manager: TexturesManager, - reloading_machine_index: Option, - last_reload: Instant, + backend_broker: BackendBroker, - url_input_line: String, + sender: Sender<(usize, ChannelDTO)>, + receiver: Receiver<(usize, ChannelDTO)>, } -struct MainPanels { - panels: Vec>, - current_panel_index: usize, +enum ChannelDTO { + Generic, } -#[derive(Clone, EnumIter)] -pub enum MainPanelType { - MachineInspection, - MachineCreation, -} +impl eframe::App for Gui { + fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { + self.receive_msgs(); -enum ChannelDataType { - Machine(Machine, usize), - StoreRecovered, + self.update_ui(ctx); + } } -pub(crate) enum MainPanelMsgType { - MachineInspection(machine_inspection::ChannelDataType), -} +impl Gui { + fn new() -> Self { + let (sender, receiver) = channel(); + Self { + side_panel: GuiSidePanel::new(), + main_panel: MainPanel::new(), -impl eframe::App for Gui { - fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { - if self.reloading_machine_index.is_none() { - let now = Instant::now(); - if now.duration_since(self.last_reload).as_secs() >= 10 { - self.reloading_machine_index = Some(0); - self.machine_configs = - MachineConfig::list().expect("Machine-configs could not be listed"); - self.reload_machine(); - } - } - if let Some(main_panel_msg) = self.main_panels_msg_queue.borrow_mut().pop_front() { - match main_panel_msg { - MainPanelMsgType::MachineInspection(msg) => match msg { - machine_inspection::ChannelDataType::Applications(_) => {} - machine_inspection::ChannelDataType::Modules(_) => {} - machine_inspection::ChannelDataType::Secrets(_) => {} - machine_inspection::ChannelDataType::RebuiltMachine => {} - machine_inspection::ChannelDataType::DuplicatedMachine(new_machine) => { - if let Some(machine) = self.machines.last() - && machine.config.name < new_machine.config.name - { - self.machines.push(new_machine); - } else { - for (i, machine) in self.machines.iter().enumerate() { - if new_machine.config.name < machine.config.name { - self.machines.insert(i, new_machine); - break; - } - } - } - } - machine_inspection::ChannelDataType::StoppedMachine(machine_name) => { - let mut i = 0; - for machine in &self.machines { - if machine.config.name == machine_name { - break; - } - i += 1; - } - if let Some(machine) = self.machines.get_mut(i) { - machine.platform_status = PlatformStatus::Stopped; - } - } - machine_inspection::ChannelDataType::DeletedMachine(machine_name) => { - let mut i = 0; - for machine in &self.machines { - if machine.config.name == machine_name { - break; - } - i += 1; - } - if i < self.machines.len() { - self.machines.remove(i); - } - } - machine_inspection::ChannelDataType::ClearStatus => {} - }, - } - } + status_entries: StatusEntries::new(), + dialog_manager: DialogManager::new(), + textures_manager: TexturesManager::new(), - let received_answer = if let Ok(mut answer_queue) = self.answer_queue.try_lock() { - answer_queue.pop_front() - } else { - None - }; - if let Some((status_index, data_type)) = received_answer { - self.status_text.decrease(status_index); - match data_type { - ChannelDataType::Machine(machine, machine_index) => { - if machine_index < self.machines.len() - && self.machines[machine_index].config.name == machine.config.name - { - self.machines[machine_index] = machine; - } else { - // machine is new - self.machines.insert(machine_index, machine); - } + backend_broker: BackendBroker::new(), - let next_machine_index = machine_index + 1; - if next_machine_index < self.machine_configs.len() { - self.reloading_machine_index = Some(next_machine_index); - } else { - // remove remaining machines that were not listed by MachineConfig::list() - while self.machines.get(next_machine_index).is_some() { - self.machines.remove(next_machine_index); - } - self.reloading_machine_index = None; - self.last_reload = Instant::now(); - } - self.reload_machine(); - } - ChannelDataType::StoreRecovered => {} - } + sender, + receiver, } + } - self.menu_bar_panel(ctx); - self.status_bar_panel(ctx); - self.side_panel(ctx); - self.main_panel(ctx); + fn receive_msgs(&mut self) { + while let Ok((status_index, channel_dto)) = self.receiver.try_recv() { + self.status_entries.decrease(status_index); + match channel_dto { + ChannelDTO::Generic => {} + } + } } -} -impl Gui { - fn new(textures: HashMap) -> Self { - let mut main_panels_msg_queue = Rc::new(RefCell::new(VecDeque::new())); - Self { - main_panels: MainPanels::new(&mut main_panels_msg_queue), - machine_configs: MachineConfig::list().expect("Machine-configs could not be listed"), - machines: Machine::list(true).expect("Machines could not be listed"), - textures, + fn update_ui(&mut self, ctx: &Context) { + self.update_menubar(ctx); + self.update_status_bar(ctx); - status_text: StatusEntries::new(), + self.update_side_panel(ctx); - answer_queue: Arc::new(Mutex::new(VecDeque::new())), - main_panels_msg_queue, + self.update_main_panel(ctx); - reloading_machine_index: None, - last_reload: Instant::now(), + self.update_modals(ctx); + self.update_textures(ctx); - url_input_line: String::from(""), - } + self.update_backend_broker(); } - fn menu_bar_panel(&mut self, ctx: &Context) { - let height = 50.0; - TopBottomPanel::top("menubar_panel") + fn update_status_bar(&self, ctx: &Context) { + TopBottomPanel::bottom("status_bar_panel") + .exact_height(25.0) .resizable(false) - .exact_height(height) .show(ctx, |ui| { - ui.horizontal_centered(|ui| { - let codchi_button = Button::image(include_image!("../../assets/logo.png")); - if ui.add(codchi_button).on_hover_text("Home").clicked() { - self.main_panels.renew(); - self.main_panels.change(MainPanelType::MachineInspection); - } - ui.separator(); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.set_height(25.0); - let github_button = - Button::image(include_image!("../../assets/github_logo.png")); - let bug_report_button = - Button::image(include_image!("../../assets/bug_icon.png")); - if ui.add(github_button).on_hover_text("Github").clicked() { - ui.ctx() - .open_url(OpenUrl::new_tab("https://github.com/aformatik/codchi/")); - } - if ui - .add(bug_report_button) - .on_hover_text("Bug-Report") - .clicked() - { - ui.ctx().open_url(OpenUrl::new_tab( - "https://github.com/aformatik/codchi/issues", - )); - } - ui.menu_image_button(include_image!("../../assets/settings.png"), |ui| { - if ui.button("Zoom In").clicked() { - gui_zoom::zoom_in(ctx); - ui.close_menu(); - } - if ui.button("Zoom Out").clicked() { - gui_zoom::zoom_out(ctx); - ui.close_menu(); - } + ui.horizontal(|ui| { + let mut is_first_status = true; + for status in self.status_entries.get_status() { + if is_first_status { + is_first_status = false; + ui.add(egui::Spinner::new()); + } else { ui.separator(); - #[cfg(target_os = "windows")] - { - if ui.button("Recover store").clicked() { - let index = self - .status_text - .insert(1, String::from("Recovering Codchi store...")); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let _ = crate::platform::platform::store_recover(); - answer_queue_clone - .lock() - .unwrap() - .push_back((index, ChannelDataType::StoreRecovered)); - }); - ui.close_menu(); - } - ui.separator(); - } - ui.add(create_advanced_checkbox( - "codchi_tray_checkbox", - CodchiConfig::get().tray.autostart, - |checked| { - let mut doc = - CodchiConfig::open_mut().expect("Failed to open config"); - doc.tray_autostart(checked); - doc.write().expect("Failed to write config"); - }, - "Show tray icon", - )); - #[cfg(target_os = "windows")] - { - ui.menu_button("VcXsrv", |ui| { - ui.add(create_advanced_checkbox( - "vcxsrv_enable_checkbox", - CodchiConfig::get().vcxsrv.enable, - |checked| { - let mut doc = CodchiConfig::open_mut() - .expect("Failed to open config"); - doc.vcxsrv_enable(checked); - doc.write().expect("Failed to write config"); - }, - "Enable", - )); - ui.add(create_advanced_checkbox( - "vcxsrv_tray_checkbox", - CodchiConfig::get().vcxsrv.tray, - |checked| { - let mut doc = CodchiConfig::open_mut() - .expect("Failed to open config"); - doc.vcxsrv_tray(checked); - doc.write().expect("Failed to write config"); - }, - "Show tray icon", - )); - }); - ui.add(create_advanced_checkbox( - "wsl_vpnkit_enable_checkbox", - CodchiConfig::get().enable_wsl_vpnkit, - |checked| { - let mut doc = CodchiConfig::open_mut() - .expect("Failed to open config"); - doc.enable_wsl_vpnkit(checked); - doc.write().expect("Failed to write config"); - }, - "Enable wsl-vpnkit", - )); - } - }) - .response - .on_hover_text("Setting"); - - let url_input_line = TextEdit::singleline(&mut self.url_input_line); - let url_input_line_handle = ui.add_sized([400.0, 0.0], url_input_line); - if url_input_line_handle.lost_focus() - && url_input_line_handle - .ctx - .input(|i| i.key_pressed(Key::Enter)) - { - self.main_panels.change(MainPanelType::MachineCreation); - self.main_panels - .transfer_data(DTO::Text(self.url_input_line.take())); } - }); + ui.label(status); + } }); }); } - fn status_bar_panel(&self, ctx: &Context) { - let height = 25.0; - TopBottomPanel::bottom("statusbar_panel") - .exact_height(height) + fn update_menubar(&mut self, ctx: &Context) { + TopBottomPanel::top("menubar_panel") .resizable(false) .show(ctx, |ui| { - ui.horizontal(|ui| { - let mut first_status = true; - for status_option in self.status_text.get_status() { - if let Some((_pending_msgs, status)) = status_option { - if first_status { - first_status = false; - ui.add(egui::Spinner::new()); - } else { - ui.separator(); - } - ui.label(status); - } - } - for status_option in self.main_panels.get_status_text().get_status() { - if let Some((_pending_msgs, status)) = status_option { - if first_status { - first_status = false; - ui.add(egui::Spinner::new()); - } else { - ui.separator(); - } - ui.label(status); - } - } - }); + let intents = menubar::update(ui, &mut self.textures_manager); + for intent in intents { + self.eval_menubar_intent(ctx, intent); + } }); } - fn side_panel(&mut self, ctx: &Context) { - let width = 200.0; + fn update_side_panel(&mut self, ctx: &Context) { SidePanel::left("side_panel") - .exact_width(width) + .exact_width(200.0) .resizable(false) .show(ctx, |ui| { ScrollArea::vertical().auto_shrink(false).show(ui, |ui| { - let new_machine_button = Button::new(RichText::new("New").heading()); - let new_machin_button_handle = - ui.add_sized([ui.available_width(), 0.0], new_machine_button); - if new_machin_button_handle.clicked() { - self.main_panels.change(MainPanelType::MachineCreation); + let intents = self.side_panel.update( + ui, + &mut self.status_entries, + &self.textures_manager, + ); + for intent in intents { + self.eval_side_panel_intent(intent); } - ui.separator(); - - ui.horizontal_top(|ui| { - ui.separator(); - ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { - for (i, machine) in self.machines.iter().enumerate() { - let icon = - if self.reloading_machine_index.is_some_and(|index| index == i) - { - let texture = &self.textures["gray"]; - Some(Image::from_texture(texture)) - } else { - match machine.platform_status { - PlatformStatus::NotInstalled => { - let texture = &self.textures["yellow"]; - Some(Image::from_texture(texture)) - } - PlatformStatus::Stopped => { - let texture = &self.textures["red"]; - Some(Image::from_texture(texture)) - } - PlatformStatus::Running => { - let texture = &self.textures["green"]; - Some(Image::from_texture(texture)) - } - } - }; - let button_text = RichText::new(&machine.config.name).strong(); - let machine_button = Button::opt_image_and_text( - icon, - Some(WidgetText::RichText(button_text)), - ); - let button_handle = ui.add(machine_button); - if button_handle.clicked() { - self.main_panels.change(MainPanelType::MachineInspection); - self.main_panels - .transfer_data(DTO::Machine(machine.clone())); - } - } - }) - }); }); }); } - fn main_panel(&mut self, ctx: &Context) { + fn update_main_panel(&mut self, ctx: &Context) { CentralPanel::default().show(ctx, |ui| { - ScrollArea::both() - .id_salt("machine_info_scroll") - .auto_shrink(false) - .show(ui, |ui| { - ui.spacing_mut().scroll = style::ScrollStyle::solid(); - - if let Some(next_panel_type) = self.main_panels.next_panel() { - self.main_panels.change(next_panel_type); - } - - self.main_panels.update(ui); - self.main_panels.modal_update(ctx); - }) + let intents = self.main_panel.update( + ui, + &mut self.status_entries, + &mut self.dialog_manager, + &mut self.textures_manager, + ); + for intent in intents { + self.eval_main_panel_intent(intent); + } }); } - fn reload_machine(&mut self) { - if let Some(machine_index) = self.reloading_machine_index { - if let Some(machine_config) = self.machine_configs.get_mut(machine_index) { - let index = self.status_text.insert( - 1, - String::from(format!( - "Updating status for machine '{}'", - machine_config.name - )), - ); - - if let Some(machine) = self.machines.get(machine_index + 1) - && machine.config.name == machine_config.name - { - // machine at machine_index was deleted - self.machines.remove(machine_index); - } - - let machine_config_clone = machine_config.clone(); - let answer_queue_clone = self.answer_queue.clone(); - thread::spawn(move || { - let machine = Machine::read(machine_config_clone, true) - .expect("Machine could not be read"); - - answer_queue_clone - .lock() - .unwrap() - .push_back((index, ChannelDataType::Machine(machine, machine_index))); - }); - } + fn update_modals(&mut self, ctx: &Context) { + let intent_option = self.dialog_manager.update(ctx); + if let Some(intent) = intent_option { + self.eval_modals_intent(intent); } } - fn get_callback( - answer_queue: &mut Rc>>, - ) -> Box { - let answer_queue_clone = answer_queue.clone(); - - Box::new(move |msg: MainPanelMsgType| { - answer_queue_clone.borrow_mut().push_back(msg); - }) + fn update_textures(&mut self, ctx: &Context) { + self.textures_manager.update(ctx); } -} -impl MainPanels { - fn new(answer_queue: &mut Rc>>) -> Self { - let mut panels: Vec> = Vec::new(); - for main_panel in MainPanelType::iter() { - let panel: Box = match main_panel { - MainPanelType::MachineInspection => Box::new(MachineInspectionMainPanel::new( - Gui::get_callback(answer_queue), - )), - MainPanelType::MachineCreation => Box::new(MachineCreationMainPanel::default()), - }; - panels.push(panel); - } - Self { - panels, - current_panel_index: 0, + fn update_backend_broker(&mut self) { + let intents = self + .backend_broker + .update(&mut self.status_entries, &mut self.dialog_manager); + for intent in intents { + self.eval_backend_intent(intent); } } -} -impl MainPanel for MainPanels { - fn update(&mut self, ui: &mut Ui) { - self.get_current_main_panel_mut().update(ui); - } - - fn modal_update(&mut self, ctx: &Context) { - for main_panel in &mut self.panels { - main_panel.modal_update(ctx); + fn eval_menubar_intent(&mut self, ctx: &Context, menubar_intent: MenubarIntent) { + match menubar_intent { + menubar::MenubarIntent::Home => self.main_panel.reset(), + menubar::MenubarIntent::OpenGithub => { + ctx.open_url(OpenUrl::new_tab("https://github.com/aformatik/codchi/")) + } + menubar::MenubarIntent::OpenIssues => ctx.open_url(OpenUrl::new_tab( + "https://github.com/aformatik/codchi/issues", + )), + menubar::MenubarIntent::ZoomIn => gui_zoom::zoom_in(ctx), + menubar::MenubarIntent::ZoomOut => gui_zoom::zoom_out(ctx), + menubar::MenubarIntent::RecoverStore => { + let index = self + .status_entries + .push(String::from("Recovering Codchi store..."), 1); + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let _ = crate::platform::platform::store_recover(); + sender_clone.send((index, ChannelDTO::Generic)).unwrap(); + }); + } + menubar::MenubarIntent::ShowTray(val) => { + let mut doc = CodchiConfig::open_mut().expect("Failed to open config"); + doc.tray_autostart(val); + doc.write().expect("Failed to write config"); + } + menubar::MenubarIntent::EnableVcxsrv(val) => { + let mut doc = CodchiConfig::open_mut().expect("Failed to open config"); + doc.vcxsrv_enable(val); + doc.write().expect("Failed to write config"); + } + menubar::MenubarIntent::ShowTrayVcxsrv(val) => { + let mut doc = CodchiConfig::open_mut().expect("Failed to open config"); + doc.vcxsrv_tray(val); + doc.write().expect("Failed to write config"); + } + menubar::MenubarIntent::EnableWslVpnkit(val) => { + let mut doc = CodchiConfig::open_mut().expect("Failed to open config"); + doc.enable_wsl_vpnkit(val); + doc.write().expect("Failed to write config"); + } + menubar::MenubarIntent::InsertUrl(url) => todo!(), } } - fn next_panel(&mut self) -> Option { - self.get_current_main_panel_mut().next_panel() - } - - fn transfer_data(&mut self, dto: DTO) { - self.get_current_main_panel_mut().transfer_data(dto); - } - - fn get_status_text(&self) -> &StatusEntries { - self.get_current_main_panel().get_status_text() - } - - fn renew(&mut self) { - self.get_current_main_panel_mut().renew(); - } -} - -impl MainPanels { - fn get_current_main_panel(&self) -> &dyn MainPanel { - self.panels[self.current_panel_index].as_ref() - } - - fn get_current_main_panel_mut(&mut self) -> &mut dyn MainPanel { - self.panels[self.current_panel_index].as_mut() - } - - fn change(&mut self, new_main_panel: MainPanelType) { - self.current_panel_index = new_main_panel as usize; - } -} - -pub trait MainPanel { - fn update(&mut self, ui: &mut Ui); - - fn modal_update(&mut self, ctx: &Context); - - fn next_panel(&mut self) -> Option; - - fn transfer_data(&mut self, dto: DTO); - - fn get_status_text(&self) -> &StatusEntries; - - fn renew(&mut self); -} - -pub enum DTO { - Machine(Machine), - Text(String), -} - -pub fn create_modal( - ctx: &Context, - id: &str, - show_modal_bool: &mut bool, - add_contents: impl FnOnce(&mut Ui) -> R, -) { - if *show_modal_bool { - let modal = Modal::new(Id::new(id)).show(ctx, |ui| { - add_contents(ui); - ui.vertical_centered(|ui| { - if ui.button("Ok").clicked() { - *show_modal_bool = false; - } - }); - }); - if modal.should_close() { - *show_modal_bool = false; + fn eval_side_panel_intent(&mut self, side_panel_intent: SidePanelIntent) { + match side_panel_intent { + SidePanelIntent::DisplayMachine(machine) => self + .main_panel + .panels + .get_machine_inspection() + .change(machine, &mut self.status_entries), + SidePanelIntent::BeginMachineCreation => todo!(), } } -} -pub fn create_password_field<'a>( - id: &'a str, - write_closure: impl FnOnce(String) + 'a, - password: &'a str, -) -> impl Widget + 'a { - move |ui: &mut Ui| create_password_field_ui(ui, id, write_closure, password) -} - -pub fn create_password_field_ui( - ui: &mut Ui, - id: &str, - write_closure: impl FnOnce(String), - password: &str, -) -> Response { - let state_id = ui.id().with("show_plaintext"); - let text_id = ui.id().with(id); - let mut show_plaintext = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(false)); - let mut text = ui.data_mut(|d| { - d.get_temp::(text_id) - .unwrap_or(String::from(password)) - }); - - let result = ui.horizontal(|ui| { - let response = ui - .add(SelectableLabel::new(show_plaintext, "👁")) - .on_hover_text("Show/hide password"); - - if response.clicked() { - show_plaintext = !show_plaintext; - } - - ui.add_sized( - [200.0, ui.available_height()], - TextEdit::singleline(&mut text).password(!show_plaintext), - ); - - if password != text { - if ui.button("Write").clicked() { - write_closure(text.clone()); + fn eval_modals_intent(&mut self, dialog_intent: DialogIntent) { + match dialog_intent { + util::dialog_manager::DialogIntent::Rebuild { + machine, + update_modules, + } => self + .backend_broker + .rebuild(machine, update_modules, &mut self.status_entries), + util::dialog_manager::DialogIntent::Duplicate { + machine, + duplicate_name, + } => self + .backend_broker + .duplicate(machine, duplicate_name, &mut self.status_entries), + util::dialog_manager::DialogIntent::Tar { machine, file_path } => self + .backend_broker + .tar(machine, file_path, &mut self.status_entries), + util::dialog_manager::DialogIntent::Stop { machine } => { + self.backend_broker.stop(machine, &mut self.status_entries) + } + util::dialog_manager::DialogIntent::Delete { machine, confirm } => { + if confirm { + self.backend_broker + .delete(machine, &mut self.status_entries); + } + } + util::dialog_manager::DialogIntent::Secret { + machine_name, + name, + value, + } => { + self.backend_broker + .set_secret(machine_name, name, value, &mut self.status_entries) } } - }); - ui.data_mut(|d| d.insert_temp(state_id, show_plaintext)); - ui.data_mut(|d| d.insert_temp(text_id, text)); - - result.response -} - -pub fn create_advanced_checkbox<'a>( - id: &'a str, - initial: bool, - write_closure: impl FnOnce(bool) + 'a, - text: &'a str, -) -> impl Widget + 'a { - move |ui: &mut Ui| create_advanced_checkbox_ui(ui, id, initial, write_closure, text) -} - -pub fn create_advanced_checkbox_ui( - ui: &mut Ui, - id: &str, - initial: bool, - write_closure: impl FnOnce(bool), - text: &str, -) -> Response { - let state_id = ui.id().with(id); - let mut checked = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(initial)); - - let result = ui.checkbox(&mut checked, text); - - if result.clicked() { - write_closure(checked); } - ui.data_mut(|d| d.insert_temp(state_id, checked)); - result -} - -fn load_textures(ctx: &Context) -> HashMap { - let green_square = ColorImage::new([10, 10], Color32::GREEN); - let yellow_square = ColorImage::new([10, 10], Color32::YELLOW); - let red_square = ColorImage::new([10, 10], Color32::RED); - let gray_square = ColorImage::new( - [10, 10], - get_visuals().widgets.noninteractive.fg_stroke.color, - ); - - let green_handle = ctx.load_texture("green_texture", green_square, TextureOptions::default()); - let yellow_handle = - ctx.load_texture("yellow_texture", yellow_square, TextureOptions::default()); - let red_handle = ctx.load_texture("red_texture", red_square, TextureOptions::default()); - let gray_handle = ctx.load_texture("gray_texture", gray_square, TextureOptions::default()); - - let mut textures = HashMap::new(); - textures.insert("green".to_string(), green_handle); - textures.insert("yellow".to_string(), yellow_handle); - textures.insert("red".to_string(), red_handle); - textures.insert("gray".to_string(), gray_handle); - - textures -} - -fn get_visuals() -> Visuals { - let mut visuals = Visuals::dark(); - visuals.widgets.active.fg_stroke.color = Color32::WHITE; - visuals.widgets.noninteractive.fg_stroke.color = Color32::LIGHT_GRAY; - visuals -} - -pub struct StatusEntries { - status: Vec>, - empty_entries: Vec, -} - -impl StatusEntries { - fn new() -> Self { - Self { - status: Vec::new(), - empty_entries: Vec::new(), + fn eval_main_panel_intent(&mut self, main_panel_intent: MainPanelIntent) { + match main_panel_intent { + MainPanelIntent::MachineInspection(intent) => { + self.eval_machine_inspection_intent(intent) + } } } - fn insert(&mut self, pending_msgs: usize, value: String) -> usize { - if let Some(index) = self.empty_entries.pop() { - self.status[index] = Some((pending_msgs, value)); - index - } else { - self.status.push(Some((pending_msgs, value))); - self.status.len() - 1 + fn eval_machine_inspection_intent( + &mut self, + machine_inspection_intent: MachineInspectionIntent, + ) { + match machine_inspection_intent { + MachineInspectionIntent::Rebuild(machine) => { + self.dialog_manager.queue_generic(DialogIntent::Rebuild { + machine, + update_modules: true, + }); + } + MachineInspectionIntent::Duplicate(machine) => { + self.dialog_manager.queue_generic(DialogIntent::Duplicate { + machine, + duplicate_name: "".to_string(), + }); + } + MachineInspectionIntent::Tar(machine) => { + self.dialog_manager.queue_generic(DialogIntent::Tar { + machine, + file_path: PathBuf::default(), + }); + } + MachineInspectionIntent::Stop(machine) => { + self.dialog_manager + .queue_generic(DialogIntent::Stop { machine }); + } + MachineInspectionIntent::Delete(machine) => { + self.dialog_manager.queue_generic(DialogIntent::Delete { + machine, + confirm: false, + }); + } } } - fn decrease(&mut self, index: usize) -> bool { - if index < self.status.len() { - if let Some((pending_msgs, _value)) = self.status[index].as_mut() { - *pending_msgs = *pending_msgs - 1; - if *pending_msgs == 0 { - self.status[index] = None; - self.empty_entries.push(index); - return true; - } + fn eval_backend_intent(&mut self, backend_intent: BackendIntent) { + match backend_intent { + BackendIntent::DuplicatedMachine(machine_name) => { + self.side_panel + .add_machine(machine_name, &mut self.status_entries); + } + BackendIntent::DeletedMachine(machine_name) => { + self.side_panel.remove_machine(machine_name); } } - false - } - - fn get_status(&self) -> &Vec> { - &self.status } } -pub fn run() -> anyhow::Result<()> { +pub fn run() -> Result<()> { let options = eframe::NativeOptions { viewport: ViewportBuilder::default().with_inner_size((1280.0, 720.0)), ..Default::default() @@ -732,17 +341,16 @@ pub fn run() -> anyhow::Result<()> { let ppp = cc.egui_ctx.pixels_per_point(); cc.egui_ctx.set_pixels_per_point(1.25 * ppp); - Ok(Box::::new(Gui::new(load_textures(&cc.egui_ctx)))) + Ok(Box::::new(Gui::new())) }), ) .unwrap(); Ok(()) } -fn content_or_none(content: &String) -> Option { - if content.is_empty() { - None - } else { - Some(content.to_string()) - } +pub fn get_visuals() -> Visuals { + let mut visuals = Visuals::dark(); + visuals.widgets.active.fg_stroke.color = Color32::WHITE; + visuals.widgets.noninteractive.fg_stroke.color = Color32::LIGHT_GRAY; + visuals } diff --git a/codchi/src/gui/side_panel.rs b/codchi/src/gui/side_panel.rs new file mode 100644 index 00000000..fcb3bd94 --- /dev/null +++ b/codchi/src/gui/side_panel.rs @@ -0,0 +1,181 @@ +use std::{ + collections::{HashMap, VecDeque}, + sync::mpsc::{channel, Receiver, Sender}, + thread, +}; + +use egui::*; +use std::time::Instant; + +use crate::platform::{Machine, PlatformStatus}; + +use super::util::{status_entries::StatusEntries, textures_manager::TexturesManager}; + +pub struct GuiSidePanel { + machine_button_names: Vec, + machine_load_names: VecDeque, + machines: HashMap, + + last_reload: Instant, + reloading_machine_name: Option, + + sender: Sender<(usize, Option)>, + receiver: Receiver<(usize, Option)>, + + initialized: bool, +} + +impl GuiSidePanel { + pub fn new() -> Self { + let (sender, receiver) = channel(); + Self { + machine_button_names: Vec::new(), + machine_load_names: VecDeque::new(), + machines: HashMap::new(), + + last_reload: Instant::now(), + reloading_machine_name: None, + + sender, + receiver, + + initialized: false, + } + } + + pub fn update( + &mut self, + ui: &mut Ui, + status_entries: &mut StatusEntries, + textures_manager: &TexturesManager, + ) -> Vec { + if self.reloading_machine_name.is_none() { + let now = Instant::now(); + if !self.initialized || now.duration_since(self.last_reload).as_secs() >= 10 { + self.initialized = true; + self.refresh_machine_list(); + self.reload_machine_from_list(status_entries); + } + } + + self.handle_received_msgs(status_entries); + + self.update_ui(ui, textures_manager) + } + + fn update_ui(&self, ui: &mut Ui, textures_manager: &TexturesManager) -> Vec { + let mut intent = Vec::new(); + + ScrollArea::vertical().auto_shrink(false).show(ui, |ui| { + let new_machine_button = Button::new(RichText::new("New").heading()); + let new_machin_button_handle = + ui.add_sized([ui.available_width(), 0.0], new_machine_button); + if new_machin_button_handle.clicked() { + intent.push(SidePanelIntent::BeginMachineCreation); + } + ui.separator(); + + ui.horizontal_top(|ui| { + ui.separator(); + ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { + for machine_button_name in &self.machine_button_names { + if let Some(machine) = self.machines.get(machine_button_name) { + let icon_option = if self + .reloading_machine_name + .as_ref() + .is_some_and(|machine_name| &machine.config.name == machine_name) + { + None + } else { + match machine.platform_status { + PlatformStatus::NotInstalled => { + textures_manager.get_image("dark_red") + } + PlatformStatus::Stopped => textures_manager.get_image("red"), + PlatformStatus::Running => textures_manager.get_image("green"), + } + }; + let button_text = RichText::new(&machine.config.name).strong(); + let machine_button = Button::opt_image_and_text( + icon_option, + Some(WidgetText::RichText(button_text)), + ); + let button_handle = ui.add(machine_button); + if button_handle.clicked() { + intent.push(SidePanelIntent::DisplayMachine(machine.clone())); + } + } + } + }) + }); + }); + + intent + } + + fn refresh_machine_list(&mut self) { + if let Ok(machine_list) = Machine::list(false) { + self.machine_load_names = machine_list + .iter() + .map(|machine| machine.config.name.clone()) + .collect(); + + self.machine_button_names + .retain(|name| self.machine_load_names.contains(name)); + } + } + + fn reload_machine_from_list(&mut self, status_entries: &mut StatusEntries) { + self.reloading_machine_name = self.machine_load_names.pop_front(); + + if let Some(machine_name) = &self.reloading_machine_name { + let status_index = status_entries + .create_entry(format!("Updating status for machine '{}'", machine_name)); + + let machine_name_clone = machine_name.clone(); + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let machine_result = Machine::by_name(&machine_name_clone, true); + + sender_clone + .send((status_index, machine_result.ok())) + .unwrap(); + }); + } else { + self.last_reload = Instant::now(); + } + } + + fn handle_received_msgs(&mut self, status_entries: &mut StatusEntries) { + while let Ok((status_index, machine_option)) = self.receiver.try_recv() { + status_entries.decrease(status_index); + + if let Some(machine) = machine_option { + let machine_name = machine.config.name.clone(); + self.machines.insert(machine_name.clone(), machine); + + if let Err(pos) = self.machine_button_names.binary_search(&machine_name) { + self.machine_button_names.insert(pos, machine_name); + } + + self.reload_machine_from_list(status_entries); + } + } + } + + pub fn add_machine(&mut self, machine_name: String, status_entries: &mut StatusEntries) { + self.machine_load_names.push_front(machine_name); + self.reload_machine_from_list(status_entries); + } + + pub fn remove_machine(&mut self, machine_name: String) { + self.machine_load_names.retain(|name| name != &machine_name); + self.machine_button_names + .retain(|name| name != &machine_name); + } +} + +pub enum SidePanelIntent { + DisplayMachine(Machine), + BeginMachineCreation, +} diff --git a/codchi/src/gui/util/backend_broker.rs b/codchi/src/gui/util/backend_broker.rs new file mode 100644 index 00000000..8d8929c5 --- /dev/null +++ b/codchi/src/gui/util/backend_broker.rs @@ -0,0 +1,261 @@ +use super::{ + dialog_manager::{DialogIntent, DialogManager}, + status_entries::StatusEntries, +}; +use crate::{ + config::MachineConfig, + platform::{Machine, MachineDriver}, + secrets::{EnvSecret, MachineSecrets}, +}; +use anyhow::Result; +use itertools::Itertools; +use std::{ + collections::HashMap, + path::PathBuf, + sync::mpsc::{channel, Receiver, Sender}, + thread, +}; + +pub struct BackendBroker { + unset_secrets: HashMap>)>, + + sender: Sender<(usize, ChannelDTO)>, + receiver: Receiver<(usize, ChannelDTO)>, +} + +impl BackendBroker { + pub fn new() -> Self { + let (sender, receiver) = channel(); + Self { + unset_secrets: HashMap::new(), + + sender, + receiver, + } + } + + pub fn update( + &mut self, + status_entries: &mut StatusEntries, + dialog_manager: &mut DialogManager, + ) -> Vec { + self.receive_msgs(status_entries, dialog_manager) + } + + fn receive_msgs( + &mut self, + status_entries: &mut StatusEntries, + dialog_manager: &mut DialogManager, + ) -> Vec { + let mut intent = Vec::new(); + + while let Ok((status_index, dto)) = self.receiver.try_recv() { + status_entries.decrease(status_index); + + match dto { + ChannelDTO::BuildFinished(machine) => { + let status_index = status_entries.create_entry(format!( + "Getting unset secrets of '{}'", + &machine.config.name + )); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let unset_secrets_result = get_unset_secrets_for(&machine); + let dto = match unset_secrets_result { + Ok(unsets) => ChannelDTO::FoundUnsetSecrets(machine, unsets), + Err(_) => ChannelDTO::Default, + }; + sender_clone.send((status_index, dto)).unwrap(); + }); + } + ChannelDTO::DuplicateFinished(duplicate_name) => { + intent.push(BackendIntent::DuplicatedMachine(duplicate_name)) + } + ChannelDTO::DeleteFinished(machine_name) => { + intent.push(BackendIntent::DeletedMachine(machine_name)) + } + ChannelDTO::FoundUnsetSecrets(machine, unsets) => { + for secret in &unsets { + dialog_manager.queue_generic(DialogIntent::Secret { + machine_name: machine.config.name.clone(), + name: secret.name.clone(), + value: "".to_string(), + }) + } + let unsets_map = unsets + .into_iter() + .map(|secret| (secret.name, None)) + .collect(); + self.unset_secrets + .insert(machine.config.name.clone(), (machine, unsets_map)); + } + ChannelDTO::WroteSecrets(mut machine) => { + // TODO: make it wörk async + if machine.build_install().is_ok() { + println!("wololo"); + } + } + ChannelDTO::Default => {} + } + } + + intent + } + + pub fn set_secret( + &mut self, + machine_name: String, + secret_name: String, + value: String, + status_entries: &mut StatusEntries, + ) { + if let Some((mut machine, mut unset_secrets)) = self.unset_secrets.remove(&machine_name) { + unset_secrets.insert(secret_name, Some(value)); + + if !unset_secrets.values().contains(&None) { + let status_index = + status_entries.create_entry(format!("Writing Secrets for '{}'", &machine_name)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let dto = match write_secrets(&mut machine, unset_secrets) { + Ok(_) => ChannelDTO::WroteSecrets(machine), + Err(_) => ChannelDTO::Default, + }; + sender_clone.send((status_index, dto)).unwrap(); + }); + } else { + self.unset_secrets + .insert(machine_name, (machine, unset_secrets)); + } + } + } + + pub fn rebuild( + &mut self, + mut machine: Machine, + update_modules: bool, + status_entries: &mut StatusEntries, + ) { + let status_index = + status_entries.create_entry(format!("Building Machine '{}'", machine.config.name)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let dto = if machine.build(!update_modules).is_ok() { + ChannelDTO::BuildFinished(machine) + } else { + ChannelDTO::Default + }; + sender_clone.send((status_index, dto)).unwrap(); + }); + } + + pub fn duplicate( + &mut self, + machine: Machine, + duplicate_name: String, + status_entries: &mut StatusEntries, + ) { + let status_index = + status_entries.create_entry(format!("Duplicating Machine '{}'", machine.config.name)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let dto = if machine.duplicate(&duplicate_name).is_ok() { + ChannelDTO::DuplicateFinished(duplicate_name) + } else { + ChannelDTO::Default + }; + sender_clone.send((status_index, dto)).unwrap(); + }); + } + + pub fn tar( + &mut self, + machine: Machine, + export_path: PathBuf, + status_entries: &mut StatusEntries, + ) { + let status_index = + status_entries.create_entry(format!("Exporting Machine '{}'", machine.config.name)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + machine.tar(&export_path).unwrap(); + sender_clone + .send((status_index, ChannelDTO::Default)) + .unwrap(); + }); + } + + pub fn stop(&mut self, machine: Machine, status_entries: &mut StatusEntries) { + let status_index = + status_entries.create_entry(format!("Stopping Machine '{}'", machine.config.name)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + machine.stop(false).unwrap(); + sender_clone + .send((status_index, ChannelDTO::Default)) + .unwrap(); + }); + } + + pub fn delete(&mut self, machine: Machine, status_entries: &mut StatusEntries) { + let status_index = + status_entries.create_entry(format!("Deleting Machine '{}'", machine.config.name)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let machine_name = machine.config.name.clone(); + let dto = if machine.delete(true).is_ok() { + ChannelDTO::DeleteFinished(machine_name) + } else { + ChannelDTO::Default + }; + sender_clone.send((status_index, dto)).unwrap(); + }); + } +} + +enum ChannelDTO { + BuildFinished(Machine), + DuplicateFinished(String), + DeleteFinished(String), + + FoundUnsetSecrets(Machine, Vec), + WroteSecrets(Machine), + + Default, +} + +pub enum BackendIntent { + DuplicatedMachine(String), + DeletedMachine(String), +} + +fn get_unset_secrets_for(machine: &Machine) -> Result> { + let mut all_secrets = machine.eval_env_secrets()?; + let (_, cfg) = MachineConfig::open_existing(&machine.config.name, false)?; + let set_secrets = cfg.secrets; + + all_secrets.retain(|name, _| !set_secrets.contains_key(name)); + + Ok(all_secrets.into_values().collect()) +} + +fn write_secrets(machine: &mut Machine, secrets: HashMap>) -> Result<()> { + let (lock, mut cfg) = MachineConfig::open_existing(&machine.config.name, true)?; + for (name, value) in secrets { + if let Some(val) = value { + cfg.secrets.insert(name, val); + } + } + + cfg.write(lock)?; + machine.config = cfg; + + Ok(()) +} diff --git a/codchi/src/gui/util/dialog_manager.rs b/codchi/src/gui/util/dialog_manager.rs new file mode 100644 index 00000000..f752e158 --- /dev/null +++ b/codchi/src/gui/util/dialog_manager.rs @@ -0,0 +1,275 @@ +use std::path::PathBuf; + +use egui::*; + +use crate::platform::Machine; + +use super::password_field; + +pub struct DialogManager { + dialogs: Vec, +} + +impl DialogManager { + pub fn new() -> Self { + Self { + dialogs: Vec::new(), + } + } + + pub fn update(&mut self, ctx: &Context) -> Option { + if !self.dialogs.is_empty() { + let dialog_result = self.dialogs[0].update_ui(ctx); + + if let Some(result) = dialog_result { + let dialog = self.dialogs.remove(0); + + match result { + DialogResult::Confirm => return Some(dialog.intent), + DialogResult::Cancel => {} + } + } + } + + None + } + + fn queue( + &mut self, + intent: DialogIntent, + heading: String, + primary_button_text: String, + primary_button_color: Option<(u8, u8, u8)>, + secondary_button_text: Option, + ) { + self.queue_dialog(GuiDialog { + intent, + heading, + primary_button_text, + primary_button_color, + secondary_button_text, + }); + } + + fn queue_dialog(&mut self, gui_dialog: GuiDialog) { + self.dialogs.push(gui_dialog); + } + + pub fn queue_generic(&mut self, dialog_intent: DialogIntent) { + dialog_intent.to_gui_modal(self); + } +} + +struct GuiDialog { + intent: DialogIntent, + heading: String, + primary_button_text: String, + primary_button_color: Option<(u8, u8, u8)>, + secondary_button_text: Option, +} + +impl GuiDialog { + fn update_ui(&mut self, ctx: &Context) -> Option { + let modal = Modal::new(Id::new("global_modal")).show(ctx, |ui| { + ui.heading(&self.heading); + ui.separator(); + + let confirm_enabled = Self::display_intent_element(ui, &mut self.intent); + + Sides::new().show( + ui, + |ui_left| { + self.secondary_button_text + .as_ref() + .and_then(|text| Some(ui_left.button(text))) + }, + |ui_right| { + let confirm_button = Button::new(&self.primary_button_text); + let confirm_button = match self.primary_button_color { + Some((r, g, b)) => confirm_button.fill(Color32::from_rgb(r, g, b)), + None => confirm_button, + }; + ui_right.add_enabled(confirm_enabled, confirm_button) + }, + ) + }); + + let (cancel_button_option, confirm_button) = modal.inner; + + if let Some(cancel_button) = cancel_button_option { + if cancel_button.clicked() { + return Some(DialogResult::Cancel); + } + } + + if confirm_button.clicked() { + return Some(DialogResult::Confirm); + } + + None + } + + fn display_intent_element(ui: &mut Ui, intent: &mut DialogIntent) -> bool { + match intent { + DialogIntent::Rebuild { + machine: _, + update_modules, + } => { + ui.checkbox(update_modules, "update modules"); + ui.separator(); + + true + } + DialogIntent::Duplicate { + machine: _, + duplicate_name, + } => { + ui.add(TextEdit::singleline(duplicate_name).hint_text("Duplicate Name")); + ui.separator(); + + !duplicate_name.is_empty() + } + DialogIntent::Tar { + machine: _, + file_path, + } => { + let eligible_file = file_path.extension().is_some_and(|ext| ext == "gz"); + + if ui.button("Select").clicked() { + if let Some(selected_folder) = rfd::FileDialog::new() + .add_filter("TAR (*.tar.gz)", &["tar.gz"]) + .save_file() + { + *file_path = selected_folder; + } + } + if eligible_file { + ui.label(format!("{}", file_path.to_string_lossy())); + } + ui.separator(); + + eligible_file + } + DialogIntent::Stop { machine: _ } => true, + DialogIntent::Delete { + machine: _, + confirm, + } => { + ui.checkbox(confirm, "Are you sure?"); + ui.separator(); + + *confirm + } + DialogIntent::Secret { + machine_name: _, + name: _, + value, + } => { + ui.add(password_field(value)); + ui.separator(); + + true + } + } + } +} + +enum DialogResult { + Confirm, + Cancel, +} + +pub enum DialogIntent { + Rebuild { + machine: Machine, + update_modules: bool, + }, + Duplicate { + machine: Machine, + duplicate_name: String, + }, + Tar { + machine: Machine, + file_path: PathBuf, + }, + Stop { + machine: Machine, + }, + Delete { + machine: Machine, + confirm: bool, + }, + + Secret { + machine_name: String, + name: String, + value: String, + }, +} + +impl DialogIntent { + pub fn to_gui_modal(self, dialog_manager: &mut DialogManager) { + let (heading, p_text, p_color, s_text) = match &self { + DialogIntent::Rebuild { + machine, + update_modules: _, + } => { + let heading = format!("Rebuilding Machine '{}'", &machine.config.name); + let p_text = "Rebuild".to_string(); + let p_color = Some((0, 128, 0)); + let s_text = Some("Cancel".to_string()); + (heading, p_text, p_color, s_text) + } + DialogIntent::Duplicate { + machine, + duplicate_name: _, + } => { + let heading = format!("Duplicating Machine '{}'", &machine.config.name); + let p_text = "Duplicate".to_string(); + let p_color = Some((0, 128, 0)); + let s_text = Some("Cancel".to_string()); + (heading, p_text, p_color, s_text) + } + DialogIntent::Tar { + machine, + file_path: _, + } => { + let heading = format!("Exporting Machine '{}'", &machine.config.name); + let p_text = "Export".to_string(); + let p_color = Some((0, 128, 0)); + let s_text = Some("Cancel".to_string()); + (heading, p_text, p_color, s_text) + } + DialogIntent::Stop { machine } => { + let heading = format!("Stopping Machine '{}'", &machine.config.name); + let p_text = "Stop".to_string(); + let p_color = Some((128, 0, 0)); + let s_text = Some("Cancel".to_string()); + (heading, p_text, p_color, s_text) + } + DialogIntent::Delete { + machine, + confirm: _, + } => { + let heading = format!("Delete Machine '{}'", &machine.config.name); + let p_text = "Delete".to_string(); + let p_color = Some((128, 0, 0)); + let s_text = Some("Cancel".to_string()); + (heading, p_text, p_color, s_text) + } + DialogIntent::Secret { + machine_name, + name, + value: _, + } => { + let heading = format!("Setting '{}' for '{}'", name, &machine_name); + let p_text = "Set".to_string(); + let p_color = Some((0, 128, 0)); + let s_text = None; + (heading, p_text, p_color, s_text) + } + }; + + dialog_manager.queue(self, heading, p_text, p_color, s_text); + } +} diff --git a/codchi/src/gui/util/mod.rs b/codchi/src/gui/util/mod.rs new file mode 100644 index 00000000..8d03dcc3 --- /dev/null +++ b/codchi/src/gui/util/mod.rs @@ -0,0 +1,83 @@ +pub mod backend_broker; +pub mod dialog_manager; +pub mod status_entries; +pub mod textures_manager; + +use egui::*; + +pub fn password_field<'a>(password: &'a mut String) -> impl Widget + 'a { + move |ui: &mut Ui| password_field_ui(ui, password) +} + +fn password_field_ui(ui: &mut Ui, password: &mut String) -> Response { + let state_id = ui.id().with("show_plaintext"); + + let mut show_plaintext = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(false)); + + let result = ui.horizontal(|ui| { + let response = ui + .add(SelectableLabel::new(show_plaintext, "👁")) + .on_hover_text("Show/hide password"); + + if response.clicked() { + show_plaintext = !show_plaintext; + } + + ui.add_sized( + [200.0, ui.available_height()], + TextEdit::singleline(password).password(!show_plaintext), + ); + }); + ui.data_mut(|d| d.insert_temp(state_id, show_plaintext)); + + result.response +} + +pub fn advanced_password_field<'a>( + id: &'a str, + write_closure: impl FnOnce(String) + 'a, + password: &'a str, +) -> impl Widget + 'a { + move |ui: &mut Ui| advanced_password_field_ui(ui, id, write_closure, password) +} + +pub fn advanced_password_field_ui( + ui: &mut Ui, + id: &str, + write_closure: impl FnOnce(String), + password: &str, +) -> Response { + let state_id = ui.id().with("show_plaintext"); + let text_id = ui.id().with(id); + + let mut show_plaintext = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(false)); + let mut text = ui.data_mut(|d| { + d.get_temp::(text_id) + .unwrap_or(String::from(password)) + }); + + let result = ui.horizontal(|ui| { + let response = ui + .add(SelectableLabel::new(show_plaintext, "👁")) + .on_hover_text("Show/hide password"); + + if response.clicked() { + show_plaintext = !show_plaintext; + } + + ui.add_sized( + [200.0, ui.available_height()], + TextEdit::singleline(&mut text).password(!show_plaintext), + ); + + if password != text { + if ui.button("Write").clicked() { + write_closure(text.clone()); + } + } + }); + ui.data_mut(|d| d.insert_temp(state_id, show_plaintext)); + ui.data_mut(|d| d.insert_temp(text_id, text)); + + result.response +} diff --git a/codchi/src/gui/util/status_entries.rs b/codchi/src/gui/util/status_entries.rs new file mode 100644 index 00000000..220fa5e2 --- /dev/null +++ b/codchi/src/gui/util/status_entries.rs @@ -0,0 +1,68 @@ +pub(crate) struct StatusEntries { + entries: Vec>, + free_entries: Vec, +} + +impl StatusEntries { + pub fn new() -> Self { + Self { + entries: Vec::new(), + free_entries: Vec::new(), + } + } + + pub fn create_entry(&mut self, text: String) -> usize { + self.push(text, 1) + } + + pub fn push(&mut self, text: String, durability: usize) -> usize { + self.push_entry(StatusEntry { text, durability }) + } + + fn push_entry(&mut self, status_entry: StatusEntry) -> usize { + if let Some(free_index) = self.free_entries.pop() { + self.entries[free_index] = Some(status_entry); + free_index + } else { + self.entries.push(Some(status_entry)); + self.entries.len() - 1 + } + } + + pub fn decrease(&mut self, index: usize) { + if index < self.entries.len() { + if let Some(status_entry) = self.entries[index].as_mut() { + if status_entry.decrement() == 0 { + self.entries[index] = None; + self.free_entries.push(index); + } + } + } + } + + pub fn get_status(&self) -> Vec<&str> { + let mut result = Vec::new(); + for status_entry in &self.entries { + if let Some(entry) = status_entry { + result.push(entry.get_text()); + } + } + result + } +} + +struct StatusEntry { + text: String, + durability: usize, +} + +impl StatusEntry { + fn decrement(&mut self) -> usize { + self.durability = self.durability.saturating_sub(1); + self.durability + } + + fn get_text(&self) -> &str { + &self.text + } +} diff --git a/codchi/src/gui/util/textures_manager.rs b/codchi/src/gui/util/textures_manager.rs new file mode 100644 index 00000000..225fe0f1 --- /dev/null +++ b/codchi/src/gui/util/textures_manager.rs @@ -0,0 +1,171 @@ +use egui::*; +use image::{ImageBuffer, ImageReader, Rgba}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::mpsc::{channel, Receiver, Sender}, + thread, +}; + +pub struct TexturesManager { + textures: HashMap, + + sender: Sender, + receiver: Receiver, + awaits_answer: bool, +} + +enum ChannelDTO { + ColorImage(String, ColorImage), + Image(String, ImageBuffer, Vec>, [usize; 2]), +} + +impl TexturesManager { + pub fn new() -> Self { + let (sender, receiver) = channel(); + load_square_textures_async(&sender); + Self { + textures: HashMap::new(), + + sender, + receiver, + awaits_answer: false, + } + } + + pub fn update(&mut self, ctx: &Context) { + self.receive_msgs(ctx); + } + + pub fn get(&self, name: &str) -> Option<&TextureHandle> { + self.textures.get(name) + } + + pub fn get_image(&self, name: &str) -> Option { + self.get(name).map(|tex| Image::from_texture(tex)) + } + + pub fn deliver(&mut self, name: &str, path: &str, dim: u32) -> Option<&TextureHandle> { + let texture_handle = self.textures.get(name); + + if !self.awaits_answer && texture_handle.is_none() { + self.awaits_answer = true; + load_color_image_async(&self.sender, name.to_string(), path.to_string(), dim); + } + + texture_handle + } + + pub fn deliver_image(&mut self, name: &str, path: &PathBuf) -> Option<&TextureHandle> { + let texture_handle = self.textures.get(name); + + if !self.awaits_answer && texture_handle.is_none() { + self.awaits_answer = true; + load_image_async(&self.sender, name.to_string(), path.clone()); + } + + texture_handle + } + + fn receive_msgs(&mut self, ctx: &Context) { + while let Ok(channel_dto) = self.receiver.try_recv() { + self.awaits_answer = false; + match channel_dto { + ChannelDTO::ColorImage(name, color_image) => { + let texture_handle = ctx.load_texture(&name, color_image, Default::default()); + self.textures.insert(name, texture_handle); + } + ChannelDTO::Image(name, image_buffer, size) => { + let texture_handle = ctx.load_texture( + "icon_texture", + ColorImage::from_rgba_unmultiplied(size, &image_buffer), + Default::default(), + ); + self.textures.insert(name, texture_handle); + } + } + } + } +} + +fn load_image_async(sender: &Sender, name: String, path: PathBuf) { + let sender_clone = sender.clone(); + thread::spawn(move || { + if let Some((image_buffer, size)) = load_image_from_path(&path) { + let dto = ChannelDTO::Image(name, image_buffer, size); + sender_clone.send(dto).unwrap(); + } + }); +} + +fn load_image_from_path(path: &PathBuf) -> Option<(ImageBuffer, Vec>, [usize; 2])> { + let image = image::open(path) + .expect("Failed to open image icon") + .to_rgba8(); + let size = [image.width() as usize, image.height() as usize]; + + Some((image, size)) +} + +fn load_color_image_async(sender: &Sender, name: String, path: String, dim: u32) { + let sender_clone = sender.clone(); + thread::spawn(move || { + if let Some(color_image) = load_color_image_from_path(&path, dim) { + let dto = ChannelDTO::ColorImage(name, color_image); + sender_clone.send(dto).unwrap(); + } + }); +} + +fn load_color_image_from_path(path: &str, dim: u32) -> Option { + let img = ImageReader::open(Path::new(path)) + .ok()? + .decode() + .ok()? + .resize(dim, dim, image::imageops::FilterType::Lanczos3) + .to_rgba8(); + + let (width, height) = img.dimensions(); + let pixels = img.into_raw(); + + Some(ColorImage::from_rgba_unmultiplied( + [width as usize, height as usize], + &pixels, + )) +} + +fn load_square_textures_async(sender: &Sender) { + let sender_clone = sender.clone(); + thread::spawn(move || { + let dto = ChannelDTO::ColorImage( + "black".to_string(), + ColorImage::new([10, 10], Color32::BLACK), + ); + sender_clone.send(dto).unwrap(); + }); + + let sender_clone = sender.clone(); + thread::spawn(move || { + let dto = ChannelDTO::ColorImage( + "dark_red".to_string(), + ColorImage::new([10, 10], Color32::DARK_RED), + ); + sender_clone.send(dto).unwrap(); + }); + + let sender_clone = sender.clone(); + thread::spawn(move || { + let dto = + ChannelDTO::ColorImage("red".to_string(), ColorImage::new([10, 10], Color32::RED)); + sender_clone.send(dto).unwrap(); + }); + + let sender_clone = sender.clone(); + thread::spawn(move || { + let dto = ChannelDTO::ColorImage( + "green".to_string(), + ColorImage::new([10, 10], Color32::GREEN), + ); + sender_clone.send(dto).unwrap(); + }); +} diff --git a/codchi/src/main.rs b/codchi/src/main.rs index 1013d6d6..90edeb91 100644 --- a/codchi/src/main.rs +++ b/codchi/src/main.rs @@ -173,7 +173,7 @@ Thank you kindly!"# module_paths, )?; if !options.no_build { - machine.build(true)?; + machine.full_build(true)?; machine.run_init_script(*dont_run_init)?; log::info!("Machine '{machine_name}' is ready! Use `codchi exec {machine_name}` to start it.") } else { @@ -220,7 +220,7 @@ Thank you kindly!"# } Cmd::Rebuild { no_update, name } => { - Machine::by_name(name, true)?.build(*no_update)?; + Machine::by_name(name, true)?.full_build(*no_update)?; log::info!("Machine {name} rebuilt successfully!"); } @@ -317,7 +317,7 @@ secret. Is this OK? [y/n]", let mut machine = module::add(machine_name, GitUrl::from(url), options, module_paths)?; if !options.no_build { - machine.build(true)?; + machine.full_build(true)?; } else { alert_dirty(machine); } @@ -339,7 +339,7 @@ secret. Is this OK? [y/n]", url.as_ref().map(GitUrl::from), )?; if !options.no_build { - machine.build(false)?; + machine.full_build(false)?; } else { alert_dirty(machine); } diff --git a/codchi/src/module.rs b/codchi/src/module.rs index 4079e4b7..51d6a6d3 100644 --- a/codchi/src/module.rs +++ b/codchi/src/module.rs @@ -600,7 +600,7 @@ pub fn clone( &input_options, module_paths, )?; - machine.build(true)?; + machine.full_build(true)?; progress_scope! { set_progress_status("Cloning git repository..."); diff --git a/codchi/src/platform/machine.rs b/codchi/src/platform/machine.rs index 1faa6b13..14c1874c 100644 --- a/codchi/src/platform/machine.rs +++ b/codchi/src/platform/machine.rs @@ -301,6 +301,14 @@ fi Ok(()) } + pub fn full_build(&mut self, no_update: bool) -> Result<()> { + self.build(no_update)?; + self.build_eval_secrets()?; + self.build_install()?; + + Ok(()) + } + pub fn build(&mut self, no_update: bool) -> Result<()> { self.write_flake()?; @@ -365,6 +373,10 @@ git add flake.* ); } + Ok(()) + } + + fn build_eval_secrets(&mut self) -> Result<()> { set_progress_status("Evaluating secrets..."); let secrets = self.eval_env_secrets()?; @@ -387,6 +399,10 @@ git add flake.* cfg.write(lock)?; self.config = cfg; + Ok(()) + } + + pub fn build_install(&mut self) -> Result<()> { set_progress_status(format!("Building {}...", self.config.name)); let status = Self::read_platform_status(&self.config.name)?; if status == PlatformStatus::NotInstalled { @@ -599,7 +615,7 @@ git add flake.* }; self.duplicate_container(&new_machine)?; new_machine.write_flake()?; - new_machine.build(true)?; + new_machine.full_build(true)?; log::info!( "Successfully duplicated machine '{}' to '{target_name}'", From 5dcc5ccfc7a7a08bc43e6a53727f376552b4dc1f Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:11:34 +0000 Subject: [PATCH 44/49] Reimplemented Machine Creation --- .../gui/main_panel/empty_machine_creation.rs | 69 +++ .../machine_creation/generics_panel.rs | 84 ++++ .../gui/main_panel/machine_creation/mod.rs | 360 +++++++++++++++ .../machine_creation/modules_panel.rs | 147 ++++++ .../machine_creation/repository_panel.rs | 153 +++++++ .../src/gui/main_panel/machine_inspection.rs | 22 +- codchi/src/gui/main_panel/mod.rs | 60 ++- codchi/src/gui/mod.rs | 180 ++++++-- codchi/src/gui/side_panel.rs | 19 +- codchi/src/gui/util/backend_broker.rs | 418 +++++++++++++++--- codchi/src/gui/util/mod.rs | 8 +- 11 files changed, 1378 insertions(+), 142 deletions(-) create mode 100644 codchi/src/gui/main_panel/empty_machine_creation.rs create mode 100644 codchi/src/gui/main_panel/machine_creation/generics_panel.rs create mode 100644 codchi/src/gui/main_panel/machine_creation/mod.rs create mode 100644 codchi/src/gui/main_panel/machine_creation/modules_panel.rs create mode 100644 codchi/src/gui/main_panel/machine_creation/repository_panel.rs diff --git a/codchi/src/gui/main_panel/empty_machine_creation.rs b/codchi/src/gui/main_panel/empty_machine_creation.rs new file mode 100644 index 00000000..eedcc814 --- /dev/null +++ b/codchi/src/gui/main_panel/empty_machine_creation.rs @@ -0,0 +1,69 @@ +use egui::*; + +use super::MainPanelIntent; + +#[derive(Default)] +pub struct EmptyMachineCreation { + machine_name: String, + dont_build: bool, +} + +impl EmptyMachineCreation { + pub fn update(&mut self, ui: &mut Ui) -> Vec { + self.update_ui(ui); + + self.update_buttons(ui) + } + + fn update_ui(&mut self, ui: &mut Ui) { + ui.heading("Empty Machine Creation"); + ui.label(""); + ui.separator(); + + Grid::new("empty_generics_grid") + .num_columns(2) + .show(ui, |ui| { + ui.label("Machine Name"); + ui.add_sized( + ui.available_size(), + TextEdit::singleline(&mut self.machine_name).hint_text("My awesome Name"), + ); + ui.end_row(); + + ui.label(""); + ui.separator(); + ui.end_row(); + + ui.checkbox(&mut self.dont_build, "Don't build"); + }); + } + + fn update_buttons(&mut self, ui: &mut Ui) -> Vec { + let mut intent = Vec::new(); + + ui.with_layout(Layout::bottom_up(Align::Min), |ui| { + ui.with_layout(Layout::right_to_left(Align::Max), |ui| { + let create_button = Button::new("Create").fill(Color32::DARK_GREEN); + if ui.add(create_button).clicked() { + let temp = std::mem::take(self); + intent.push(MainPanelIntent::EmptyMachineCreation( + EmptyMachineCreationIntent::CreateMachine( + temp.machine_name, + temp.dont_build, + ), + )); + } + }); + }); + + intent + } + + pub fn reset(&mut self) { + *self = Self::default(); + } +} + +pub enum EmptyMachineCreationIntent { + CreateMachine(String, bool), +} diff --git a/codchi/src/gui/main_panel/machine_creation/generics_panel.rs b/codchi/src/gui/main_panel/machine_creation/generics_panel.rs new file mode 100644 index 00000000..2e355e57 --- /dev/null +++ b/codchi/src/gui/main_panel/machine_creation/generics_panel.rs @@ -0,0 +1,84 @@ +use super::{AuthUrl, InternalIntent}; +use crate::gui::util::password_field; +use egui::*; + +#[derive(Default)] +pub struct GenericsPanel { + pub(super) machine_name: String, + auth_url: AuthUrl, + pub(super) dont_build: bool, +} + +impl GenericsPanel { + pub fn update(&mut self, ui: &mut Ui) -> Vec { + self.update_ui(ui); + + self.update_buttons(ui) + } + + fn update_ui(&mut self, ui: &mut Ui) { + ui.heading("Machine Creation"); + ui.separator(); + + Grid::new("generics_grid").num_columns(2).show(ui, |ui| { + ui.label("Machine Name"); + ui.add_sized( + ui.available_size(), + TextEdit::singleline(&mut self.machine_name).hint_text("My awesome Name"), + ); + ui.end_row(); + + ui.label(""); + ui.separator(); + ui.end_row(); + + ui.label("URL"); + ui.add_sized( + ui.available_size(), + TextEdit::singleline(&mut self.auth_url.url) + .hint_text("https://gitlab.example.com/my_repo"), + ); + ui.end_row(); + + ui.label("Username"); + ui.add_sized( + ui.available_size(), + TextEdit::singleline(&mut self.auth_url.username), + ); + ui.end_row(); + + ui.label("Password"); + ui.add_sized( + ui.available_size(), + password_field(&mut self.auth_url.password), + ); + ui.end_row(); + + ui.label(""); + ui.separator(); + ui.end_row(); + + ui.checkbox(&mut self.dont_build, "Don't build"); + }); + } + + fn update_buttons(&mut self, ui: &mut Ui) -> Vec { + let mut intent = Vec::new(); + + ui.with_layout(Layout::bottom_up(Align::Max), |ui| { + if ui.button("Next").clicked() { + if !self.auth_url.url.is_empty() { + intent.push(InternalIntent::ToRepositoryPanel(Some( + self.auth_url.clone(), + ))); + } + } + }); + + intent + } + + pub fn set_url(&mut self, url: String) { + self.auth_url.url = url; + } +} diff --git a/codchi/src/gui/main_panel/machine_creation/mod.rs b/codchi/src/gui/main_panel/machine_creation/mod.rs new file mode 100644 index 00000000..18814120 --- /dev/null +++ b/codchi/src/gui/main_panel/machine_creation/mod.rs @@ -0,0 +1,360 @@ +mod generics_panel; +mod modules_panel; +mod repository_panel; + +use super::MainPanelIntent; +use crate::cli::{InputOptions, ModuleAttrPath, NixpkgsLocation}; +use anyhow::Result; +use egui::*; +use generics_panel::GenericsPanel; +use git_url_parse::GitUrl; +use modules_panel::ModulesPanel; +use repository_panel::RepositoryPanel; +use strum::{EnumIter, IntoEnumIterator}; + +#[derive(Default)] +pub struct MachineCreation { + creation_step: CreationStep, + + generics_panel: GenericsPanel, + repository_panel: RepositoryPanel, + modules_panel: ModulesPanel, +} + +impl MachineCreation { + pub fn update(&mut self, ui: &mut Ui) -> Vec { + self.change_step(self.update_creation_step_buttons(ui)); + + let intents = self.update_panel(ui); + + self.eval_intents(intents) + } + + fn update_creation_step_buttons(&self, ui: &mut Ui) -> Option { + let (button_1_handle, button_2_handle, button_3_handle) = self.add_step_buttons(ui); + + if button_1_handle.clicked() { + return Some(CreationStep::Generics); + } + if button_2_handle.clicked() { + return Some(CreationStep::Repository); + } + if button_3_handle.clicked() { + return Some(CreationStep::Modules); + } + + None + } + + fn add_step_buttons(&self, ui: &mut Ui) -> (Response, Response, Response) { + let steps: Vec = CreationStep::iter().collect(); + + let button_1 = self.get_step_button(&steps[0]); + let button_2 = self.get_step_button(&steps[1]); + let button_3 = self.get_step_button(&steps[2]); + + let enabled_1 = self.is_step_reachable(&steps[0]); + let enabled_2 = self.is_step_reachable(&steps[1]); + let enabled_3 = self.is_step_reachable(&steps[2]); + + let width = (ui.available_width() - (2 as f32 * ui.spacing().item_spacing.x)) / 3 as f32; + + let result = ui + .horizontal_top(|ui| { + ( + ui.add_enabled_ui(enabled_1, |ui| ui.add_sized([width, 0.0], button_1)) + .inner, + ui.add_enabled_ui(enabled_2, |ui| ui.add_sized([width, 0.0], button_2)) + .inner, + ui.add_enabled_ui(enabled_3, |ui| { + ui.add_sized([ui.available_width(), 0.0], button_3) + }) + .inner, + ) + }) + .inner; + + ui.separator(); + + result + } + + fn update_panel(&mut self, ui: &mut Ui) -> Vec { + match self.creation_step { + CreationStep::Generics => self.generics_panel.update(ui), + CreationStep::Repository => self.repository_panel.update(ui), + CreationStep::Modules => self.modules_panel.update(ui), + } + } + + fn get_step_button(&self, creation_step: &CreationStep) -> Button<'_> { + let text = RichText::new(creation_step.to_text()).heading(); + if creation_step == &self.creation_step { + Button::new(text).fill(Color32::DARK_GRAY) + } else { + Button::new(text) + } + } + + fn is_step_reachable(&self, creation_step: &CreationStep) -> bool { + match &creation_step { + CreationStep::Generics => true, + CreationStep::Repository => self.repository_panel.is_current_auth_url_loaded(), + CreationStep::Modules => self.modules_panel.is_current_repo_loaded(), + } + } + + fn change_step(&mut self, creation_step_option: Option) { + if let Some(creation_step) = creation_step_option { + self.creation_step = creation_step; + } + } + + pub fn reset(&mut self) { + *self = Self::default(); + } + + pub fn pass_url(&mut self, url: String) { + self.reset(); + self.generics_panel.set_url(url); + } + + pub fn is_auth_url_loaded(&self, auth_url: &AuthUrl) -> bool { + self.repository_panel.is_auth_url_loaded(auth_url) + } + + pub fn pass_branches(&mut self, auth_url: AuthUrl, branches: Vec) { + self.repository_panel + .insert_branches(auth_url.clone(), branches); + if self.repository_panel.is_auth_url_loaded(&auth_url) { + self.to_repository_panel(Some(auth_url)); + } + } + + pub fn pass_tags(&mut self, auth_url: AuthUrl, tags: Vec) { + self.repository_panel.insert_tags(auth_url.clone(), tags); + if self.repository_panel.is_auth_url_loaded(&auth_url) { + self.to_repository_panel(Some(auth_url)); + } + } + + pub fn pass_modules( + &mut self, + repo: (AuthUrl, RepositorySpecification), + modules: Vec, + ) { + self.modules_panel.insert_modules(repo.clone(), modules); + self.to_modules_panel(Some(repo)); + } + + pub fn to_repository_panel(&mut self, auth_url_option: Option) { + if let Some(auth_url) = auth_url_option { + self.repository_panel.set_current_auth_url(auth_url); + } + self.creation_step = CreationStep::Repository; + } + + fn to_modules_panel(&mut self, repo_option: Option<(AuthUrl, RepositorySpecification)>) { + if let Some(repo) = repo_option { + self.modules_panel.set_current_repo(repo); + } + self.creation_step = CreationStep::Modules; + } + + fn eval_intents(&mut self, intents: Vec) -> Vec { + intents + .into_iter() + .filter_map(|intent| match intent { + InternalIntent::ToGenericsPanel => { + self.creation_step = CreationStep::Generics; + None + } + InternalIntent::ToRepositoryPanel(auth_url_option) => { + if let Some(auth_url) = &auth_url_option { + if !self.repository_panel.is_auth_url_loaded(auth_url) { + return Some(MainPanelIntent::MachineCreation( + MachineCreationIntent::LoadRepository(auth_url.clone()), + )); + } + } + self.to_repository_panel(auth_url_option); + None + } + InternalIntent::ToModulesPanel(repo_option) => { + if let Some(repo) = &repo_option { + if !self.modules_panel.is_repo_loaded(repo) { + return Some(MainPanelIntent::MachineCreation( + MachineCreationIntent::LoadModules(repo.clone()), + )); + } + } + self.to_modules_panel(repo_option); + None + } + InternalIntent::CreateMachine => { + if self.modules_panel.get_current_repo().is_some() { + let temp = std::mem::take(self); + + let modules = temp.modules_panel.get_current_selected_modules(); + let (auth_url, repo_spec) = temp.modules_panel.current_repo.unwrap(); + + let use_nixpkgs = if temp.repository_panel.dont_use_nixpkgs { + Some(NixpkgsLocation::Local) + } else { + Some(NixpkgsLocation::Remote) + }; + let auth = auth_url.get_auth(); + let (branch, tag, commit) = repo_spec.to_triple(); + + let machine_name = temp.generics_panel.machine_name; + let git_url = auth_url.get_git_url().unwrap(); + let options = InputOptions { + dont_prompt: true, + no_build: temp.generics_panel.dont_build, + use_nixpkgs, + auth, + branch, + tag, + commit, + }; + let dont_run_init = temp.repository_panel.dont_run_init_script; + + Some(MainPanelIntent::MachineCreation( + MachineCreationIntent::CreateMachine( + machine_name, + git_url, + options, + modules, + dont_run_init, + ), + )) + } else { + None + } + } + }) + .collect() + } +} + +#[derive(PartialEq, Clone, EnumIter, Default)] +enum CreationStep { + #[default] + Generics, + Repository, + Modules, +} + +impl CreationStep { + fn to_text(&self) -> String { + match self { + CreationStep::Generics => "General".to_string(), + CreationStep::Repository => "Repository".to_string(), + CreationStep::Modules => "Modules".to_string(), + } + } +} + +pub enum MachineCreationIntent { + CreateMachine(String, GitUrl, InputOptions, Vec, bool), + LoadRepository(AuthUrl), + LoadModules((AuthUrl, RepositorySpecification)), +} + +enum InternalIntent { + ToGenericsPanel, + ToRepositoryPanel(Option), + ToModulesPanel(Option<(AuthUrl, RepositorySpecification)>), + CreateMachine, +} + +#[derive(Hash, PartialEq, Eq, Default, Clone)] +pub struct AuthUrl { + pub url: String, + pub username: String, + pub password: String, +} + +impl AuthUrl { + pub fn get_auth(&self) -> Option { + if !self.username.is_empty() { + Some(format!("{}:{}", &self.username, &self.password)) + } else { + None + } + } + + pub fn get_git_url(&self) -> Result { + if let Some(auth) = self.get_auth() { + let url_parts: Vec<&str> = self.url.split("://").collect(); + if url_parts.len() == 2 { + return Ok(GitUrl::parse(&format!( + "{}://{}@{}", + url_parts[0], auth, url_parts[1] + ))?); + } + } + Ok(GitUrl::parse(&self.url)?) + } + + pub fn to_string(&self) -> String { + if let Some(auth_val) = self.get_auth() { + let url_parts: Vec<&str> = self.url.split("://").collect(); + if url_parts.len() == 2 { + return format!("{}://{}@{}", url_parts[0], auth_val, url_parts[1]); + } + } + self.url.to_string() + } +} + +#[derive(EnumIter, Eq, PartialEq, Clone, Hash)] +pub enum RepositorySpecification { + Branch(String), + Tag(String), + Commit(String), +} + +impl Default for RepositorySpecification { + fn default() -> Self { + RepositorySpecification::Branch("".to_string()) + } +} + +impl RepositorySpecification { + fn to_text(&self) -> String { + match self { + RepositorySpecification::Branch(_) => "Branch".to_string(), + RepositorySpecification::Tag(_) => "Tag".to_string(), + RepositorySpecification::Commit(_) => "Commit".to_string(), + } + } + + fn to_full_text(&self) -> String { + match self { + RepositorySpecification::Branch(branch) => format!("Branch '{}'", branch), + RepositorySpecification::Tag(tag) => format!("Tag '{}'", tag), + RepositorySpecification::Commit(commit) => format!("Commit '{}'", commit), + } + } + + fn is_empty(&self) -> bool { + match self { + RepositorySpecification::Branch(branch) => branch.is_empty(), + RepositorySpecification::Tag(tag) => tag.is_empty(), + RepositorySpecification::Commit(commit) => commit.is_empty(), + } + } + + pub fn to_triple(&self) -> (Option, Option, Option) { + if self.is_empty() { + (None, None, None) + } else { + match self { + RepositorySpecification::Branch(branch) => (Some(branch.clone()), None, None), + RepositorySpecification::Tag(tag) => (None, Some(tag.clone()), None), + RepositorySpecification::Commit(commit) => (None, None, Some(commit.clone())), + } + } + } +} diff --git a/codchi/src/gui/main_panel/machine_creation/modules_panel.rs b/codchi/src/gui/main_panel/machine_creation/modules_panel.rs new file mode 100644 index 00000000..77f33306 --- /dev/null +++ b/codchi/src/gui/main_panel/machine_creation/modules_panel.rs @@ -0,0 +1,147 @@ +use super::{AuthUrl, InternalIntent, RepositorySpecification}; +use crate::cli::ModuleAttrPath; +use egui::*; +use std::collections::HashMap; + +#[derive(Default)] +pub struct ModulesPanel { + un_selected_modules: HashMap<(AuthUrl, RepositorySpecification), ModulePaths>, + + pub(super) current_repo: Option<(AuthUrl, RepositorySpecification)>, + is_current_initialized: bool, +} + +impl ModulesPanel { + pub fn update(&mut self, ui: &mut Ui) -> Vec { + self.update_panel(ui); + + self.update_buttons(ui) + } + + fn update_panel(&mut self, ui: &mut Ui) { + ui.heading("Repository Specification"); + if let Some((auth_url, repo_spec)) = &self.current_repo { + ui.label(&auth_url.url); + if !repo_spec.is_empty() { + ui.label(repo_spec.to_full_text()); + } + } + ui.separator(); + + if let Some(current_repo) = &self.current_repo { + if let Some(module_paths) = self.un_selected_modules.get_mut(current_repo) { + let num_cols = 2; + ui.columns(num_cols, |columns| { + fn add_column( + ui: &mut Ui, + text: &str, + module_path_vec: &mut Vec, + pendant_vec: &mut Vec, + ) { + ui.vertical(|ui| { + ui.strong(text); + let mut clicked_index = None; + for i in 0..module_path_vec.len() { + let module_path = &module_path_vec[i]; + let button_text = + format!("{}.{}", module_path.base, module_path.module); + if ui.button(button_text).clicked() { + clicked_index = Some(i); + } + } + if let Some(index) = clicked_index { + let unselected_module_path = module_path_vec.remove(index); + pendant_vec.push(unselected_module_path); + } + }); + } + add_column( + &mut columns[0], + "Unselected Modules", + module_paths.unselected_module_paths.as_mut(), + module_paths.selected_module_paths.as_mut(), + ); + add_column( + &mut columns[1], + "Selected Modules", + module_paths.selected_module_paths.as_mut(), + module_paths.unselected_module_paths.as_mut(), + ); + }); + } + } + } + + fn update_buttons(&mut self, ui: &mut Ui) -> Vec { + let mut intent = Vec::new(); + + ui.with_layout(Layout::bottom_up(Align::Min), |ui| { + ui.horizontal(|ui| { + if ui.button("Previous").clicked() { + if let Some((auth_url, _)) = &self.current_repo { + intent.push(InternalIntent::ToRepositoryPanel(Some(auth_url.clone()))); + } + } + ui.with_layout(Layout::right_to_left(Align::Max), |ui| { + let create_button = Button::new("Create").fill(Color32::DARK_GREEN); + if ui.add(create_button).clicked() { + intent.push(InternalIntent::CreateMachine); + } + }); + }); + }); + + intent + } + + pub fn set_current_repo(&mut self, repo: (AuthUrl, RepositorySpecification)) { + self.is_current_initialized = self.is_repo_loaded(&repo); + self.current_repo = Some(repo); + } + + pub fn is_current_repo_loaded(&self) -> bool { + self.is_current_initialized + } + + pub fn is_repo_loaded(&self, repo: &(AuthUrl, RepositorySpecification)) -> bool { + self.un_selected_modules.contains_key(repo) + } + + pub fn insert_modules( + &mut self, + repo: (AuthUrl, RepositorySpecification), + modules: Vec, + ) { + let value = ModulePaths::new(modules); + self.un_selected_modules.insert(repo, value); + self.is_current_initialized = true; + } + + pub fn get_current_selected_modules(&self) -> Vec { + if let Some(current_repo) = &self.current_repo + && let Some(module_paths) = self.un_selected_modules.get(current_repo) + { + module_paths.selected_module_paths.clone() + } else { + Vec::new() + } + } + + pub fn get_current_repo(&self) -> &Option<(AuthUrl, RepositorySpecification)> { + &self.current_repo + } +} + +struct ModulePaths { + unselected_module_paths: Vec, + selected_module_paths: Vec, +} + +impl ModulePaths { + fn new(unselected_module_paths: Vec) -> Self { + Self { + unselected_module_paths, + selected_module_paths: Vec::new(), + } + } +} diff --git a/codchi/src/gui/main_panel/machine_creation/repository_panel.rs b/codchi/src/gui/main_panel/machine_creation/repository_panel.rs new file mode 100644 index 00000000..eb1c0663 --- /dev/null +++ b/codchi/src/gui/main_panel/machine_creation/repository_panel.rs @@ -0,0 +1,153 @@ +use egui::*; +use std::collections::HashMap; +use strum::IntoEnumIterator; + +use super::{AuthUrl, InternalIntent, RepositorySpecification}; + +#[derive(Default)] +pub struct RepositoryPanel { + repo_infos: HashMap>, Option>)>, + + repo_spec: RepositorySpecification, + pub(super) dont_run_init_script: bool, + pub(super) dont_use_nixpkgs: bool, + + current_auth_url: Option, + is_current_initialized: bool, +} + +impl RepositoryPanel { + pub fn update(&mut self, ui: &mut Ui) -> Vec { + self.update_ui(ui); + + self.update_buttons(ui) + } + + pub fn update_ui(&mut self, ui: &mut Ui) { + let Self { + repo_infos, + repo_spec: repo_specification, + dont_run_init_script, + dont_use_nixpkgs, + current_auth_url, + is_current_initialized: _, + } = self; + + if let Some(auth_url) = current_auth_url { + ui.heading("Repository Specification"); + ui.label(&auth_url.url); + ui.separator(); + + ui.with_layout(Layout::left_to_right(Align::Min), |ui| { + ComboBox::from_id_salt("repo_specification_combobox") + .selected_text(repo_specification.to_text()) + .width(150.0) + .show_ui(ui, |ui| { + for repo_spec in RepositorySpecification::iter() { + let text = repo_spec.to_text(); + ui.selectable_value(repo_specification, repo_spec, text); + } + }); + + match repo_specification { + RepositorySpecification::Branch(branch_name) => { + if let Some((branches_option, _)) = repo_infos.get(auth_url) { + if let Some(branches) = branches_option { + ComboBox::from_id_salt("branch_specification_combobox") + .selected_text(branch_name.to_string()) + .width(ui.available_width()) + .show_ui(ui, |ui| { + for branch in branches { + ui.selectable_value( + branch_name, + branch.to_string(), + branch, + ); + } + }); + } + } + } + RepositorySpecification::Tag(tag_name) => { + if let Some((_, tags_option)) = repo_infos.get(auth_url) { + if let Some(tags) = tags_option { + ComboBox::from_id_salt("branch_specification_combobox") + .selected_text(tag_name.to_string()) + .width(ui.available_width()) + .show_ui(ui, |ui| { + for tag in tags { + ui.selectable_value(tag_name, tag.to_string(), tag); + } + }); + } + } + } + RepositorySpecification::Commit(commit) => { + let commit_line = TextEdit::singleline(commit).hint_text("Commit-Hash"); + ui.add_sized([ui.available_width(), 0.0], commit_line); + } + } + }); + + ui.separator(); + ui.checkbox(dont_run_init_script, "Skip Configuration's Init-Script"); + ui.checkbox(dont_use_nixpkgs, "Ignore provided package versions"); + } + } + + fn update_buttons(&mut self, ui: &mut Ui) -> Vec { + let mut intent = Vec::new(); + + ui.with_layout(Layout::bottom_up(Align::Min), |ui| { + ui.horizontal(|ui| { + if ui.button("Previous").clicked() { + intent.push(InternalIntent::ToGenericsPanel); + } + ui.with_layout(Layout::right_to_left(Align::Max), |ui| { + if ui.button("Next").clicked() { + if let Some(current_auth_url) = &self.current_auth_url { + intent.push(InternalIntent::ToModulesPanel(Some(( + current_auth_url.clone(), + self.repo_spec.clone(), + )))); + } + } + }); + }); + }); + + intent + } + + pub fn set_current_auth_url(&mut self, auth_url: AuthUrl) { + self.is_current_initialized = self.is_auth_url_loaded(&auth_url); + self.current_auth_url = Some(auth_url); + } + + pub fn insert_branches(&mut self, auth_url: AuthUrl, branches: Vec) { + let (branches_option, tags_option) = + self.repo_infos.entry(auth_url).or_insert((None, None)); + *branches_option = Some(branches); + + self.is_current_initialized = branches_option.is_some() && tags_option.is_some(); + } + + pub fn insert_tags(&mut self, auth_url: AuthUrl, tags: Vec) { + let (branches_option, tags_option) = + self.repo_infos.entry(auth_url).or_insert((None, None)); + *tags_option = Some(tags); + + self.is_current_initialized = branches_option.is_some() && tags_option.is_some(); + } + + pub fn is_current_auth_url_loaded(&self) -> bool { + self.is_current_initialized + } + + pub fn is_auth_url_loaded(&self, auth_url: &AuthUrl) -> bool { + match self.repo_infos.get(auth_url) { + Some((branches, tags)) => branches.is_some() && tags.is_some(), + None => false, + } + } +} diff --git a/codchi/src/gui/main_panel/machine_inspection.rs b/codchi/src/gui/main_panel/machine_inspection.rs index c3ce166f..a0d5061d 100644 --- a/codchi/src/gui/main_panel/machine_inspection.rs +++ b/codchi/src/gui/main_panel/machine_inspection.rs @@ -1,8 +1,5 @@ use super::{ - super::util::{ - dialog_manager::DialogManager, status_entries::StatusEntries, - textures_manager::TexturesManager, - }, + super::util::{status_entries::StatusEntries, textures_manager::TexturesManager}, MainPanelIntent, }; use crate::{ @@ -36,20 +33,11 @@ enum ChannelDTO { Secrets(String, Result>), } -pub enum MachineInspectionIntent { - Rebuild(Machine), - Duplicate(Machine), - Tar(Machine), - Stop(Machine), - Delete(Machine), -} - impl MachineInspection { pub fn update( &mut self, ui: &mut Ui, status_entries: &mut StatusEntries, - _dialog_manager: &mut DialogManager, textures_manager: &mut TexturesManager, ) -> Vec { self.receive_msgs(status_entries); @@ -417,3 +405,11 @@ impl MachineData { self.applications.is_some() && self.modules.is_some() && self.secrets.is_some() } } + +pub enum MachineInspectionIntent { + Rebuild(Machine), + Duplicate(Machine), + Tar(Machine), + Stop(Machine), + Delete(Machine), +} diff --git a/codchi/src/gui/main_panel/mod.rs b/codchi/src/gui/main_panel/mod.rs index a1b12c8d..1bb20408 100644 --- a/codchi/src/gui/main_panel/mod.rs +++ b/codchi/src/gui/main_panel/mod.rs @@ -1,9 +1,13 @@ +pub mod empty_machine_creation; +pub mod machine_creation; pub mod machine_inspection; use super::util::{ dialog_manager::DialogManager, status_entries::StatusEntries, textures_manager::TexturesManager, }; use egui::*; +use empty_machine_creation::{EmptyMachineCreation, EmptyMachineCreationIntent}; +use machine_creation::{MachineCreation, MachineCreationIntent}; use machine_inspection::{MachineInspection, MachineInspectionIntent}; pub struct MainPanel { @@ -14,7 +18,7 @@ pub struct MainPanel { impl MainPanel { pub fn new() -> Self { Self { - panels: MainPanels::new(), + panels: MainPanels::default(), current_main_panel_type: MainPanelType::MachineInspection, } } @@ -26,46 +30,70 @@ impl MainPanel { dialog_manager: &mut DialogManager, textures_manager: &mut TexturesManager, ) -> Vec { - match self.current_main_panel_type { - MainPanelType::MachineInspection => self.panels.get_machine_inspection().update( - ui, - status_entries, - dialog_manager, - textures_manager, - ), - } + ScrollArea::both() + .id_salt("main_panel_scroll") + .auto_shrink(false) + .show(ui, |ui| match self.current_main_panel_type { + MainPanelType::MachineInspection => self.panels.get_machine_inspection().update( + ui, + status_entries, + textures_manager, + ), + MainPanelType::MachineCreation => self.panels.get_machine_creation().update(ui), + MainPanelType::EmptyMachineCreation => { + self.panels.get_empty_machine_creation().update(ui) + } + }) + .inner } pub fn reset(&mut self) { match self.current_main_panel_type { MainPanelType::MachineInspection => self.panels.get_machine_inspection().reset(), + MainPanelType::MachineCreation => self.panels.get_machine_creation().reset(), + MainPanelType::EmptyMachineCreation => self.panels.get_empty_machine_creation().reset(), } - self.current_main_panel_type = MainPanelType::MachineInspection; + self.change(MainPanelType::MachineInspection); + } + + pub fn change(&mut self, main_panel_type: MainPanelType) { + self.current_main_panel_type = main_panel_type; } } +#[derive(Default)] pub struct MainPanels { machine_inspection: Option, + machine_creation: Option, + empty_machine_creation: Option, } impl MainPanels { - pub fn new() -> Self { - Self { - machine_inspection: None, - } - } - pub fn get_machine_inspection(&mut self) -> &mut MachineInspection { self.machine_inspection .get_or_insert(MachineInspection::new()) } + + pub fn get_machine_creation(&mut self) -> &mut MachineCreation { + self.machine_creation + .get_or_insert(MachineCreation::default()) + } + + pub fn get_empty_machine_creation(&mut self) -> &mut EmptyMachineCreation { + self.empty_machine_creation + .get_or_insert(EmptyMachineCreation::default()) + } } pub enum MainPanelType { MachineInspection, + MachineCreation, + EmptyMachineCreation, } pub enum MainPanelIntent { MachineInspection(MachineInspectionIntent), + MachineCreation(MachineCreationIntent), + EmptyMachineCreation(EmptyMachineCreationIntent), } diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 53953b0d..5189970b 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -5,7 +5,10 @@ mod util; use anyhow::Result; use egui::*; -use main_panel::{machine_inspection::MachineInspectionIntent, MainPanel, MainPanelIntent}; +use main_panel::{ + empty_machine_creation::EmptyMachineCreationIntent, machine_creation::MachineCreationIntent, + machine_inspection::MachineInspectionIntent, MainPanel, MainPanelIntent, MainPanelType, +}; use menubar::MenubarIntent; use side_panel::{GuiSidePanel, SidePanelIntent}; use std::{ @@ -14,7 +17,7 @@ use std::{ thread, }; use util::{ - backend_broker::{self, BackendBroker, BackendIntent}, + backend_broker::{BackendBroker, BackendIntent}, dialog_manager::{DialogIntent, DialogManager}, status_entries::StatusEntries, textures_manager::TexturesManager, @@ -140,15 +143,21 @@ impl Gui { fn update_main_panel(&mut self, ctx: &Context) { CentralPanel::default().show(ctx, |ui| { - let intents = self.main_panel.update( - ui, - &mut self.status_entries, - &mut self.dialog_manager, - &mut self.textures_manager, - ); - for intent in intents { - self.eval_main_panel_intent(intent); - } + ui.with_layout(Layout::right_to_left(Align::Min), |ui| { + ui.separator(); + ui.vertical(|ui| { + let intents = self.main_panel.update( + ui, + &mut self.status_entries, + &mut self.dialog_manager, + &mut self.textures_manager, + ); + + for intent in intents { + self.eval_main_panel_intent(intent); + } + }) + }) }); } @@ -213,18 +222,28 @@ impl Gui { doc.enable_wsl_vpnkit(val); doc.write().expect("Failed to write config"); } - menubar::MenubarIntent::InsertUrl(url) => todo!(), + menubar::MenubarIntent::InsertUrl(url) => { + self.main_panel.panels.get_machine_creation().pass_url(url); + self.main_panel.change(MainPanelType::MachineCreation); + } } } fn eval_side_panel_intent(&mut self, side_panel_intent: SidePanelIntent) { match side_panel_intent { - SidePanelIntent::DisplayMachine(machine) => self - .main_panel - .panels - .get_machine_inspection() - .change(machine, &mut self.status_entries), - SidePanelIntent::BeginMachineCreation => todo!(), + SidePanelIntent::DisplayMachine(machine) => { + self.main_panel + .panels + .get_machine_inspection() + .change(machine, &mut self.status_entries); + self.main_panel.change(MainPanelType::MachineInspection) + } + SidePanelIntent::BeginMachineCreation => { + self.main_panel.change(MainPanelType::MachineCreation) + } + SidePanelIntent::BeginEmptyMachineCreation => { + self.main_panel.change(MainPanelType::EmptyMachineCreation) + } } } @@ -233,35 +252,42 @@ impl Gui { util::dialog_manager::DialogIntent::Rebuild { machine, update_modules, - } => self - .backend_broker - .rebuild(machine, update_modules, &mut self.status_entries), + } => self.backend_broker.rebuild_machine( + machine, + update_modules, + false, + &mut self.status_entries, + ), util::dialog_manager::DialogIntent::Duplicate { machine, duplicate_name, - } => self - .backend_broker - .duplicate(machine, duplicate_name, &mut self.status_entries), + } => self.backend_broker.duplicate_machine( + machine, + duplicate_name, + &mut self.status_entries, + ), util::dialog_manager::DialogIntent::Tar { machine, file_path } => self .backend_broker - .tar(machine, file_path, &mut self.status_entries), - util::dialog_manager::DialogIntent::Stop { machine } => { - self.backend_broker.stop(machine, &mut self.status_entries) - } + .tar_machine(machine, file_path, &mut self.status_entries), + util::dialog_manager::DialogIntent::Stop { machine } => self + .backend_broker + .stop_machine(machine, &mut self.status_entries), util::dialog_manager::DialogIntent::Delete { machine, confirm } => { if confirm { self.backend_broker - .delete(machine, &mut self.status_entries); + .delete_machine(machine, &mut self.status_entries); } } util::dialog_manager::DialogIntent::Secret { machine_name, name, value, - } => { - self.backend_broker - .set_secret(machine_name, name, value, &mut self.status_entries) - } + } => self.backend_broker.insert_secret( + machine_name, + name, + value, + &mut self.status_entries, + ), } } @@ -270,6 +296,10 @@ impl Gui { MainPanelIntent::MachineInspection(intent) => { self.eval_machine_inspection_intent(intent) } + MainPanelIntent::MachineCreation(intent) => self.eval_machine_creation_intent(intent), + MainPanelIntent::EmptyMachineCreation(intent) => { + self.eval_empty_machine_creation_intent(intent) + } } } @@ -309,6 +339,62 @@ impl Gui { } } + fn eval_machine_creation_intent(&mut self, machine_creation_intent: MachineCreationIntent) { + match machine_creation_intent { + MachineCreationIntent::CreateMachine( + machine_name, + git_url, + options, + modules, + dont_run_init, + ) => { + self.backend_broker.create_machine( + machine_name, + git_url, + options, + modules, + dont_run_init, + &mut self.status_entries, + ); + } + MachineCreationIntent::LoadRepository(auth_url) => { + if self + .main_panel + .panels + .get_machine_creation() + .is_auth_url_loaded(&auth_url) + { + self.main_panel + .panels + .get_machine_creation() + .to_repository_panel(Some(auth_url)); + } else { + self.backend_broker + .access_repository(auth_url, &mut self.status_entries) + } + } + MachineCreationIntent::LoadModules(repo) => { + self.backend_broker + .load_modules(repo, &mut self.status_entries); + } + } + } + + fn eval_empty_machine_creation_intent( + &mut self, + empty_machine_creation_intent: EmptyMachineCreationIntent, + ) { + match empty_machine_creation_intent { + EmptyMachineCreationIntent::CreateMachine(machine_name, dont_build) => { + self.backend_broker.create_empty_machine( + machine_name, + dont_build, + &mut self.status_entries, + ); + } + } + } + fn eval_backend_intent(&mut self, backend_intent: BackendIntent) { match backend_intent { BackendIntent::DuplicatedMachine(machine_name) => { @@ -318,6 +404,34 @@ impl Gui { BackendIntent::DeletedMachine(machine_name) => { self.side_panel.remove_machine(machine_name); } + BackendIntent::AccessedRepository(auth_url) => { + self.backend_broker + .load_repository(auth_url, &mut self.status_entries); + } + BackendIntent::LoadedBranches(auth_url, branches_result) => { + if let Ok(branches) = branches_result { + self.main_panel + .panels + .get_machine_creation() + .pass_branches(auth_url, branches); + } + } + BackendIntent::LoadedTags(auth_url, tags_result) => { + if let Ok(tags) = tags_result { + self.main_panel + .panels + .get_machine_creation() + .pass_tags(auth_url, tags); + } + } + BackendIntent::LoadedModules(repo, modules_result) => { + if let Ok(modules) = modules_result { + self.main_panel + .panels + .get_machine_creation() + .pass_modules(repo, modules); + } + } } } } diff --git a/codchi/src/gui/side_panel.rs b/codchi/src/gui/side_panel.rs index fcb3bd94..0efe0bd5 100644 --- a/codchi/src/gui/side_panel.rs +++ b/codchi/src/gui/side_panel.rs @@ -67,12 +67,18 @@ impl GuiSidePanel { let mut intent = Vec::new(); ScrollArea::vertical().auto_shrink(false).show(ui, |ui| { - let new_machine_button = Button::new(RichText::new("New").heading()); - let new_machin_button_handle = - ui.add_sized([ui.available_width(), 0.0], new_machine_button); - if new_machin_button_handle.clicked() { - intent.push(SidePanelIntent::BeginMachineCreation); - } + ui.vertical_centered_justified(|ui| { + ui.menu_button(RichText::new("New").heading(), |ui| { + if ui.button("Standard").clicked() { + intent.push(SidePanelIntent::BeginMachineCreation); + ui.close_menu(); + } + if ui.button("Empty").clicked() { + intent.push(SidePanelIntent::BeginEmptyMachineCreation); + ui.close_menu(); + } + }); + }); ui.separator(); ui.horizontal_top(|ui| { @@ -178,4 +184,5 @@ impl GuiSidePanel { pub enum SidePanelIntent { DisplayMachine(Machine), BeginMachineCreation, + BeginEmptyMachineCreation, } diff --git a/codchi/src/gui/util/backend_broker.rs b/codchi/src/gui/util/backend_broker.rs index 8d8929c5..d25b67fe 100644 --- a/codchi/src/gui/util/backend_broker.rs +++ b/codchi/src/gui/util/backend_broker.rs @@ -3,31 +3,70 @@ use super::{ status_entries::StatusEntries, }; use crate::{ + cli::{InputOptions, ModuleAttrPath}, config::MachineConfig, - platform::{Machine, MachineDriver}, + gui::main_panel::machine_creation::{AuthUrl, RepositorySpecification}, + module, + platform::{Machine, MachineDriver, NixDriver, Store}, secrets::{EnvSecret, MachineSecrets}, + util::LinuxPath, }; use anyhow::Result; -use itertools::Itertools; +use git_url_parse::GitUrl; use std::{ collections::HashMap, path::PathBuf, + process::Command, sync::mpsc::{channel, Receiver, Sender}, thread, }; pub struct BackendBroker { - unset_secrets: HashMap>)>, + unresolved_machines: HashMap>)>, sender: Sender<(usize, ChannelDTO)>, receiver: Receiver<(usize, ChannelDTO)>, } +enum ChannelDTO { + CreatedMachine(Machine, bool, bool), + + BuiltMachine(Machine, bool), + DuplicatedMachine(String), + DeletedMachine(String), + + RepositoryAccessed(AuthUrl), + BranchesLoaded(AuthUrl, Result>), + TagsLoaded(AuthUrl, Result>), + ModulesLoaded( + (AuthUrl, RepositorySpecification), + Result>, + ), + + FoundUnsetSecrets(Machine, Vec, bool), + WroteSecrets(Machine, bool), + InstalledMachine(Machine, bool), + + Default, +} + +pub enum BackendIntent { + DuplicatedMachine(String), + DeletedMachine(String), + AccessedRepository(AuthUrl), + LoadedBranches(AuthUrl, Result>), + LoadedTags(AuthUrl, Result>), + LoadedModules( + (AuthUrl, RepositorySpecification), + Result>, + ), +} + impl BackendBroker { pub fn new() -> Self { let (sender, receiver) = channel(); Self { - unset_secrets: HashMap::new(), + unresolved_machines: HashMap::new(), sender, receiver, @@ -53,29 +92,33 @@ impl BackendBroker { status_entries.decrease(status_index); match dto { - ChannelDTO::BuildFinished(machine) => { - let status_index = status_entries.create_entry(format!( - "Getting unset secrets of '{}'", - &machine.config.name - )); - - let sender_clone = self.sender.clone(); - thread::spawn(move || { - let unset_secrets_result = get_unset_secrets_for(&machine); - let dto = match unset_secrets_result { - Ok(unsets) => ChannelDTO::FoundUnsetSecrets(machine, unsets), - Err(_) => ChannelDTO::Default, - }; - sender_clone.send((status_index, dto)).unwrap(); - }); + ChannelDTO::CreatedMachine(machine, dont_build, dont_run_init) => { + if !dont_build { + self.rebuild_machine(machine, true, dont_run_init, status_entries); + } + } + ChannelDTO::BuiltMachine(machine, dont_run_init) => { + self.get_unset_secrets(machine, dont_run_init, status_entries); + } + ChannelDTO::DuplicatedMachine(duplicate_name) => { + intent.push(BackendIntent::DuplicatedMachine(duplicate_name)); + } + ChannelDTO::DeletedMachine(machine_name) => { + intent.push(BackendIntent::DeletedMachine(machine_name)); + } + ChannelDTO::RepositoryAccessed(auth_url) => { + intent.push(BackendIntent::AccessedRepository(auth_url)); } - ChannelDTO::DuplicateFinished(duplicate_name) => { - intent.push(BackendIntent::DuplicatedMachine(duplicate_name)) + ChannelDTO::BranchesLoaded(auth_url, branches) => { + intent.push(BackendIntent::LoadedBranches(auth_url, branches)); } - ChannelDTO::DeleteFinished(machine_name) => { - intent.push(BackendIntent::DeletedMachine(machine_name)) + ChannelDTO::TagsLoaded(auth_url, tags) => { + intent.push(BackendIntent::LoadedTags(auth_url, tags)); } - ChannelDTO::FoundUnsetSecrets(machine, unsets) => { + ChannelDTO::ModulesLoaded(repo, modules) => { + intent.push(BackendIntent::LoadedModules(repo, modules)); + } + ChannelDTO::FoundUnsetSecrets(machine, unsets, dont_run_init) => { for secret in &unsets { dialog_manager.queue_generic(DialogIntent::Secret { machine_name: machine.config.name.clone(), @@ -87,13 +130,17 @@ impl BackendBroker { .into_iter() .map(|secret| (secret.name, None)) .collect(); - self.unset_secrets - .insert(machine.config.name.clone(), (machine, unsets_map)); + self.unresolved_machines.insert( + machine.config.name.clone(), + (machine, dont_run_init, unsets_map), + ); + } + ChannelDTO::WroteSecrets(machine, dont_run_init) => { + self.install_machine(machine, dont_run_init, status_entries); } - ChannelDTO::WroteSecrets(mut machine) => { - // TODO: make it wörk async - if machine.build_install().is_ok() { - println!("wololo"); + ChannelDTO::InstalledMachine(machine, dont_run_init) => { + if !dont_run_init { + self.run_init_script(machine, dont_run_init, status_entries); } } ChannelDTO::Default => {} @@ -103,39 +150,53 @@ impl BackendBroker { intent } - pub fn set_secret( + pub fn create_empty_machine( &mut self, machine_name: String, - secret_name: String, - value: String, + dont_build: bool, status_entries: &mut StatusEntries, ) { - if let Some((mut machine, mut unset_secrets)) = self.unset_secrets.remove(&machine_name) { - unset_secrets.insert(secret_name, Some(value)); + let status_index = + status_entries.create_entry(format!("Creating empty machine '{}'", &machine_name)); - if !unset_secrets.values().contains(&None) { - let status_index = - status_entries.create_entry(format!("Writing Secrets for '{}'", &machine_name)); - - let sender_clone = self.sender.clone(); - thread::spawn(move || { - let dto = match write_secrets(&mut machine, unset_secrets) { - Ok(_) => ChannelDTO::WroteSecrets(machine), - Err(_) => ChannelDTO::Default, - }; - sender_clone.send((status_index, dto)).unwrap(); - }); - } else { - self.unset_secrets - .insert(machine_name, (machine, unset_secrets)); - } - } + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let dto = match module::init(&machine_name, None, &InputOptions::default(), &Vec::new()) + { + Ok(machine) => ChannelDTO::CreatedMachine(machine, dont_build, false), + Err(_) => ChannelDTO::Default, + }; + sender_clone.send((status_index, dto)).unwrap(); + }); } - pub fn rebuild( + pub fn create_machine( + &mut self, + machine_name: String, + git_url: GitUrl, + options: InputOptions, + modules: Vec, + dont_run_init: bool, + status_entries: &mut StatusEntries, + ) { + let status_index = + status_entries.create_entry(format!("Creating machine '{}'", &machine_name)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let dto = match module::init(&machine_name, Some(git_url), &options, &modules) { + Ok(machine) => ChannelDTO::CreatedMachine(machine, options.no_build, dont_run_init), + Err(_) => ChannelDTO::Default, + }; + sender_clone.send((status_index, dto)).unwrap(); + }); + } + + pub fn rebuild_machine( &mut self, mut machine: Machine, update_modules: bool, + dont_run_init: bool, status_entries: &mut StatusEntries, ) { let status_index = @@ -144,7 +205,7 @@ impl BackendBroker { let sender_clone = self.sender.clone(); thread::spawn(move || { let dto = if machine.build(!update_modules).is_ok() { - ChannelDTO::BuildFinished(machine) + ChannelDTO::BuiltMachine(machine, dont_run_init) } else { ChannelDTO::Default }; @@ -152,7 +213,7 @@ impl BackendBroker { }); } - pub fn duplicate( + pub fn duplicate_machine( &mut self, machine: Machine, duplicate_name: String, @@ -164,7 +225,7 @@ impl BackendBroker { let sender_clone = self.sender.clone(); thread::spawn(move || { let dto = if machine.duplicate(&duplicate_name).is_ok() { - ChannelDTO::DuplicateFinished(duplicate_name) + ChannelDTO::DuplicatedMachine(duplicate_name) } else { ChannelDTO::Default }; @@ -172,7 +233,7 @@ impl BackendBroker { }); } - pub fn tar( + pub fn tar_machine( &mut self, machine: Machine, export_path: PathBuf, @@ -190,7 +251,7 @@ impl BackendBroker { }); } - pub fn stop(&mut self, machine: Machine, status_entries: &mut StatusEntries) { + pub fn stop_machine(&mut self, machine: Machine, status_entries: &mut StatusEntries) { let status_index = status_entries.create_entry(format!("Stopping Machine '{}'", machine.config.name)); @@ -203,7 +264,7 @@ impl BackendBroker { }); } - pub fn delete(&mut self, machine: Machine, status_entries: &mut StatusEntries) { + pub fn delete_machine(&mut self, machine: Machine, status_entries: &mut StatusEntries) { let status_index = status_entries.create_entry(format!("Deleting Machine '{}'", machine.config.name)); @@ -211,29 +272,246 @@ impl BackendBroker { thread::spawn(move || { let machine_name = machine.config.name.clone(); let dto = if machine.delete(true).is_ok() { - ChannelDTO::DeleteFinished(machine_name) + ChannelDTO::DeletedMachine(machine_name) } else { ChannelDTO::Default }; sender_clone.send((status_index, dto)).unwrap(); }); } + + pub fn access_repository(&mut self, auth_url: AuthUrl, status_entries: &mut StatusEntries) { + let status_index = + status_entries.create_entry(format!("Accessing Repository {}", auth_url.url)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let dto = match access_repo(&auth_url) { + Ok(_) => ChannelDTO::RepositoryAccessed(auth_url), + Err(_) => ChannelDTO::Default, + }; + sender_clone.send((status_index, dto)).unwrap(); + }); + } + + pub fn load_repository(&mut self, auth_url: AuthUrl, status_entries: &mut StatusEntries) { + let status_index = status_entries.push(format!("Loading Repository {}", auth_url.url), 2); + + let sender_clone = self.sender.clone(); + let auth_url_clone = auth_url.clone(); + thread::spawn(move || { + let branches_result = load_branches(&auth_url_clone); + let dto = ChannelDTO::BranchesLoaded(auth_url_clone, branches_result); + sender_clone.send((status_index, dto)).unwrap(); + }); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let tags_result = load_tags(&auth_url); + let dto = ChannelDTO::TagsLoaded(auth_url, tags_result); + sender_clone.send((status_index, dto)).unwrap(); + }); + } + + pub fn load_modules( + &mut self, + repo: (AuthUrl, RepositorySpecification), + status_entries: &mut StatusEntries, + ) { + let status_index = + status_entries.create_entry(format!("Loading Modules for {}", repo.0.url)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let modules_result = load_modules(&repo); + + let dto = ChannelDTO::ModulesLoaded(repo, modules_result); + sender_clone.send((status_index, dto)).unwrap(); + }); + } + + fn get_unset_secrets( + &mut self, + machine: Machine, + dont_run_init: bool, + status_entries: &mut StatusEntries, + ) { + let status_index = status_entries.create_entry(format!( + "Getting unset secrets of '{}'", + &machine.config.name + )); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let unset_secrets_result = get_unset_secrets_for(&machine); + let dto = match unset_secrets_result { + Ok(unsets) => { + if unsets.is_empty() { + ChannelDTO::WroteSecrets(machine, dont_run_init) + } else { + ChannelDTO::FoundUnsetSecrets(machine, unsets, dont_run_init) + } + } + Err(_) => ChannelDTO::Default, + }; + sender_clone.send((status_index, dto)).unwrap(); + }); + } + + fn install_machine( + &mut self, + mut machine: Machine, + dont_run_init: bool, + status_entries: &mut StatusEntries, + ) { + let status_index = + status_entries.create_entry(format!("Installing machine '{}'", &machine.config.name)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let install_result = machine.build_install(); + let dto = match install_result { + Ok(_) => ChannelDTO::InstalledMachine(machine, dont_run_init), + Err(_) => ChannelDTO::Default, + }; + sender_clone.send((status_index, dto)).unwrap(); + }); + } + + fn run_init_script( + &mut self, + machine: Machine, + dont_run_init: bool, + status_entries: &mut StatusEntries, + ) { + let status_index = status_entries.create_entry(format!( + "Running Init-Script for '{}'", + &machine.config.name + )); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let _ = machine.run_init_script(dont_run_init); + let dto = ChannelDTO::Default; + sender_clone.send((status_index, dto)).unwrap(); + }); + } + + pub fn insert_secret( + &mut self, + machine_name: String, + secret_name: String, + value: String, + status_entries: &mut StatusEntries, + ) { + if let Some((_, _, unset_secrets)) = self.unresolved_machines.get_mut(&machine_name) { + unset_secrets.insert(secret_name, Some(value)); + + if unset_secrets.values().all(|secret| secret.is_some()) { + if let Some(entry) = self.unresolved_machines.remove(&machine_name) { + self.write_unset_secrets(entry, status_entries); + } + } + } + } + + fn write_unset_secrets( + &mut self, + entry: (Machine, bool, HashMap>), + status_entries: &mut StatusEntries, + ) { + let (mut machine, dont_run_init, unset_secrets) = entry; + let status_index = + status_entries.create_entry(format!("Writing Secrets for '{}'", &machine.config.name)); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let dto = match write_secrets(&mut machine, unset_secrets) { + Ok(_) => ChannelDTO::WroteSecrets(machine, dont_run_init), + Err(_) => ChannelDTO::Default, + }; + sender_clone.send((status_index, dto)).unwrap(); + }); + } } -enum ChannelDTO { - BuildFinished(Machine), - DuplicateFinished(String), - DeleteFinished(String), +fn access_repo(auth_url: &AuthUrl) -> Result> { + let opts = InputOptions { + dont_prompt: true, + no_build: true, + use_nixpkgs: None, + auth: auth_url.get_auth().or(Some(String::from(""))), + branch: None, + tag: None, + commit: None, + }; + let git_url = auth_url.get_git_url()?; + let flake_url = crate::module::inquire_module_url(&opts, &git_url, false)?; + let dummy_path = LinuxPath(String::from("")); + let nix_url = flake_url.to_nix_url(dummy_path); + + let modules = crate::platform::Driver::store() + .cmd() + .list_nixos_modules(&nix_url)?; + + Ok(modules) +} - FoundUnsetSecrets(Machine, Vec), - WroteSecrets(Machine), +fn load_branches(auth_url: &AuthUrl) -> Result> { + let output = Command::new("git") + .args(["ls-remote", "--heads", &auth_url.to_string()]) + .output()?; - Default, + let branches = String::from_utf8_lossy(&output.stdout) + .to_string() + .lines() + .filter_map(|line| line.split('\t').nth(1)) + .filter_map(|ref_name| ref_name.strip_prefix("refs/heads/")) + .map(String::from) + .collect(); + + Ok(branches) } -pub enum BackendIntent { - DuplicatedMachine(String), - DeletedMachine(String), +fn load_tags(auth_url: &AuthUrl) -> Result> { + let output = Command::new("git") + .args(["ls-remote", "--tags", &auth_url.to_string()]) + .output()?; + + let tags = String::from_utf8_lossy(&output.stdout) + .to_string() + .lines() + .filter_map(|line| line.split('\t').nth(1)) + .filter_map(|ref_name| ref_name.strip_prefix("refs/tags/")) + .map(String::from) + .collect(); + + Ok(tags) +} + +fn load_modules(repo: &(AuthUrl, RepositorySpecification)) -> Result> { + let (auth_url, repo_spec) = repo; + + let git_url = auth_url.get_git_url()?; + let (branch, tag, commit) = repo_spec.to_triple(); + let opts = InputOptions { + dont_prompt: true, + auth: auth_url.get_auth(), + no_build: false, + use_nixpkgs: None, + branch, + tag, + commit, + }; + + let flake_url = crate::module::inquire_module_url(&opts, &git_url, false)?; + let dummy_path = LinuxPath(String::from("")); + let nix_url = flake_url.to_nix_url(dummy_path); + let modules = crate::platform::Driver::store() + .cmd() + .list_nixos_modules(&nix_url)?; + + Ok(modules) } fn get_unset_secrets_for(machine: &Machine) -> Result> { diff --git a/codchi/src/gui/util/mod.rs b/codchi/src/gui/util/mod.rs index 8d03dcc3..316293da 100644 --- a/codchi/src/gui/util/mod.rs +++ b/codchi/src/gui/util/mod.rs @@ -14,7 +14,7 @@ fn password_field_ui(ui: &mut Ui, password: &mut String) -> Response { let mut show_plaintext = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(false)); - let result = ui.horizontal(|ui| { + let result = ui.with_layout(Layout::right_to_left(Align::Min), |ui| { let response = ui .add(SelectableLabel::new(show_plaintext, "👁")) .on_hover_text("Show/hide password"); @@ -24,7 +24,7 @@ fn password_field_ui(ui: &mut Ui, password: &mut String) -> Response { } ui.add_sized( - [200.0, ui.available_height()], + [ui.available_width(), 0.0], TextEdit::singleline(password).password(!show_plaintext), ); }); @@ -56,7 +56,7 @@ pub fn advanced_password_field_ui( .unwrap_or(String::from(password)) }); - let result = ui.horizontal(|ui| { + let result = ui.with_layout(Layout::right_to_left(Align::Min), |ui| { let response = ui .add(SelectableLabel::new(show_plaintext, "👁")) .on_hover_text("Show/hide password"); @@ -66,7 +66,7 @@ pub fn advanced_password_field_ui( } ui.add_sized( - [200.0, ui.available_height()], + [250.0, 0.0], TextEdit::singleline(&mut text).password(!show_plaintext), ); From eb3bf3cb07eb826d0452332f04c01e2de33a92a6 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:13:00 +0000 Subject: [PATCH 45/49] Improved resolution of used images --- codchi/Cargo.lock | 397 +++++++++++++++++- codchi/Cargo.toml | 4 +- codchi/assets/bug_icon.png | Bin 51083 -> 0 bytes codchi/assets/bug_icon.svg | 1 + codchi/assets/github_logo.png | Bin 17356 -> 0 bytes codchi/assets/github_logo.svg | 1 + codchi/assets/menu_icon.svg | 1 + codchi/assets/settings.png | Bin 26925 -> 0 bytes .../src/gui/main_panel/machine_inspection.rs | 6 +- codchi/src/gui/menubar.rs | 33 +- codchi/src/gui/util/textures_manager.rs | 168 ++++---- 11 files changed, 525 insertions(+), 86 deletions(-) delete mode 100755 codchi/assets/bug_icon.png create mode 100755 codchi/assets/bug_icon.svg delete mode 100755 codchi/assets/github_logo.png create mode 100755 codchi/assets/github_logo.svg create mode 100755 codchi/assets/menu_icon.svg delete mode 100755 codchi/assets/settings.png diff --git a/codchi/Cargo.lock b/codchi/Cargo.lock index 9d106810..df54edaa 100644 --- a/codchi/Cargo.lock +++ b/codchi/Cargo.lock @@ -567,6 +567,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -982,6 +988,7 @@ dependencies = [ "number_prefix", "petname", "rand 0.9.0", + "resvg 0.45.1", "rfd", "serde", "serde_json", @@ -993,6 +1000,7 @@ dependencies = [ "throttle", "toml_edit 0.22.24", "tray-icon", + "usvg 0.45.1", "uuid", "version-compare", "which", @@ -1010,6 +1018,12 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.3" @@ -1133,6 +1147,15 @@ dependencies = [ "libc", ] +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1284,6 +1307,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "data-url" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" + [[package]] name = "deranged" version = "0.3.11" @@ -1570,6 +1599,7 @@ dependencies = [ "log", "mime_guess2", "profiling", + "resvg 0.37.0", ] [[package]] @@ -1796,6 +1826,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "fnv" version = "1.0.7" @@ -1808,6 +1844,29 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +[[package]] +name = "fontconfig-parser" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -2095,6 +2154,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -2638,6 +2707,28 @@ dependencies = [ "tiff", ] +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + [[package]] name = "immutable-chunkmap" version = "2.0.6" @@ -2820,6 +2911,25 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "kurbo" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" +dependencies = [ + "arrayvec", + "smallvec", +] + [[package]] name = "lazy-regex" version = "3.4.1" @@ -2899,6 +3009,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "libm" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" + [[package]] name = "libredox" version = "0.1.3" @@ -3694,6 +3810,12 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.9" @@ -3871,6 +3993,12 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.30.0" @@ -3995,6 +4123,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rctree" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -4070,6 +4204,37 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "resvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadccb3d99a9efb8e5e00c16fbb732cbe400db2ec7fc004697ee7d97d86cf1f4" +dependencies = [ + "log", + "pico-args", + "rgb", + "svgtypes 0.13.0", + "tiny-skia", + "usvg 0.37.0", +] + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.45.1", + "zune-jpeg", +] + [[package]] name = "rfd" version = "0.15.3" @@ -4094,12 +4259,33 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + [[package]] name = "roff" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" +[[package]] +name = "roxmltree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -4140,6 +4326,24 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.8.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + [[package]] name = "ryu" version = "1.0.19" @@ -4291,7 +4495,7 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -4397,6 +4601,27 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -4492,6 +4717,9 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] [[package]] name = "strsim" @@ -4543,6 +4771,26 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "svgtypes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44e288cd960318917cbd540340968b90becc8bc81f171345d706e7a89d9d70" +dependencies = [ + "kurbo 0.9.5", + "siphasher 0.3.11", +] + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.1", + "siphasher 1.0.1", +] + [[package]] name = "syn" version = "1.0.109" @@ -4801,6 +5049,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", + "png", "tiny-skia-path", ] @@ -4825,6 +5074,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "toml" version = "0.8.20" @@ -4938,6 +5202,9 @@ name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] [[package]] name = "type-map" @@ -4971,18 +5238,54 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + [[package]] name = "unicode-ident" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "unicode-width" version = "0.1.14" @@ -5025,6 +5328,77 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "usvg" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b0a51b72ab80ca511d126b77feeeb4fb1e972764653e61feac30adc161a756" +dependencies = [ + "base64 0.21.7", + "log", + "pico-args", + "usvg-parser", + "usvg-tree", + "xmlwriter", +] + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.1", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher 1.0.1", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "usvg-parser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd4e3c291f45d152929a31f0f6c819245e2921bfd01e7bd91201a9af39a2bdc" +dependencies = [ + "data-url", + "flate2", + "imagesize 0.12.0", + "kurbo 0.9.5", + "log", + "roxmltree 0.19.0", + "simplecss", + "siphasher 0.3.11", + "svgtypes 0.13.0", + "usvg-tree", +] + +[[package]] +name = "usvg-tree" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee3d202ebdb97a6215604b8f5b4d6ef9024efd623cf2e373a6416ba976ec7d3" +dependencies = [ + "rctree", + "strict-num", + "svgtypes 0.13.0", + "tiny-skia-path", +] + [[package]] name = "utf16_iter" version = "1.0.5" @@ -6167,6 +6541,12 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + [[package]] name = "yoke" version = "0.7.5" @@ -6437,6 +6817,21 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "4.2.0" diff --git a/codchi/Cargo.toml b/codchi/Cargo.toml index 4aed9ba3..3724b377 100644 --- a/codchi/Cargo.toml +++ b/codchi/Cargo.toml @@ -63,8 +63,10 @@ rand = "0.9.0" ctrlc = { version = "3.4.5", features = ["termination"] } egui = "0.31.0" eframe = "0.31.0" -egui_extras = { version = "0.31.0", features = ["default", "image"] } +egui_extras = { version = "0.31.0", features = ["default", "image", "svg"] } rfd = "0.15.3" +usvg = "0.45.1" +resvg = "0.45.1" [target.'cfg(unix)'.dependencies] indoc = "2.0.5" diff --git a/codchi/assets/bug_icon.png b/codchi/assets/bug_icon.png deleted file mode 100755 index 6c845dd434a71662aa874d772ee9d111e6665d78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51083 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 HJ4F8%kbWgX diff --git a/codchi/assets/bug_icon.svg b/codchi/assets/bug_icon.svg new file mode 100755 index 00000000..3073c705 --- /dev/null +++ b/codchi/assets/bug_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/codchi/assets/github_logo.png b/codchi/assets/github_logo.png deleted file mode 100755 index 3325d7bfe6d41f4999ad4fc173d5ef394e7f5378..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17356 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 diff --git a/codchi/assets/github_logo.svg b/codchi/assets/github_logo.svg new file mode 100755 index 00000000..8e3a992b --- /dev/null +++ b/codchi/assets/github_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/codchi/assets/menu_icon.svg b/codchi/assets/menu_icon.svg new file mode 100755 index 00000000..a4c2e73c --- /dev/null +++ b/codchi/assets/menu_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/codchi/assets/settings.png b/codchi/assets/settings.png deleted file mode 100755 index 2507242dfbf5a49e5792ac3c55e494bc583718fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26925 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 z z2Ny@UE&^_@E{89o&ROLJkX2ttxE!sISqH5s+6yjE%-UKO^Z|nsq_y z%Z5fq%0e;!FfMTs!f-f(+UW(kpl5$+$bSud@THdC(qZBPuqb6s;gQwR`cMa%JM}x? zm)a91a_$Y%or?4wu~}*n8GS3v^K)b00D0PgPBSS)mTVV;oy#*;V(e=3J7bG%q_c?5 zWvk>xoC5Wn>N3&21Hi=XKpDoUcCWHCFa4jeQ^JC+J`oTP!4-ON;1)o-Hk&ZA2A?{? zaF6h==JFx9%*Y%_RUZMNM}3&NaR78S@NyQ*3o~u6YIeuIYHU_0Ew%L9H9+?o<d=5~<+bWuoYUEE#}+ybr$I3S*A9(;Fuu9pP1Bq4QiLlY_{+l7$8 z!ATN6O831eZa_uP4D}oDcrP*%Q2ar<%S-gkZXC-7rej^u{5*={N|})r08DlOoe!X_ zDc{}jEbgtmObqae`9u&hH=_C#|IvIv7Y>ndOlSV3&xiKYCGgIqo7}l2JV+tk?#{JP zVQTj$vc!ve5qH5JgJA^dpzc~y6s7S^4}zTMg~WPz(!#l30n&XRG<>gT#t7CGZ&}nD zy$!?fPcY)>3g@!x@8~QGQ$BbE68(id^X52N;%2=N!;K`2F9AgwbaE!oGwc=oc^|9d z-mh=yd%Fq1=mg#!QhiHc^CLcOnHNZNl*R4!r|2lWsyQ;TxBd&!8Sm8upY8eM{vE}} z9bb_6OkN|J3iiN4t+veJiyS4N+O^3Kbk0TWpCno~?9m;v@kLB|;7=Kn?9>|nBo5$1 zao)iNieVu5z&cP6@OyCC$9&9A_Z5*}l{`n&1|v#|AMrY0e0*7f@XgaXPa~?Y73)i zU-U$|G{PfsP4V)%fMTUJ+J^GvxBW4kzVEtePwGb-v_Fli#O6w3C{5&-on$95$_-L9 ziNrPN_BW!+-pSu#asHsK5u=- zzGkMnR$52kHYu2=#=E_ucBPJUDt{%qLm}*X*jimH-FPm1udc#+%osAUE_(25_aU?4 zBlQ~3CfO#{yMC(X&UBGE1|7Zu2fOa;uR#cb!wRhaskmXXlMtYroQmiR(lue$LnENd zcF&Y(DIA54wTwEAY6?I6b5eI?D$ak;*$fyh9P=$NZ1kQ;e140X&S_5fmn^xON&94> zCnxH4?669zYqKz1rWjih@61W!!ATG1dhtO45VmAH z<_a&h9fp9MHJV?;e3R@(yucptxs;#G-W+OP&#>b}MB&6*G3un@J@7C*-whd-8BEmf zaC{oI9hyP#Wnj3g6pY`+djACbVI-Ba1DsA9Gv;Opk<#RZbKxYfjlO0-IIlOj#EDL5 zjpqx|2?>}6v0(@@9D#Hhhv@M2tq@J8^T@IvtD}S;2dcyMh!%XV=~QAoa1IN4+!}8h zl5al=O6QzvfMb(^QPYt0o;Qopb7QsmzOB6Vbv;ry{!8{$-A~|*euFCq!sf^Fu8L9v zh;s=Tu2;Xl*hT%_+&aLwu!nSo7<+WZ7$PK@B7-lH+EJ(R&+t-3wGdDzH+h|z|BLYh z^wQsOKiG#iGu8e^_~D4+@1G!!#D5WfjP$n~^7nVE6;3z(jRN%l{sXMmX0}5LnMB=; bWD9xox0Zb?EOY{4kRbnB+aIs8^hx|5`Ce<8 diff --git a/codchi/src/gui/main_panel/machine_inspection.rs b/codchi/src/gui/main_panel/machine_inspection.rs index a0d5061d..13dd5b7b 100644 --- a/codchi/src/gui/main_panel/machine_inspection.rs +++ b/codchi/src/gui/main_panel/machine_inspection.rs @@ -214,10 +214,8 @@ impl MachineInspection { for desktop_entry in desktop_entries { let app_icon = match &desktop_entry.icon { Some(icon_path) => textures_manager - .deliver_image(&desktop_entry.app_name, icon_path) - .map(|tex| { - Image::from_texture(tex).max_size(Vec2 { x: 12.0, y: 12.0 }) - }), + .deliver_ico(&desktop_entry.app_name, icon_path) + .map(|tex| Image::from_texture(tex).shrink_to_fit()), None => None, }; let app_text = WidgetText::RichText(RichText::new(&desktop_entry.app_name)); diff --git a/codchi/src/gui/menubar.rs b/codchi/src/gui/menubar.rs index ecc80917..5b238543 100644 --- a/codchi/src/gui/menubar.rs +++ b/codchi/src/gui/menubar.rs @@ -23,8 +23,11 @@ pub fn update(ui: &mut Ui, textures_manager: &mut TexturesManager) -> Vec Button::image(image), + let codchi_button = match textures_manager.deliver("logo", "assets/logo.png") { + Some(tex_handle) => { + let image = Image::new(tex_handle).max_size(vec2(48.0, 48.0)); + Button::image(image) + } None => Button::new("Home"), }; if ui.add(codchi_button).on_hover_text("Home").clicked() { @@ -35,8 +38,12 @@ pub fn update(ui: &mut Ui, textures_manager: &mut TexturesManager) -> Vec Button::image(image), + match textures_manager.deliver_svg("github_logo", "assets/github_logo.svg") { + Some(tex_handle) => Button::image( + Image::new(tex_handle) + .tint(ui.visuals().widgets.inactive.fg_stroke.color) + .max_size(vec2(24.0, 24.0)), + ), None => Button::new("Github"), }; if ui.add(github_button).on_hover_text("Github").clicked() { @@ -45,8 +52,12 @@ pub fn update(ui: &mut Ui, textures_manager: &mut TexturesManager) -> Vec Button::image(image), + match textures_manager.deliver_svg("bug_icon", "assets/bug_icon.svg") { + Some(tex_handle) => Button::image( + Image::new(tex_handle) + .tint(ui.visuals().widgets.inactive.fg_stroke.color) + .max_size(vec2(24.0, 24.0)), + ), None => Button::new("Bug-Report"), }; if ui @@ -73,11 +84,15 @@ pub fn update(ui: &mut Ui, textures_manager: &mut TexturesManager) -> Vec Vec { let mut intent = Vec::new(); - let settings_button = match textures_manager.deliver("settings", "assets/settings.png", 25) { - Some(image) => Button::image(image), + let menu_button = match textures_manager.deliver_svg("menu", "assets/menu_icon.svg") { + Some(tex_handle) => Button::image( + Image::new(tex_handle) + .tint(ui.visuals().widgets.inactive.fg_stroke.color) + .max_size(vec2(24.0, 24.0)), + ), None => Button::new("Settings"), }; - menu::menu_custom_button(ui, settings_button, |ui| { + menu::menu_custom_button(ui, menu_button, |ui| { if ui.button("Zoom In").clicked() { intent.push(MenubarIntent::ZoomIn); ui.close_menu(); diff --git a/codchi/src/gui/util/textures_manager.rs b/codchi/src/gui/util/textures_manager.rs index 225fe0f1..c8ba3c4f 100644 --- a/codchi/src/gui/util/textures_manager.rs +++ b/codchi/src/gui/util/textures_manager.rs @@ -1,8 +1,10 @@ +use anyhow::Result; use egui::*; use image::{ImageBuffer, ImageReader, Rgba}; +use resvg::tiny_skia::Pixmap; use std::{ - collections::HashMap, - path::{Path, PathBuf}, + collections::{HashMap, HashSet}, + path::PathBuf, sync::mpsc::{channel, Receiver, Sender}, thread, }; @@ -10,14 +12,14 @@ use std::{ pub struct TexturesManager { textures: HashMap, - sender: Sender, - receiver: Receiver, - awaits_answer: bool, + sender: Sender<(String, ChannelDTO)>, + receiver: Receiver<(String, ChannelDTO)>, + + open_requests: HashSet, } enum ChannelDTO { - ColorImage(String, ColorImage), - Image(String, ImageBuffer, Vec>, [usize; 2]), + ColorImage(ColorImage), } impl TexturesManager { @@ -29,7 +31,8 @@ impl TexturesManager { sender, receiver, - awaits_answer: false, + + open_requests: HashSet::new(), } } @@ -45,127 +48,150 @@ impl TexturesManager { self.get(name).map(|tex| Image::from_texture(tex)) } - pub fn deliver(&mut self, name: &str, path: &str, dim: u32) -> Option<&TextureHandle> { + pub fn deliver(&mut self, name: &str, path: &str) -> Option<&TextureHandle> { let texture_handle = self.textures.get(name); - if !self.awaits_answer && texture_handle.is_none() { - self.awaits_answer = true; - load_color_image_async(&self.sender, name.to_string(), path.to_string(), dim); + if texture_handle.is_none() { + let name_string = name.to_string(); + if !self.open_requests.contains(&name_string) { + self.open_requests.insert(name_string); + load_color_image_async(&self.sender, name.to_string(), path.to_string()); + } } texture_handle } - pub fn deliver_image(&mut self, name: &str, path: &PathBuf) -> Option<&TextureHandle> { + pub fn deliver_ico(&mut self, name: &str, path: &PathBuf) -> Option<&TextureHandle> { let texture_handle = self.textures.get(name); - if !self.awaits_answer && texture_handle.is_none() { - self.awaits_answer = true; - load_image_async(&self.sender, name.to_string(), path.clone()); + if texture_handle.is_none() { + let name_string = name.to_string(); + if !self.open_requests.contains(&name_string) { + self.open_requests.insert(name_string); + load_image_buffer_async(&self.sender, name.to_string(), path.clone()); + } + } + + texture_handle + } + + pub fn deliver_svg(&mut self, name: &str, path: &str) -> Option<&TextureHandle> { + let texture_handle = self.textures.get(name); + + if texture_handle.is_none() { + let name_string = name.to_string(); + if !self.open_requests.contains(&name_string) { + self.open_requests.insert(name_string); + load_svg_async(&self.sender, name.to_string(), path.to_string()); + } } texture_handle } fn receive_msgs(&mut self, ctx: &Context) { - while let Ok(channel_dto) = self.receiver.try_recv() { - self.awaits_answer = false; + while let Ok((name, channel_dto)) = self.receiver.try_recv() { + self.open_requests.remove(&name); match channel_dto { - ChannelDTO::ColorImage(name, color_image) => { + ChannelDTO::ColorImage(color_image) => { let texture_handle = ctx.load_texture(&name, color_image, Default::default()); self.textures.insert(name, texture_handle); } - ChannelDTO::Image(name, image_buffer, size) => { - let texture_handle = ctx.load_texture( - "icon_texture", - ColorImage::from_rgba_unmultiplied(size, &image_buffer), - Default::default(), - ); - self.textures.insert(name, texture_handle); - } } } } } -fn load_image_async(sender: &Sender, name: String, path: PathBuf) { +fn load_color_image_async(sender: &Sender<(String, ChannelDTO)>, name: String, location: String) { let sender_clone = sender.clone(); thread::spawn(move || { - if let Some((image_buffer, size)) = load_image_from_path(&path) { - let dto = ChannelDTO::Image(name, image_buffer, size); - sender_clone.send(dto).unwrap(); + let path = PathBuf::from(&location); + if let Ok(color_image) = load_color_image_from_path(&path) { + let dto = ChannelDTO::ColorImage(color_image); + sender_clone.send((name, dto)).unwrap(); } }); } -fn load_image_from_path(path: &PathBuf) -> Option<(ImageBuffer, Vec>, [usize; 2])> { - let image = image::open(path) - .expect("Failed to open image icon") - .to_rgba8(); - let size = [image.width() as usize, image.height() as usize]; - - Some((image, size)) +fn load_image_buffer_async(sender: &Sender<(String, ChannelDTO)>, name: String, path: PathBuf) { + let sender_clone = sender.clone(); + thread::spawn(move || { + if let Ok((image_buffer, size)) = load_image_from_path(&path) { + let color_image = ColorImage::from_rgba_unmultiplied(size, &image_buffer); + let dto = ChannelDTO::ColorImage(color_image); + sender_clone.send((name, dto)).unwrap(); + } + }); } -fn load_color_image_async(sender: &Sender, name: String, path: String, dim: u32) { +fn load_svg_async(sender: &Sender<(String, ChannelDTO)>, name: String, location: String) { let sender_clone = sender.clone(); thread::spawn(move || { - if let Some(color_image) = load_color_image_from_path(&path, dim) { - let dto = ChannelDTO::ColorImage(name, color_image); - sender_clone.send(dto).unwrap(); + let path = PathBuf::from(&location); + if let Ok(color_image) = load_svg_from_path(&path) { + let dto = ChannelDTO::ColorImage(color_image); + sender_clone.send((name, dto)).unwrap(); } }); } -fn load_color_image_from_path(path: &str, dim: u32) -> Option { - let img = ImageReader::open(Path::new(path)) - .ok()? - .decode() - .ok()? - .resize(dim, dim, image::imageops::FilterType::Lanczos3) - .to_rgba8(); +fn load_color_image_from_path(path: &PathBuf) -> Result { + let image = ImageReader::open(path)?.decode()?.to_rgba8(); - let (width, height) = img.dimensions(); - let pixels = img.into_raw(); + let (width, height) = image.dimensions(); + let pixels = image.into_raw(); - Some(ColorImage::from_rgba_unmultiplied( + Ok(ColorImage::from_rgba_unmultiplied( [width as usize, height as usize], &pixels, )) } -fn load_square_textures_async(sender: &Sender) { +fn load_image_from_path(path: &PathBuf) -> Result<(ImageBuffer, Vec>, [usize; 2])> { + let image = image::open(path)?.to_rgba8(); + let size = [image.width() as usize, image.height() as usize]; + + Ok((image, size)) +} + +fn load_svg_from_path(path: &PathBuf) -> Result { + let rtree = usvg::Tree::from_data(&std::fs::read(path)?, &usvg::Options::default())?; + + let w = rtree.size().width(); + let h = rtree.size().height(); + + let mut pixmap = Pixmap::new(w as u32, h as u32).unwrap(); + resvg::render(&rtree, Default::default(), &mut pixmap.as_mut()); + + Ok(ColorImage::from_rgba_unmultiplied( + [w as usize, h as usize], + pixmap.data(), + )) +} + +fn load_square_textures_async(sender: &Sender<(String, ChannelDTO)>) { let sender_clone = sender.clone(); thread::spawn(move || { - let dto = ChannelDTO::ColorImage( - "black".to_string(), - ColorImage::new([10, 10], Color32::BLACK), - ); - sender_clone.send(dto).unwrap(); + let dto = ChannelDTO::ColorImage(ColorImage::new([10, 10], Color32::BLACK)); + sender_clone.send(("black".to_string(), dto)).unwrap(); }); let sender_clone = sender.clone(); thread::spawn(move || { - let dto = ChannelDTO::ColorImage( - "dark_red".to_string(), - ColorImage::new([10, 10], Color32::DARK_RED), - ); - sender_clone.send(dto).unwrap(); + let dto = ChannelDTO::ColorImage(ColorImage::new([10, 10], Color32::DARK_RED)); + sender_clone.send(("dark_red".to_string(), dto)).unwrap(); }); let sender_clone = sender.clone(); thread::spawn(move || { - let dto = - ChannelDTO::ColorImage("red".to_string(), ColorImage::new([10, 10], Color32::RED)); - sender_clone.send(dto).unwrap(); + let dto = ChannelDTO::ColorImage(ColorImage::new([10, 10], Color32::RED)); + sender_clone.send(("red".to_string(), dto)).unwrap(); }); let sender_clone = sender.clone(); thread::spawn(move || { - let dto = ChannelDTO::ColorImage( - "green".to_string(), - ColorImage::new([10, 10], Color32::GREEN), - ); - sender_clone.send(dto).unwrap(); + let dto = ChannelDTO::ColorImage(ColorImage::new([10, 10], Color32::GREEN)); + sender_clone.send(("green".to_string(), dto)).unwrap(); }); } From af17ea33318fa4272d45f49112c9137e0970720b Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Mon, 28 Apr 2025 14:52:32 +0000 Subject: [PATCH 46/49] Inspected machine now gets unselected, if deleted --- codchi/src/gui/main_panel/machine_inspection.rs | 10 ++++++++++ codchi/src/gui/mod.rs | 6 +++++- codchi/src/gui/side_panel.rs | 6 +++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/codchi/src/gui/main_panel/machine_inspection.rs b/codchi/src/gui/main_panel/machine_inspection.rs index 13dd5b7b..c2b247b2 100644 --- a/codchi/src/gui/main_panel/machine_inspection.rs +++ b/codchi/src/gui/main_panel/machine_inspection.rs @@ -368,6 +368,16 @@ impl MachineInspection { intent } + + pub fn unselect_machine(&mut self, machine_name: &String) { + if self + .current_machine + .as_ref() + .is_some_and(|current_machine| current_machine == machine_name) + { + self.current_machine = None; + } + } } struct MachineData { diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 5189970b..1d42c394 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -402,7 +402,11 @@ impl Gui { .add_machine(machine_name, &mut self.status_entries); } BackendIntent::DeletedMachine(machine_name) => { - self.side_panel.remove_machine(machine_name); + self.side_panel.remove_machine(&machine_name); + self.main_panel + .panels + .get_machine_inspection() + .unselect_machine(&machine_name); } BackendIntent::AccessedRepository(auth_url) => { self.backend_broker diff --git a/codchi/src/gui/side_panel.rs b/codchi/src/gui/side_panel.rs index 0efe0bd5..8d02a14c 100644 --- a/codchi/src/gui/side_panel.rs +++ b/codchi/src/gui/side_panel.rs @@ -174,10 +174,10 @@ impl GuiSidePanel { self.reload_machine_from_list(status_entries); } - pub fn remove_machine(&mut self, machine_name: String) { - self.machine_load_names.retain(|name| name != &machine_name); + pub fn remove_machine(&mut self, machine_name: &String) { + self.machine_load_names.retain(|name| name != machine_name); self.machine_button_names - .retain(|name| name != &machine_name); + .retain(|name| name != machine_name); } } From 0870a96557b95273724e84cf7576a3e9421fb559 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:03:20 +0000 Subject: [PATCH 47/49] inaccessible repository will be communicated --- .../machine_creation/generics_panel.rs | 59 ++++++++++++++++--- .../gui/main_panel/machine_creation/mod.rs | 6 ++ codchi/src/gui/mod.rs | 35 ++++++----- codchi/src/gui/util/backend_broker.rs | 14 ++--- codchi/src/gui/util/mod.rs | 29 +++++---- 5 files changed, 100 insertions(+), 43 deletions(-) diff --git a/codchi/src/gui/main_panel/machine_creation/generics_panel.rs b/codchi/src/gui/main_panel/machine_creation/generics_panel.rs index 2e355e57..077d612f 100644 --- a/codchi/src/gui/main_panel/machine_creation/generics_panel.rs +++ b/codchi/src/gui/main_panel/machine_creation/generics_panel.rs @@ -7,6 +7,9 @@ pub struct GenericsPanel { pub(super) machine_name: String, auth_url: AuthUrl, pub(super) dont_build: bool, + + occupied_loading_repo: bool, + repo_inaccessible: bool, } impl GenericsPanel { @@ -33,7 +36,9 @@ impl GenericsPanel { ui.end_row(); ui.label("URL"); - ui.add_sized( + add_sized_colored( + ui, + &mut self.repo_inaccessible, ui.available_size(), TextEdit::singleline(&mut self.auth_url.url) .hint_text("https://gitlab.example.com/my_repo"), @@ -41,14 +46,18 @@ impl GenericsPanel { ui.end_row(); ui.label("Username"); - ui.add_sized( + add_sized_colored( + ui, + &mut self.repo_inaccessible, ui.available_size(), TextEdit::singleline(&mut self.auth_url.username), ); ui.end_row(); ui.label("Password"); - ui.add_sized( + add_sized_colored( + ui, + &mut self.repo_inaccessible, ui.available_size(), password_field(&mut self.auth_url.password), ); @@ -66,12 +75,16 @@ impl GenericsPanel { let mut intent = Vec::new(); ui.with_layout(Layout::bottom_up(Align::Max), |ui| { - if ui.button("Next").clicked() { - if !self.auth_url.url.is_empty() { - intent.push(InternalIntent::ToRepositoryPanel(Some( - self.auth_url.clone(), - ))); - } + let button = Button::new("Next"); + let enabled = !self.occupied_loading_repo + && !self.auth_url.url.is_empty() + && !self.machine_name.is_empty(); + let button_handle = ui.add_enabled(enabled, button); + if button_handle.clicked() { + intent.push(InternalIntent::ToRepositoryPanel(Some( + self.auth_url.clone(), + ))); + self.occupied_loading_repo = true; } }); @@ -81,4 +94,32 @@ impl GenericsPanel { pub fn set_url(&mut self, url: String) { self.auth_url.url = url; } + + pub fn finished_loading_repo(&mut self) { + self.occupied_loading_repo = false; + } + + pub fn set_repo_inaccessible(&mut self) { + self.repo_inaccessible = true; + } +} + +fn add_sized_colored( + ui: &mut Ui, + condition: &mut bool, + max_size: impl Into, + widget: impl Widget, +) -> Response { + ui.horizontal(|ui| { + if *condition { + ui.visuals_mut().widgets.inactive.bg_stroke.width = 1.0; + ui.visuals_mut().widgets.inactive.bg_stroke.color = Color32::RED; + } + let handle = ui.add_sized(max_size, widget); + if handle.gained_focus() { + *condition = false; + } + handle + }) + .inner } diff --git a/codchi/src/gui/main_panel/machine_creation/mod.rs b/codchi/src/gui/main_panel/machine_creation/mod.rs index 18814120..7f93f98a 100644 --- a/codchi/src/gui/main_panel/machine_creation/mod.rs +++ b/codchi/src/gui/main_panel/machine_creation/mod.rs @@ -119,6 +119,11 @@ impl MachineCreation { self.generics_panel.set_url(url); } + pub fn repo_is_inaccessible(&mut self) { + self.generics_panel.finished_loading_repo(); + self.generics_panel.set_repo_inaccessible(); + } + pub fn is_auth_url_loaded(&self, auth_url: &AuthUrl) -> bool { self.repository_panel.is_auth_url_loaded(auth_url) } @@ -150,6 +155,7 @@ impl MachineCreation { pub fn to_repository_panel(&mut self, auth_url_option: Option) { if let Some(auth_url) = auth_url_option { self.repository_panel.set_current_auth_url(auth_url); + self.generics_panel.finished_loading_repo(); } self.creation_step = CreationStep::Repository; } diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 1d42c394..3145ec3f 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -183,16 +183,16 @@ impl Gui { fn eval_menubar_intent(&mut self, ctx: &Context, menubar_intent: MenubarIntent) { match menubar_intent { - menubar::MenubarIntent::Home => self.main_panel.reset(), - menubar::MenubarIntent::OpenGithub => { + MenubarIntent::Home => self.main_panel.reset(), + MenubarIntent::OpenGithub => { ctx.open_url(OpenUrl::new_tab("https://github.com/aformatik/codchi/")) } - menubar::MenubarIntent::OpenIssues => ctx.open_url(OpenUrl::new_tab( + MenubarIntent::OpenIssues => ctx.open_url(OpenUrl::new_tab( "https://github.com/aformatik/codchi/issues", )), - menubar::MenubarIntent::ZoomIn => gui_zoom::zoom_in(ctx), - menubar::MenubarIntent::ZoomOut => gui_zoom::zoom_out(ctx), - menubar::MenubarIntent::RecoverStore => { + MenubarIntent::ZoomIn => gui_zoom::zoom_in(ctx), + MenubarIntent::ZoomOut => gui_zoom::zoom_out(ctx), + MenubarIntent::RecoverStore => { let index = self .status_entries .push(String::from("Recovering Codchi store..."), 1); @@ -202,27 +202,27 @@ impl Gui { sender_clone.send((index, ChannelDTO::Generic)).unwrap(); }); } - menubar::MenubarIntent::ShowTray(val) => { + MenubarIntent::ShowTray(val) => { let mut doc = CodchiConfig::open_mut().expect("Failed to open config"); doc.tray_autostart(val); doc.write().expect("Failed to write config"); } - menubar::MenubarIntent::EnableVcxsrv(val) => { + MenubarIntent::EnableVcxsrv(val) => { let mut doc = CodchiConfig::open_mut().expect("Failed to open config"); doc.vcxsrv_enable(val); doc.write().expect("Failed to write config"); } - menubar::MenubarIntent::ShowTrayVcxsrv(val) => { + MenubarIntent::ShowTrayVcxsrv(val) => { let mut doc = CodchiConfig::open_mut().expect("Failed to open config"); doc.vcxsrv_tray(val); doc.write().expect("Failed to write config"); } - menubar::MenubarIntent::EnableWslVpnkit(val) => { + MenubarIntent::EnableWslVpnkit(val) => { let mut doc = CodchiConfig::open_mut().expect("Failed to open config"); doc.enable_wsl_vpnkit(val); doc.write().expect("Failed to write config"); } - menubar::MenubarIntent::InsertUrl(url) => { + MenubarIntent::InsertUrl(url) => { self.main_panel.panels.get_machine_creation().pass_url(url); self.main_panel.change(MainPanelType::MachineCreation); } @@ -408,9 +408,16 @@ impl Gui { .get_machine_inspection() .unselect_machine(&machine_name); } - BackendIntent::AccessedRepository(auth_url) => { - self.backend_broker - .load_repository(auth_url, &mut self.status_entries); + BackendIntent::AccessedRepository(auth_url, result) => { + if result.is_ok() { + self.backend_broker + .load_repository(auth_url, &mut self.status_entries); + } else { + self.main_panel + .panels + .get_machine_creation() + .repo_is_inaccessible(); + } } BackendIntent::LoadedBranches(auth_url, branches_result) => { if let Ok(branches) = branches_result { diff --git a/codchi/src/gui/util/backend_broker.rs b/codchi/src/gui/util/backend_broker.rs index d25b67fe..7145c7be 100644 --- a/codchi/src/gui/util/backend_broker.rs +++ b/codchi/src/gui/util/backend_broker.rs @@ -35,7 +35,7 @@ enum ChannelDTO { DuplicatedMachine(String), DeletedMachine(String), - RepositoryAccessed(AuthUrl), + RepositoryAccessed(AuthUrl, Result<()>), BranchesLoaded(AuthUrl, Result>), TagsLoaded(AuthUrl, Result>), ModulesLoaded( @@ -53,7 +53,7 @@ enum ChannelDTO { pub enum BackendIntent { DuplicatedMachine(String), DeletedMachine(String), - AccessedRepository(AuthUrl), + AccessedRepository(AuthUrl, Result<()>), LoadedBranches(AuthUrl, Result>), LoadedTags(AuthUrl, Result>), LoadedModules( @@ -106,8 +106,8 @@ impl BackendBroker { ChannelDTO::DeletedMachine(machine_name) => { intent.push(BackendIntent::DeletedMachine(machine_name)); } - ChannelDTO::RepositoryAccessed(auth_url) => { - intent.push(BackendIntent::AccessedRepository(auth_url)); + ChannelDTO::RepositoryAccessed(auth_url, result) => { + intent.push(BackendIntent::AccessedRepository(auth_url, result)); } ChannelDTO::BranchesLoaded(auth_url, branches) => { intent.push(BackendIntent::LoadedBranches(auth_url, branches)); @@ -287,8 +287,8 @@ impl BackendBroker { let sender_clone = self.sender.clone(); thread::spawn(move || { let dto = match access_repo(&auth_url) { - Ok(_) => ChannelDTO::RepositoryAccessed(auth_url), - Err(_) => ChannelDTO::Default, + Ok(_) => ChannelDTO::RepositoryAccessed(auth_url, Ok(())), + Err(err) => ChannelDTO::RepositoryAccessed(auth_url, Err(err)), }; sender_clone.send((status_index, dto)).unwrap(); }); @@ -440,7 +440,7 @@ fn access_repo(auth_url: &AuthUrl) -> Result> { dont_prompt: true, no_build: true, use_nixpkgs: None, - auth: auth_url.get_auth().or(Some(String::from(""))), + auth: auth_url.get_auth().or(Some(String::from(":"))), branch: None, tag: None, commit: None, diff --git a/codchi/src/gui/util/mod.rs b/codchi/src/gui/util/mod.rs index 316293da..21def6b3 100644 --- a/codchi/src/gui/util/mod.rs +++ b/codchi/src/gui/util/mod.rs @@ -14,23 +14,26 @@ fn password_field_ui(ui: &mut Ui, password: &mut String) -> Response { let mut show_plaintext = ui.data_mut(|d| d.get_temp::(state_id).unwrap_or(false)); - let result = ui.with_layout(Layout::right_to_left(Align::Min), |ui| { - let response = ui - .add(SelectableLabel::new(show_plaintext, "👁")) - .on_hover_text("Show/hide password"); + let result = ui + .with_layout(Layout::right_to_left(Align::Min), |ui| { + let response = ui + .add(SelectableLabel::new(show_plaintext, "👁")) + .on_hover_text("Show/hide password"); + + if response.clicked() { + show_plaintext = !show_plaintext; + } - if response.clicked() { - show_plaintext = !show_plaintext; - } + ui.add_sized( + [ui.available_width(), 0.0], + TextEdit::singleline(password).password(!show_plaintext), + ) + }) + .inner; - ui.add_sized( - [ui.available_width(), 0.0], - TextEdit::singleline(password).password(!show_plaintext), - ); - }); ui.data_mut(|d| d.insert_temp(state_id, show_plaintext)); - result.response + result } pub fn advanced_password_field<'a>( From f1158c2662ec49e675759bb23d0293c1c35a18f8 Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:32:31 +0000 Subject: [PATCH 48/49] moved recover_store to backend_broker --- codchi/src/gui/mod.rs | 37 ++------------------------- codchi/src/gui/util/backend_broker.rs | 10 ++++++++ 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 3145ec3f..3bb02704 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -11,11 +11,7 @@ use main_panel::{ }; use menubar::MenubarIntent; use side_panel::{GuiSidePanel, SidePanelIntent}; -use std::{ - path::PathBuf, - sync::mpsc::{channel, Receiver, Sender}, - thread, -}; +use std::path::PathBuf; use util::{ backend_broker::{BackendBroker, BackendIntent}, dialog_manager::{DialogIntent, DialogManager}, @@ -34,26 +30,16 @@ struct Gui { textures_manager: TexturesManager, backend_broker: BackendBroker, - - sender: Sender<(usize, ChannelDTO)>, - receiver: Receiver<(usize, ChannelDTO)>, -} - -enum ChannelDTO { - Generic, } impl eframe::App for Gui { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { - self.receive_msgs(); - self.update_ui(ctx); } } impl Gui { fn new() -> Self { - let (sender, receiver) = channel(); Self { side_panel: GuiSidePanel::new(), main_panel: MainPanel::new(), @@ -63,18 +49,6 @@ impl Gui { textures_manager: TexturesManager::new(), backend_broker: BackendBroker::new(), - - sender, - receiver, - } - } - - fn receive_msgs(&mut self) { - while let Ok((status_index, channel_dto)) = self.receiver.try_recv() { - self.status_entries.decrease(status_index); - match channel_dto { - ChannelDTO::Generic => {} - } } } @@ -193,14 +167,7 @@ impl Gui { MenubarIntent::ZoomIn => gui_zoom::zoom_in(ctx), MenubarIntent::ZoomOut => gui_zoom::zoom_out(ctx), MenubarIntent::RecoverStore => { - let index = self - .status_entries - .push(String::from("Recovering Codchi store..."), 1); - let sender_clone = self.sender.clone(); - thread::spawn(move || { - let _ = crate::platform::platform::store_recover(); - sender_clone.send((index, ChannelDTO::Generic)).unwrap(); - }); + self.backend_broker.recover_store(&mut self.status_entries) } MenubarIntent::ShowTray(val) => { let mut doc = CodchiConfig::open_mut().expect("Failed to open config"); diff --git a/codchi/src/gui/util/backend_broker.rs b/codchi/src/gui/util/backend_broker.rs index 7145c7be..c5746059 100644 --- a/codchi/src/gui/util/backend_broker.rs +++ b/codchi/src/gui/util/backend_broker.rs @@ -433,6 +433,16 @@ impl BackendBroker { sender_clone.send((status_index, dto)).unwrap(); }); } + + pub fn recover_store(&mut self, status_entries: &mut StatusEntries) { + let index = status_entries.create_entry(String::from("Recovering Codchi store...")); + + let sender_clone = self.sender.clone(); + thread::spawn(move || { + let _ = crate::platform::platform::store_recover(); + sender_clone.send((index, ChannelDTO::Default)).unwrap(); + }); + } } fn access_repo(auth_url: &AuthUrl) -> Result> { From aa6d55844aff8e9d4a9dcc5415e61fba9e3f364c Mon Sep 17 00:00:00 2001 From: paizin <44706868+paizin@users.noreply.github.com> Date: Wed, 21 May 2025 11:27:08 +0000 Subject: [PATCH 49/49] Enabled light/dark mode toggle --- codchi/src/gui/menubar.rs | 4 ++++ codchi/src/gui/mod.rs | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/codchi/src/gui/menubar.rs b/codchi/src/gui/menubar.rs index 5b238543..47e80b8f 100644 --- a/codchi/src/gui/menubar.rs +++ b/codchi/src/gui/menubar.rs @@ -9,6 +9,7 @@ pub enum MenubarIntent { Home, OpenGithub, OpenIssues, + ToggleMode, ZoomIn, ZoomOut, RecoverStore, @@ -93,6 +94,9 @@ fn update_settings_menu(ui: &mut Ui, textures_manager: &mut TexturesManager) -> None => Button::new("Settings"), }; menu::menu_custom_button(ui, menu_button, |ui| { + if ui.button("Change mode").clicked() { + intent.push(MenubarIntent::ToggleMode); + } if ui.button("Zoom In").clicked() { intent.push(MenubarIntent::ZoomIn); ui.close_menu(); diff --git a/codchi/src/gui/mod.rs b/codchi/src/gui/mod.rs index 3bb02704..ee2b5f89 100644 --- a/codchi/src/gui/mod.rs +++ b/codchi/src/gui/mod.rs @@ -164,6 +164,13 @@ impl Gui { MenubarIntent::OpenIssues => ctx.open_url(OpenUrl::new_tab( "https://github.com/aformatik/codchi/issues", )), + MenubarIntent::ToggleMode => { + if ctx.style().visuals.dark_mode { + ctx.set_visuals(light_visuals()); + } else { + ctx.set_visuals(dark_visuals()); + } + } MenubarIntent::ZoomIn => gui_zoom::zoom_in(ctx), MenubarIntent::ZoomOut => gui_zoom::zoom_out(ctx), MenubarIntent::RecoverStore => { @@ -424,7 +431,7 @@ pub fn run() -> Result<()> { options, Box::new(|cc| { // set custom theme - cc.egui_ctx.set_visuals(get_visuals()); + cc.egui_ctx.set_visuals(dark_visuals()); // This gives us image support: egui_extras::install_image_loaders(&cc.egui_ctx); @@ -440,9 +447,12 @@ pub fn run() -> Result<()> { Ok(()) } -pub fn get_visuals() -> Visuals { - let mut visuals = Visuals::dark(); - visuals.widgets.active.fg_stroke.color = Color32::WHITE; - visuals.widgets.noninteractive.fg_stroke.color = Color32::LIGHT_GRAY; +pub fn light_visuals() -> Visuals { + let visuals = Visuals::light(); + visuals +} + +pub fn dark_visuals() -> Visuals { + let visuals = Visuals::dark(); visuals }