commit 175e7c23e989d25d0f7d97a2d4201853ad4b8eab Author: Victor Fuentes Date: Fri Aug 26 17:48:43 2022 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..749ee00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.envrc +/.direnv +/.vscode +/result +/src/config.rs +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3deb418 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3089 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.7", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "ammonia" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b477377562f3086b7778d241786e9406b883ccfaa03557c0fe0924b9349f13a" +dependencies = [ + "html5ever 0.26.0", + "maplit", + "once_cell", + "tendril", + "url", +] + +[[package]] +name = "anyhow" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305" + +[[package]] +name = "async-broadcast" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" +dependencies = [ + "event-listener", + "futures-core", + "parking_lot", +] + +[[package]] +name = "async-oneshot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec7c75bcbcb0139e9177f30692fd617405ca4e0c27802e128d53171f7042e2c" +dependencies = [ + "futures-micro", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + +[[package]] +name = "bytemuck" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + +[[package]] +name = "cairo-rs" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b6ad25ae296159fb0da12b970b2fe179b234584d7cd294c891e2bbb284466b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-expr" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aacacf4d96c24b2ad6eb8ee6df040e4f27b0d0b39a5710c30091baa830485db" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "countme" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328b822bdcba4d4e402be8d9adb6eebf269f969f8eadef977a553ff3c4fbcb58" + +[[package]] +name = "cpufeatures" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "once_cell", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "dashmap" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if", + "num_cpus", +] + +[[package]] +name = "deflate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" +dependencies = [ + "adler32", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "edit-distance" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbaaaf38131deb9ca518a274a45bfdb8771f139517b073b16c2d3d32ae5037b" + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "exr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c26a90d9dd411a3d119d6f55752fb4c134ca243250c32fb9cab7b2561638d2" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "smallvec", + "threadpool", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "field-offset" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab30e97ab6aacfe635fad58f22c2bb06c8b685f7421eb1e064a729e2a5f481fa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" + +[[package]] +name = "futures-executor" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5" + +[[package]] +name = "futures-macro" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0db9cce532b0eae2ccf2766ab246f114b56b9cf6d445e00c2549fbc100ca045d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-micro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b460264b3593d68b16a7bc35f7bc226ddfebdf9a1c8db1ed95d5cc6b7168c826" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "futures-sink" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765" + +[[package]] +name = "futures-task" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" + +[[package]] +name = "futures-util" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "bitflags", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.5.0" +source = "git+https://github.com/gtk-rs/gtk4-rs#d6139f4e294437491a72f66de9d8363bf7c004d1" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.5.0" +source = "git+https://github.com/gtk-rs/gtk4-rs#d6139f4e294437491a72f66de9d8363bf7c004d1" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + +[[package]] +name = "gio" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "rustc-hash", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "anyhow", + "heck", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.5.0" +source = "git+https://github.com/gtk-rs/gtk4-rs#d6139f4e294437491a72f66de9d8363bf7c004d1" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.5.0" +source = "git+https://github.com/gtk-rs/gtk4-rs#d6139f4e294437491a72f66de9d8363bf7c004d1" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.5.0" +source = "git+https://github.com/gtk-rs/gtk4-rs#d6139f4e294437491a72f66de9d8363bf7c004d1" +dependencies = [ + "bitflags", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "once_cell", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.5.0" +source = "git+https://github.com/gtk-rs/gtk4-rs#d6139f4e294437491a72f66de9d8363bf7c004d1" +dependencies = [ + "anyhow", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.5.0" +source = "git+https://github.com/gtk-rs/gtk4-rs#d6139f4e294437491a72f66de9d8363bf7c004d1" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "h2" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "halfbrown" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce69ed202df415a3d4a01e6f3341320ca88b9bd4f0bf37be6fa239cdea06d9bf" +dependencies = [ + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html2pango" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a7f65103a4da1b629f519474a51ae89077c61f88954eb9e6df7b22e1a7fd98" +dependencies = [ + "ammonia", + "anyhow", + "html5ever 0.25.2", + "lazy_static", + "linkify", + "maplit", + "markup5ever_rcdom", + "regex", +] + +[[package]] +name = "html5ever" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" +dependencies = [ + "log", + "mac", + "markup5ever 0.10.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever 0.11.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "hyper" +version = "0.14.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ijson" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96214564d1f12875bd9661b183d8494dd10e373cb693629536fe2f3125e254b" +dependencies = [ + "dashmap", + "lazy_static", + "serde", + "serde_json", +] + +[[package]] +name = "image" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "jpeg-decoder" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libadwaita" +version = "0.2.0" +source = "git+https://gitlab.gnome.org/World/Rust/libadwaita-rs#01881b0c9f67ed5a351b78fcd9b172fee246fdd4" +dependencies = [ + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "gtk4", + "libadwaita-sys", + "libc", + "once_cell", + "pango", +] + +[[package]] +name = "libadwaita-sys" +version = "0.2.0" +source = "git+https://gitlab.gnome.org/World/Rust/libadwaita-rs#01881b0c9f67ed5a351b78fcd9b172fee246fdd4" +dependencies = [ + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk4-sys", + "libc", + "system-deps", +] + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "linkify" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03951527dd24d2c59f407502e7d88e0948ef06fac23335b556a4c2bc03c22096" +dependencies = [ + "memchr", +] + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b" +dependencies = [ + "html5ever 0.25.2", + "markup5ever 0.10.1", + "tendril", + "xml5ever", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.7", +] + +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nix-editor" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf248c2a02080d305cf67a51b989755e29a4256e896ed091cbb5fad578cf032" +dependencies = [ + "clap", + "failure", + "owo-colors", + "rnix", +] + +[[package]] +name = "nix-software-center" +version = "0.0.1" +dependencies = [ + "brotli", + "edit-distance", + "flate2", + "html2pango", + "ijson", + "image", + "libadwaita", + "log", + "nix-editor", + "pretty_env_logger", + "quick-xml", + "rand 0.8.5", + "relm4", + "relm4-components", + "reqwest", + "serde", + "serde_json", + "serde_yaml", + "sha256", + "simd-json", + "sourceview5", + "spdx", + "strum", + "strum_macros", + "tokio", + "tracker", +] + +[[package]] +name = "nsc-helper" +version = "0.1.0" +dependencies = [ + "clap", + "users", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "pango" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "bitflags", + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.16.0" +source = "git+https://github.com/gtk-rs/gtk-rs-core#679b3717abeea7ef0837995180d8a85dc342dadd" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "png" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-crate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9279fbdacaad3baf559d8cabe0acc3d06e30ea14931af31af79578ac0946decc" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.7", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "relm4" +version = "0.5.0-beta.2" +source = "git+https://github.com/Relm4/Relm4?tag=v0.5.0-beta.2#7b0a9387833e6a1d6bf6afbba323451b06b0fb08" +dependencies = [ + "async-broadcast", + "async-oneshot", + "flume", + "futures", + "gtk4", + "libadwaita", + "log", + "once_cell", + "relm4-macros", + "tokio", + "tracing", +] + +[[package]] +name = "relm4-components" +version = "0.5.0-beta.2" +source = "git+https://github.com/Relm4/Relm4?tag=v0.5.0-beta.2#7b0a9387833e6a1d6bf6afbba323451b06b0fb08" +dependencies = [ + "log", + "once_cell", + "relm4", + "tracker", +] + +[[package]] +name = "relm4-macros" +version = "0.5.0-beta.2" +source = "git+https://github.com/Relm4/Relm4?tag=v0.5.0-beta.2#7b0a9387833e6a1d6bf6afbba323451b06b0fb08" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rnix" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8024a523e8836f1a5d051203dc00d833357fee94e351b51348dfaeca5364daa9" +dependencies = [ + "cbitset", + "rowan", + "smol_str", +] + +[[package]] +name = "rowan" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b36e449f3702f3b0c821411db1cbdf30fb451726a9456dce5dabcd44420043" +dependencies = [ + "countme", + "hashbrown 0.9.1", + "memoffset", + "rustc-hash", + "text-size", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[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.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a09f551ccc8210268ef848f0bab37b306e87b85b2e017b899e7fb815f5aed62" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "sha256" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e84a7f596c081d359de5e06a83877138bc3c4483591e1af1916e1472e6e146e" +dependencies = [ + "hex", + "sha2", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-json" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd78b840b9de64fa3f7d72909b76343849f68e8c3d32608db8d38e4e5481f84" +dependencies = [ + "halfbrown", + "serde", + "serde_json", + "simdutf8", + "value-trait", +] + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "smol_str" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7475118a28b7e3a2e157ce0131ba8c5526ea96e90ee601d9f6bb2e286a35ab44" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c98bba371b9b22a71a9414e420f92ddeb2369239af08200816169d5e2dd7aa" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "sourceview5" +version = "0.3.0" +source = "git+https://gitlab.gnome.org/World/Rust/sourceview5-rs/?rev=6082210f7d1fc32b100bd9c714e9521eecacb3f7#6082210f7d1fc32b100bd9c714e9521eecacb3f7" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "gtk4", + "libc", + "pango", + "sourceview5-sys", +] + +[[package]] +name = "sourceview5-sys" +version = "0.3.0" +source = "git+https://gitlab.gnome.org/World/Rust/sourceview5-rs/?rev=6082210f7d1fc32b100bd9c714e9521eecacb3f7#6082210f7d1fc32b100bd9c714e9521eecacb3f7" +dependencies = [ + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "spdx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a346909b3fd07776f9b96b98d4a58e3666f831c9a672c279b10f795a34c36425" +dependencies = [ + "smallvec", +] + +[[package]] +name = "spin" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +dependencies = [ + "lock_api", +] + +[[package]] +name = "string_cache" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "system-deps" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "text-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a" + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiff" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7259662e32d1e219321eb309d5f9d898b779769d81b76e762c07c8e5d38fcb65" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66d89d37f24af7a53e394a412441c803df73f1a5adfcb3b9c37a2e0a75392eb" +dependencies = [ + "tracker-macros", +] + +[[package]] +name = "tracker-macros" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca57dc00ed70e0acce16b1a4994ba9caf7718b9247382285d5e5192d3f6cd8d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "ucd-trie" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "931179334a56395bcf64ba5e0ff56781381c1a5832178280c7d7f91d1679aeb0" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "value-trait" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a635407649b66e125e4d2ffd208153210179f8c7c8b71c030aa2ad3eeb4c8f" +dependencies = [ + "float-cmp", + "halfbrown", + "itoa", + "ryu", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" + +[[package]] +name = "web-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "xml5ever" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9234163818fd8e2418fcde330655e757900d4236acd8cc70fef345ef91f6d865" +dependencies = [ + "log", + "mac", + "markup5ever 0.10.1", + "time", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..88c17ed --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "nix-software-center" +version = "0.0.1" +edition = "2021" +default-run = "nix-software-center" + +[dependencies] +relm4 = { git = "https://github.com/Relm4/Relm4", tag = "v0.5.0-beta.2", features = ["all"] } +relm4-components = { package = "relm4-components", git = "https://github.com/Relm4/Relm4", tag = "v0.5.0-beta.2"} +adw = { package = "libadwaita", git = "https://gitlab.gnome.org/World/Rust/libadwaita-rs", features = ["v1_2"] } +tokio = { version = "1.20", features = ["rt", "macros", "time", "rt-multi-thread", "sync", "process"] } +sourceview5 = { git = "https://gitlab.gnome.org/World/Rust/sourceview5-rs/", rev = "6082210f7d1fc32b100bd9c714e9521eecacb3f7", features = ["v5_4"] } +tracker = "0.1" + +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.9" +simd-json = { version = "0.6", features = ["allow-non-simd"] } + +nix-editor = "0.2.12" + +html2pango = "0.4" +brotli = "3.3" +log = "0.4" +pretty_env_logger = "0.4" +flate2 = "1.0" +quick-xml = { version = "0.23", features = ["serialize"] } +rand = "0.8" +reqwest = { version = "0.11", features = ["blocking"] } +sha256 = "1.0" +image = "0.24" +spdx = "0.9" +# { git = "https://github.com/EmbarkStudios/spdx", version = "0.8" } +edit-distance = "2.1" +ijson = "0.1" +strum = "0.24" +strum_macros = "0.24" + +[workspace] +members = [".", "nsc-helper"] +default-members = [".", "nsc-helper"] diff --git a/build-aux/dist-vendor.sh b/build-aux/dist-vendor.sh new file mode 100644 index 0000000..216edd7 --- /dev/null +++ b/build-aux/dist-vendor.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +export SOURCE_ROOT="$1" +export DIST="$2" + +cd "$SOURCE_ROOT" +mkdir "$DIST"/.cargo +cargo vendor | sed 's/^directory = ".*"/directory = "vendor"/g' > $DIST/.cargo/config +# Move vendor into dist tarball directory +mv vendor "$DIST" diff --git a/data/dev.vlinkz.NixSoftwareCenter.desktop.in.in b/data/dev.vlinkz.NixSoftwareCenter.desktop.in.in new file mode 100644 index 0000000..6edf0a7 --- /dev/null +++ b/data/dev.vlinkz.NixSoftwareCenter.desktop.in.in @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Software Center +Comment=Install Applications +Type=Application +Exec=nix-software-center +Terminal=false +Categories=Settings;System;Utility; +# Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! +Keywords=Nix;Nixos;nix;nixos;software;package; +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=@icon@ +StartupNotify=true diff --git a/data/dev.vlinkz.NixSoftwareCenter.gschema.xml.in b/data/dev.vlinkz.NixSoftwareCenter.gschema.xml.in new file mode 100644 index 0000000..c6318c4 --- /dev/null +++ b/data/dev.vlinkz.NixSoftwareCenter.gschema.xml.in @@ -0,0 +1,5 @@ + + + + + diff --git a/data/dev.vlinkz.NixSoftwareCenter.metainfo.xml.in.in b/data/dev.vlinkz.NixSoftwareCenter.metainfo.xml.in.in new file mode 100644 index 0000000..c675487 --- /dev/null +++ b/data/dev.vlinkz.NixSoftwareCenter.metainfo.xml.in.in @@ -0,0 +1,27 @@ + + + @app-id@ + CC-BY-SA-4.0 + MIT + Nix Software Center + A simple application to manage your NixOS packages. + +

A simple software center to install and manage Nix packages, built with libadwaita, GTK4, and Relm4.

+
+ + + + https://github.com/vlinkz/nix-software-center + https://github.com/vlinkz/nix-software-center/issues + Victor Fuentes + vmfuentes64@gmail.com + @gettext-package@ + @app-id@.desktop +
diff --git a/data/dev.vlinkz.NixSoftwareCenter.policy.in.in b/data/dev.vlinkz.NixSoftwareCenter.policy.in.in new file mode 100644 index 0000000..4dd441c --- /dev/null +++ b/data/dev.vlinkz.NixSoftwareCenter.policy.in.in @@ -0,0 +1,17 @@ + + + + Victor Fuentes + https://github.com/vlinkz + + Give Nix Software Center root access + Authentication is required to install NixOS system packages + + no + no + auth_admin_keep + + @pkglibexecdir@/nsc-helper + + diff --git a/data/icons/dev.vlinkz.NixSoftwareCenter-symbolic.svg b/data/icons/dev.vlinkz.NixSoftwareCenter-symbolic.svg new file mode 100644 index 0000000..e69de29 diff --git a/data/icons/dev.vlinkz.NixSoftwareCenter.Devel.svg b/data/icons/dev.vlinkz.NixSoftwareCenter.Devel.svg new file mode 100644 index 0000000..c434e2b --- /dev/null +++ b/data/icons/dev.vlinkz.NixSoftwareCenter.Devel.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/dev.vlinkz.NixSoftwareCenter.svg b/data/icons/dev.vlinkz.NixSoftwareCenter.svg new file mode 100644 index 0000000..057a0a1 --- /dev/null +++ b/data/icons/dev.vlinkz.NixSoftwareCenter.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/dev.vlinkz.NixSoftwareCenter.template.svg b/data/icons/dev.vlinkz.NixSoftwareCenter.template.svg new file mode 100644 index 0000000..28f9421 --- /dev/null +++ b/data/icons/dev.vlinkz.NixSoftwareCenter.template.svg @@ -0,0 +1,825 @@ + + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + GNOME Design Team + + + + + Adwaita Icon Template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hicolor + Symbolic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/meson.build b/data/icons/meson.build new file mode 100644 index 0000000..2ab86e9 --- /dev/null +++ b/data/icons/meson.build @@ -0,0 +1,10 @@ +install_data( + '@0@.svg'.format(application_id), + install_dir: iconsdir / 'hicolor' / 'scalable' / 'apps' +) + +install_data( + '@0@-symbolic.svg'.format(base_id), + install_dir: iconsdir / 'hicolor' / 'symbolic' / 'apps', + rename: '@0@-symbolic.svg'.format(application_id) +) diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..7bdcad9 --- /dev/null +++ b/data/meson.build @@ -0,0 +1,93 @@ +subdir('icons') +# Desktop file +desktop_conf = configuration_data() +desktop_conf.set('icon', application_id) +desktop_file = i18n.merge_file( + type: 'desktop', + input: configure_file( + input: '@0@.desktop.in.in'.format(base_id), + output: '@BASENAME@', + configuration: desktop_conf + ), + output: '@0@.desktop'.format(application_id), + po_dir: podir, + install: true, + install_dir: datadir / 'applications' +) +# Validate Desktop file +if desktop_file_validate.found() + test( + 'validate-desktop', + desktop_file_validate, + args: [ + desktop_file.full_path() + ], + depends: desktop_file, + ) +endif + +# Appdata +appdata_conf = configuration_data() +appdata_conf.set('app-id', application_id) +appdata_conf.set('gettext-package', gettext_package) +appdata_file = i18n.merge_file( + input: configure_file( + input: '@0@.metainfo.xml.in.in'.format(base_id), + output: '@BASENAME@', + configuration: appdata_conf + ), + output: '@0@.metainfo.xml'.format(application_id), + po_dir: podir, + install: true, + install_dir: datadir / 'metainfo' +) + +# Validate Appdata +if appstream_util.found() + test( + 'validate-appdata', appstream_util, + args: [ + 'validate', '--nonet', appdata_file.full_path() + ], + depends: appdata_file, + ) +endif + +# Policy file +dataconf = configuration_data() +dataconf.set('pkglibexecdir', + libexecdir +) +i18n.merge_file( + input : configure_file( + configuration: dataconf, + input : '@0@.policy.in.in'.format(base_id), + output: '@BASENAME@' + ), + output: '@0@.policy'.format(base_id), + po_dir: podir, + install: true, + install_dir: datadir / 'polkit-1' / 'actions' +) + +# GSchema +gschema_conf = configuration_data() +gschema_conf.set('app-id', application_id) +gschema_conf.set('gettext-package', gettext_package) +configure_file( + input: '@0@.gschema.xml.in'.format(base_id), + output: '@0@.gschema.xml'.format(application_id), + configuration: gschema_conf, + install: true, + install_dir: datadir / 'glib-2.0' / 'schemas' +) + +# Validata GSchema +if glib_compile_schemas.found() + test( + 'validate-gschema', glib_compile_schemas, + args: [ + '--strict', '--dry-run', meson.current_build_dir() + ], + ) +endif diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..e47e8d5 --- /dev/null +++ b/default.nix @@ -0,0 +1,71 @@ +{ + pkgs ? import {}, + lib ? import , +}: +let + libadwaita-git = pkgs.libadwaita.overrideAttrs (oldAttrs: rec { + version = "1.2.beta"; + src = pkgs.fetchFromGitLab { + domain = "gitlab.gnome.org"; + owner = "GNOME"; + repo = "libadwaita"; + rev = version; + hash = "sha256-QBblkeNAgfHi5YQxaV9ceqNDyDIGu8d6pvLcT6apm6o="; + }; + }); + nixos-appstream-data = (import (pkgs.fetchFromGitHub { + owner = "vlinkz"; + repo = "nixos-appstream-data"; + rev = "66b3399e6d81017c10265611a151d1109ff1af1b"; + hash = "sha256-oiEZD4sMpb2djxReg99GUo0RHWAehxSyQBbiz8Z4DJk="; + }) {stdenv = pkgs.stdenv; lib = pkgs.lib; pkgs = pkgs; }); +in pkgs.stdenv.mkDerivation rec { + pname = "nix-software-center"; + version = "0.0.1"; + + src = [ ./. ]; + + cargoDeps = pkgs.rustPlatform.fetchCargoTarball { + inherit src; + name = "${pname}-${version}"; + hash = "sha256-EI9zULrlN+GvtDO0PvtAEA1YjJAbK+SDZ8NSRZf+2Rw="; + }; + + nativeBuildInputs = with pkgs; [ + appstream-glib + polkit + gettext + desktop-file-utils + meson + ninja + pkg-config + git + wrapGAppsHook4 + ] ++ (with pkgs.rustPlatform; [ + cargoSetupHook + rust.cargo + rust.rustc + ]); + + buildInputs = with pkgs; [ + gdk-pixbuf + glib + gtk4 + gtksourceview5 + libadwaita-git + openssl + wayland + gnome.adwaita-icon-theme + desktop-file-utils + nixos-appstream-data + ]; + + # mesonFlags = [ + # "-Dprofile=development" + # ]; + + patchPhase = '' + substituteInPlace ./src/lib.rs \ + --replace "/usr/share/app-info" "${nixos-appstream-data}/share/app-info" + ''; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..36c7d0d --- /dev/null +++ b/flake.lock @@ -0,0 +1,109 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1642700792, + "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "mach-nix": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "pypi-deps-db": "pypi-deps-db" + }, + "locked": { + "lastModified": 1661359648, + "narHash": "sha256-BRz30Xg/g535oRJA3xEcXf0KM6GTJPugt2lgaom3D6g=", + "owner": "DavHau", + "repo": "mach-nix", + "rev": "6cd3929b1561c3eef68f5fc6a08b57cf95c41ec1", + "type": "github" + }, + "original": { + "id": "mach-nix", + "type": "indirect" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1643805626, + "narHash": "sha256-AXLDVMG+UaAGsGSpOtQHPIKB+IZ0KSd9WS77aanGzgc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "554d2d8aa25b6e583575459c297ec23750adb6cb", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1661361016, + "narHash": "sha256-Bjf6ZDnDc6glTwIIItvwfcaeJ5zWFM6GYfPajSArdUY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b784c5ae63dd288375af1b4d37b8a27dd8061887", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pypi-deps-db": { + "flake": false, + "locked": { + "lastModified": 1661155889, + "narHash": "sha256-t00mBTZhmZBT4jteO6pJbU0wyRS6/ep4pKmQNeztEms=", + "owner": "DavHau", + "repo": "pypi-deps-db", + "rev": "49c620f3de2b557c9d5c44f58a00fee59f27d1b0", + "type": "github" + }, + "original": { + "owner": "DavHau", + "repo": "pypi-deps-db", + "type": "github" + } + }, + "root": { + "inputs": { + "mach-nix": "mach-nix", + "nixpkgs": "nixpkgs_2", + "utils": "utils" + } + }, + "utils": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..53f5063 --- /dev/null +++ b/flake.nix @@ -0,0 +1,71 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, utils, mach-nix }: + utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + }; + libadwaita-git = pkgs.libadwaita.overrideAttrs (oldAttrs: rec { + version = "1.2.beta"; + src = pkgs.fetchFromGitLab { + domain = "gitlab.gnome.org"; + owner = "GNOME"; + repo = "libadwaita"; + rev = version; + hash = "sha256-QBblkeNAgfHi5YQxaV9ceqNDyDIGu8d6pvLcT6apm6o="; + }; + }); + nixos-appstream-data = pkgs.fetchFromGitHub { + owner = "vlinkz"; + repo = "nixos-appstream-data"; + rev = "66b3399e6d81017c10265611a151d1109ff1af1b"; + hash = "sha256-oiEZD4sMpb2djxReg99GUo0RHWAehxSyQBbiz8Z4DJk="; + }; + name = "nix-software-center"; + in + rec + { + packages.${name} = pkgs.callPackage ./default.nix { + inherit (inputs); + }; + + # `nix build` + defaultPackage = packages.${name}; + + # `nix run` + apps.${name} = utils.lib.mkApp { + inherit name; + drv = packages.${name}; + }; + defaultApp = packages.${name}; + + devShell = pkgs.mkShell { + buildInputs = with pkgs; [ + cargo + clippy + rust-analyzer + rustc + rustfmt + cairo + gdk-pixbuf + gobject-introspection + graphene + gtk4 + gtksourceview5 + libadwaita-git + openssl + pandoc + pango + pkgconfig + wrapGAppsHook4 + nixos-appstream-data + ]; + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + }; + }); +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..30b8497 --- /dev/null +++ b/meson.build @@ -0,0 +1,70 @@ +project( + 'nix-software-center', + 'rust', + version: '0.0.1', + meson_version: '>= 0.59', + license: 'MIT', +) + +i18n = import('i18n') +gnome = import('gnome') + +base_id = 'dev.vlinkz.NixSoftwareCenter' + +dependency('openssl', version: '>= 1.0') +dependency('glib-2.0', version: '>= 2.66') +dependency('gio-2.0', version: '>= 2.66') +dependency('gtk4', version: '>= 4.0.0') +dependency('libadwaita-1', version: '>=1.2.alpha') +dependency('polkit-gobject-1', version: '>= 0.103') + +glib_compile_resources = find_program('glib-compile-resources', required: true) +glib_compile_schemas = find_program('glib-compile-schemas', required: true) +desktop_file_validate = find_program('desktop-file-validate', required: false) +appstream_util = find_program('appstream-util', required: false) +cargo = find_program('cargo', required: true) + +version = meson.project_version() + +prefix = get_option('prefix') +bindir = prefix / get_option('bindir') +libexecdir = prefix / get_option('libexecdir') +localedir = prefix / get_option('localedir') + +datadir = prefix / get_option('datadir') +pkgdatadir = datadir / meson.project_name() +iconsdir = datadir / 'icons' +podir = meson.project_source_root() / 'po' +gettext_package = meson.project_name() + +if get_option('profile') == 'development' + profile = 'Devel' + vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip() + if vcs_tag == '' + version_suffix = '-devel' + else + version_suffix = '-@0@'.format(vcs_tag) + endif + application_id = '@0@.@1@'.format(base_id, profile) +else + profile = '' + version_suffix = '' + application_id = base_id +endif + +meson.add_dist_script( + 'build-aux/dist-vendor.sh', + meson.project_build_root() / 'meson-dist' / meson.project_name() + '-' + version, + meson.project_source_root() +) + +subdir('data') +subdir('po') +subdir('src') +subdir('nsc-helper/src') + +gnome.post_install( + gtk_update_icon_cache: true, + glib_compile_schemas: true, + update_desktop_database: true, +) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..6e2ca96 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,9 @@ +option( + 'profile', + type: 'combo', + choices: [ + 'default', + 'development' + ], + value: 'default', +) diff --git a/nsc-helper/.gitignore b/nsc-helper/.gitignore new file mode 100644 index 0000000..0f84cc9 --- /dev/null +++ b/nsc-helper/.gitignore @@ -0,0 +1,2 @@ +/target +/.vscode \ No newline at end of file diff --git a/nsc-helper/Cargo.toml b/nsc-helper/Cargo.toml new file mode 100644 index 0000000..965a721 --- /dev/null +++ b/nsc-helper/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nsc-helper" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "3.2", features = ["derive"] } +users = "0.11" + +[[bin]] +name = "nsc-helper" +path = "src/main.rs" \ No newline at end of file diff --git a/nsc-helper/src/main.rs b/nsc-helper/src/main.rs new file mode 100644 index 0000000..78bb6e4 --- /dev/null +++ b/nsc-helper/src/main.rs @@ -0,0 +1,134 @@ +use clap::{self, FromArgMatches, Subcommand}; +use std::{ + error::Error, + fs::{File, self}, + io::{self, Read, Write}, + process::Command, +}; + +#[derive(clap::Subcommand)] +enum SubCommands { + Config { + /// Write stdin to file in path output + #[clap(short, long)] + output: String, + /// Run `nixos-rebuild` with the given arguments + arguments: Vec, + }, + Rebuild { + /// Run `nixos-rebuild` with the given arguments + arguments: Vec, + }, + Channel { + /// Whether to rebuild the system after updating channels + #[clap(short, long)] + rebuild: bool, + /// Run `nixos-rebuild` with the given arguments + arguments: Vec, + } +} + +fn main() { + let cli = SubCommands::augment_subcommands(clap::Command::new( + "Helper binary for Nix Software Center", + )); + let matches = cli.get_matches(); + let derived_subcommands = SubCommands::from_arg_matches(&matches) + .map_err(|err| err.exit()) + .unwrap(); + + if users::get_effective_uid() != 0 { + eprintln!("nsc-helper must be run as root"); + std::process::exit(1); + } + + match derived_subcommands { + SubCommands::Config { output, arguments } => { + let old = fs::read_to_string(&output); + match write_file(&output) { + Ok(_) => match rebuild(arguments) { + Ok(_) => {} + Err(err) => { + eprintln!("{}", err); + if let Ok(o) = old { + if fs::write(&output, o).is_err() { + eprintln!("Could not restore old file"); + } + } + std::process::exit(1); + } + }, + Err(err) => { + eprintln!("{}", err); + std::process::exit(1); + } + }; + } + SubCommands::Rebuild { arguments } => match rebuild(arguments) { + Ok(_) => (), + Err(err) => { + eprintln!("{}", err); + std::process::exit(1); + } + }, + SubCommands::Channel { rebuild: dorebuild, arguments } => { + match dorebuild { + true => match rebuild(arguments) { + Ok(_) => (), + Err(err) => { + eprintln!("{}", err); + std::process::exit(1); + } + }, + false => match channel() { + Ok(_) => (), + Err(err) => { + eprintln!("{}", err); + std::process::exit(1); + } + }, + } + } + } +} + +fn write_file(path: &str) -> Result<(), Box> { + let stdin = io::stdin(); + let mut buf = String::new(); + stdin.lock().read_to_string(&mut buf)?; + let mut file = File::create(path)?; + write!(file, "{}", &buf)?; + Ok(()) +} + +fn rebuild(args: Vec) -> Result<(), Box> { + let mut cmd = Command::new("nixos-rebuild") + .args(args) + .spawn()?; + let x = cmd.wait()?; + if x.success() { + Ok(()) + } else { + eprintln!("nixos-rebuild failed with exit code {}", x.code().unwrap()); + Err(Box::new(io::Error::new( + io::ErrorKind::Other, + "nixos-rebuild failed", + ))) + } +} + +fn channel() -> Result<(), Box> { + let mut cmd = Command::new("nix-channel") + .arg("--update") + .spawn()?; + let x = cmd.wait()?; + if x.success() { + Ok(()) + } else { + eprintln!("nixos-rebuild failed with exit code {}", x.code().unwrap()); + Err(Box::new(io::Error::new( + io::ErrorKind::Other, + "nix-channel failed", + ))) + } +} \ No newline at end of file diff --git a/nsc-helper/src/meson.build b/nsc-helper/src/meson.build new file mode 100644 index 0000000..977fe4e --- /dev/null +++ b/nsc-helper/src/meson.build @@ -0,0 +1,32 @@ +global_conf = configuration_data() +cargo_options = [ '--manifest-path', meson.project_source_root() / 'nsc-helper' / 'Cargo.toml' ] +cargo_options += [ '--target-dir', meson.project_build_root() / 'nsc-helper' / 'src' ] + +if get_option('profile') == 'default' + cargo_options += [ '--release' ] + rust_target = 'release' + message('Building in release mode') +else + rust_target = 'debug' + message('Building in debug mode') +endif + +cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'nsc-helper' / 'cargo-home' ] + +cargo_build = custom_target( + 'cargo-build', + build_by_default: true, + build_always_stale: true, + output: 'nsc-helper', + console: true, + install: true, + install_dir: get_option('libexecdir'), + command: [ + 'env', + cargo_env, + cargo, 'build', + cargo_options, + '&&', + 'cp', 'src' / rust_target / 'nsc-helper', '@OUTPUT@', + ] +) diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..e69de29 diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..35e17b0 --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,4 @@ +data/dev.vlinkz.NixSoftwareCenter.policy.in.in +data/dev.vlinkz.NixSoftwareCenter.desktop.in.in +data/dev.vlinkz.NixSoftwareCenter.metainfo.xml.in.in +data/dev.vlinkz.NixSoftwareCenter.metainfo.gschema.xml.in diff --git a/po/meson.build b/po/meson.build new file mode 100644 index 0000000..6a87565 --- /dev/null +++ b/po/meson.build @@ -0,0 +1 @@ +i18n.gettext(gettext_package, preset: 'glib') \ No newline at end of file diff --git a/src/config.rs.in b/src/config.rs.in new file mode 100644 index 0000000..699897f --- /dev/null +++ b/src/config.rs.in @@ -0,0 +1,7 @@ +pub const APP_ID: &str = @APP_ID@; +pub const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@; +pub const LOCALEDIR: &str = @LOCALEDIR@; +pub const PKGDATADIR: &str = @PKGDATADIR@; +pub const PROFILE: &str = @PROFILE@; +pub const RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/resources.gresource"); +pub const VERSION: &str = @VERSION@; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..511d423 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod ui; +pub mod parse; +pub mod config; +static APPINFO: &str = "/usr/share/app-info"; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9c13c3b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,8 @@ +use nix_software_center::ui::window::AppModel; +use relm4::*; +fn main() { + pretty_env_logger::init(); + let app = RelmApp::new("dev.vlinkz.NixSoftwareCenter"); + let application = app.app.clone(); + app.run::(application); +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..ecca7c6 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,51 @@ +global_conf = configuration_data() +global_conf.set_quoted('APP_ID', application_id) +global_conf.set_quoted('PKGDATADIR', pkgdatadir) +global_conf.set_quoted('PROFILE', profile) +global_conf.set_quoted('VERSION', version + version_suffix) +global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package) +global_conf.set_quoted('LOCALEDIR', localedir) +config = configure_file( + input: 'config.rs.in', + output: 'config.rs', + configuration: global_conf +) +# Copy the config.rs output to the source directory. +run_command( + 'cp', + meson.project_build_root() / 'src' / 'config.rs', + meson.project_source_root() / 'src' / 'config.rs', + check: true +) + +cargo_options = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ] +cargo_options += [ '--target-dir', meson.project_build_root() / 'src' ] + +if get_option('profile') == 'default' + cargo_options += [ '--release' ] + rust_target = 'release' + message('Building in release mode') +else + rust_target = 'debug' + message('Building in debug mode') +endif + +cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ] + +cargo_build = custom_target( + 'cargo-build', + build_by_default: true, + build_always_stale: true, + output: meson.project_name(), + console: true, + install: true, + install_dir: bindir, + command: [ + 'env', + cargo_env, + cargo, 'build', + cargo_options, + '&&', + 'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@', + ] +) diff --git a/src/parse/cache.rs b/src/parse/cache.rs new file mode 100644 index 0000000..48c21fb --- /dev/null +++ b/src/parse/cache.rs @@ -0,0 +1,263 @@ +use ijson::IString; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + env, + error::Error, + fs::{self, File}, + io::{BufReader, Read, Write}, + path::Path, + process::Command, +}; + +#[derive(Serialize, Deserialize, Debug)] +struct NewPackageBase { + packages: HashMap, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct NewPackage { + version: IString, +} + +pub fn checkcache() -> Result<(), Box> { + setuppkgscache()?; + setupupdatecache()?; + setupnewestver()?; + Ok(()) +} + +pub fn uptodate() -> Result, Box> { + let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?); + let oldversion = fs::read_to_string(format!("{}/sysver.txt", cachedir))? + .trim() + .to_string(); + let newversion = fs::read_to_string(format!("{}/chnver.txt", cachedir))? + .trim() + .to_string(); + if oldversion == newversion { + println!("System is up to date"); + Ok(None) + } else { + println!("OLD {:?} != NEW {:?}", oldversion, newversion); + Ok(Some((oldversion, newversion))) + } +} + +pub fn channelver() -> Result, Box> { + let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?); + let oldversion = fs::read_to_string(format!("{}/chnver.txt", cachedir))? + .trim() + .to_string(); + let newversion = fs::read_to_string(format!("{}/newver.txt", cachedir))? + .trim() + .to_string(); + if oldversion == newversion { + println!("Channels match"); + Ok(None) + } else { + println!("chnver {:?} != newver {:?}", oldversion, newversion); + Ok(Some((oldversion, newversion))) + } +} + +fn setuppkgscache() -> Result<(), Box> { + let vout = Command::new("nix-instantiate") + .arg("-I") + .arg("nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos") + .arg("") + .arg("-A") + .arg("version") + .arg("--eval") + .arg("--json") + .output()?; + + let dlver = String::from_utf8_lossy(&vout.stdout) + .to_string() + .replace('"', ""); + + let mut relver = dlver.split('.').collect::>().join(".")[0..5].to_string(); + + if dlver.len() >= 8 && &dlver[5..8] == "pre" { + relver = "unstable".to_string(); + } + + let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?); + fs::create_dir_all(&cachedir).expect("Failed to create cache directory"); + let url = format!( + "https://releases.nixos.org/nixos/{}/nixos-{}/packages.json.br", + relver, dlver + ); + + println!("VERSION {}", relver); + // let response = reqwest::blocking::get(url)?; + // if let Some(latest) = response.url().to_string().split('/').last() { + let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?); + if !Path::new(&cachedir).exists() { + fs::create_dir_all(&cachedir).expect("Failed to create cache directory"); + } + + if !Path::new(&format!("{}/chnver.txt", &cachedir)).exists() { + let mut sysver = fs::File::create(format!("{}/chnver.txt", &cachedir))?; + sysver.write_all(dlver.as_bytes())?; + } + + if Path::new(format!("{}/chnver.txt", &cachedir).as_str()).exists() + && fs::read_to_string(&Path::new(format!("{}/chnver.txt", &cachedir).as_str()))? == dlver + && Path::new(format!("{}/packages.json", &cachedir).as_str()).exists() + { + return Ok(()); + } else { + let oldver = fs::read_to_string(&Path::new(format!("{}/chnver.txt", &cachedir).as_str()))?; + let sysver = &dlver; + // Change to debug msg + println!("OLD: {}, != NEW: {}", oldver, sysver); + } + if Path::new(format!("{}/chnver.txt", &cachedir).as_str()).exists() { + fs::remove_file(format!("{}/chnver.txt", &cachedir).as_str())?; + } + let mut sysver = fs::File::create(format!("{}/chnver.txt", &cachedir))?; + sysver.write_all(dlver.as_bytes())?; + let outfile = format!("{}/packages.json", &cachedir); + dlfile(&url, &outfile)?; + // } + Ok(()) +} + +fn setupupdatecache() -> Result<(), Box> { + let dlver = fs::read_to_string("/run/current-system/nixos-version")?; + + let mut relver = dlver.split('.').collect::>().join(".")[0..5].to_string(); + + if dlver.len() >= 8 && &dlver[5..8] == "pre" { + relver = "unstable".to_string(); + } + + let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?); + fs::create_dir_all(&cachedir).expect("Failed to create cache directory"); + let url = format!( + "https://releases.nixos.org/nixos/{}/nixos-{}/packages.json.br", + relver, dlver + ); + + println!("VERSION {}", relver); + let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?); + if !Path::new(&cachedir).exists() { + fs::create_dir_all(&cachedir).expect("Failed to create cache directory"); + } + + if !Path::new(&format!("{}/sysver.txt", &cachedir)).exists() { + let mut sysver = fs::File::create(format!("{}/sysver.txt", &cachedir))?; + sysver.write_all(dlver.as_bytes())?; + } + + if Path::new(format!("{}/sysver.txt", &cachedir).as_str()).exists() + && fs::read_to_string(&Path::new(format!("{}/sysver.txt", &cachedir).as_str()))? == dlver + && Path::new(format!("{}/syspackages.json", &cachedir).as_str()).exists() + { + return Ok(()); + } else { + let oldver = fs::read_to_string(&Path::new(format!("{}/sysver.txt", &cachedir).as_str()))?; + let sysver = &dlver; + // Change to debug msg + println!("OLD: {}, != NEW: {}", oldver, sysver); + } + if Path::new(format!("{}/sysver.txt", &cachedir).as_str()).exists() { + fs::remove_file(format!("{}/sysver.txt", &cachedir).as_str())?; + } + let mut sysver = fs::File::create(format!("{}/sysver.txt", &cachedir))?; + sysver.write_all(dlver.as_bytes())?; + let outfile = format!("{}/syspackages.json", &cachedir); + dlfile(&url, &outfile)?; + let file = File::open(&outfile)?; + let reader = BufReader::new(file); + let pkgbase: NewPackageBase = simd_json::serde::from_reader(reader).unwrap(); + let mut outbase = HashMap::new(); + for (pkg, ver) in pkgbase.packages { + outbase.insert(pkg.clone(), ver.version.clone()); + } + let out = simd_json::serde::to_string(&outbase)?; + fs::write(&outfile, out)?; + Ok(()) +} + +fn setupnewestver() -> Result<(), Box> { + let version = fs::read_to_string("/run/current-system/nixos-version")?; + + let mut relver = version.split('.').collect::>().join(".")[0..5].to_string(); + + if version.len() >= 8 && &version[5..8] == "pre" { + relver = "unstable".to_string(); + } + println!("VERSION {}", relver); + let response = reqwest::blocking::get(format!("https://channels.nixos.org/nixos-{}", relver))?; + if let Some(latest) = response.url().to_string().split('/').last() { + let latest = latest.strip_prefix("nixos-").unwrap_or(latest); + let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?); + if !Path::new(&cachedir).exists() { + fs::create_dir_all(&cachedir).expect("Failed to create cache directory"); + } + + if !Path::new(format!("{}/newver.txt", &cachedir).as_str()).exists() { + let mut newver = fs::File::create(format!("{}/newver.txt", &cachedir))?; + newver.write_all(latest.as_bytes())?; + } + + if Path::new(format!("{}/newver.txt", &cachedir).as_str()).exists() + && fs::read_to_string(&Path::new(format!("{}/newver.txt", &cachedir).as_str()))? + == latest + { + return Ok(()); + } else { + let oldver = + fs::read_to_string(&Path::new(format!("{}/newver.txt", &cachedir).as_str()))?; + let newver = latest; + // Change to debug msg + println!("OLD: {}, != NEW: {}", oldver, newver); + } + if Path::new(format!("{}/newver.txt", &cachedir).as_str()).exists() { + fs::remove_file(format!("{}/newver.txt", &cachedir).as_str())?; + } + let mut newver = fs::File::create(format!("{}/newver.txt", &cachedir))?; + newver.write_all(latest.as_bytes())?; + } + Ok(()) +} + +fn dlfile(url: &str, path: &str) -> Result<(), Box> { + println!("Downloading {}", url); + let response = reqwest::blocking::get(url)?; + if response.status().is_success() { + let cachedir = format!("{}/.cache/nix-software-center", env::var("HOME")?); + if !Path::new(&cachedir).exists() { + fs::create_dir_all(&cachedir).expect("Failed to create cache directory"); + } + + let dst: Vec = response.bytes()?.to_vec(); + { + let mut file = File::create(path)?; + let mut reader = brotli::Decompressor::new( + dst.as_slice(), + 4096, // buffer size + ); + let mut buf = [0u8; 4096]; + loop { + match reader.read(&mut buf[..]) { + Err(e) => { + if let std::io::ErrorKind::Interrupted = e.kind() { + continue; + } + return Err(Box::new(e)); + } + Ok(size) => { + if size == 0 { + break; + } + file.write_all(&buf[..size])? + } + } + } + } + } + Ok(()) +} diff --git a/src/parse/config.rs b/src/parse/config.rs new file mode 100644 index 0000000..9ed5344 --- /dev/null +++ b/src/parse/config.rs @@ -0,0 +1,98 @@ +use std::{error::Error, env, path::Path, fs::{self, File}, io::Write}; + +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +pub struct NscConfig { + pub systemconfig: String, + pub flake: Option, +} + +pub fn getconfig() -> NscConfig { + if let Ok(c) = getconfigval() { + c + } else { + NscConfig { + systemconfig: String::from("/etc/nixos/configuration.nix"), + flake: None, + } + } +} + +fn getconfigval() -> Result> { + let configfile = checkconfig()?; + let config: NscConfig = serde_json::from_reader(File::open(format!("{}/config.json", configfile))?)?; + Ok(config) +} + +fn checkconfig() -> Result> { + let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?); + if !Path::is_file(Path::new(&format!("{}/config.json", &cfgdir))) { + if !Path::is_file(Path::new("/etc/nix-software-center/config.json")) { + createdefaultconfig()?; + Ok(cfgdir) + } else { + Ok("/etc/nix-software-center/".to_string()) + } + } else { + Ok(cfgdir) + } +} + +// fn configexists() -> Result> { +// let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?); +// if !Path::is_file(Path::new(&format!("{}/config.json", &cfgdir))) { +// if !Path::is_file(Path::new("/etc/nix-software-center/config.json")) { +// Ok(false) +// } else { +// Ok(true) +// } +// } else { +// Ok(true) +// } +// } + +pub fn editconfig(config: NscConfig) -> Result<(), Box> { + let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?); + fs::create_dir_all(&cfgdir)?; + let json = serde_json::to_string_pretty(&config)?; + let mut file = File::create(format!("{}/config.json", cfgdir))?; + file.write_all(json.as_bytes())?; + Ok(()) +} + +fn createdefaultconfig() -> Result<(), Box> { + let cfgdir = format!("{}/.config/nix-software-center", env::var("HOME")?); + fs::create_dir_all(&cfgdir)?; + let config = NscConfig { + systemconfig: "/etc/nixos/configuration.nix".to_string(), + flake: None, + }; + let json = serde_json::to_string_pretty(&config)?; + let mut file = File::create(format!("{}/config.json", cfgdir))?; + file.write_all(json.as_bytes())?; + Ok(()) +} + + +// pub fn readconfig(cfg: String) -> Result<(String, Option), Box> { +// let file = fs::read_to_string(cfg)?; +// let config: NscConfig = match serde_json::from_str(&file) { +// Ok(x) => x, +// Err(_) => { +// createdefaultconfig()?; +// return Ok(( +// "/etc/nixos/configuration.nix".to_string(), +// None, +// )); +// } +// }; +// if Path::is_file(Path::new(&config.systemconfig)) { +// Ok((config.systemconfig, config.flake)) +// } else { +// Ok(( +// "/etc/nixos/configuration.nix".to_string(), +// None, +// )) +// } +// } diff --git a/src/parse/mod.rs b/src/parse/mod.rs new file mode 100644 index 0000000..199c472 --- /dev/null +++ b/src/parse/mod.rs @@ -0,0 +1,3 @@ +pub mod cache; +pub mod packages; +pub mod config; \ No newline at end of file diff --git a/src/parse/packages.rs b/src/parse/packages.rs new file mode 100644 index 0000000..c1f918e --- /dev/null +++ b/src/parse/packages.rs @@ -0,0 +1,190 @@ +use flate2::bufread::GzDecoder; +use ijson::IString; +use serde::{Deserialize, Serialize}; +use std::io::Read; +use std::{self, fs::File, collections::HashMap, error::Error, env, io::BufReader}; + +use crate::APPINFO; + +#[derive(Serialize, Deserialize, Debug)] +pub struct PackageBase { + packages: HashMap, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct Package { + pub system: IString, + pub pname: IString, + pub meta: Meta, + pub version: IString, + #[serde(skip_deserializing)] + pub appdata: Option, +} +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct Meta { + pub broken: Option, + pub insecure: Option, + pub unsupported: Option, + pub unfree: Option, + pub description: Option, + #[serde(rename = "longDescription")] + pub longdescription: Option, + pub homepage: Option, + pub maintainers: Option>, + pub position: Option, + pub license: Option, + pub platforms: Option +} + +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +#[serde(untagged)] +pub enum StrOrVec { + Single(IString), + List(Vec), +} + +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +#[serde(untagged)] +pub enum Platform { + Single(IString), + List(Vec), + ListList(Vec>), +} + +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +#[serde(untagged)] +pub enum LicenseEnum { + Single(License), + List(Vec), + SingleStr(IString), + VecStr(Vec), + Mixed(Vec) +} + +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +pub struct License { + pub free: Option, + #[serde(rename = "fullName")] + pub fullname: Option, + #[serde(rename = "spdxId")] + pub spdxid: Option, + pub url: Option, +} + +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +pub struct PkgMaintainer { + pub email: IString, + pub github: IString, + pub matrix: Option, + pub name: Option +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct AppData { + #[serde(rename = "Type")] + pub metatype: IString, + #[serde(rename = "ID")] + pub id: String, + #[serde(rename = "Package")] + pub package: String, + #[serde(rename = "Name")] + pub name: Option>, + #[serde(rename = "Description")] + pub description: Option>, + #[serde(rename = "Summary")] + pub summary: Option>, + #[serde(rename = "Url")] + pub url: Option, + #[serde(rename = "Icon")] + pub icon: Option, + #[serde(rename = "Launchable")] + pub launchable: Option, + #[serde(rename = "Provides")] + pub provides: Option, + #[serde(rename = "Screenshots")] + pub screenshots: Option>, + #[serde(rename = "Categories")] + pub categories: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct AppUrl { + pub homepage: Option, + pub bugtracker: Option, + pub help: Option, + pub donation: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct AppIconList { + pub cached: Option>, + pub stock: Option, + // TODO: add support for other icon types +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct AppIcon { + pub name: String, + pub width: u32, + pub height: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct AppLaunchable { + #[serde(rename = "desktop-id")] + pub desktopid: Vec +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct AppProvides { + pub binaries: Option>, + pub ids: Option>, + pub mediatypes: Option>, + pub libraries: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct AppScreenshot { + pub default: Option, + pub thumbnails: Option>, + #[serde(rename = "source-image")] + pub sourceimage: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct AppScreenshotImage { + pub url: String, +} + +pub async fn readpkgs() -> Result, Box> { + let cachedir = format!("{}/.cache/nix-software-center/", env::var("HOME")?); + let cachefile = format!("{}/packages.json", cachedir); + let file = File::open(cachefile).unwrap(); + let reader = BufReader::new(file); + let pkgbase: PackageBase = simd_json::serde::from_reader(reader).unwrap(); + let mut pkgs = pkgbase.packages; + println!("APPDATADIR {}", APPINFO); + let appdata = File::open(&format!("{}/xmls/nixos_x86_64_linux.yml.gz", APPINFO)).unwrap(); + let appreader = BufReader::new(appdata); + let mut d = GzDecoder::new(appreader); + let mut s = String::new(); + d.read_to_string(&mut s).unwrap(); + let mut files = s.split("\n---\n").collect::>(); + files.remove(0); + for f in files { + let appstream: AppData = serde_yaml::from_str(f).unwrap(); + if let Some(p) = pkgs.get_mut(&appstream.package.to_string()) { + p.appdata = Some(appstream); + } + } + Ok(pkgs) +} + +pub fn readsyspkgs() -> Result, Box> { + let cachedir = format!("{}/.cache/nix-software-center/", env::var("HOME")?); + let cachefile = format!("{}/syspackages.json", cachedir); + let file = File::open(cachefile)?; + let reader = BufReader::new(file); + let newpkgs: HashMap = simd_json::serde::from_reader(reader).unwrap(); + Ok(newpkgs) +} \ No newline at end of file diff --git a/src/ui/about.rs b/src/ui/about.rs new file mode 100644 index 0000000..96c2570 --- /dev/null +++ b/src/ui/about.rs @@ -0,0 +1,64 @@ +use adw::prelude::*; +use relm4::*; + +use crate::config; + +use super::window::AppMsg; + +#[derive(Debug)] +pub struct AboutPageModel { + hidden: bool, +} + +#[derive(Debug)] +pub enum AboutPageMsg { + Show, +} + +#[relm4::component(pub)] +impl SimpleComponent for AboutPageModel { + type InitParams = gtk::Window; + type Input = AboutPageMsg; + type Output = AppMsg; + type Widgets = AboutPageWidgets; + + view! { + adw::AboutWindow { + #[watch] + set_visible: !model.hidden, + set_transient_for: Some(&parent_window), + set_modal: true, + set_application_name: "Nix Software Center", + set_application_icon: config::APP_ID, + set_developer_name: "Victor Fuentes", + set_version: env!("CARGO_PKG_VERSION"), + set_issue_url: "https://github.com/vlinkz/nix-software-center/issues", + set_license_type: gtk::License::MitX11, + set_website: "https://github.com/vlinkz/nix-software-center", + set_developers: &["Victor Fuentes https://github.com/vlinkz"], + } + } + + fn init( + parent_window: Self::InitParams, + root: &Self::Root, + _sender: ComponentSender, + ) -> ComponentParts { + let model = AboutPageModel { + hidden: true, + }; + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, _sender: ComponentSender) { + match msg { + AboutPageMsg::Show => { + self.hidden = false; + } + } + } + +} \ No newline at end of file diff --git a/src/ui/categories.rs b/src/ui/categories.rs new file mode 100644 index 0000000..d492f12 --- /dev/null +++ b/src/ui/categories.rs @@ -0,0 +1,105 @@ +use relm4::adw::prelude::*; +use relm4::gtk::pango; +use relm4::{factory::*, *}; +use strum_macros::{EnumIter, Display}; + +use super::window::AppMsg; + +#[derive(Debug, PartialEq)] +pub struct PkgGroup { + pub category: PkgCategory, +} + +#[derive(Debug, Display, Hash, EnumIter, Eq, PartialEq, Clone)] +pub enum PkgCategory { + Audio, + Development, + Games, + Graphics, + Network, + Video, +} + +#[derive(Debug)] +pub enum PkgCategoryMsg { + Open(PkgCategory), +} + +#[relm4::factory(pub)] +impl FactoryComponent for PkgGroup { + type CommandOutput = (); + type Init = PkgCategory; + type Input = (); + type Output = PkgCategoryMsg; + type Widgets = PkgGroupWidgets; + type ParentWidget = gtk::FlowBox; + type ParentMsg = AppMsg; + + view! { + gtk::FlowBoxChild { + set_width_request: 210, + set_height_request: 70, + gtk::Button { + add_css_class: "card", + gtk::Box { + set_margin_start: 15, + set_margin_end: 15, + set_margin_top: 10, + set_margin_bottom: 10, + set_spacing: 10, + set_halign: gtk::Align::Center, + gtk::Image { + add_css_class: "icon-dropshadow", + set_icon_name: match self.category { + PkgCategory::Audio => Some("audio-x-generic"), + PkgCategory::Development => Some("computer"), + PkgCategory::Games => Some("input-gaming"), + PkgCategory::Graphics => Some("image-x-generic"), + PkgCategory::Network => Some("network-server"), + PkgCategory::Video => Some("video-x-generic"), + }, + set_pixel_size: 32, + }, + gtk::Label { + add_css_class: "title-2", + set_valign: gtk::Align::Center, + set_hexpand: true, + set_label: match self.category { + PkgCategory::Audio => "Audio", + PkgCategory::Development => "Development", + PkgCategory::Games => "Games", + PkgCategory::Graphics => "Graphics", + PkgCategory::Network => "Network", + PkgCategory::Video => "Video", + }, + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + } + }, + connect_clicked[sender, category = self.category.clone()] => move |_| { + println!("CLICKED {}", category); + sender.output(PkgCategoryMsg::Open(category.clone())); + } + } + } + } + + fn init_model( + parent: Self::Init, + _index: &DynamicIndex, + _sender: FactoryComponentSender, + ) -> Self { + Self { + category: parent, + } + } + + fn output_to_parent_msg(output: Self::Output) -> Option { + Some(match output { + PkgCategoryMsg::Open(x) => AppMsg::OpenCategoryPage(x), + }) + } + +} diff --git a/src/ui/categorypage.rs b/src/ui/categorypage.rs new file mode 100644 index 0000000..af9099f --- /dev/null +++ b/src/ui/categorypage.rs @@ -0,0 +1,204 @@ +use super::{window::*, categories::PkgCategory, categorytile::CategoryTile}; +use adw::prelude::*; +use relm4::{factory::*, *}; + +#[tracker::track] +#[derive(Debug)] +pub struct CategoryPageModel { + category: PkgCategory, + #[tracker::no_eq] + recommendedapps: FactoryVecDeque, + #[tracker::no_eq] + apps: FactoryVecDeque, + busy: bool, +} + +#[derive(Debug)] +pub enum CategoryPageMsg { + Close, + OpenPkg(String), + Open(PkgCategory, Vec, Vec), + Loading(PkgCategory), + UpdateInstalled(Vec, Vec) +} + +#[relm4::component(pub)] +impl SimpleComponent for CategoryPageModel { + type InitParams = (); + type Input = CategoryPageMsg; + type Output = AppMsg; + type Widgets = CategoryPageWidgets; + + view! { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + adw::HeaderBar { + pack_start = >k::Button { + add_css_class: "flat", + gtk::Image { + set_icon_name: Some("go-previous-symbolic"), + }, + connect_clicked[sender] => move |_| { + sender.input(CategoryPageMsg::Close) + }, + }, + #[wrap(Some)] + set_title_widget = >k::Label { + #[watch] + set_label: &model.category.to_string(), + }, + }, + gtk::ScrolledWindow { + set_vexpand: true, + set_hexpand: true, + set_hscrollbar_policy: gtk::PolicyType::Never, + set_vscrollbar_policy: gtk::PolicyType::Automatic, + #[track(model.changed(CategoryPageModel::category()))] + set_vadjustment: gtk::Adjustment::NONE, + adw::Clamp { + set_maximum_size: 1000, + set_tightening_threshold: 750, + if model.busy { + gtk::Spinner { + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + set_spinning: true, + set_height_request: 32, + } + } else { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_valign: gtk::Align::Start, + set_margin_all: 15, + set_spacing: 15, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "title-4", + set_label: "Recommended", + }, + #[local_ref] + recbox -> gtk::FlowBox { + set_halign: gtk::Align::Fill, + set_hexpand: true, + set_valign: gtk::Align::Center, + set_orientation: gtk::Orientation::Horizontal, + set_selection_mode: gtk::SelectionMode::None, + set_homogeneous: true, + set_max_children_per_line: 3, + set_min_children_per_line: 1, + set_column_spacing: 14, + set_row_spacing: 14, + }, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "title-4", + set_label: "Other", + }, + #[local_ref] + allbox -> gtk::FlowBox { + set_halign: gtk::Align::Fill, + set_hexpand: true, + set_valign: gtk::Align::Center, + set_orientation: gtk::Orientation::Horizontal, + set_selection_mode: gtk::SelectionMode::None, + set_homogeneous: true, + set_max_children_per_line: 3, + set_min_children_per_line: 1, + set_column_spacing: 14, + set_row_spacing: 14, + } + } + } + } + } + } + } + + fn init( + (): Self::InitParams, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let model = CategoryPageModel { + category: PkgCategory::Audio, + recommendedapps: FactoryVecDeque::new(gtk::FlowBox::new(), &sender.input), + apps: FactoryVecDeque::new(gtk::FlowBox::new(), &sender.input), + busy: true, + tracker: 0 + }; + + let recbox = model.recommendedapps.widget(); + let allbox = model.apps.widget(); + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + self.reset(); + match msg { + CategoryPageMsg::Close => { + let mut recapps_guard = self.recommendedapps.guard(); + let mut apps_guard = self.apps.guard(); + recapps_guard.clear(); + apps_guard.clear(); + sender.output(AppMsg::FrontFrontPage) + } + CategoryPageMsg::OpenPkg(pkg) => { + sender.output(AppMsg::OpenPkg(pkg)) + } + CategoryPageMsg::Open(category, catrec, catall) => { + println!("CategoryPageMsg::Open"); + self.set_category(category); + let mut recapps_guard = self.recommendedapps.guard(); + recapps_guard.clear(); + for app in catrec { + recapps_guard.push_back(app); + } + let mut apps_guard = self.apps.guard(); + apps_guard.clear(); + for app in catall { + apps_guard.push_back(app); + } + self.busy = false; + } + CategoryPageMsg::Loading(category) => { + println!("CATEGORY PAGE LOADING {}", category); + self.set_category(category); + self.busy = true; + } + CategoryPageMsg::UpdateInstalled(installeduserpkgs, installedsystempkgs) => { + let mut recapps_guard = self.recommendedapps.guard(); + for i in 0..recapps_guard.len() { + let app = recapps_guard.get_mut(i).unwrap(); + if installeduserpkgs.contains(&app.pname) { + app.installeduser = true; + } else { + app.installeduser = false; + } + if installedsystempkgs.contains(&app.pkg) { + app.installedsystem = true; + } else { + app.installedsystem = false; + } + } + let mut apps_guard = self.apps.guard(); + for i in 0..apps_guard.len() { + let app = apps_guard.get_mut(i).unwrap(); + if installeduserpkgs.contains(&app.pname) { + app.installeduser = true; + } else { + app.installeduser = false; + } + if installedsystempkgs.contains(&app.pkg) { + app.installedsystem = true; + } else { + app.installedsystem = false; + } + } + } + } + } + +} \ No newline at end of file diff --git a/src/ui/categorytile.rs b/src/ui/categorytile.rs new file mode 100644 index 0000000..0a26109 --- /dev/null +++ b/src/ui/categorytile.rs @@ -0,0 +1,185 @@ +use std::path::Path; + +use crate::APPINFO; + +use super::categorypage::CategoryPageMsg; +use relm4::adw::prelude::*; +use relm4::gtk::pango; +use relm4::{factory::*, *}; + +#[derive(Default, Debug, PartialEq, Clone)] +pub struct CategoryTile { + pub name: String, + pub pkg: String, + pub pname: String, + pub summary: Option, + pub icon: Option, + pub installeduser: bool, + pub installedsystem: bool, +} + +#[derive(Debug)] +pub enum CategoryTileMsg { + Open(String), +} + +#[relm4::factory(pub)] +impl FactoryComponent for CategoryTile { + type CommandOutput = (); + type Init = CategoryTile; + type Input = (); + type Output = CategoryTileMsg; + type Widgets = CategoryTileWidgets; + type ParentWidget = gtk::FlowBox; + type ParentMsg = CategoryPageMsg; + + view! { + gtk::FlowBoxChild { + set_width_request: 270, + gtk::Overlay { + add_overlay = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + set_valign: gtk::Align::Start, + set_halign: gtk::Align::End, + gtk::Image { + add_css_class: "accent", + set_valign: gtk::Align::Start, + set_halign: gtk::Align::End, + set_pixel_size: 16, + set_margin_top: 8, + set_margin_end: 8, + set_icon_name: Some("emblem-default-symbolic"), + #[watch] + set_visible: self.installeduser, + }, + gtk::Image { + add_css_class: "success", + set_valign: gtk::Align::Start, + set_halign: gtk::Align::End, + set_pixel_size: 16, + set_margin_top: 8, + set_margin_end: 8, + set_icon_name: Some("emblem-default-symbolic"), + #[watch] + set_visible: self.installedsystem, + } + }, + gtk::Button { + add_css_class: "card", + connect_clicked[sender, pkg = self.pkg.clone()] => move |_| { + println!("CLICKED"); + sender.output(CategoryTileMsg::Open(pkg.to_string())) + }, + set_can_focus: false, + gtk::Box { + set_margin_start: 15, + set_margin_end: 15, + set_margin_top: 10, + set_margin_bottom: 10, + set_spacing: 20, + append = if self.icon.is_some() { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_from_file: { + if let Some(i) = &self.icon { + let iconpath = format!("{}/icons/nixos/128x128/{}", APPINFO, i); + let iconpath64 = format!("{}/icons/nixos/64x64/{}", APPINFO, i); + if Path::new(&iconpath).is_file() { + Some(iconpath) + } else if Path::new(&iconpath64).is_file() { + Some(iconpath64) + } else { + None + } + } else { + None + } + }, + set_pixel_size: 64, + } + } else { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_icon_name: Some("package-x-generic"), + set_pixel_size: 64, + } + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_spacing: 3, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "heading", + set_label: &self.name, + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "dim-label", + add_css_class: "caption", + set_label: &self.pkg, + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + // add_css_class: "dim-label", + #[watch] + set_visible: self.summary.is_some(), + set_label: &(if let Some(s) = &self.summary { s.to_string() } else { String::default() }), + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 2, + set_wrap: true, + set_max_width_chars: 0, + } + } + } + } + } + } + } + + fn init_model( + parent: Self::Init, + _index: &DynamicIndex, + _sender: FactoryComponentSender, + ) -> Self { + let mut sum = parent.summary; + sum = sum.map(|mut s| { + s.trim().to_string(); + while s.contains('\n') { + s = s.replace('\n', " "); + } + while s.contains(" ") { + s = s.replace(" ", " "); + } + s + }); + + Self { + name: parent.name, + pkg: parent.pkg, + pname: parent.pname, + summary: sum, + icon: parent.icon, + installeduser: parent.installeduser, + installedsystem: parent.installedsystem, + } + } + + fn output_to_parent_msg(output: Self::Output) -> Option { + Some(match output { + CategoryTileMsg::Open(x) => CategoryPageMsg::OpenPkg(x), + }) + } +} diff --git a/src/ui/installdialog.rs b/src/ui/installdialog.rs new file mode 100644 index 0000000..dcca218 --- /dev/null +++ b/src/ui/installdialog.rs @@ -0,0 +1 @@ +// TODO add a dialog where user can view `nix-env` and `nixos-rebuild` output \ No newline at end of file diff --git a/src/ui/installedpage.rs b/src/ui/installedpage.rs new file mode 100644 index 0000000..22a9489 --- /dev/null +++ b/src/ui/installedpage.rs @@ -0,0 +1,378 @@ +use std::path::Path; +use crate::APPINFO; + +use super::{window::*, pkgpage::{InstallType, WorkPkg, PkgAction, NotifyPage}}; +use adw::prelude::*; +use relm4::{factory::*, *, gtk::pango}; + +#[tracker::track] +#[derive(Debug)] +pub struct InstalledPageModel { + #[tracker::no_eq] + installeduserlist: FactoryVecDeque, + #[tracker::no_eq] + installedsystemlist: FactoryVecDeque, + updatetracker: u8, +} + +#[derive(Debug)] +pub enum InstalledPageMsg { + Update(Vec, Vec), + OpenRow(usize, InstallType), + Remove(InstalledItem), + UnsetBusy(WorkPkg), +} + +#[relm4::component(pub)] +impl SimpleComponent for InstalledPageModel { + type InitParams = (); + type Input = InstalledPageMsg; + type Output = AppMsg; + type Widgets = InstalledPageWidgets; + + view! { + gtk::ScrolledWindow { + set_hscrollbar_policy: gtk::PolicyType::Never, + #[track(model.changed(InstalledPageModel::updatetracker()))] + set_vadjustment: gtk::Adjustment::NONE, + adw::Clamp { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_valign: gtk::Align::Start, + set_margin_all: 15, + set_spacing: 15, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "title-4", + set_label: "User (nix-env)", + }, + #[local_ref] + installeduserlist -> gtk::ListBox { + set_valign: gtk::Align::Start, + add_css_class: "boxed-list", + set_selection_mode: gtk::SelectionMode::None, + connect_row_activated[sender] => move |listbox, row| { + if let Some(i) = listbox.index_of_child(row) { + sender.input(InstalledPageMsg::OpenRow(i as usize, InstallType::User)) + } + // sender.input(InstalledPageMsg::OpenRow(row.clone(), InstallType::User)); + } + }, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "title-4", + set_label: "System (configuration.nix)", + }, + #[local_ref] + installedsystemlist -> gtk::ListBox { + set_valign: gtk::Align::Start, + add_css_class: "boxed-list", + set_selection_mode: gtk::SelectionMode::None, + connect_row_activated[sender] => move |listbox, row| { + if let Some(i) = listbox.index_of_child(row) { + sender.input(InstalledPageMsg::OpenRow(i as usize, InstallType::System)) + } + // sender.input(InstalledPageMsg::OpenRow(row.clone(), InstallType::System)); + } + } + } + } + } + } + + fn init( + (): Self::InitParams, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let model = InstalledPageModel { + installeduserlist: FactoryVecDeque::new(gtk::ListBox::new(), &sender.input), + installedsystemlist: FactoryVecDeque::new(gtk::ListBox::new(), &sender.input), + updatetracker: 0, + tracker: 0 + }; + + let installeduserlist = model.installeduserlist.widget(); + let installedsystemlist = model.installedsystemlist.widget(); + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + self.reset(); + match msg { + InstalledPageMsg::Update(installeduserlist, installedsystemlist) => { + self.update_updatetracker(|_| ()); + let mut installeduserlist_guard = self.installeduserlist.guard(); + installeduserlist_guard.clear(); + for installeduser in installeduserlist { + installeduserlist_guard.push_back(installeduser); + } + let mut installedsystemlist_guard = self.installedsystemlist.guard(); + installedsystemlist_guard.clear(); + for installedsystem in installedsystemlist { + installedsystemlist_guard.push_back(installedsystem); + } + } + InstalledPageMsg::OpenRow(row, pkgtype) => { + match pkgtype { + InstallType::User => { + let installeduserlist_guard = self.installeduserlist.guard(); + if let Some(item) = installeduserlist_guard.get(row) { + if let Some(pkg) = &item.item.pkg { + sender.output(AppMsg::OpenPkg(pkg.to_string())); + } + } + // for (i, child) in installeduserlist_guard.widget().iter_children().enumerate() { + // if child == row { + // if let Some(item) = installeduserlist_guard.get(i) { + // if let Some(pkg) = &item.item.pkg { + // sender.output(AppMsg::OpenPkg(pkg.to_string())); + // } + // } + // } + // } + } + InstallType::System => { + let installedsystemlist_guard = self.installedsystemlist.guard(); + if let Some(item) = installedsystemlist_guard.get(row) { + if let Some(pkg) = &item.item.pkg { + sender.output(AppMsg::OpenPkg(pkg.to_string())); + } + } + // for (i, child) in installedsystemlist_guard.widget().iter_children().enumerate() { + // if child == row { + // if let Some(item) = installedsystemlist_guard.get(i) { + // if let Some(pkg) = &item.item.pkg { + // sender.output(AppMsg::OpenPkg(pkg.to_string())); + // } + // } + // } + // } + } + } + } + InstalledPageMsg::Remove(item) => { + let work = WorkPkg { + pkg: item.pkg.unwrap_or_default(), + pname: item.pname, + pkgtype: item.pkgtype, + action: PkgAction::Remove, + block: false, + notify: Some(NotifyPage::Installed) + }; + sender.output(AppMsg::AddInstalledToWorkQueue(work)) + } + InstalledPageMsg::UnsetBusy(work) => { + match work.pkgtype { + InstallType::User => { + let mut installeduserlist_guard = self.installeduserlist.guard(); + for i in 0..installeduserlist_guard.len() { + if let Some(item) = installeduserlist_guard.get_mut(i) { + if item.item.pname == work.pname && item.item.pkgtype == work.pkgtype { + item.item.busy = false; + } + } + } + } + InstallType::System => { + let mut installedsystemlist_guard = self.installedsystemlist.guard(); + for i in 0..installedsystemlist_guard.len() { + if let Some(item) = installedsystemlist_guard.get_mut(i) { + if item.item.pkg == Some(work.pkg.clone()) && item.item.pkgtype == work.pkgtype { + item.item.busy = false; + } + } + } + } + } + } + } + } +} + + + + +#[derive(Debug, PartialEq, Clone)] +pub struct InstalledItem { + pub name: String, + pub pkg: Option, + pub pname: String, + pub summary: Option, + pub icon: Option, + pub pkgtype: InstallType, + pub busy: bool, +} + +#[derive(Debug, PartialEq)] +pub struct InstalledItemModel { + pub item: InstalledItem, +} + +#[derive(Debug)] +pub enum InstalledItemMsg { + Delete(InstalledItem), +} + +#[derive(Debug)] +pub enum InstalledItemInputMsg { + Busy(bool), +} + +#[relm4::factory(pub)] +impl FactoryComponent for InstalledItemModel { + type CommandOutput = (); + type Init = InstalledItem; + type Input = InstalledItemInputMsg; + type Output = InstalledItemMsg; + type Widgets = InstalledItemWidgets; + type ParentWidget = adw::gtk::ListBox; + type ParentMsg = InstalledPageMsg; + + view! { + adw::PreferencesRow { + set_activatable: self.item.pkg.is_some(), + set_can_focus: false, + #[wrap(Some)] + set_child = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + set_spacing: 10, + set_margin_all: 10, + adw::Bin { + set_valign: gtk::Align::Center, + #[wrap(Some)] + set_child = if self.item.icon.is_some() { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_from_file: { + if let Some(i) = &self.item.icon { + let iconpath = format!("{}/icons/nixos/128x128/{}", APPINFO, i); + let iconpath64 = format!("{}/icons/nixos/64x64/{}", APPINFO, i); + if Path::new(&iconpath).is_file() { + Some(iconpath) + } else if Path::new(&iconpath64).is_file() { + Some(iconpath64) + } else { + None + } + } else { + None + } + }, + set_pixel_size: 64, + } + } else { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_icon_name: Some("package-x-generic"), + set_pixel_size: 64, + } + } + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_spacing: 2, + gtk::Label { + set_halign: gtk::Align::Start, + set_label: self.item.name.as_str(), + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "dim-label", + add_css_class: "caption", + set_label: if let Some(p) = &self.item.pkg { p } else { &self.item.pname }, + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + set_label: self.item.summary.as_deref().unwrap_or(""), + set_visible: self.item.summary.is_some(), + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + }, + if self.item.busy { + gtk::Spinner { + set_spinning: true, + } + } else { + gtk::Button { + add_css_class: "destructive-action", + set_valign: gtk::Align::Center, + set_halign: gtk::Align::End, + set_icon_name: "user-trash-symbolic", + set_can_focus: false, + connect_clicked[sender, item = self.item.clone()] => move |_| { + sender.input(InstalledItemInputMsg::Busy(true)); + sender.output(InstalledItemMsg::Delete(item.clone())) + } + } + } + } + } + } + + fn init_model( + parent: Self::Init, + _index: &DynamicIndex, + _sender: FactoryComponentSender, + ) -> Self { + let sum = if let Some(s) = parent.summary { + let mut sum = s.trim().to_string(); + while sum.contains('\n') { + sum = sum.replace('\n', " "); + } + while sum.contains(" ") { + sum = sum.replace(" ", " "); + } + Some(sum) + } else { + None + }; + + let item = InstalledItem { + name: parent.name, + pkg: parent.pkg, + pname: parent.pname, + summary: sum, + icon: parent.icon, + pkgtype: parent.pkgtype, + busy: parent.busy, + }; + + Self { + item, + } + } + + fn output_to_parent_msg(output: Self::Output) -> Option { + Some(match output { + InstalledItemMsg::Delete(item) => InstalledPageMsg::Remove(item), + }) + } + + fn update(&mut self, msg: Self::Input, _sender: FactoryComponentSender) { + match msg { + InstalledItemInputMsg::Busy(b) => self.item.busy = b, + } + } + +} diff --git a/src/ui/installworker.rs b/src/ui/installworker.rs new file mode 100644 index 0000000..6398454 --- /dev/null +++ b/src/ui/installworker.rs @@ -0,0 +1,346 @@ +use crate::parse::config::NscConfig; + +use super::pkgpage::{InstallType, PkgAction, PkgMsg, WorkPkg}; +use relm4::*; +use std::error::Error; +use std::path::Path; +use std::process::Stdio; +use std::{fs, io}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; + +#[tracker::track] +#[derive(Debug)] +pub struct InstallAsyncHandler { + #[tracker::no_eq] + process: Option>, + work: Option, + systemconfig: String, + flakeargs: Option, + pid: Option, +} + +#[derive(Debug)] +pub enum InstallAsyncHandlerMsg { + SetConfig(NscConfig), + Process(WorkPkg), + CancelProcess, + SetPid(Option), +} + +impl Worker for InstallAsyncHandler { + type InitParams = (); + type Input = InstallAsyncHandlerMsg; + type Output = PkgMsg; + + fn init(_params: Self::InitParams, _sender: relm4::ComponentSender) -> Self { + Self { + process: None, + work: None, + systemconfig: String::new(), + flakeargs: None, + pid: None, + tracker: 0, + } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + self.reset(); + match msg { + InstallAsyncHandlerMsg::SetConfig(config) => { + self.systemconfig = config.systemconfig; + self.flakeargs = config.flake; + } + InstallAsyncHandlerMsg::Process(work) => { + if work.block { + return; + } + let systemconfig = self.systemconfig.clone(); + let rebuildargs = self.flakeargs.clone(); + match work.pkgtype { + InstallType::User => { + match work.action { + PkgAction::Install => { + println!("Installing user package: {}", work.pkg); + self.process = Some(relm4::spawn(async move { + let mut p = tokio::process::Command::new("nix-env") + .arg("-iA") + .arg(format!("nixos.{}", work.pkg)) + .kill_on_drop(true) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to run nix-env"); + + let stderr = p.stderr.take().unwrap(); + let reader = tokio::io::BufReader::new(stderr); + + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + println!("CAUGHT LINE: {}", line); + } + + match p.wait().await { + Ok(o) => { + if o.success() { + println!( + "Removed user package: {} success", + work.pkg + ); + // println!("{}", String::from_utf8_lossy(&pstdout)); + sender.output(PkgMsg::FinishedProcess(work)) + } else { + println!( + "Removed user package: {} failed", + work.pkg + ); + // println!("{}", String::from_utf8_lossy(&p.stderr)); + sender.output(PkgMsg::FailedProcess(work)); + } + } + Err(e) => { + println!("Error removing user package: {}", e); + sender.output(PkgMsg::FailedProcess(work)); + } + } + })); + } + PkgAction::Remove => { + println!("Removing user package: {}", work.pkg); + self.process = Some(relm4::spawn(async move { + let mut p = tokio::process::Command::new("nix-env") + .arg("-e") + .arg(&work.pname) + .kill_on_drop(true) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to run nix-env"); + let stderr = p.stderr.take().unwrap(); + let reader = tokio::io::BufReader::new(stderr); + + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + println!("CAUGHT LINE: {}", line); + } + match p.wait().await { + Ok(o) => { + if o.success() { + println!( + "Removed user package: {} success", + work.pkg + ); + // println!("{}", String::from_utf8_lossy(&pstdout)); + sender.output(PkgMsg::FinishedProcess(work)) + } else { + println!( + "Removed user package: {} failed", + work.pkg + ); + // println!("{}", String::from_utf8_lossy(&p.stderr)); + sender.output(PkgMsg::FailedProcess(work)); + } + } + Err(e) => { + println!("Error removing user package: {}", e); + sender.output(PkgMsg::FailedProcess(work)); + } + } + })); + } + } + } + InstallType::System => match work.action { + PkgAction::Install => { + println!("Installing system package: {}", work.pkg); + self.process = Some(relm4::spawn(async move { + match installsys( + work.pkg.to_string(), + work.action.clone(), + systemconfig, + rebuildargs, + sender.clone(), + ) + .await + { + Ok(b) => { + if b { + sender.output(PkgMsg::FinishedProcess(work)) + } else { + sender.output(PkgMsg::FailedProcess(work)) + } + } + Err(e) => { + sender.output(PkgMsg::FailedProcess(work)); + println!("Error installing system package: {}", e); + } + } + })); + } + PkgAction::Remove => { + println!("Removing system package: {}", work.pkg); + self.process = Some(relm4::spawn(async move { + match installsys( + work.pkg.to_string(), + work.action.clone(), + systemconfig, + rebuildargs, + sender.clone(), + ) + .await + { + Ok(b) => { + if b { + sender.output(PkgMsg::FinishedProcess(work)) + } else { + sender.output(PkgMsg::FailedProcess(work)) + } + } + Err(e) => { + sender.output(PkgMsg::FailedProcess(work)); + println!("Error removing system package: {}", e); + } + } + })); + } + }, + } + } + InstallAsyncHandlerMsg::CancelProcess => { + println!("CANCELING PROCESS"); + // if let Some(p) = self.pid { + // println!("Killing process: {}", p); + // Command::new("pkexec") + // .arg("kill") + // .arg("-INT") + // .arg(p.to_string()) + // .spawn() + // .expect("Failed to kill process"); + // } + if let Some(p) = &mut self.process { + p.abort() + } + self.process = None; + self.pid = None; + sender.output(PkgMsg::CancelFinished); + } + InstallAsyncHandlerMsg::SetPid(p) => self.pid = p, + } + } +} + +async fn installsys( + pkg: String, + action: PkgAction, + systemconfig: String, + flakeargs: Option, + _sender: ComponentSender, +) -> Result> { + let mut p = pkg; + let f = fs::read_to_string(&systemconfig)?; + if let Ok(s) = nix_editor::read::getwithvalue(&f, "environment.systemPackages") { + if !s.contains(&"pkgs".to_string()) { + p = format!("pkgs.{}", p); + } + } else { + return Err(Box::new(io::Error::new( + io::ErrorKind::InvalidData, + "Failed to write configuration.nix", + ))); + } + + let out = match action { + PkgAction::Install => { + match nix_editor::write::addtoarr(&f, "environment.systemPackages", vec![p]) { + Ok(x) => x, + Err(_) => { + return Err(Box::new(io::Error::new( + io::ErrorKind::InvalidData, + "Failed to write configuration.nix", + ))) + } + } + } + PkgAction::Remove => { + match nix_editor::write::rmarr(&f, "environment.systemPackages", vec![p]) { + Ok(x) => x, + Err(_) => { + return Err(Box::new(io::Error::new( + io::ErrorKind::InvalidData, + "Failed to write configuration.nix", + ))) + } + } + } + }; + + let exe = match std::env::current_exe() { + Ok(mut e) => { + e.pop(); // root/bin + // e.pop(); // root/ + // e.push("libexec"); // root/libexec + e.push("nsc-helper"); + let x = e.to_string_lossy().to_string(); + println!("CURRENT PATH {}", x); + if Path::new(&x).is_file() { + x + } else { + String::from("nsc-helper") + } + } + Err(_) => String::from("nsc-helper"), + }; + + println!("EXECUTING {}", exe); + + // let rebuildargs = match flakeargs { + // Some(x) => format!("--flake {}", x),//.split(' ').map(|x| x.to_string()).collect::>(), + // None => String::default(), + // }; + + let rebuildargs = if let Some(x) = flakeargs { + let mut v = vec![String::from("--flake")]; + for arg in x.split(' ') { + if !arg.is_empty() { + v.push(String::from(arg)); + } + } + v + } else { + vec![] + }; + println!("Rebuild args: {:?}", rebuildargs); + + let mut cmd = tokio::process::Command::new("pkexec") + .arg(&exe) + .arg("config") + .arg("--output") + .arg(&systemconfig) + .arg("--") + .arg("switch") + .args(&rebuildargs) + .stderr(Stdio::piped()) + .stdin(Stdio::piped()) + .spawn()?; + + // sender.input(InstallAsyncHandlerMsg::SetPid(cmd.id())); + + cmd.stdin.take().unwrap().write_all(out.as_bytes()).await?; + println!("SENT INPUT"); + let stderr = cmd.stderr.take().unwrap(); + let reader = tokio::io::BufReader::new(stderr); + + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + println!("CAUGHT LINE: {}", line); + } + println!("READER DONE"); + if cmd.wait().await?.success() { + println!("SUCCESS"); + // sender.input(InstallAsyncHandlerMsg::SetPid(None)); + Ok(true) + } else { + println!("FAILURE"); + // sender.input(InstallAsyncHandlerMsg::SetPid(None)); + Ok(false) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..4190d00 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,16 @@ +pub mod window; +pub mod windowloading; +pub mod pkgtile; +pub mod categories; +pub mod pkgpage; +pub mod screenshotfactory; +pub mod installworker; +pub mod searchpage; +pub mod installedpage; +pub mod updatepage; +pub mod updatedialog; +pub mod updateworker; +pub mod about; +pub mod preferencespage; +pub mod categorypage; +pub mod categorytile; diff --git a/src/ui/pkgpage.rs b/src/ui/pkgpage.rs new file mode 100644 index 0000000..4ae43aa --- /dev/null +++ b/src/ui/pkgpage.rs @@ -0,0 +1,1472 @@ +use adw::gio; +use adw::prelude::*; +use html2pango; +use image::{imageops::FilterType, ImageFormat}; +use relm4::actions::RelmAction; +use relm4::actions::RelmActionGroup; +use relm4::gtk::pango; +use relm4::{factory::FactoryVecDeque, *}; +use sha256::digest; +use std::collections::HashSet; +use std::convert::identity; +use std::process::Command; +use std::{ + env, + error::Error, + fmt::Write, + fs::{self, File}, + io::BufReader, + path::Path, + time::Duration, +}; + +use crate::parse::config::NscConfig; +use crate::parse::config::getconfig; +use crate::parse::packages::PkgMaintainer; +use crate::parse::packages::StrOrVec; +use crate::ui::installworker::InstallAsyncHandlerMsg; + +use super::installworker::InstallAsyncHandler; +use super::{screenshotfactory::ScreenshotItem, window::AppMsg}; + +#[tracker::track] +#[derive(Debug)] +pub struct PkgModel { + config: NscConfig, + name: String, + pkg: String, + pname: String, + summary: Option, + description: Option, + icon: Option, + + homepage: Option, + licenses: Vec, + platforms: Vec, + maintainers: Vec, + launchable: Option, + + #[tracker::no_eq] + screenshots: FactoryVecDeque, + #[tracker::no_eq] + installworker: WorkerController, + carpage: CarouselPage, + installtype: InstallType, + installeduserpkgs: HashSet, + installedsystempkgs: HashSet, + + workqueue: HashSet, +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub struct WorkPkg { + pub pkg: String, + pub pname: String, + pub pkgtype: InstallType, + pub action: PkgAction, + pub block: bool, + pub notify: Option, +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub enum NotifyPage { + Installed, +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub enum PkgAction { + Install, + Remove +} + + +#[derive(Debug, PartialEq)] +pub enum Launch { + GtkApp(String), + TerminalApp(String), +} + +#[derive(Debug, PartialEq)] +pub enum CarouselPage { + First, + Middle, + Last, + Single, +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone)] +pub enum InstallType { + User, + System, +} + +#[derive(Debug, PartialEq)] +pub struct License { + pub free: Option, + pub fullname: String, + pub spdxid: Option, + pub url: Option, +} + +#[derive(Debug)] +pub struct PkgInitModel { + pub name: String, + pub pkg: String, + pub installeduserpkgs: HashSet, + pub installedsystempkgs: HashSet, + pub pname: String, + pub summary: Option, + pub description: Option, + pub icon: Option, + pub screenshots: Vec, + pub homepage: Option, + pub licenses: Vec, + pub platforms: Vec, + pub maintainers: Vec, + pub launchable: Option, +} + +#[derive(Debug)] +pub enum PkgMsg { + UpdateConfig(NscConfig), + Open(Box), + LoadScreenshot(String, usize, String), + SetError(String, usize), + SetCarouselPage(CarouselPage), + OpenHomepage, + Close, + InstallUser, + RemoveUser, + InstallSystem, + RemoveSystem, + Cancel, + CancelFinished, + // FinishedInstallUser(String, String), + // FailedInstallUser(String, String), + // FinishedRemoveUser(String, String), + // FailedRemoveUser(String, String), + FinishedProcess(WorkPkg), + FailedProcess(WorkPkg), + Launch, + SetInstallType(InstallType), + AddToQueue(WorkPkg), +} + +#[derive(Debug)] +pub enum PkgAsyncMsg { + LoadScreenshot(String, usize, String), + SetError(String, usize), +} + +#[relm4::component(pub)] +impl Component for PkgModel { + type Init = (); + type Input = PkgMsg; + type Output = AppMsg; + type Widgets = PkgWidgets; + type CommandOutput = PkgAsyncMsg; + + view! { + #[root] + #[name(pkg_window)] + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + adw::HeaderBar { + pack_start = >k::Button { + add_css_class: "flat", + gtk::Image { + set_icon_name: Some("go-previous-symbolic"), + }, + connect_clicked[sender] => move |_| { + sender.input(PkgMsg::Close) + }, + }, + #[wrap(Some)] + set_title_widget = >k::Label { + #[watch] + set_label: &model.name + }, + pack_end = >k::MenuButton { + #[watch] + set_label: match model.installtype { + InstallType::User => "User (nix-env)", + InstallType::System => "System (configuration.nix)", + }, + #[wrap(Some)] + set_popover = >k::PopoverMenu::from_model(Some(&installtype)) {} + } + }, + gtk::ScrolledWindow { + set_vexpand: true, + set_hexpand: true, + set_hscrollbar_policy: gtk::PolicyType::Never, + set_vscrollbar_policy: gtk::PolicyType::Automatic, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + adw::Clamp { + set_maximum_size: 1000, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Start, + // Details box + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 10, + set_margin_all: 15, + append = if model.icon.is_some() { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + #[watch] + set_from_file: model.icon.clone(), + set_pixel_size: 128, + } + } else { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_icon_name: Some("package-x-generic"), + set_pixel_size: 128, + } + }, + gtk::Box { + set_halign: gtk::Align::Fill, + // Details + gtk::Box { + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_orientation: gtk::Orientation::Vertical, + set_spacing: 6, + gtk::Label { + add_css_class: "title-1", + set_halign: gtk::Align::Start, + #[watch] + set_label: &model.name, + }, + gtk::Label { + add_css_class: "dim-label", + add_css_class: "heading", + set_halign: gtk::Align::Start, + #[watch] + set_label: &model.pkg, + }, + }, + // Install options + adw::Bin { + set_width_request: 150, + set_halign: gtk::Align::End, + gtk::Box { + set_halign: gtk::Align::End, + set_spacing: 5, + match model.installtype { + InstallType::User => { + gtk::Box { + if model.workqueue.iter().any(|x| x.pkg == model.pkg && x.pkgtype == InstallType::User) /*model.installinguserpkgs.contains(&model.pkg)*/ { + gtk::Box { + gtk::Spinner { + set_halign: gtk::Align::End, + #[watch] + set_spinning: true, //model.installinguserpkgs.contains(&model.pkg), + set_size_request: (32, 32), + set_can_focus: false, + }, + gtk::Button { + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_can_focus: false, + set_width_request: 105, + set_label: "Cancel", + connect_clicked[sender] => move |_| { + sender.input(PkgMsg::Cancel) + }, + } + } + } else if model.installeduserpkgs.contains(&model.pname) { + gtk::Box { + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_spacing: 10, + gtk::Button { + #[watch] + set_css_classes: if model.launchable.is_some() { &["suggested-action"] } else { &[] }, + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_can_focus: false, + set_width_request: 105, + #[watch] + set_label: if model.launchable.is_some() { "Open" } else { "Installed" }, + #[watch] + set_sensitive: model.launchable.is_some(), + connect_clicked[sender] => move |_| { + println!("SENT LAUNCH"); + sender.input(PkgMsg::Launch) + } + }, + gtk::Button { + set_halign: gtk::Align::End, + add_css_class: "destructive-action", + set_icon_name: "user-trash-symbolic", + set_can_focus: false, + connect_clicked[sender] => move |_| { + sender.input(PkgMsg::RemoveUser) + } + } + } + // } else if !model.installinguserpkgs.is_empty() { + // gtk::Box { + // gtk::Button { + // set_halign: gtk::Align::End, + // set_valign: gtk::Align::Center, + // set_can_focus: false, + // set_width_request: 105, + // set_label: "Busy", + // set_sensitive: false, + // } + // } + } else { + adw::SplitButton { + add_css_class: "suggested-action", + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_can_focus: false, + set_label: "Install", + set_width_request: 105, + connect_clicked[sender] => move |_| { + sender.input(PkgMsg::InstallUser); + println!("CLICKED INSTALL!!!"); + }, + // #[watch] + // set_visible: !model.installeduserpkgs.contains(&model.pname) && !model.installinguserpkgs.contains(&model.pkg), + #[wrap(Some)] + set_popover = >k::PopoverMenu::from_model(Some(&runaction)) {} + } + } + } + } + InstallType::System => { + gtk::Box { + if model.workqueue.iter().any(|x| x.pkg == model.pkg && x.pkgtype == InstallType::System) { + gtk::Box { + gtk::Spinner { + set_halign: gtk::Align::End, + #[watch] + set_spinning: true, //model.installingsystempkgs.contains(&model.pkg), + set_size_request: (32, 32), + set_can_focus: false, + }, + gtk::Button { + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_can_focus: false, + set_width_request: 105, + set_label: "Cancel", + #[watch] + set_sensitive: if let Some(w) = model.workqueue.iter().next() { w.pkg != model.pkg } else { + false + }, + connect_clicked[sender] => move |_| { + sender.input(PkgMsg::Cancel) + }, + } + } + } else if model.installedsystempkgs.contains(&model.pkg) { + gtk::Box { + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_spacing: 10, + gtk::Button { + #[watch] + set_css_classes: if model.launchable.is_some() { &["suggested-action"] } else { &[] }, + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_can_focus: false, + set_width_request: 105, + #[watch] + set_label: if model.launchable.is_some() { "Open" } else { "Installed" }, + #[watch] + set_sensitive: model.launchable.is_some(), + connect_clicked[sender] => move |_| { + println!("SENT LAUNCH"); + sender.input(PkgMsg::Launch) + } + }, + gtk::Button { + set_halign: gtk::Align::End, + add_css_class: "destructive-action", + set_icon_name: "user-trash-symbolic", + set_can_focus: false, + connect_clicked[sender] => move |_| { + sender.input(PkgMsg::RemoveSystem) + } + } + } + // } else if !model.installingsystempkgs.is_empty() { + // gtk::Box { + // gtk::Button { + // set_halign: gtk::Align::End, + // set_valign: gtk::Align::Center, + // set_can_focus: false, + // set_width_request: 105, + // set_label: "Busy", + // set_sensitive: false, + // } + // } + } else { + adw::SplitButton { + add_css_class: "suggested-action", + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_can_focus: false, + set_label: "Install", + set_width_request: 105, + connect_clicked[sender] => move |_| { + sender.input(PkgMsg::InstallSystem); + println!("CLICKED INSTALL!!!"); + }, + // #[watch] + // set_visible: !model.installedsystempkgs.contains(&model.pname) && !model.installingsystempkgs.contains(&model.pkg), + #[wrap(Some)] + set_popover = >k::PopoverMenu::from_model(Some(&runaction)) {} + } + } + } + } + } + } + } + } + } + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_valign: gtk::Align::Start, + add_css_class: "view", + add_css_class: "frame", + add_css_class: "scrnbox", + #[watch] + set_visible: !model.screenshots.is_empty(), + gtk::Overlay { + set_valign: gtk::Align::Start, + #[local_ref] + scrnfactory -> adw::Carousel { + set_valign: gtk::Align::Fill, + set_hexpand: true, + set_vexpand: true, + set_height_request: 400, + set_allow_scroll_wheel: false, + connect_page_changed[sender] => move |x, _| { + let n = adw::Carousel::n_pages(x); + let i = adw::Carousel::position(x) as u32; + if i == 0 && n == 1 { + sender.input(PkgMsg::SetCarouselPage(CarouselPage::Single)); + } else if i == 0 { + sender.input(PkgMsg::SetCarouselPage(CarouselPage::First)); + } else if i == n - 1 { + sender.input(PkgMsg::SetCarouselPage(CarouselPage::Last)); + } else { + sender.input(PkgMsg::SetCarouselPage(CarouselPage::Middle)); + } + }, + }, + add_overlay = >k::Revealer { + set_transition_type: gtk::RevealerTransitionType::Crossfade, + #[watch] + set_reveal_child: model.carpage != CarouselPage::First && model.carpage != CarouselPage::Single, + set_halign: gtk::Align::Start, + set_valign: gtk::Align::Fill, + gtk::Button { + set_can_focus: false, + set_margin_all: 15, + set_height_request: 40, + set_width_request: 40, + add_css_class: "circular", + add_css_class: "osd", + set_halign: gtk::Align::Start, + set_valign: gtk::Align::Center, + set_icon_name: "go-previous-symbolic", + connect_clicked[sender, scrnfactory] => move |_| { + let i = adw::Carousel::position(&scrnfactory) as u32; + if i > 0 { + let w = scrnfactory.nth_page(i-1); + scrnfactory.scroll_to(&w, true); + } + if i == 1 { + sender.input(PkgMsg::SetCarouselPage(CarouselPage::First)); + } else if i > 0 { + sender.input(PkgMsg::SetCarouselPage(CarouselPage::Middle)); + } + } + } + }, + add_overlay = >k::Revealer { + set_transition_type: gtk::RevealerTransitionType::Crossfade, + #[watch] + set_reveal_child: model.carpage != CarouselPage::Last && model.carpage != CarouselPage::Single, + set_halign: gtk::Align::End, + set_valign: gtk::Align::Fill, + gtk::Button { + set_can_focus: false, + set_margin_all: 15, + set_height_request: 40, + set_width_request: 40, + add_css_class: "circular", + add_css_class: "osd", + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_icon_name: "go-next-symbolic", + connect_clicked[sender, scrnfactory] => move |_| { + let i = adw::Carousel::position(&scrnfactory) as u32; + if i < scrnfactory.n_pages() -1 { + let w = scrnfactory.nth_page(i+1); + scrnfactory.scroll_to(&w, true); + } + let n = scrnfactory.n_pages() as u32; + if i == n - 2 { + sender.input(PkgMsg::SetCarouselPage(CarouselPage::Last)); + } else if i <= n - 2 { + sender.input(PkgMsg::SetCarouselPage(CarouselPage::Middle)); + } else { + sender.input(PkgMsg::SetCarouselPage(CarouselPage::Last)); + } + } + } + } + }, + adw::CarouselIndicatorDots { + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::End, + set_carousel: Some(scrnfactory) + } + }, + adw::Clamp { + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Start, + set_vexpand_set: true, + set_maximum_size: 1000, + #[watch] + set_visible: !(model.summary.is_none() && model.description.is_none()), + gtk::Box { + set_vexpand: true, + set_valign: gtk::Align::Start, + set_orientation: gtk::Orientation::Vertical, + set_margin_all: 15, + set_spacing: 10, + gtk::Label { + add_css_class: "title-2", + set_valign: gtk::Align::Start, + set_halign: gtk::Align::Start, + #[watch] + set_label: if let Some(s) = model.summary.as_ref() { s } else { "" }, + #[watch] + set_visible: model.summary.is_some(), + set_wrap: true, + set_xalign: 0.0, + }, + gtk::Label { + set_valign: gtk::Align::Start, + set_halign: gtk::Align::Start, + #[watch] + set_markup: { + if let Some(d) = model.description.as_ref() { + d + } else { "" } + }, + #[watch] + set_visible: model.description.is_some(), + set_wrap: true, + set_xalign: 0.0, + }, + }, + }, + adw::Clamp { + set_vexpand: true, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Start, + set_maximum_size: 1000, + #[name(btnbox)] + gtk::FlowBox { + add_css_class: "linked", + set_halign: gtk::Align::Fill, + set_hexpand: true, + set_homogeneous: true, + set_row_spacing: 5, + set_column_spacing: 4, + // set_margin_all: 15, + set_selection_mode: gtk::SelectionMode::None, + set_max_children_per_line: 2, + append = >k::FlowBoxChild { + set_hexpand: true, + gtk::Box { + set_spacing: 10, + set_hexpand: true, + set_homogeneous: true, + gtk::Button { + set_hexpand: true, + add_css_class: "card", + set_height_request: 100, + set_width_request: 100, + connect_clicked[sender] => move |_| { + sender.input(PkgMsg::OpenHomepage) + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_spacing: 10, + set_margin_all: 15, + gtk::Image { + add_css_class: "accent", + set_halign: gtk::Align::Center, + set_icon_name: Some("user-home-symbolic"), + set_pixel_size: 24, + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_spacing: 5, + gtk::Label { + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + add_css_class: "heading", + set_label: "Homepage" + }, + gtk::Label { + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + add_css_class: "caption", + add_css_class: "dim-label", + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 2, + set_wrap: true, + set_max_width_chars: 0, + set_justify: gtk::Justification::Center, + #[watch] + set_label: if let Some(u) = &model.homepage { + u + } else { + "" + }, + #[watch] + set_visible: model.homepage.is_some(), + } + } + + } + }, + gtk::Button { + set_hexpand: true, + add_css_class: "card", + set_height_request: 100, + set_width_request: 100, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_spacing: 10, + set_margin_all: 15, + gtk::Image { + #[watch] + set_css_classes: &[ if model.licenses.iter().any(|x| x.free == Some(false)) { "error" } else if model.licenses.iter().all(|x| x.free == Some(true)) { "success" } else { "warning" } ], + set_halign: gtk::Align::Center, + #[watch] + set_icon_name : if model.licenses.iter().any(|x| x.free == Some(false)) { Some("dialog-warning-symbolic") } else if model.licenses.iter().all(|x| x.free == Some(true)) { Some("emblem-default-symbolic") } else { Some("dialog-question-symbolic") }, + set_pixel_size: 24, + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_spacing: 5, + gtk::Label { + set_halign: gtk::Align::Center, + add_css_class: "heading", + #[watch] + set_label: if model.licenses.len() > 1 { "Licenses" } else { "License" } + }, + gtk::Label { + set_halign: gtk::Align::Fill, + set_hexpand: true, + add_css_class: "caption", + add_css_class: "dim-label", + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 2, + set_wrap: true, + set_max_width_chars: 0, + set_justify: gtk::Justification::Center, + #[watch] + set_label: { + let mut s = String::new(); + for license in model.licenses.iter() { + if model.licenses.iter().len() == 1 { + if let Some(id) = &license.spdxid { + s.push_str(id) + } else { + s.push_str(&license.fullname) + } + } else if model.licenses.iter().len() == 2 && model.licenses.get(0) == Some(license) { + if let Some(id) = &license.spdxid { + let _ = write!(s, "{} ", id); + } else { + let _ = write!(s, "{} ", license.fullname); + } + } else if Some(license) == model.licenses.iter().last() { + if let Some(id) = &license.spdxid { + let _ = write!(s, "and {}", id); + } else { + let _ = write!(s, "and {}", license.fullname); + } + } else if let Some(id) = &license.spdxid { + let _ = write!(s, "{}, ", id); + } else { + let _ = write!(s, "{}, ", license.fullname); + } + } + if model.licenses.is_empty() { + s.push_str("Unknown"); + } + &s.to_string() + }, + #[watch] + set_visible: !model.licenses.is_empty() + } + } + } + }, + } + }, + append = >k::FlowBoxChild { + set_hexpand: true, + gtk::Box { + set_spacing: 10, + set_hexpand: true, + set_homogeneous: true, + gtk::Button { + set_hexpand: true, + add_css_class: "card", + set_height_request: 100, + set_width_request: 100, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_valign: gtk::Align::Center, + set_spacing: 10, + set_margin_all: 15, + gtk::Image { + add_css_class: "success", + set_icon_name: Some("video-display-symbolic"), + set_pixel_size: 24, + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_valign: gtk::Align::Center, + set_spacing: 5, + gtk::Label { + set_halign: gtk::Align::Center, + add_css_class: "heading", + set_label: "Platforms" + }, + gtk::Label { + set_halign: gtk::Align::Fill, + set_hexpand: true, + add_css_class: "caption", + add_css_class: "dim-label", + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 2, + set_wrap: true, + set_max_width_chars: 0, + set_justify: gtk::Justification::Center, + #[watch] + set_label: { + let mut s = String::new(); + for p in model.platforms.iter() { + if model.platforms.iter().len() == 1 { + s.push_str(p); + } else if model.platforms.iter().len() == 2 && model.platforms.get(0) == Some(p) { + let _ = write!(s, "{} ", p); + } else if Some(p) == model.platforms.iter().last() { + let _ = write!(s, "and {}", p); + } else { + let _ = write!(s, "{}, ", p); + } + } + if model.platforms.is_empty() { + s.push_str("Unknown"); + } + &s.to_string() + }, + #[watch] + set_visible: !model.platforms.is_empty() + } + } + } + }, + gtk::Button { + set_hexpand: true, + add_css_class: "card", + set_height_request: 100, + set_width_request: 100, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_spacing: 10, + set_margin_all: 15, + gtk::Image { + add_css_class: "circular", + #[watch] + set_css_classes: &[ if model.maintainers.is_empty() { "error" } else { "accent" } ], + set_halign: gtk::Align::Center, + set_icon_name: Some("system-users-symbolic"), + set_pixel_size: 24, + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_valign: gtk::Align::Center, + set_spacing: 5, + gtk::Label { + set_halign: gtk::Align::Center, + add_css_class: "heading", + #[watch] + set_label: if model.maintainers.len() > 1 { "Maintainers" } else { "Maintainer" } + }, + gtk::Label { + set_halign: gtk::Align::Fill, + set_hexpand: true, + add_css_class: "caption", + add_css_class: "dim-label", + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 2, + set_wrap: true, + set_max_width_chars: 0, + set_justify: gtk::Justification::Center, + #[watch] + set_label: { + let mut s = String::new(); + for p in model.maintainers.iter() { + if model.maintainers.iter().len() == 1 { + if let Some(n) = &p.name { + s.push_str(n); + } else { + s.push_str(&p.github); + } + } else if model.maintainers.iter().len() == 2 && model.maintainers.get(0) == Some(p) { + if let Some(n) = &p.name { + let _ = write!(s, "{} ", n.to_string()); + } else { + let _ = write!(s, "{} ", p.github.to_string()); + } + } else if Some(p) == model.maintainers.iter().last() { + if let Some(n) = &p.name { + let _ = write!(s, "and {}", n.to_string()); + } else { + let _ = write!(s, "and {}", p.github.to_string()); + } + } else if let Some(n) = &p.name { + let _ = write!(s, "{}, ", n.to_string()); + } else { + let _ = write!(s, "{}, ", p.github.to_string()); + } + } + if model.maintainers.is_empty() { + s.push_str("Unknown"); + } + &s.to_string() + } + } + } + } + }, + } + }, + } + }, + gtk::Separator { + set_vexpand: true, + add_css_class: "spacer" + } + } + } + } + } + + fn post_view() { + println!("INSTALL TYPE {:?}", model.installtype) + } + + menu! { + installtype: { + "User (nix-env)" => NixEnvAction, + "System (configuration.nix)" => NixSystemAction, + }, + runaction: { + "Run without installing" => LaunchAction, + "Open interactive shell" => TermShellAction, + } + } + + fn init( + (): Self::Init, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let installworker = InstallAsyncHandler::builder() + .detach_worker(()) + .forward(sender.input_sender(), identity); + let config = getconfig(); + installworker.emit(InstallAsyncHandlerMsg::SetConfig(config.clone())); + let model = PkgModel { + config, + name: String::default(), + pkg: String::default(), + pname: String::default(), + summary: None, + description: None, + icon: None, + homepage: None, + licenses: vec![], + screenshots: FactoryVecDeque::new(adw::Carousel::new(), sender.input_sender()), + installworker, + platforms: vec![], + carpage: CarouselPage::Single, + installtype: InstallType::User, + maintainers: vec![], + installeduserpkgs: HashSet::new(), + installedsystempkgs: HashSet::new(), + // installinguserpkgs: HashSet::new(), + // installingsystempkgs: HashSet::new(), + // removinguserpkgs: HashSet::new(), + workqueue: HashSet::new(), + launchable: None, + tracker: 0, + }; + + let scrnfactory = model.screenshots.widget(); + relm4::set_global_css( + b".scrnbox { + border-left-width: 0; + border-right-width: 0; + border-top-width: 1px; + border-bottom-width: 1px; + }", + ); + let widgets = view_output!(); + + let group = RelmActionGroup::::new(); + let nixenv: RelmAction = { + let sender = sender.clone(); + RelmAction::new_stateless(move |_| { + println!("NIX ENV!"); + sender.input(PkgMsg::SetInstallType(InstallType::User)); + // sender.input(AppMsg::Increment); + }) + }; + + let nixsystem: RelmAction = { + let sender = sender.clone(); + RelmAction::new_stateless(move |_| { + println!("NIX SYSTEM!"); + sender.input(PkgMsg::SetInstallType(InstallType::System)); + // sender.input(AppMsg::Increment); + }) + }; + + group.add_action(nixenv); + group.add_action(nixsystem); + + let actions = group.into_action_group(); + widgets + .pkg_window + .insert_action_group("mode", Some(&actions)); + + let rungroup = RelmActionGroup::::new(); + let launchaction: RelmAction = { + let _sender = sender.clone(); + RelmAction::new_stateless(move |_| { + println!("LAUNCH!"); + // sender.input(AppMsg::Increment); + }) + }; + + let termaction: RelmAction = { + let _sender = sender; + RelmAction::new_stateless(move |_| { + println!("TERM!"); + // sender.input(AppMsg::Increment); + }) + }; + + rungroup.add_action(launchaction); + rungroup.add_action(termaction); + + let runactions = rungroup.into_action_group(); + widgets + .pkg_window + .insert_action_group("run", Some(&runactions)); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + self.reset(); + match msg { + PkgMsg::UpdateConfig(config) => { + self.config = config.clone(); + self.installworker.emit(InstallAsyncHandlerMsg::SetConfig(config)); + } + PkgMsg::Open(pkgmodel) => { + self.set_pkg(pkgmodel.pkg); + self.set_name(pkgmodel.name); + self.set_icon(pkgmodel.icon); + self.set_platforms(pkgmodel.platforms); + self.set_maintainers(pkgmodel.maintainers); + self.set_licenses(pkgmodel.licenses); + self.set_pname(pkgmodel.pname); + self.set_installeduserpkgs(pkgmodel.installeduserpkgs); + self.set_installedsystempkgs(pkgmodel.installedsystempkgs); + + if self.installedsystempkgs.contains(&self.pkg) && !self.installeduserpkgs.contains(&self.pname) { + self.set_installtype(InstallType::System) + } else { + self.set_installtype(InstallType::User) + } + + self.launchable = if let Some(l) = pkgmodel.launchable { + Some(Launch::GtkApp(l)) + } else if self.installeduserpkgs.contains(&self.pname) { + if let Ok(o) = Command::new("command").arg("-v").arg(&self.pname).output() { + if o.status.success() { + Some(Launch::TerminalApp(self.pname.to_string())) + } else { + None + } + } else { + None + } + } else { + None + }; + self.summary = if let Some(s) = pkgmodel.summary { + let mut sum = s.trim().to_string(); + while sum.contains('\n') { + sum = sum.replace('\n', " "); + } + while sum.contains(" ") { + sum = sum.replace(" ", " "); + } + Some(sum) + } else { + None + }; + + if let Some(d) = pkgmodel.description { + let mut input = d; + // Fix formatting + while input.contains('\n') { + input = input.replace('\n', " "); + } + while input.contains('\t') { + input = input.replace('\t', " "); + } + while input.contains(" ") { + input = input.replace(" ", " "); + } + let mut pango = html2pango::markup_html(&input) + .unwrap_or_else(|_| { + println!("BAD PANGO"); + input.to_string() + }) + .trim() + .to_string(); + while pango.contains("\n ") { + pango = pango.replace("\n ", "\n"); + } + while pango.ends_with('\n') { + pango.pop(); + } + self.description = Some(pango.strip_prefix('\n').unwrap_or(&pango).to_string()); + } + + if let Some(h) = pkgmodel.homepage { + match h { + StrOrVec::Single(h) => { + self.homepage = Some(h.to_string()); + } + StrOrVec::List(h) => { + if let Some(first) = h.get(0) { + self.homepage = Some(first.to_string()); + } else { + self.homepage = None; + } + } + } + } else { + self.homepage = None; + } + + if pkgmodel.screenshots.len() <= 1 { + self.carpage = CarouselPage::Single; + } else { + self.carpage = CarouselPage::First; + } + + { + let mut scrn_guard = self.screenshots.guard(); + scrn_guard.clear(); + for _i in 0..pkgmodel.screenshots.len() { + scrn_guard.push_back(()) + } + } + + for (i, url) in pkgmodel.screenshots.into_iter().enumerate() { + if let Ok(home) = env::var("HOME") { + let cachedir = format!("{}/.cache/nix-software-center", home); + let sha = digest(&url); + let scrnpath = format!("{}/screenshots/{}", cachedir, sha); + let pkg = self.pkg.clone(); + sender.command(move |out, shutdown| { + let url = url.clone(); + let home = home.clone(); + let scrnpath = scrnpath.clone(); + let pkg = pkg.clone(); + shutdown + .register(async move { + tokio::time::sleep(Duration::from_millis(5)).await; + if Path::new(&format!("{}.png", scrnpath)).exists() { + out.send(PkgAsyncMsg::LoadScreenshot(pkg, i, format!("{}.png", scrnpath))); + } else { + match reqwest::blocking::get(&url) { + Ok(mut response) => { + if response.status().is_success() { + if !Path::new(&format!( + "{}/.cache/nix-software-center/screenshots", + home + )) + .exists() + { + match fs::create_dir_all(format!( + "{}/.cache/nix-software-center/screenshots", + home + )) { + Ok(_) => {} + Err(_) => { + out.send(PkgAsyncMsg::SetError(pkg, i)); + return; + } + } + } + if let Ok(mut file) = File::create(&scrnpath) { + if response.copy_to(&mut file).is_ok() { + fn openimg(scrnpath: &str) -> Result<(), Box> { + // let mut reader = Reader::new(Cursor::new(imgdata.buffer())).with_guessed_format().expect("Cursor io never fails"); + let img = if let Ok(x) = image::load(BufReader::new(File::open(scrnpath)?), image::ImageFormat::Png) { + x + } else if let Ok(x) = image::load(BufReader::new(File::open(scrnpath)?), image::ImageFormat::Jpeg) { + x + } else if let Ok(x) = image::load(BufReader::new(File::open(scrnpath)?), image::ImageFormat::WebP) { + x + } else { + let imgdata = BufReader::new(File::open(scrnpath)?); + let format = image::guess_format(imgdata.buffer())?; + image::load(imgdata, format)? + }; + let scaled = img.resize(640, 360, FilterType::Lanczos3); + let mut output = File::create(&format!("{}.png", scrnpath))?; + scaled.write_to(&mut output, ImageFormat::Png)?; + if let Err(e) = fs::remove_file(&scrnpath) { + eprintln!("{}", e); + } + Ok(()) + } + + match openimg(&scrnpath) { + Ok(_) => { + out.send(PkgAsyncMsg::LoadScreenshot( + pkg, i, format!("{}.png", scrnpath), + )); + } + Err(_) => { + if let Err(e) = fs::remove_file(&scrnpath) { + eprintln!("{}", e); + } + out.send(PkgAsyncMsg::SetError(pkg, i)); + } + } + } + } + } else { + out.send(PkgAsyncMsg::SetError(pkg, i)); + eprintln!("Error: {}", response.status()); + } + } + Err(e) => { + out.send(PkgAsyncMsg::SetError(pkg, i)); + eprintln!("Error: {}", e); + } + } + } + }) + .drop_on_shutdown() + }) + } + } + } + PkgMsg::LoadScreenshot(pkg, i, u) => { + println!("LOAD SCREENSHOT {}", u); + // let mut scrn_guard = self.screenshots.guard(); + // println!("GOT GUARD"); + if pkg == self.pkg { + let mut scrn_guard = self.screenshots.guard(); + if let Some(mut scrn_widget) = scrn_guard.get_mut(i) { + scrn_widget.path = Some(u); + println!("GOT PATH") + } else { + println!("NO SCRN WIDGET") + } + } else { + println!("WRONG PACKAGE") + } + + // println!("LOADED SCREENSHOT"); + // scrn_guard.drop(); + } + PkgMsg::SetError(pkg, i) => { + if pkg == self.pkg { + let mut scrn_guard = self.screenshots.guard(); + if let Some(mut scrn_widget) = scrn_guard.get_mut(i) { + scrn_widget.error = true; + } + } + } + PkgMsg::SetCarouselPage(page) => { + self.carpage = page; + } + PkgMsg::OpenHomepage => { + if let Some(u) = &self.homepage { + if let Err(e) = + gio::AppInfo::launch_default_for_uri(u, gio::AppLaunchContext::NONE) + { + eprintln!("error: {}", e); + } + } + } + PkgMsg::Close => { + self.pkg = String::default(); + self.name = String::default(); + self.summary = None; + self.description = None; + self.icon = None; + let mut scrn_guard = self.screenshots.guard(); + scrn_guard.clear(); + sender.output(AppMsg::FrontPage) + } + PkgMsg::InstallUser => { + println!("INSTALL USER"); + let w = WorkPkg { + pkg: self.pkg.to_string(), + pname: self.pname.to_string(), + pkgtype: InstallType::User, + action: PkgAction::Install, + block: false, + notify: None, + }; + self.workqueue.insert(w.clone()); + // self.installinguserpkgs.insert(self.pkg.clone()); + if self.workqueue.len() == 1 { + self.installworker.emit(InstallAsyncHandlerMsg::Process(w)); + } + } + PkgMsg::RemoveUser => { + println!("REMOVE USER"); + let w = WorkPkg { + pkg: self.pkg.to_string(), + pname: self.pname.to_string(), + pkgtype: InstallType::User, + action: PkgAction::Remove, + block: false, + notify: None, + }; + self.workqueue.insert(w.clone()); + // self.installinguserpkgs.insert(self.pkg.clone()); + if self.workqueue.len() == 1 { + self.installworker.emit(InstallAsyncHandlerMsg::Process(w)); + } + } + PkgMsg::InstallSystem => { + let w = WorkPkg { + pkg: self.pkg.to_string(), + pname: self.pname.to_string(), + pkgtype: InstallType::System, + action: PkgAction::Install, + block: false, + notify: None, + }; + self.workqueue.insert(w.clone()); + if self.workqueue.len() == 1 { + self.installworker.emit(InstallAsyncHandlerMsg::Process(w)); + } + } + PkgMsg::RemoveSystem => { + let w = WorkPkg { + pkg: self.pkg.to_string(), + pname: self.pname.to_string(), + pkgtype: InstallType::System, + action: PkgAction::Remove, + block: false, + notify: None, + }; + self.workqueue.insert(w.clone()); + if self.workqueue.len() == 1 { + self.installworker.emit(InstallAsyncHandlerMsg::Process(w)); + } + } + PkgMsg::FinishedProcess(work) => { + self.workqueue.remove(&work); + println!("FINISHED PROCESS"); + println!("WORK QUEUE: {}", self.workqueue.len()); + match work.pkgtype { + InstallType::User => { + match work.action { + PkgAction::Install => { + self.installeduserpkgs.insert(work.pname.clone()); + // sender.output(AppMsg::AddUserPkg(work.pname)); + if self.launchable.is_none() { + if let Ok(o) = Command::new("command").arg("-v").arg(&self.pname).output() { + if o.status.success() { + self.set_launchable(Some(Launch::TerminalApp(self.pname.to_string()))) + } + } + } + } + PkgAction::Remove => { + self.installeduserpkgs.remove(&work.pname); + // sender.output(AppMsg::RemoveUserPkg(work.pname)); + } + } + } + InstallType::System => { + match work.action { + PkgAction::Install => { + self.installedsystempkgs.insert(work.pkg.clone()); + // sender.output(AppMsg::AddSystemPkg(work.pkg)); + if self.launchable.is_none() { + if let Ok(o) = Command::new("command").arg("-v").arg(&self.pname).output() { + if o.status.success() { + self.set_launchable(Some(Launch::TerminalApp(self.pname.to_string()))) + } + } + } + } + PkgAction::Remove => { + self.installedsystempkgs.remove(&work.pkg); + // sender.output(AppMsg::RemoveSystemPkg(work.pkg)); + } + } + } + } + sender.output(AppMsg::UpdatePkgs(None)); + if let Some(n) = &work.notify { + match n { + NotifyPage::Installed => { + sender.output(AppMsg::RemoveInstalledBusy(work)); + } + } + } + + if !self.workqueue.is_empty() { + if let Some(w) = self.workqueue.clone().iter().next() { + self.installworker.emit(InstallAsyncHandlerMsg::Process(w.clone())); + } + } + } + PkgMsg::FailedProcess(work) => { + self.workqueue.remove(&work); + if let Some(n) = &work.notify { + match n { + NotifyPage::Installed => { + sender.output(AppMsg::RemoveInstalledBusy(work)); + } + } + } + if !self.workqueue.is_empty() { + if let Some(w) = self.workqueue.clone().iter().next() { + self.installworker.emit(InstallAsyncHandlerMsg::Process(w.clone())); + } + } + } + PkgMsg::Cancel => { + // If running, cancel the current process + if let Some(h) = self.workqueue.iter().next() { + if h.pkg == self.pkg { + self.installworker. + emit(InstallAsyncHandlerMsg::CancelProcess); + return + } + } + + // If not running, remove from queue + for w in self.workqueue.clone() { + if w.pkg == self.pkg { + self.workqueue.remove(&w); + } + } + } + PkgMsg::CancelFinished => { + // If running, cancel the current process + if let Some(h) = self.workqueue.clone().iter().next() { + if h.pkg == self.pkg { + self.workqueue.remove(h); + return + } + } + + // If not running, remove from queue + for w in self.workqueue.clone() { + if w.pkg == self.pkg { + self.workqueue.remove(&w); + } + } + } + PkgMsg::Launch => { + if let Some(l) = &self.launchable { + match l { + Launch::GtkApp(x) => { + let _ = Command::new("gtk-launch").arg(x).spawn(); + } + Launch::TerminalApp(x) => { + let _ = Command::new("kgx").arg("-e").arg(x).spawn(); + } + } + } + } + PkgMsg::SetInstallType(t) => { + self.set_installtype(t); + } + PkgMsg::AddToQueue(work) => { + self.workqueue.insert(work.clone()); + if self.workqueue.len() == 1 { + self.installworker.emit(InstallAsyncHandlerMsg::Process(work)); + } + } + } + } + + fn update_cmd(&mut self, msg: Self::CommandOutput, sender: ComponentSender) { + match msg { + PkgAsyncMsg::LoadScreenshot(pkg, i, u) => { + sender.input(PkgMsg::LoadScreenshot(pkg, i, u)); + } + PkgAsyncMsg::SetError(pkg, i) => { + sender.input(PkgMsg::SetError(pkg, i)); + } + } + } +} + +relm4::new_action_group!(ModeActionGroup, "mode"); +relm4::new_stateless_action!(NixEnvAction, ModeActionGroup, "env"); +relm4::new_stateless_action!(NixSystemAction, ModeActionGroup, "system"); + +relm4::new_action_group!(RunActionGroup, "run"); +relm4::new_stateless_action!(LaunchAction, RunActionGroup, "launch"); +relm4::new_stateless_action!(TermShellAction, RunActionGroup, "term"); diff --git a/src/ui/pkgtile.rs b/src/ui/pkgtile.rs new file mode 100644 index 0000000..01d64ba --- /dev/null +++ b/src/ui/pkgtile.rs @@ -0,0 +1,180 @@ +use std::path::Path; + +use relm4::adw::prelude::*; +use relm4::gtk::pango; +use relm4::{factory::*, *}; + +use crate::APPINFO; + +use super::window::AppMsg; + +#[derive(Default, Debug, PartialEq)] +pub struct PkgTile { + pub name: String, + pub pkg: String, + pub pname: String, + pub summary: String, + pub icon: Option, + pub installeduser: bool, + pub installedsystem: bool, +} + +#[derive(Debug)] +pub enum PkgTileMsg { + Open(String), +} + +#[relm4::factory(pub)] +impl FactoryComponent for PkgTile { + type CommandOutput = (); + type Init = PkgTile; + type Input = (); + type Output = PkgTileMsg; + type Widgets = PkgTileWidgets; + type ParentWidget = gtk::FlowBox; + type ParentMsg = AppMsg; + + view! { + gtk::FlowBoxChild { + set_width_request: 270, + gtk::Overlay { + add_overlay = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + set_valign: gtk::Align::Start, + set_halign: gtk::Align::End, + gtk::Image { + add_css_class: "accent", + set_valign: gtk::Align::Start, + set_halign: gtk::Align::End, + set_pixel_size: 16, + set_margin_top: 8, + set_margin_end: 8, + set_icon_name: Some("emblem-default-symbolic"), + #[watch] + set_visible: self.installeduser, + }, + gtk::Image { + add_css_class: "success", + set_valign: gtk::Align::Start, + set_halign: gtk::Align::End, + set_pixel_size: 16, + set_margin_top: 8, + set_margin_end: 8, + set_icon_name: Some("emblem-default-symbolic"), + #[watch] + set_visible: self.installedsystem, + } + }, + gtk::Button { + add_css_class: "card", + connect_clicked[sender, pkg = self.pkg.clone()] => move |_| { + println!("CLICKED"); + sender.output(PkgTileMsg::Open(pkg.to_string())) + }, + gtk::Box { + set_margin_start: 15, + set_margin_end: 15, + set_margin_top: 10, + set_margin_bottom: 10, + set_spacing: 20, + append = if self.icon.is_some() { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_from_file: { + if let Some(i) = &self.icon { + let iconpath = format!("{}/icons/nixos/128x128/{}", APPINFO, i); + let iconpath64 = format!("{}/icons/nixos/64x64/{}", APPINFO, i); + if Path::new(&iconpath).is_file() { + Some(iconpath) + } else if Path::new(&iconpath64).is_file() { + Some(iconpath64) + } else { + None + } + } else { + None + } + }, + set_pixel_size: 64, + } + } else { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_icon_name: Some("package-x-generic"), + set_pixel_size: 64, + } + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_spacing: 3, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "heading", + set_label: &self.name, + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "dim-label", + add_css_class: "caption", + set_label: &self.pkg, + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + // add_css_class: "dim-label", + set_label: &self.summary, + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 2, + set_wrap: true, + set_max_width_chars: 0, + } + } + } + } + } + } + } + + fn init_model( + parent: Self::Init, + _index: &DynamicIndex, + _sender: FactoryComponentSender, + ) -> Self { + + let mut sum = parent.summary.trim().to_string(); + while sum.contains('\n') { + sum = sum.replace('\n', " "); + } + while sum.contains(" ") { + sum = sum.replace(" ", " "); + } + + Self { + name: parent.name, + pkg: parent.pkg, + pname: parent.pname, + summary: sum, + icon: parent.icon, + installeduser: parent.installeduser, + installedsystem: parent.installedsystem, + } + } + + fn output_to_parent_msg(output: Self::Output) -> Option { + Some(match output { + PkgTileMsg::Open(x) => AppMsg::OpenPkg(x), + }) + } +} diff --git a/src/ui/preferencespage.rs b/src/ui/preferencespage.rs new file mode 100644 index 0000000..021508a --- /dev/null +++ b/src/ui/preferencespage.rs @@ -0,0 +1,236 @@ +use std::path::PathBuf; + +use super::window::AppMsg; +use adw::prelude::*; +use relm4::*; +use relm4_components::open_dialog::*; + +#[derive(Debug)] +pub struct PreferencesPageModel { + hidden: bool, + configpath: PathBuf, + flake: Option<(PathBuf, String)>, + open_dialog: Controller, + flake_file_dialog: Controller, +} + +#[derive(Debug)] +pub enum PreferencesPageMsg { + Show(PathBuf, Option<(PathBuf, String)>), + Open, + OpenFlake, + SetConfigPath(PathBuf), + SetFlake(Option<(PathBuf, String)>), + SetFlakePath(PathBuf), + SetFlakeArg(String), + ModifyFlake, + Ignore, +} + +#[relm4::component(pub)] +impl SimpleComponent for PreferencesPageModel { + type InitParams = gtk::Window; + type Input = PreferencesPageMsg; + type Output = AppMsg; + type Widgets = PreferencesPageWidgets; + + view! { + adw::PreferencesWindow { + #[watch] + set_visible: !model.hidden, + set_transient_for: Some(&parent_window), + set_modal: true, + set_search_enabled: false, + add = &adw::PreferencesPage { + add = &adw::PreferencesGroup { + // set_title: "Preferences", + add = &adw::ActionRow { + set_title: "Configuration file", + add_suffix = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_spacing: 10, + gtk::Button { + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + gtk::Image { + set_icon_name: Some("document-open-symbolic"), + }, + gtk::Label { + #[watch] + set_label: { + let x = model.configpath.file_name().unwrap_or_default().to_str().unwrap_or_default(); + if x.is_empty() { + "(None)" + } else { + x + } + } + } + }, + connect_clicked[sender] => move |_| { + sender.input(PreferencesPageMsg::Open); + } + }, + gtk::Button { + add_css_class: "flat", + set_icon_name: "view-refresh-symbolic", + connect_clicked[sender] => move |_| { + sender.input(PreferencesPageMsg::SetConfigPath(PathBuf::from("/etc/nixos/configuration.nix"))); + } + } + } + }, + add = &adw::ActionRow { + set_title: "Use nix flakes", + add_suffix = >k::Switch { + set_valign: gtk::Align::Center, + connect_state_set[sender] => move |_, b| { + if b { + sender.input(PreferencesPageMsg::SetFlake(Some((PathBuf::new(), String::default())))); + } else { + sender.input(PreferencesPageMsg::SetFlake(None)); + } + gtk::Inhibit(false) + } + } + }, + add = &adw::ActionRow { + set_title: "Flake file", + #[watch] + set_visible: model.flake.is_some(), + add_suffix = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_spacing: 10, + gtk::Button { + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + gtk::Image { + set_icon_name: Some("document-open-symbolic"), + }, + gtk::Label { + #[watch] + set_label: { + let x = if let Some((f, _)) = &model.flake { + f.file_name().unwrap_or_default().to_str().unwrap_or_default() + } else { + "" + }; + if x.is_empty() { + "(None)" + } else { + x + } + } + } + }, + connect_clicked[sender] => move |_| { + sender.input(PreferencesPageMsg::OpenFlake); + } + }, + gtk::Button { + add_css_class: "flat", + set_icon_name: "user-trash-symbolic", + connect_clicked[sender] => move |_| { + sender.input(PreferencesPageMsg::SetFlakePath(PathBuf::new())); + } + } + } + }, + add = &adw::EntryRow { + #[watch] + set_visible: model.flake.is_some(), + set_title: "Flake arguments (--flake path/to/flake.nix#)", + connect_changed[sender] => move |x| { + sender.input(PreferencesPageMsg::SetFlakeArg(x.text().to_string())); + } + } + + } + } + } + } + + fn init( + parent_window: Self::InitParams, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let open_dialog = OpenDialog::builder() + .transient_for_native(root) + .launch(OpenDialogSettings::default()) + .forward(&sender.input, |response| match response { + OpenDialogResponse::Accept(path) => PreferencesPageMsg::SetConfigPath(path), + OpenDialogResponse::Cancel => PreferencesPageMsg::Ignore, + }); + let flake_file_dialog = OpenDialog::builder() + .transient_for_native(root) + .launch(OpenDialogSettings::default()) + .forward(&sender.input, |response| match response { + OpenDialogResponse::Accept(path) => PreferencesPageMsg::SetFlakePath(path), + OpenDialogResponse::Cancel => PreferencesPageMsg::Ignore, + }); + let model = PreferencesPageModel { + hidden: true, + configpath: PathBuf::new(), + flake: None, + open_dialog, + flake_file_dialog, + }; + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + match msg { + PreferencesPageMsg::Show(path, flake) => { + self.configpath = path; + self.flake = flake; + self.hidden = false; + println!("FLAKE {:?}", self.flake); + } + PreferencesPageMsg::Open => self.open_dialog.emit(OpenDialogMsg::Open), + PreferencesPageMsg::OpenFlake => self.flake_file_dialog.emit(OpenDialogMsg::Open), + PreferencesPageMsg::SetConfigPath(path) => { + self.configpath = path.clone(); + sender.output(AppMsg::UpdateSysconfig(path.to_string_lossy().to_string())); + } + PreferencesPageMsg::SetFlake(flake) => { + self.flake = flake; + println!("FLAKE {:?}", self.flake); + sender.input(PreferencesPageMsg::ModifyFlake) + } + PreferencesPageMsg::SetFlakePath(path) => { + self.flake = Some(( + path, + self.flake.as_ref().map(|x| x.1.clone()).unwrap_or_default(), + )); + println!("FLAKE {:?}", self.flake); + sender.input(PreferencesPageMsg::ModifyFlake) + } + PreferencesPageMsg::SetFlakeArg(arg) => { + self.flake = Some(( + self.flake.as_ref().map(|x| x.0.clone()).unwrap_or_default(), + arg, + )); + println!("FLAKE {:?}", self.flake); + sender.input(PreferencesPageMsg::ModifyFlake) + } + PreferencesPageMsg::ModifyFlake => { + let out = self + .flake + .as_ref() + .map(|(path, arg)| format!("{}#{}", path.to_string_lossy(), arg)); + sender.output(AppMsg::UpdateFlake(out.map(|x| x.strip_suffix('#').unwrap_or(&x).to_string()))); + } + _ => {} + } + } +} diff --git a/src/ui/screenshotfactory.rs b/src/ui/screenshotfactory.rs new file mode 100644 index 0000000..5f791a3 --- /dev/null +++ b/src/ui/screenshotfactory.rs @@ -0,0 +1,73 @@ +use relm4::adw::prelude::*; +use relm4::{factory::*, *}; + +use super::pkgpage::PkgMsg; + +#[derive(Default, Debug, PartialEq)] +pub struct ScreenshotItem { + pub path: Option, + pub error: bool, +} + +#[derive(Debug)] +pub enum ScreenshotItemMsg {} + +#[relm4::factory(pub)] +impl FactoryComponent for ScreenshotItem { + type CommandOutput = (); + type Init = (); + type Input = (); + type Output = ScreenshotItemMsg; + type Widgets = PkgTileWidgets; + type ParentWidget = adw::Carousel; + type ParentMsg = PkgMsg; + + view! { + gtk::Box { + set_margin_all: 15, + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Fill, + set_vexpand: true, + gtk::Picture { + #[watch] + set_visible: self.path.is_some() && !self.error, + #[watch] + set_filename: self.path.as_ref(), + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_vexpand: true, + }, + gtk::Spinner { + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_vexpand: true, + #[watch] + set_visible: self.path.is_none() && !self.error, + set_spinning: true, + set_height_request: 80, + set_width_request: 80, + set_margin_all: 30, + }, + gtk::Image { + add_css_class: "error", + set_pixel_size: 64, + set_icon_name: Some("dialog-error-symbolic"), + #[watch] + set_visible: self.error, + } + } + } + + fn init_model( + _parent: Self::Init, + _index: &DynamicIndex, + _sender: FactoryComponentSender, + ) -> Self { + Self { + path: None, + error: false, + } + } +} diff --git a/src/ui/searchpage.rs b/src/ui/searchpage.rs new file mode 100644 index 0000000..d3d1cf4 --- /dev/null +++ b/src/ui/searchpage.rs @@ -0,0 +1,274 @@ +use std::{path::Path, collections::HashSet}; +use crate::APPINFO; + +use super::window::*; +use adw::prelude::*; +use relm4::{factory::*, *, gtk::pango}; + +#[tracker::track] +#[derive(Debug)] +pub struct SearchPageModel { + #[tracker::no_eq] + searchitems: FactoryVecDeque, + searchitemtracker: u8, +} + +#[derive(Debug)] +pub enum SearchPageMsg { + Open, + Search(Vec), + UpdateInstalled(HashSet, HashSet), + Close, + OpenRow(gtk::ListBoxRow) +} + +#[relm4::component(pub)] +impl SimpleComponent for SearchPageModel { + type InitParams = (); + type Input = SearchPageMsg; + type Output = AppMsg; + type Widgets = SearchPageWidgets; + + view! { + gtk::ScrolledWindow { + set_hscrollbar_policy: gtk::PolicyType::Never, + #[track(model.changed(SearchPageModel::searchitemtracker()))] + set_vadjustment: gtk::Adjustment::NONE, + adw::Clamp { + gtk::Stack { + set_margin_all: 20, + #[local_ref] + searchlist -> gtk::ListBox { + set_valign: gtk::Align::Start, + add_css_class: "boxed-list", + set_selection_mode: gtk::SelectionMode::None, + connect_row_activated[sender] => move |_, row| { + sender.input(SearchPageMsg::OpenRow(row.clone())); + } + } + } + } + } + } + + fn init( + (): Self::InitParams, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let model = SearchPageModel { + searchitems: FactoryVecDeque::new(gtk::ListBox::new(), &sender.input), + searchitemtracker: 0, + tracker: 0, + }; + + let searchlist = model.searchitems.widget(); + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + self.reset(); + match msg { + SearchPageMsg::Search(items) => { + let mut searchitem_guard = self.searchitems.guard(); + searchitem_guard.clear(); + for item in items { + searchitem_guard.push_back(item); + } + searchitem_guard.drop(); + self.update_searchitemtracker(|_| ()); + } + SearchPageMsg::Open => {} + SearchPageMsg::Close => {} + SearchPageMsg::OpenRow(row) => { + let searchitem_guard = self.searchitems.guard(); + for (i, child) in searchitem_guard.widget().iter_children().enumerate() { + if child == row { + if let Some(item) = searchitem_guard.get(i) { + let pkg = &item.get_item().pkg; + sender.output(AppMsg::OpenPkg(pkg.to_string())); + } + } + } + } + SearchPageMsg::UpdateInstalled(installeduserpkgs, installedsystempkgs) => { + let mut searchitem_guard = self.searchitems.guard(); + for i in 0..searchitem_guard.len() { + if let Some(item) = searchitem_guard.get_mut(i) { + let mut pkgitem = item.get_mut_item(); + pkgitem.installeduser = installeduserpkgs.contains(&pkgitem.pname.to_string()); + pkgitem.installedsystem = installedsystempkgs.contains(&pkgitem.pkg.to_string()); + + } + } + } + } + } +} + +#[derive(Default, Debug, PartialEq)] +pub struct SearchItem { + pub name: String, + pub pkg: String, + pub pname: String, + pub summary: Option, + pub icon: Option, + pub installeduser: bool, + pub installedsystem: bool, +} + +#[tracker::track] +#[derive(Default, Debug, PartialEq)] +pub struct SearchItemModel { + pub item: SearchItem, +} + +#[derive(Debug)] +pub enum SearchItemMsg {} + +#[relm4::factory(pub)] +impl FactoryComponent for SearchItemModel { + type CommandOutput = (); + type Init = SearchItem; + type Input = (); + type Output = SearchItemMsg; + type Widgets = SearchItemWidgets; + type ParentWidget = adw::gtk::ListBox; + type ParentMsg = SearchPageMsg; + + view! { + adw::PreferencesRow { + #[wrap(Some)] + set_child = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + set_spacing: 10, + set_margin_all: 10, + adw::Bin { + set_valign: gtk::Align::Center, + #[wrap(Some)] + set_child = if self.item.icon.is_some() { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_from_file: { + if let Some(i) = &self.item.icon { + let iconpath = format!("{}/icons/nixos/128x128/{}", APPINFO, i); + let iconpath64 = format!("{}/icons/nixos/64x64/{}", APPINFO, i); + if Path::new(&iconpath).is_file() { + Some(iconpath) + } else if Path::new(&iconpath64).is_file() { + Some(iconpath64) + } else { + None + } + } else { + None + } + }, + set_pixel_size: 64, + } + } else { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_icon_name: Some("package-x-generic"), + set_pixel_size: 64, + } + } + }, + gtk::Overlay { + add_overlay = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + set_valign: gtk::Align::Start, + set_halign: gtk::Align::End, + gtk::Image { + add_css_class: "accent", + set_valign: gtk::Align::Start, + set_halign: gtk::Align::End, + set_icon_name: Some("emblem-default-symbolic"), + #[watch] + set_visible: self.item.installeduser, + }, + gtk::Image { + add_css_class: "success", + set_valign: gtk::Align::Start, + set_halign: gtk::Align::End, + set_icon_name: Some("emblem-default-symbolic"), + #[watch] + set_visible: self.item.installedsystem, + } + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_spacing: 2, + gtk::Label { + set_halign: gtk::Align::Start, + set_label: self.item.name.as_str(), + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "dim-label", + add_css_class: "caption", + set_label: self.item.pkg.as_str(), + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + set_label: self.item.summary.as_deref().unwrap_or(""), + set_visible: self.item.summary.is_some(), + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + } + } + } + } + } + + fn init_model( + parent: Self::Init, + _index: &DynamicIndex, + _sender: FactoryComponentSender, + ) -> Self { + let sum = if let Some(s) = parent.summary { + let mut sum = s.trim().to_string(); + while sum.contains('\n') { + sum = sum.replace('\n', " "); + } + while sum.contains(" ") { + sum = sum.replace(" ", " "); + } + Some(sum) + } else { + None + }; + + let item = SearchItem { + name: parent.name, + pkg: parent.pkg, + pname: parent.pname, + summary: sum, + icon: parent.icon, + installeduser: parent.installeduser, + installedsystem: parent.installedsystem, + }; + + Self { item, tracker: 0 } + } +} diff --git a/src/ui/updatedialog.rs b/src/ui/updatedialog.rs new file mode 100644 index 0000000..301ba5d --- /dev/null +++ b/src/ui/updatedialog.rs @@ -0,0 +1,137 @@ +use adw::prelude::*; +use relm4::*; + +use super::updatepage::UpdatePageMsg; + +#[derive(Debug)] +pub struct UpdateDialogModel { + hidden: bool, + message: String, + done: bool, + failed: bool, +} + +#[derive(Debug)] +pub enum UpdateDialogMsg { + Show(String), + Close, + Done, + Failed, +} + +#[relm4::component(pub)] +impl SimpleComponent for UpdateDialogModel { + type InitParams = gtk::Window; + type Input = UpdateDialogMsg; + type Output = UpdatePageMsg; + type Widgets = UpdateDialogWidgets; + + view! { + dialog = adw::Window { + #[watch] + set_visible: !model.hidden, + set_transient_for: Some(&parent_window), + set_modal: true, + set_resizable: false, + set_default_width: 500, + set_default_height: 200, + add_css_class: "dialog", + add_css_class: "message", + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Fill, + set_hexpand: true, + set_vexpand: true, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_vexpand: true, + set_margin_all: 15, + set_spacing: 10, + gtk::Label { + #[watch] + set_visible: !model.message.is_empty(), + add_css_class: "title-1", + #[watch] + set_label: &model.message, + }, + if model.done { + gtk::Image { + add_css_class: "success", + set_icon_name: Some("emblem-ok-symbolic"), + set_pixel_size: 128, + } + } else if model.failed { + gtk::Image { + add_css_class: "error", + set_icon_name: Some("dialog-error-symbolic"), + set_pixel_size: 128, + } + } else { + gtk::Spinner { + #[watch] + set_visible: !model.done, + #[watch] + set_spinning: !model.done, + } + } + }, + gtk::Box { + #[watch] + set_visible: model.done || model.failed, + add_css_class: "dialog-action-area", + set_valign: gtk::Align::End, + set_vexpand: true, + set_orientation: gtk::Orientation::Horizontal, + set_homogeneous: true, + gtk::Button { + set_label: "Close", + connect_clicked[sender] => move |_| { + sender.input(UpdateDialogMsg::Close); + } + } + }, + } + } + } + + fn init( + parent_window: Self::InitParams, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let model = UpdateDialogModel { + hidden: true, + done: false, + failed: false, + message: String::default(), + }; + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, _sender: ComponentSender) { + match msg { + UpdateDialogMsg::Show(desc) => { + self.message = desc; + self.hidden = false; + self.done = false; + self.failed = false; + } + UpdateDialogMsg::Close => { + self.hidden = true; + } + UpdateDialogMsg::Done => { + self.done = true; + } + UpdateDialogMsg::Failed => { + self.failed = true; + } + } + } +} \ No newline at end of file diff --git a/src/ui/updatepage.rs b/src/ui/updatepage.rs new file mode 100644 index 0000000..e7c3930 --- /dev/null +++ b/src/ui/updatepage.rs @@ -0,0 +1,537 @@ +use crate::{parse::{cache::channelver, config::{getconfig, NscConfig}}, APPINFO}; + +use super::{pkgpage::InstallType, window::*, updatedialog::{UpdateDialogModel, UpdateDialogMsg}, updateworker::{UpdateAsyncHandler, UpdateAsyncHandlerMsg}}; +use adw::prelude::*; +use relm4::{factory::*, gtk::pango, *}; +use std::{path::Path, convert::identity}; + +#[tracker::track] +#[derive(Debug)] +pub struct UpdatePageModel { + #[tracker::no_eq] + updateuserlist: FactoryVecDeque, + #[tracker::no_eq] + updatesystemlist: FactoryVecDeque, + channelupdate: Option<(String, String)>, + #[tracker::no_eq] + updatedialog: Controller, + #[tracker::no_eq] + updateworker: WorkerController, + config: NscConfig, + updatetracker: u8, +} + +#[derive(Debug)] +pub enum UpdatePageMsg { + UpdateConfig(NscConfig), + Update(Vec, Vec), + OpenRow(usize, InstallType), + UpdateSystem, + UpdateAllUser, + UpdateUser(String), + UpdateChannels, + UpdateSystemAndChannels, + UpdateAll, + DoneWorking, + DoneLoading, + FailedWorking, +} + +#[relm4::component(pub)] +impl SimpleComponent for UpdatePageModel { + type InitParams = gtk::Window; + type Input = UpdatePageMsg; + type Output = AppMsg; + type Widgets = UpdatePageWidgets; + + view! { + gtk::ScrolledWindow { + set_hscrollbar_policy: gtk::PolicyType::Never, + #[track(model.changed(UpdatePageModel::updatetracker()))] + set_vadjustment: gtk::Adjustment::NONE, + adw::Clamp { + if model.channelupdate.is_some() || !model.updateuserlist.is_empty() || !model.updatesystemlist.is_empty() { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_valign: gtk::Align::Start, + set_margin_all: 15, + set_spacing: 15, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "title-2", + set_label: "Updates", + }, + gtk::Button { + add_css_class: "suggested-action", + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_label: "Update Everything", + connect_clicked[sender] => move |_| { + sender.input(UpdatePageMsg::UpdateAll); + } + } + }, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + #[watch] + set_visible: model.channelupdate.is_some(), + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "title-4", + set_label: "Channels", + }, + }, + gtk::ListBox { + set_valign: gtk::Align::Start, + add_css_class: "boxed-list", + set_selection_mode: gtk::SelectionMode::None, + #[watch] + set_visible: model.channelupdate.is_some(), + adw::PreferencesRow { + set_activatable: false, + set_can_focus: false, + #[wrap(Some)] + set_child = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + set_spacing: 10, + set_margin_all: 10, + adw::Bin { + set_valign: gtk::Align::Center, + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_icon_name: Some("application-x-addon"), + set_pixel_size: 64, + } + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_spacing: 2, + gtk::Label { + set_halign: gtk::Align::Start, + set_label: "nixos", + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "dim-label", + add_css_class: "caption", + set_label: { + &(if let Some((old, new)) = &model.channelupdate { + format!("{} → {}", old, new) + } else { + String::default() + }) + }, + set_visible: model.channelupdate.is_some(), + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 5, + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + gtk::Button { + add_css_class: "suggested-action", + set_valign: gtk::Align::Center, + set_halign: gtk::Align::End, + set_label: "Update channel and system", + set_can_focus: false, + connect_clicked[sender] => move |_| { + sender.input(UpdatePageMsg::UpdateSystemAndChannels); + } + }, + gtk::Button { + set_valign: gtk::Align::Center, + set_halign: gtk::Align::End, + set_label: "Update channel only", + set_can_focus: false, + connect_clicked[sender] => move |_| { + sender.input(UpdatePageMsg::UpdateChannels); + } + }, + } + } + } + }, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + #[watch] + set_visible: !model.updateuserlist.is_empty(), + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "title-4", + set_label: "User (nix-env)", + }, + gtk::Button { + add_css_class: "suggested-action", + set_halign: gtk::Align::End, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_label: "Update All", + connect_clicked[sender] => move |_| { + sender.input(UpdatePageMsg::UpdateAllUser); + } + } + }, + #[local_ref] + updateuserlist -> gtk::ListBox { + set_valign: gtk::Align::Start, + add_css_class: "boxed-list", + set_selection_mode: gtk::SelectionMode::None, + connect_row_activated[sender] => move |listbox, row| { + if let Some(i) = listbox.index_of_child(row) { + sender.input(UpdatePageMsg::OpenRow(i as usize, InstallType::User)); + } + }, + #[watch] + set_visible: !model.updateuserlist.is_empty(), + }, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + #[watch] + set_visible: !model.updatesystemlist.is_empty(), + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "title-4", + set_label: "System (configuration.nix)", + }, + gtk::Button { + add_css_class: "suggested-action", + set_halign: gtk::Align::End, + set_hexpand: true, + set_valign: gtk::Align::Center, + set_label: "Update All", + connect_clicked[sender] => move |_|{ + sender.input(UpdatePageMsg::UpdateSystem); + }, + } + }, + #[local_ref] + updatesystemlist -> gtk::ListBox { + set_valign: gtk::Align::Start, + add_css_class: "boxed-list", + set_selection_mode: gtk::SelectionMode::None, + connect_row_activated[sender] => move |listbox, row| { + if let Some(i) = listbox.index_of_child(row) { + sender.input(UpdatePageMsg::OpenRow(i as usize, InstallType::System)); + } + }, + #[watch] + set_visible: !model.updatesystemlist.is_empty(), + } + } + } else { + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + set_hexpand: true, + set_vexpand: true, + set_spacing: 10, + gtk::Image { + add_css_class: "success", + set_icon_name: Some("emblem-ok-symbolic"), + set_pixel_size: 256, + }, + gtk::Label { + add_css_class: "title-1", + set_label: "Everything is up to date!" + } + } + } + } + } + } + + fn init( + parent_window: Self::InitParams, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let updatedialog = UpdateDialogModel::builder() + .launch(parent_window.upcast()) + .forward(sender.input_sender(), identity); + let updateworker = UpdateAsyncHandler::builder() + .detach_worker(()) + .forward(sender.input_sender(), identity); + + let config = getconfig(); + updateworker.emit(UpdateAsyncHandlerMsg::UpdateConfig(config.clone())); + + let model = UpdatePageModel { + updateuserlist: FactoryVecDeque::new(gtk::ListBox::new(), &sender.input), + updatesystemlist: FactoryVecDeque::new(gtk::ListBox::new(), &sender.input), + channelupdate: None, + updatetracker: 0, + updatedialog, + updateworker, + config, + tracker: 0, + }; + + let updateuserlist = model.updateuserlist.widget(); + let updatesystemlist = model.updatesystemlist.widget(); + + let widgets = view_output!(); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + self.reset(); + match msg { + UpdatePageMsg::UpdateConfig(config) => { + self.config = config; + self.updateworker.emit(UpdateAsyncHandlerMsg::UpdateConfig(self.config.clone())); + } + UpdatePageMsg::Update(updateuserlist, updatesystemlist) => { + self.channelupdate = channelver().unwrap_or(None); + self.update_updatetracker(|_| ()); + let mut updateuserlist_guard = self.updateuserlist.guard(); + updateuserlist_guard.clear(); + for updateuser in updateuserlist { + updateuserlist_guard.push_back(updateuser); + } + let mut updatesystemlist_guard = self.updatesystemlist.guard(); + updatesystemlist_guard.clear(); + for updatesystem in updatesystemlist { + updatesystemlist_guard.push_back(updatesystem); + } + } + UpdatePageMsg::OpenRow(row, pkgtype) => match pkgtype { + InstallType::User => { + let updateuserlist_guard = self.updateuserlist.guard(); + if let Some(item) = updateuserlist_guard.get(row) { + if let Some(pkg) = &item.item.pkg { + sender.output(AppMsg::OpenPkg(pkg.to_string())); + } + } + } + InstallType::System => { + let updatesystemlist_guard = self.updatesystemlist.guard(); + if let Some(item) = updatesystemlist_guard.get(row) { + if let Some(pkg) = &item.item.pkg { + sender.output(AppMsg::OpenPkg(pkg.to_string())); + } + } + } + }, + UpdatePageMsg::UpdateChannels => { + self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating channels..."))); + self.updateworker.emit(UpdateAsyncHandlerMsg::UpdateChannels); + } + UpdatePageMsg::UpdateSystemAndChannels => { + self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating system and channels..."))); + self.updateworker.emit(UpdateAsyncHandlerMsg::UpdateChannelsAndSystem); + } + UpdatePageMsg::UpdateSystem => { + self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating system..."))); + self.updateworker.emit(UpdateAsyncHandlerMsg::RebuildSystem); + } + UpdatePageMsg::UpdateUser(pkg) => { + println!("UPDATE USER PKG: {}", pkg); + eprintln!("unimplemented"); + // self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating user..."))); + // self.updateworker.emit(UpdateAsyncHandlerMsg::RebuildUser); + } + UpdatePageMsg::UpdateAllUser => { + self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating all user packages..."))); + self.updateworker.emit(UpdateAsyncHandlerMsg::UpdateUserPkgs); + } + UpdatePageMsg::UpdateAll => { + self.updatedialog.emit(UpdateDialogMsg::Show(String::from("Updating everything..."))); + self.updateworker.emit(UpdateAsyncHandlerMsg::UpdateAll); + } + UpdatePageMsg::DoneWorking => { + sender.output(AppMsg::ReloadUpdate); + } + UpdatePageMsg::DoneLoading => { + self.updatedialog.emit(UpdateDialogMsg::Done); + } + UpdatePageMsg::FailedWorking => { + self.updatedialog.emit(UpdateDialogMsg::Failed); + } + } + } +} + +#[derive(Debug, PartialEq)] +pub struct UpdateItem { + pub name: String, + pub pkg: Option, + pub pname: String, + pub summary: Option, + pub icon: Option, + pub pkgtype: InstallType, + pub verfrom: Option, + pub verto: Option, +} + +#[derive(Debug, PartialEq)] +pub struct UpdateItemModel { + item: UpdateItem, +} + +#[derive(Debug)] +pub enum UpdateItemMsg {} + +#[relm4::factory(pub)] +impl FactoryComponent for UpdateItemModel { + type CommandOutput = (); + type Init = UpdateItem; + type Input = (); + type Output = UpdateItemMsg; + type Widgets = UpdateItemWidgets; + type ParentWidget = adw::gtk::ListBox; + type ParentMsg = UpdatePageMsg; + + view! { + adw::PreferencesRow { + set_activatable: self.item.pkg.is_some(), + set_can_focus: false, + #[wrap(Some)] + set_child = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + set_hexpand: true, + set_spacing: 10, + set_margin_all: 10, + adw::Bin { + set_valign: gtk::Align::Center, + #[wrap(Some)] + set_child = if self.item.icon.is_some() { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_from_file: { + if let Some(i) = &self.item.icon { + let iconpath = format!("{}/icons/nixos/128x128/{}", APPINFO, i); + let iconpath64 = format!("{}/icons/nixos/64x64/{}", APPINFO, i); + if Path::new(&iconpath).is_file() { + Some(iconpath) + } else if Path::new(&iconpath64).is_file() { + Some(iconpath64) + } else { + None + } + } else { + None + } + }, + set_pixel_size: 64, + } + } else { + gtk::Image { + add_css_class: "icon-dropshadow", + set_halign: gtk::Align::Start, + set_icon_name: Some("package-x-generic"), + set_pixel_size: 64, + } + } + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_spacing: 2, + gtk::Label { + set_halign: gtk::Align::Start, + set_label: self.item.name.as_str(), + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "dim-label", + add_css_class: "caption", + set_label: { + &(if let Some(old) = &self.item.verfrom { + if let Some(new) = &self.item.verto { + format!("{} → {}", old, new) + } else { + String::default() + } + } else { + String::default() + }) + }, + set_visible: self.item.verfrom.is_some() && self.item.verto.is_some(), + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + gtk::Label { + set_halign: gtk::Align::Start, + set_label: self.item.summary.as_deref().unwrap_or(""), + set_visible: self.item.summary.is_some(), + set_ellipsize: pango::EllipsizeMode::End, + set_lines: 1, + set_wrap: true, + set_max_width_chars: 0, + }, + }, + // gtk::Button { + // set_visible: self.item.pkgtype == InstallType::User, + // set_valign: gtk::Align::Center, + // set_halign: gtk::Align::End, + // set_label: "Update", + // set_can_focus: false, + // } + } + } + } + + fn init_model( + parent: Self::Init, + _index: &DynamicIndex, + _sender: FactoryComponentSender, + ) -> Self { + let sum = if let Some(s) = parent.summary { + let mut sum = s.trim().to_string(); + while sum.contains('\n') { + sum = sum.replace('\n', " "); + } + while sum.contains(" ") { + sum = sum.replace(" ", " "); + } + Some(sum) + } else { + None + }; + + let item = UpdateItem { + name: parent.name, + pkg: parent.pkg, + pname: parent.pname, + summary: sum, + icon: parent.icon, + pkgtype: parent.pkgtype, + verfrom: parent.verfrom, + verto: parent.verto, + }; + + Self { item } + } +} diff --git a/src/ui/updateworker.rs b/src/ui/updateworker.rs new file mode 100644 index 0000000..4fc0cef --- /dev/null +++ b/src/ui/updateworker.rs @@ -0,0 +1,261 @@ +use std::{error::Error, path::Path, process::Stdio}; +use relm4::*; +use tokio::io::AsyncBufReadExt; + +use crate::parse::config::NscConfig; + +use super::updatepage::UpdatePageMsg; + +#[tracker::track] +#[derive(Debug)] +pub struct UpdateAsyncHandler { + #[tracker::no_eq] + process: Option>, + systemconfig: String, + flakeargs: Option, +} + +#[derive(Debug)] +pub enum UpdateAsyncHandlerMsg { + UpdateConfig(NscConfig), + + UpdateChannels, + UpdateChannelsAndSystem, + + RebuildSystem, + UpdateUserPkgs, + + UpdateAll, +} + +enum NscCmd { + Rebuild, + Channel, + All, +} + +impl Worker for UpdateAsyncHandler { + type InitParams = (); + type Input = UpdateAsyncHandlerMsg; + type Output = UpdatePageMsg; + + fn init(_params: Self::InitParams, _sender: relm4::ComponentSender) -> Self { + Self { + process: None, + systemconfig: String::default(), + flakeargs: None, + tracker: 0, + } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + match msg { + UpdateAsyncHandlerMsg::UpdateConfig(config) => { + self.systemconfig = config.systemconfig; + self.flakeargs = config.flake; + } + UpdateAsyncHandlerMsg::UpdateChannels => { + let systenconfig = self.systemconfig.clone(); + let flakeargs = self.flakeargs.clone(); + relm4::spawn(async move { + println!("STARTED"); + let result = runcmd(NscCmd::Channel, systenconfig, flakeargs).await; + match result { + Ok(true) => { + println!("CHANNEL DONE"); + sender.output(UpdatePageMsg::DoneWorking); + } + _ => { + println!("CHANNEL FAILED"); + sender.output(UpdatePageMsg::FailedWorking); + } + } + }); + } + UpdateAsyncHandlerMsg::UpdateChannelsAndSystem => { + let systenconfig = self.systemconfig.clone(); + let flakeargs = self.flakeargs.clone(); + relm4::spawn(async move { + println!("STARTED"); + let result = runcmd(NscCmd::All, systenconfig, flakeargs).await; + match result { + Ok(true) => { + println!("ALL DONE"); + sender.output(UpdatePageMsg::DoneWorking); + } + _ => { + println!("ALL FAILED"); + sender.output(UpdatePageMsg::FailedWorking); + } + } + }); + } + UpdateAsyncHandlerMsg::RebuildSystem => { + let systenconfig = self.systemconfig.clone(); + let flakeargs = self.flakeargs.clone(); + relm4::spawn(async move { + println!("STARTED"); + let result = runcmd(NscCmd::Rebuild, systenconfig, flakeargs).await; + match result { + Ok(true) => { + println!("REBUILD DONE"); + sender.output(UpdatePageMsg::DoneWorking); + } + _ => { + println!("REBUILD FAILED"); + sender.output(UpdatePageMsg::FailedWorking); + } + } + }); + } + UpdateAsyncHandlerMsg::UpdateUserPkgs => { + relm4::spawn(async move { + println!("STARTED"); + let result = updateenv().await; + match result { + Ok(true) => { + println!("USER DONE"); + sender.output(UpdatePageMsg::DoneWorking); + } + _ => { + println!("USER FAILED"); + sender.output(UpdatePageMsg::FailedWorking); + } + } + }); + } + UpdateAsyncHandlerMsg::UpdateAll => { + let systenconfig = self.systemconfig.clone(); + let flakeargs = self.flakeargs.clone(); + relm4::spawn(async move { + println!("STARTED"); + let result = runcmd(NscCmd::All, systenconfig, flakeargs).await; + match result { + Ok(true) => { + println!("ALL pkexec DONE"); + match updateenv().await { + Ok(true) => { + println!("ALL DONE"); + sender.output(UpdatePageMsg::DoneWorking); + } + _ => { + println!("ALL FAILED"); + sender.output(UpdatePageMsg::FailedWorking); + } + } + } + _ => { + println!("ALL FAILED"); + sender.output(UpdatePageMsg::FailedWorking); + } + } + }); + } + } + } +} + +async fn runcmd( + cmd: NscCmd, + _systemconfig: String, + flakeargs: Option, +) -> Result> { + let exe = match std::env::current_exe() { + Ok(mut e) => { + e.pop(); // root/bin + // e.pop(); // root/ + // e.push("libexec"); // root/libexec + e.push("nsc-helper"); + let x = e.to_string_lossy().to_string(); + println!("CURRENT PATH {}", x); + if Path::new(&x).is_file() { + x + } else { + String::from("nsc-helper") + } + } + Err(_) => String::from("nsc-helper"), + }; + + let rebuildargs = if let Some(x) = flakeargs { + let mut v = vec![String::from("--flake")]; + for arg in x.split(' ') { + if !arg.is_empty() { + v.push(String::from(arg)); + } + } + v + } else { + vec![] + }; + + let mut cmd = match cmd { + NscCmd::Rebuild => tokio::process::Command::new("pkexec") + .arg(&exe) + .arg("rebuild") + .arg("--") + .arg("switch") + .args(&rebuildargs) + .stderr(Stdio::piped()) + .spawn()?, + NscCmd::Channel => tokio::process::Command::new("pkexec") + .arg(&exe) + .arg("channel") + .stderr(Stdio::piped()) + .spawn()?, + NscCmd::All => tokio::process::Command::new("pkexec") + .arg(&exe) + .arg("channel") + .arg("--rebuild") + .arg("--") + .arg("switch") + .args(&rebuildargs) + .stderr(Stdio::piped()) + .spawn()?, + }; + + println!("SENT INPUT"); + let stderr = cmd.stderr.take().unwrap(); + let reader = tokio::io::BufReader::new(stderr); + + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + println!("CAUGHT REBUILD LINE: {}", line); + } + println!("READER DONE"); + if cmd.wait().await?.success() { + println!("SUCCESS"); + // sender.input(InstallAsyncHandlerMsg::SetPid(None)); + Ok(true) + } else { + println!("FAILURE"); + // sender.input(InstallAsyncHandlerMsg::SetPid(None)); + Ok(false) + } +} + +async fn updateenv() -> Result> { + let mut cmd = tokio::process::Command::new("nix-env") + .arg("-u") + .stderr(Stdio::piped()) + .spawn()?; + + println!("SENT INPUT"); + let stderr = cmd.stderr.take().unwrap(); + let reader = tokio::io::BufReader::new(stderr); + + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + println!("CAUGHT NIXENV LINE: {}", line); + } + println!("READER DONE"); + if cmd.wait().await?.success() { + println!("SUCCESS"); + // sender.input(InstallAsyncHandlerMsg::SetPid(None)); + Ok(true) + } else { + println!("FAILURE"); + // sender.input(InstallAsyncHandlerMsg::SetPid(None)); + Ok(false) + } +} diff --git a/src/ui/window.rs b/src/ui/window.rs new file mode 100644 index 0000000..0b94cdc --- /dev/null +++ b/src/ui/window.rs @@ -0,0 +1,1236 @@ +use std::{collections::{HashMap, HashSet}, convert::identity, error::Error, process::Command, fs, io, path::PathBuf}; +use ijson::IValue; +use relm4::{actions::*, factory::*, *}; +use adw::prelude::*; +use edit_distance; +use spdx::Expression; +use crate::{parse::{packages::{Package, LicenseEnum, Platform}, cache::uptodate, config::{NscConfig, getconfig, editconfig}}, ui::installedpage::InstalledItem, APPINFO}; + +use super::{ + categories::{PkgGroup, PkgCategory}, + pkgtile::PkgTile, + pkgpage::{PkgModel, PkgMsg, PkgInitModel, self, InstallType, WorkPkg}, + windowloading::{LoadErrorModel, LoadErrorMsg, WindowAsyncHandler, WindowAsyncHandlerMsg, CacheReturn}, searchpage::{SearchPageModel, SearchPageMsg, SearchItem}, installedpage::{InstalledPageModel, InstalledPageMsg}, updatepage::{UpdatePageModel, UpdatePageMsg, UpdateItem}, about::{AboutPageModel, AboutPageMsg}, preferencespage::{PreferencesPageModel, PreferencesPageMsg}, categorypage::{CategoryPageModel, CategoryPageMsg}, categorytile::CategoryTile, +}; + +#[derive(PartialEq)] +enum Page { + FrontPage, + PkgPage, +} + +#[derive(PartialEq)] +enum MainPage { + FrontPage, + CategoryPage, +} + +#[tracker::track] +pub struct AppModel { + application: adw::Application, + mainwindow: adw::ApplicationWindow, + config: NscConfig, + #[tracker::no_eq] + windowloading: WorkerController, + #[tracker::no_eq] + loaderrordialog: Controller, + busy: bool, + page: Page, + mainpage: MainPage, + #[tracker::no_eq] + pkgs: HashMap, + syspkgs: HashMap, + // pkgset: HashSet, + pkgitems: HashMap, + installeduserpkgs: HashMap, + installedsystempkgs: HashSet, + categoryrec: HashMap>, + categoryall: HashMap>, + #[tracker::no_eq] + recommendedapps: FactoryVecDeque, + #[tracker::no_eq] + categories: FactoryVecDeque, + #[tracker::no_eq] + pkgpage: Controller, + #[tracker::no_eq] + searchpage: Controller, + #[tracker::no_eq] + categorypage: Controller, + searching: bool, + searchquery: String, + vschild: String, + showvsbar: bool, + #[tracker::no_eq] + installedpage: Controller, + #[tracker::no_eq] + updatepage: Controller, + viewstack: adw::ViewStack, + installedpagebusy: Vec<(String, InstallType)>, +} + +#[derive(Debug)] +pub enum AppMsg { + UpdateSysconfig(String), + UpdateFlake(Option), + TryLoad, + ReloadUpdate, + Close, + LoadError(String, String), + Initialize(HashMap, Vec, HashMap, HashMap>, HashMap>), + ReloadUpdateItems(HashMap, HashMap), + OpenPkg(String), + FrontPage, + FrontFrontPage, + UpdatePkgs(Option>), + UpdateInstalledPkgs, + UpdateUpdatePkgs, + UpdateCategoryPkgs, + // AddUserPkg(String), + // RemoveUserPkg(String), + // AddSystemPkg(String), + // RemoveSystemPkg(String), + SetSearch(bool), + SetVsBar(bool), + SetVsChild(String), + Search(String), + OpenAboutPage, + OpenPreferencesPage, + AddInstalledToWorkQueue(WorkPkg), + RemoveInstalledBusy(WorkPkg), + OpenCategoryPage(PkgCategory), + LoadCategory(PkgCategory) + // OpenWithScrnshots(String, Option>), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PkgItem { + pkg: String, + pname: String, + name: String, + version: String, + summary: Option, + icon: Option, +} + +#[derive(Debug)] +pub enum AppAsyncMsg { + Search(String, Vec) +} + +#[relm4::component(pub)] +impl Component for AppModel { + type Init = adw::Application; + type Input = AppMsg; + type Output = (); + type Widgets = AppWidgets; + type CommandOutput = AppAsyncMsg; + + view! { + #[name(main_window)] + adw::ApplicationWindow { + set_default_width: 1150, + set_default_height: 800, + #[name(main_stack)] + if model.busy { + gtk::Box { + set_vexpand: true, + set_halign: gtk::Align::Fill, + set_valign: gtk::Align::Fill, + set_orientation: gtk::Orientation::Vertical, + adw::HeaderBar { + add_css_class: "flat", + #[wrap(Some)] + set_title_widget = >k::Label { + set_label: "Nix Software Center" + } + }, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_halign: gtk::Align::Center, + set_valign: gtk::Align::Center, + set_hexpand: true, + set_vexpand: true, + gtk::Spinner { + set_spinning: true, + set_height_request: 80, + }, + gtk::Label { + set_label: "Loading...", + }, + } + } + } else { + #[name(main_leaf)] + adw::Leaflet { + set_can_unfold: false, + set_homogeneous: false, + set_transition_type: adw::LeafletTransitionType::Over, + set_can_navigate_back: true, + #[name(front_leaf)] + append = &adw::Leaflet { + set_can_unfold: false, + set_homogeneous: false, + set_transition_type: adw::LeafletTransitionType::Over, + set_can_navigate_back: true, + #[name(main_box)] + append = >k::Box { + set_orientation: gtk::Orientation::Vertical, + adw::HeaderBar { + set_centering_policy: adw::CenteringPolicy::Strict, + pack_start: searchbtn = >k::ToggleButton { + add_css_class: "flat", + set_icon_name: "system-search-symbolic", + #[watch] + #[block_signal(searchtoggle)] + set_active: model.searching, + connect_toggled[sender] => move |x| { + println!("TOGGLED TO {}", x.is_active()); + sender.input(AppMsg::SetSearch(x.is_active())) + } @searchtoggle + + }, + #[name(viewswitchertitle)] + #[wrap(Some)] + set_title_widget = &adw::ViewSwitcherTitle { + set_title: "Nix Software Center", + set_stack: Some(viewstack), + connect_title_visible_notify[sender] => move |x| { + println!("TITLE NOTIFY: {}", x.is_title_visible()); + sender.input(AppMsg::SetVsBar(x.is_title_visible())) + }, + }, + pack_end: menu = >k::MenuButton { + add_css_class: "flat", + set_icon_name: "open-menu-symbolic", + #[wrap(Some)] + set_popover = >k::PopoverMenu::from_model(Some(&mainmenu)) { + add_css_class: "menu" + } + } + }, + gtk::SearchBar { + #[watch] + set_search_mode: model.searching, + #[wrap(Some)] + set_child = &adw::Clamp { + set_hexpand: true, + gtk::SearchEntry { + #[track(model.changed(AppModel::searching()) && model.searching)] + grab_focus: (), + #[track(model.changed(AppModel::searching()) && !model.searching)] + set_text: "", + connect_search_changed[sender] => move |x| { + if x.text().len() > 1 { + sender.input(AppMsg::Search(x.text().to_string())) + } + } + } + } + }, + #[local_ref] + viewstack -> adw::ViewStack { + connect_visible_child_notify[sender] => move |x| { + println!("VISIBLE CHILD NOTIFY: {:?}", x.visible_child_name()); + if let Some(c) = x.visible_child_name() { + sender.input(AppMsg::SetVsChild(c.to_string())) + } + }, + #[name(frontpage)] + add = >k::ScrolledWindow { + set_vexpand: true, + set_hexpand: true, + set_hscrollbar_policy: gtk::PolicyType::Never, + adw::Clamp { + set_maximum_size: 1000, + set_tightening_threshold: 750, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_valign: gtk::Align::Start, + set_margin_all: 15, + set_spacing: 15, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "title-4", + set_label: "Categories", + }, + #[local_ref] + categorybox -> gtk::FlowBox { + set_halign: gtk::Align::Fill, + set_hexpand: true, + set_valign: gtk::Align::Center, + set_orientation: gtk::Orientation::Horizontal, + set_selection_mode: gtk::SelectionMode::None, + set_homogeneous: true, + set_max_children_per_line: 3, + set_min_children_per_line: 2, + set_column_spacing: 14, + set_row_spacing: 14, + }, + gtk::Label { + set_halign: gtk::Align::Start, + add_css_class: "title-4", + set_label: "Recommended", + }, + #[local_ref] + recbox -> gtk::FlowBox { + set_halign: gtk::Align::Fill, + set_hexpand: true, + set_valign: gtk::Align::Center, + set_orientation: gtk::Orientation::Horizontal, + set_selection_mode: gtk::SelectionMode::None, + set_homogeneous: true, + set_max_children_per_line: 3, + set_min_children_per_line: 1, + set_column_spacing: 14, + set_row_spacing: 14, + } + } + } + }, + add: model.installedpage.widget(), + add: model.searchpage.widget(), + add: model.updatepage.widget(), + }, + adw::ViewSwitcherBar { + set_stack: Some(viewstack), + #[track(model.changed(AppModel::showvsbar()))] + set_reveal: model.showvsbar, + } + }, + append: model.categorypage.widget(), + }, + append: model.pkgpage.widget() + } + } + } + } + + menu! { + mainmenu: { + "Preferences" => PreferencesAction, + "About" => AboutAction, + } + } + + fn pre_view() { + match model.page { + Page::FrontPage => { + main_leaf.set_visible_child(front_leaf); + } + Page::PkgPage => { + main_leaf.set_visible_child(model.pkgpage.widget()); + } + } + match model.mainpage { + MainPage::FrontPage => { + front_leaf.set_visible_child(main_box); + } + MainPage::CategoryPage => { + front_leaf.set_visible_child(model.categorypage.widget()); + } + } + } + + fn init( + application: Self::Init, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let windowloading = WindowAsyncHandler::builder() + .detach_worker(()) + .forward(sender.input_sender(), identity); + let loaderrordialog = LoadErrorModel::builder() + .launch(root.clone().upcast()) + .forward(sender.input_sender(), identity); + let pkgpage = PkgModel::builder() + .launch(()) + .forward(sender.input_sender(), identity); + let searchpage = SearchPageModel::builder() + .launch(()) + .forward(sender.input_sender(), identity); + let categorypage = CategoryPageModel::builder() + .launch(()) + .forward(sender.input_sender(), identity); + let installedpage = InstalledPageModel::builder() + .launch(()) + .forward(sender.input_sender(), identity); + let updatepage = UpdatePageModel::builder() + .launch(root.clone().upcast()) + .forward(sender.input_sender(), identity); + let viewstack = adw::ViewStack::new(); + + let config = getconfig(); + + let model = AppModel { + application, + mainwindow: root.clone(), + config, + windowloading, + loaderrordialog, + busy: true, + page: Page::FrontPage, + mainpage: MainPage::FrontPage, + pkgs: HashMap::new(), + syspkgs: HashMap::new(), + pkgitems: HashMap::new(), + installeduserpkgs: HashMap::new(), + installedsystempkgs: HashSet::new(), + categoryrec: HashMap::new(), + categoryall: HashMap::new(), + recommendedapps: FactoryVecDeque::new(gtk::FlowBox::new(), &sender.input), + categories: FactoryVecDeque::new(gtk::FlowBox::new(), &sender.input), + pkgpage, + searchpage, + categorypage, + searching: false, + searchquery: String::default(), + vschild: String::default(), + showvsbar: false, + installedpage, + updatepage, + viewstack, + installedpagebusy: vec![], + tracker: 0, + }; + + model.windowloading.emit(WindowAsyncHandlerMsg::CheckCache(CacheReturn::Init)); + let recbox = model.recommendedapps.widget(); + let categorybox = model.categories.widget(); + let viewstack = &model.viewstack; + + let widgets = view_output!(); + + let group = RelmActionGroup::::new(); + let aboutpage: RelmAction = { + let sender = sender.clone(); + RelmAction::new_stateless(move |_| { + sender.input(AppMsg::OpenAboutPage); + }) + }; + + let prefernecespage: RelmAction = { + let sender = sender; + RelmAction::new_stateless(move |_| { + sender.input(AppMsg::OpenPreferencesPage); + }) + }; + + group.add_action(aboutpage); + group.add_action(prefernecespage); + let actions = group.into_action_group(); + widgets + .main_window + .insert_action_group("menu", Some(&actions)); + + widgets.main_stack.set_vhomogeneous(false); + widgets.main_stack.set_hhomogeneous(false); + let frontvs = widgets.viewstack.page(&widgets.frontpage); + let installedvs = widgets.viewstack.page(model.installedpage.widget()); + let updatesvs = widgets.viewstack.page(model.updatepage.widget()); + let searchvs = widgets.viewstack.page(model.searchpage.widget()); + frontvs.set_title(Some("Explore")); + installedvs.set_title(Some("Installed")); + updatesvs.set_title(Some("Updates")); + frontvs.set_name(Some("explore")); + installedvs.set_name(Some("installed")); + searchvs.set_name(Some("search")); + updatesvs.set_name(Some("updates")); + + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + self.reset(); + match msg { + AppMsg::TryLoad => { + self.busy = true; + self.windowloading.emit(WindowAsyncHandlerMsg::CheckCache(CacheReturn::Init)); + } + AppMsg::ReloadUpdate => { + self.windowloading.emit(WindowAsyncHandlerMsg::CheckCache(CacheReturn::Update)); + } + AppMsg::Close => { + self.application.quit(); + } + AppMsg::LoadError(msg, msg2) => { + self.busy = false; + self.loaderrordialog.emit(LoadErrorMsg::Show(msg, msg2)); + } + AppMsg::UpdateSysconfig(systemconfig) => { + self.config = NscConfig { + systemconfig, + flake: self.config.flake.clone(), + }; + if editconfig(self.config.clone()).is_err() { + eprintln!("Failed to update config"); + } + self.pkgpage.emit(PkgMsg::UpdateConfig(self.config.clone())); + self.updatepage.emit(UpdatePageMsg::UpdateConfig(self.config.clone())); + sender.input(AppMsg::UpdatePkgs(None)) + } + AppMsg::UpdateFlake(flake) => { + self.config = NscConfig { + systemconfig: self.config.systemconfig.clone(), + flake, + }; + if editconfig(self.config.clone()).is_err() { + eprintln!("Failed to update config"); + } + self.pkgpage.emit(PkgMsg::UpdateConfig(self.config.clone())); + self.updatepage.emit(UpdatePageMsg::UpdateConfig(self.config.clone())); + } + AppMsg::Initialize(pkgs, recommendedapps, syspkgs, categoryrec, categoryall) => { + self.syspkgs = syspkgs; + self.categoryrec = categoryrec; + self.categoryall = categoryall; + let mut pkgitems = HashMap::new(); + for (pkg, pkgdata) in &pkgs { + // if let Some(pkgdata) = pkgs.get(*pkg) { + // println!("GOT DATA FOR {}", pkg); + let pname = pkgdata.pname.to_string(); + let mut name = pkgdata.pname.to_string(); + let version = pkgdata.version.to_string(); + let mut icon = None; + let mut summary = pkgdata.meta.description.as_ref().map(|x| x.to_string()); + if let Some(appdata) = &pkgdata.appdata { + // println!("GOT APPDATA FOR {}", pkg); + if let Some(i) = &appdata.icon { + if let Some(mut iconvec) = i.cached.clone() { + iconvec.sort_by(|a, b| a.height.cmp(&b.height)); + icon = Some(iconvec[0].name.clone()); + } + } + if let Some(s) = &appdata.summary { + summary = Some(s.get("C").unwrap_or(&summary.unwrap_or_default()).to_string()); + } + if let Some(n) = &appdata.name { + name = n.get("C").unwrap_or(&name).to_string(); + } + } + pkgitems.insert(pkg.to_string(), PkgItem { + pkg: pkg.to_string(), + pname, + name, + version, + icon, + summary, + }); + // } + } + self.pkgitems = pkgitems; + self.page = Page::FrontPage; + self.pkgpage.emit(PkgMsg::UpdateConfig(self.config.clone())); + self.updatepage.emit(UpdatePageMsg::UpdateConfig(self.config.clone())); + self.pkgs = pkgs; + sender.input(AppMsg::UpdatePkgs(Some(recommendedapps))); + + let mut cat_guard = self.categories.guard(); + for c in vec![ + PkgCategory::Audio, + PkgCategory::Development, + PkgCategory::Games, + PkgCategory::Graphics, + PkgCategory::Network, + PkgCategory::Video, + ] { + cat_guard.push_back(c); + } + cat_guard.drop(); + self.busy = false; + } + AppMsg::ReloadUpdateItems(pkgs, syspkgs) => { + self.syspkgs = syspkgs; + let mut pkgitems = HashMap::new(); + for (pkg, pkgdata) in &pkgs { + // if let Some(pkgdata) = pkgs.get(*pkg) { + // println!("GOT DATA FOR {}", pkg); + let pname = pkgdata.pname.to_string(); + let mut name = pkgdata.pname.to_string(); + let version = pkgdata.version.to_string(); + let mut icon = None; + let mut summary = pkgdata.meta.description.as_ref().map(|x| x.to_string()); + if let Some(appdata) = &pkgdata.appdata { + // println!("GOT APPDATA FOR {}", pkg); + if let Some(i) = &appdata.icon { + if let Some(mut iconvec) = i.cached.clone() { + iconvec.sort_by(|a, b| a.height.cmp(&b.height)); + icon = Some(iconvec[0].name.clone()); + } + } + if let Some(s) = &appdata.summary { + summary = Some(s.get("C").unwrap_or(&summary.unwrap_or_default()).to_string()); + } + if let Some(n) = &appdata.name { + name = n.get("C").unwrap_or(&name).to_string(); + } + } + pkgitems.insert(pkg.to_string(), PkgItem { + pkg: pkg.to_string(), + pname, + name, + version, + icon, + summary, + }); + // } + } + self.pkgitems = pkgitems; + sender.input(AppMsg::UpdatePkgs(None)); + self.updatepage.emit(UpdatePageMsg::DoneLoading); + } + AppMsg::OpenPkg(pkg) => { + // if let Some(pkgs) = &self.pkgs { + if let Some(input) = self.pkgs.get(&pkg) { + let mut name = input.pname.to_string(); + let mut summary = input.meta.description.as_ref().map(|x| x.to_string()); + let mut description = input.meta.longdescription.as_ref().map(|x| x.to_string()); + let mut icon = None; + let mut screenshots = vec![]; + let mut licenses = vec![]; + let mut platforms = vec![]; + let mut maintainers = vec![]; + let mut launchable = None; + + fn addlicense(pkglicense: &LicenseEnum, licenses: &mut Vec) { + match pkglicense { + LicenseEnum::Single(l) => { + if let Some(n) = &l.fullname { + let parsed = if let Some(id) = &l.spdxid { + if let Ok(Some(license)) = Expression::parse(id).map(|p| p.requirements().map(|er| er.req.license.id()).collect::>()[0]) { + Some(license) + } else { + None + } + } else if let Ok(Some(license)) = Expression::parse(n).map(|p| p.requirements().map(|er| er.req.license.id()).collect::>()[0]) { + Some(license) + } else { + None + }; + licenses.push(pkgpage::License { + free: if let Some(f) = l.free { Some(f) } else { parsed.map(|p| p.is_osi_approved() || p.is_fsf_free_libre() )}, + fullname: n.to_string(), + spdxid: l.spdxid.clone().map(|x| x.to_string()), + url: if let Some(u) = &l.url { Some(u.to_string()) } else { parsed.map(|p| format!("https://spdx.org/licenses/{}.html", p.name))}, + }) + } else if let Some(s) = &l.spdxid { + if let Ok(Some(license)) = Expression::parse(s).map(|p| p.requirements().map(|er| er.req.license.id()).collect::>()[0]) { + licenses.push(pkgpage::License { + free: Some(license.is_osi_approved() || license.is_fsf_free_libre() || l.free.unwrap_or(false)), + fullname: license.full_name.to_string(), + spdxid: Some(license.name.to_string()), + url: if l.url.is_some() { + l.url.clone().map(|x| x.to_string()) + } else { + Some(format!("https://spdx.org/licenses/{}.html", license.name)) + }, + }) + } + } + }, + LicenseEnum::List(lst) => { + for l in lst { + addlicense(&LicenseEnum::Single(l.clone()), licenses); + } + }, + LicenseEnum::SingleStr(s) => { + if let Ok(Some(license)) = Expression::parse(s).map(|p| p.requirements().map(|er| er.req.license.id()).collect::>()[0]) { + licenses.push(pkgpage::License { + free: Some(license.is_osi_approved() || license.is_fsf_free_libre()), + fullname: license.full_name.to_string(), + spdxid: Some(license.name.to_string()), + url: Some(format!("https://spdx.org/licenses/{}.html", license.name)), + }) + } + }, + LicenseEnum::VecStr(lst) => { + for s in lst { + addlicense(&LicenseEnum::SingleStr(s.clone()), licenses); + } + }, + LicenseEnum::Mixed(v) => { + for l in v { + addlicense(l, licenses); + } + } + } + } + + if let Some(pkglicense) = &input.meta.license { + addlicense(pkglicense, &mut licenses); + } + + if let Some(data) = &input.appdata { + if let Some(n) = &data.name { + if let Some(n) = n.get("C") { + name = n.to_string(); + } + } + if let Some(s) = &data.summary { + if let Some(s) = s.get("C") { + summary = Some(s.to_string()); + } + } + if let Some(d) = &data.description { + if let Some(d) = d.get("C") { + description = Some(d.to_string()); + } + } + if let Some(i) = &data.icon { + if let Some(mut i) = i.cached.clone() { + i.sort_by(|x, y| x.height.cmp(&y.height)); + if let Some(i) = i.last() { + icon = Some(format!( + "{}/icons/nixos/{}x{}/{}", + APPINFO, i.width, i.height, i.name + )); + } + } + } + if let Some(s) = &data.screenshots { + for s in s { + if let Some(u) = &s.sourceimage { + if !screenshots.contains(&u.url) { + if s.default == Some(true) { + screenshots.insert(0, u.url.clone()); + } else { + screenshots.push(u.url.clone()); + } + } else if s.default == Some(true) { + if let Some(index) = screenshots.iter().position(|x| *x == u.url) { + screenshots.remove(index); + screenshots.insert(0, u.url.clone()); + } + } + } + } + } + if let Some(l) = &data.launchable { + if let Some(d) = l.desktopid.get(0) { + launchable = Some(d.to_string()); + } + } + } + + if let Some(p) = &input.meta.platforms { + match p { + Platform::Single(p) => { + if !platforms.contains(&p.to_string()) && p != &input.system { + platforms.push(p.to_string()); + } + }, + Platform::List(v) => { + for p in v { + if !platforms.contains(&p.to_string()) && p != &input.system { + platforms.push(p.to_string()); + } + } + }, + Platform::ListList(vv) => { + for v in vv { + for p in v { + if !platforms.contains(&p.to_string()) && p != &input.system { + platforms.push(p.to_string()); + } + } + } + } + } + } + platforms.sort(); + platforms.insert(0, input.system.to_string()); + + if let Some(m) = input.meta.maintainers.clone() { + for m in m { + maintainers.push(m); + } + } + + let out = PkgInitModel { + name, + pname: input.pname.to_string(), + summary, + description, + icon, + pkg, + screenshots, + homepage: input.meta.homepage.clone(), + platforms, + licenses, + maintainers, + installeduserpkgs: self.installeduserpkgs.keys().cloned().collect(), + installedsystempkgs: self.installedsystempkgs.clone(), + launchable + }; + self.page = Page::PkgPage; + if self.viewstack.visible_child_name() != Some(gtk::glib::GString::from("search")) { + self.searching = false; + } + self.busy = false; + self.pkgpage.emit(PkgMsg::Open(Box::new(out))); + } + } + AppMsg::FrontPage => { + self.page = Page::FrontPage; + // sender.input(AppMsg::UpdatePkgs(None)); + } + AppMsg::FrontFrontPage => { + self.page = Page::FrontPage; + self.mainpage = MainPage::FrontPage; + // sender.input(AppMsg::UpdatePkgs(None)); + } + AppMsg::UpdatePkgs(rec) => { + println!("UPDATE PKGS"); + fn getsystempkgs(config: &str) -> Result, Box> { + let f = fs::read_to_string(config)?; + match nix_editor::read::getarrvals(&f, "environment.systemPackages") { + Ok(x) => Ok(HashSet::from_iter(x.into_iter())), + Err(_) => Err(Box::new(io::Error::new( + io::ErrorKind::InvalidData, + "Failed to read value from configuration", + ))) + } + } + + fn getuserpkgs() -> Result, Box> { + let out = Command::new("nix-env").arg("-q").arg("--json").output()?; + let data: IValue = serde_json::from_str(&String::from_utf8_lossy(&out.stdout))?; + let mut pcurrpkgs = HashMap::new(); + for (_, pkg) in data.as_object().unwrap() { + pcurrpkgs.insert( + pkg.as_object().unwrap()["pname"] + .as_string() + .unwrap() + .to_string(), + pkg.as_object().unwrap()["version"] + .as_string() + .unwrap() + .to_string() + ); + } + Ok(pcurrpkgs) + } + + let systempkgs = match getsystempkgs(&self.config.systemconfig) { + Ok(x) => x, + Err(_) => { + self.installedsystempkgs.clone() + } + }; + + let userpkgs = match getuserpkgs() { + Ok(out) => out, + Err(_) => { + self.installeduserpkgs.clone() + } + }; + + self.installedsystempkgs = systempkgs; + self.installeduserpkgs = userpkgs; + + if let Some(recommendedapps) = rec { + let mut recapps_guard = self.recommendedapps.guard(); + for app in recommendedapps { + if let Some(x) = self.pkgs.get(&app) { + if let Some(data) = &x.appdata { + if let Some(icon) = &data.icon { + if let Some(summary) = &data.summary { + if let Some(name) = &data.name { + let name = name.get("C").unwrap().to_string(); + let mut iconvec = icon.cached.as_ref().unwrap().to_vec(); + iconvec.sort_by(|a, b| a.height.cmp(&b.height)); + let summary = + summary.get("C").unwrap_or(&String::new()).to_string(); + recapps_guard.push_back(PkgTile { + pkg: app, + name, + pname: x.pname.to_string(), + icon: Some(iconvec[0].name.clone()), + summary, + installeduser: self.installeduserpkgs.contains_key(&x.pname.to_string()), + installedsystem: self.installedsystempkgs.contains(&x.pname.to_string()), + }); + } + } + } + } + } + } + recapps_guard.drop(); + } else { + let mut pkgtile_guard = self.recommendedapps.guard(); + for i in 0..pkgtile_guard.len() { + if let Some(pkgtile) = &mut pkgtile_guard.get_mut(i) { + pkgtile.installedsystem = self.installedsystempkgs.contains(&pkgtile.pkg); + pkgtile.installeduser = self.installeduserpkgs.contains_key(&pkgtile.pname); + } + } + pkgtile_guard.drop(); + } + + sender.input(AppMsg::UpdateInstalledPkgs); + sender.input(AppMsg::UpdateUpdatePkgs); + sender.input(AppMsg::UpdateCategoryPkgs); + + println!("UPDATE SEARCH"); + if self.searching { + self.update_searching(|_| ()); + self.searchpage.emit(SearchPageMsg::UpdateInstalled(self.installeduserpkgs.keys().cloned().collect(), self.installedsystempkgs.clone())); + } + println!("FINISHED UPDATE SEARCH"); + } + AppMsg::UpdateInstalledPkgs => { + let mut installeduseritems = vec![]; + for installedpname in self.installeduserpkgs.keys() { + let possibleitems = self.pkgitems.iter().filter(|(_, x)| &x.pname == installedpname); + let count = possibleitems.clone().count(); + match count { + 1 => { + let (pkg, data) = possibleitems.collect::>()[0]; + installeduseritems.push(InstalledItem { + name: data.name.clone(), + pname: data.pname.clone(), + pkg: Some(pkg.clone()), + summary: data.summary.clone(), + icon: data.icon.clone(), + pkgtype: InstallType::User, + busy: self.installedpagebusy.contains(&(data.pname.clone(), InstallType::User)), + }) + } + 2.. => { + installeduseritems.push(InstalledItem { + name: installedpname.clone(), + pname: installedpname.clone(), + pkg: None, + summary: None, //data.summary.clone(), + icon: None, //data.icon.clone(), + pkgtype: InstallType::User, + busy: self.installedpagebusy.contains(&(installedpname.clone(), InstallType::User)), + }) + } + _ => {} + } + } + + installeduseritems.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + let mut installedsystemitems = vec![]; + for installedpkg in &self.installedsystempkgs { + if let Some(item) = self.pkgitems.get(installedpkg) { + installedsystemitems.push(InstalledItem { + name: item.name.clone(), + pname: item.pname.clone(), + pkg: Some(item.pkg.clone()), + summary: item.summary.clone(), + icon: item.icon.clone(), + pkgtype: InstallType::System, + busy: self.installedpagebusy.contains(&(item.pkg.clone(), InstallType::System)), + }) + } + } + installedsystemitems.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + self.installedpage.emit(InstalledPageMsg::Update(installeduseritems, installedsystemitems)); + } + AppMsg::UpdateUpdatePkgs => { + let mut updateuseritems = vec![]; + for (installedpname, version) in self.installeduserpkgs.iter() { + let possibleitems = self.pkgitems.iter().filter(|(_, x)| &x.pname == installedpname); + let count = possibleitems.clone().count(); + match count { + 1 => { + let (pkg, data) = possibleitems.collect::>()[0]; + if &data.version != version { + updateuseritems.push(UpdateItem { + name: data.name.clone(), + pname: data.pname.clone(), + pkg: Some(pkg.clone()), + summary: data.summary.clone(), + icon: data.icon.clone(), + pkgtype: InstallType::User, + verfrom: Some(version.clone()), + verto: Some(data.version.clone()), + }) + } else { + println!("Pkg {} is up to date", pkg); + } + } + 2.. => { + let mut update = true; + for (pkg, _) in possibleitems { + if let Some(ver) = self.syspkgs.get(pkg) { + if version == ver { + update = false; + } + + } + } + if update { + updateuseritems.push(UpdateItem { + name: installedpname.clone(), + pname: installedpname.clone(), + pkg: None, + summary: None, //data.summary.clone(), + icon: None, //data.icon.clone(), + pkgtype: InstallType::User, + verfrom: Some(version.clone()), + verto: None, + }) + } else { + println!("Pkg {} is up to date", installedpname); + } + } + _ => {} + } + } + updateuseritems.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + let mut updatesystemitems = vec![]; + for installedpkg in &self.installedsystempkgs { + if let Some(item) = self.pkgitems.get(installedpkg) { + if let Some(sysver) = self.syspkgs.get(installedpkg) { + if &item.version != sysver { + updatesystemitems.push(UpdateItem { + name: item.name.clone(), + pname: item.pname.clone(), + pkg: Some(item.pkg.clone()), + summary: item.summary.clone(), + icon: item.icon.clone(), + pkgtype: InstallType::System, + verfrom: Some(sysver.clone()), + verto: Some(item.version.clone()), + + }) + } else { + println!("Pkg {} is up to date", item.pkg); + } + } + } + } + updatesystemitems.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + if let Ok(Some((old, new))) = uptodate() { + updatesystemitems.insert(0, UpdateItem { + name: String::from("NixOS System"), + pname: String::new(), + pkg: None, + summary: Some(String::from("NixOS internal packages and modules")), + icon: None, + pkgtype: InstallType::System, + verfrom: Some(old), + verto: Some(new), + }) + } + self.updatepage.emit(UpdatePageMsg::Update(updateuseritems, updatesystemitems)); + } + AppMsg::UpdateCategoryPkgs => { + self.categorypage.emit(CategoryPageMsg::UpdateInstalled(self.installeduserpkgs.keys().cloned().collect::>(), self.installedsystempkgs.iter().cloned().collect::>())); + } + AppMsg::SetSearch(show) => { + self.set_searching(show); + if !show { + if let Some(s) = self.viewstack.visible_child_name() { + if s == "search" { + self.viewstack.set_visible_child_name("explore"); + } + } + } + } + AppMsg::SetVsChild(name) => { + if name != self.vschild { + self.set_vschild(name.to_string()); + if name != "search" { + sender.input(AppMsg::SetSearch(false)) + } + } + } + AppMsg::SetVsBar(vsbar) => { + self.set_showvsbar(vsbar); + } + AppMsg::Search(search) => { + println!("SEARCHING FOR: {}", search); + self.viewstack.set_visible_child_name("search"); + self.searchpage.emit(SearchPageMsg::Open); + self.set_searchquery(search.to_string()); + // let pkgs = self.pkgs.iter().map(|x| ); + let pkgitems: Vec = self.pkgitems.values().cloned().collect(); + let installeduserpkgs = self.installeduserpkgs.clone(); + let installedsystempkgs = self.installedsystempkgs.clone(); + sender.command(move |out, shutdown| { + let pkgs = pkgitems.clone(); + let search = search.clone(); + let installeduserpkgs = installeduserpkgs.clone(); + let installedsystempkgs = installedsystempkgs; + shutdown.register(async move { + let searchsplit: Vec = search.split(' ').filter(|x| x.len() > 1).map(|x| x.to_string()).collect(); + let mut namepkgs = pkgs.iter().filter(|x| searchsplit.iter().any(|s| x.name.to_lowercase().contains(&s.to_lowercase()))).collect::>(); + let mut pnamepkgs = pkgs.iter().filter(|x| searchsplit.iter().any(|s| x.pname.to_lowercase().contains(&s.to_lowercase())) && !namepkgs.contains(x)).collect::>(); + let mut pkgpkgs = pkgs.iter().filter(|x| searchsplit.iter().any(|s| x.pkg.to_lowercase().contains(&s.to_lowercase())) && !namepkgs.contains(x) && !pnamepkgs.contains(x)).collect::>(); + let mut sumpkgs = pkgs.iter().filter(|x| if let Some(sum) = &x.summary { searchsplit.iter().any(|s| sum.to_lowercase().contains(&s.to_lowercase())) } else { false } && !namepkgs.contains(x) && !pnamepkgs.contains(x) && !pkgpkgs.contains(x)).collect::>(); + println!("FOUND {} PACKAGES", namepkgs.len() + pnamepkgs.len() + pkgpkgs.len() + sumpkgs.len()); + namepkgs.sort_by(|a, b| edit_distance::edit_distance(&a.name.to_lowercase(), &search.to_lowercase()).cmp(&edit_distance::edit_distance(&b.name.to_lowercase(), &search.to_lowercase()))); + pnamepkgs.sort_by(|a, b| edit_distance::edit_distance(&a.pname.to_lowercase(), &search.to_lowercase()).cmp(&edit_distance::edit_distance(&b.pname.to_lowercase(), &search.to_lowercase()))); + pkgpkgs.sort_by(|a, b| edit_distance::edit_distance(&a.pkg.to_lowercase(), &search.to_lowercase()).cmp(&edit_distance::edit_distance(&b.pkg.to_lowercase(), &search.to_lowercase()))); + sumpkgs.sort_by(|a, b| { + let mut x = 0; + for s in &searchsplit { + x += a.summary.as_ref().unwrap_or(&String::new()).to_lowercase().matches(&s.to_lowercase()).count() + } + let mut y = 0; + for s in &searchsplit { + y += b.summary.as_ref().unwrap_or(&String::new()).to_lowercase().matches(&s.to_lowercase()).count() + } + x.cmp(&y) + }); + + + let mut combpkgs = namepkgs; + combpkgs.append(&mut pnamepkgs); + combpkgs.append(&mut pkgpkgs); + combpkgs.append(&mut sumpkgs); + + combpkgs.sort_by(|a, b| { + b.icon.is_some().cmp(&a.icon.is_some()) + }); + // namepkgs.sort_by(|a, b| a.name.cmp(&b.name)); + // tokio::time::sleep(Duration::from_secs(1)).await; + // out.send(AppMsg::Increment); + let mut outpkgs: Vec = vec![]; + for (i, p) in combpkgs.iter().enumerate() { + outpkgs.push(SearchItem { + pkg: p.pkg.to_string(), + pname: p.pname.to_string(), + name: p.name.to_string(), + summary: p.summary.clone(), + icon: p.icon.clone(), + installeduser: installeduserpkgs.contains_key(&p.pname), + installedsystem: installedsystempkgs.contains(&p.pname), + }); + if i > 100 { + break; + } + } + out.send(AppAsyncMsg::Search(search.to_string(), outpkgs)) + }).drop_on_shutdown() + }) + } + AppMsg::OpenAboutPage => { + let aboutpage = AboutPageModel::builder() + .launch(self.mainwindow.clone().upcast()) + .forward(sender.input_sender(), identity); + aboutpage.emit(AboutPageMsg::Show); + } + AppMsg::OpenPreferencesPage => { + let preferencespage = PreferencesPageModel::builder() + .launch(self.mainwindow.clone().upcast()) + .forward(sender.input_sender(), identity); + preferencespage.emit(PreferencesPageMsg::Show(PathBuf::from(&self.config.systemconfig), None)); + } + AppMsg::AddInstalledToWorkQueue(work) => { + println!("ADDING INSTALLED TO WORK QUEUE {:?}", work); + let p = match work.pkgtype { + InstallType::User => work.pname.to_string(), + InstallType::System => work.pkg.to_string(), + }; + self.installedpagebusy.push((p, work.pkgtype.clone())); + self.pkgpage.emit(PkgMsg::AddToQueue(work)); + println!("INSTALLEDBUSY: {:?}", self.installedpagebusy); + } + AppMsg::RemoveInstalledBusy(work) => { + println!("REMOVE INSTALLED BUSY {:?}", work); + let p = match work.pkgtype { + InstallType::User => work.pname.to_string(), + InstallType::System => work.pkg.to_string(), + }; + self.installedpagebusy.retain(|(x, y)| x != &p && y != &work.pkgtype); + self.installedpage.emit(InstalledPageMsg::UnsetBusy(work)); + } + AppMsg::OpenCategoryPage(category) => { + println!("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! OPEN CATEGORY PAGE {:?}", category); + self.page = Page::FrontPage; + self.mainpage = MainPage::CategoryPage; + // self.categorypage.emit(CategoryPageMsg::Open(category.clone(), self.categoryrec.get(&category).unwrap_or(&vec![]).to_vec(), self.categoryall.get(&category).unwrap_or(&vec![]).to_vec())); + sender.input(AppMsg::LoadCategory(category)); + } + AppMsg::LoadCategory(category) => { + let mut catrec = vec![]; + for app in self.categoryrec.get(&category).unwrap_or(&vec![]) { + if let Some(x) = self.pkgs.get(app) { + let mut name = x.pname.to_string(); + let mut icon = None; + let mut summary = x.meta.description.clone().map(|x| x.to_string()); + if let Some(data) = &x.appdata { + if let Some(i) = &data.icon { + let mut iconvec = i.cached.as_ref().unwrap().to_vec(); + iconvec.sort_by(|a, b| a.height.cmp(&b.height)); + icon = Some(iconvec[0].name.clone()); + } + if let Some(s) = &data.summary { + summary = + s.get("C").map(|x| x.to_string()); + + } + if let Some(n) = &data.name { + name = n.get("C").unwrap().to_string(); + } + } + catrec.push(CategoryTile { + pkg: app.to_string(), + name, + pname: x.pname.to_string(), + icon, + summary, + installeduser: self.installeduserpkgs.contains_key(&x.pname.to_string()), + installedsystem: self.installedsystempkgs.contains(&x.pname.to_string()), + }); + } + } + + let mut catall = vec![]; + for app in self.categoryall.get(&category).unwrap_or(&vec![]) { + if let Some(x) = self.pkgs.get(app) { + let mut name = x.pname.to_string(); + let mut icon = None; + let mut summary = x.meta.description.clone().map(|x| x.to_string()); + if let Some(data) = &x.appdata { + if let Some(i) = &data.icon { + let mut iconvec = i.cached.as_ref().unwrap().to_vec(); + iconvec.sort_by(|a, b| a.height.cmp(&b.height)); + icon = Some(iconvec[0].name.clone()); + } + if let Some(s) = &data.summary { + summary = + s.get("C").map(|x| x.to_string()); + + } + if let Some(n) = &data.name { + name = n.get("C").unwrap().to_string(); + } + } + catall.push(CategoryTile { + pkg: app.to_string(), + name, + pname: x.pname.to_string(), + icon, + summary, + installeduser: self.installeduserpkgs.contains_key(&x.pname.to_string()), + installedsystem: self.installedsystempkgs.contains(&x.pname.to_string()), + }); + } + } + + self.categorypage.emit(CategoryPageMsg::Open(category, catrec, catall)); + + } + } + } + + fn update_cmd(&mut self, msg: Self::CommandOutput, _sender: ComponentSender) { + match msg { + AppAsyncMsg::Search(search, pkgitems) => { + if search == self.searchquery { + self.searchpage.emit(SearchPageMsg::Search(pkgitems)) + } + } + } + } +} + +relm4::new_action_group!(MenuActionGroup, "menu"); +relm4::new_stateless_action!(AboutAction, MenuActionGroup, "about"); +relm4::new_stateless_action!(PreferencesAction, MenuActionGroup, "preferences"); diff --git a/src/ui/windowloading.rs b/src/ui/windowloading.rs new file mode 100644 index 0000000..993dcb2 --- /dev/null +++ b/src/ui/windowloading.rs @@ -0,0 +1,481 @@ +use super::window::AppMsg; +use crate::parse::cache::checkcache; +use crate::parse::packages::readpkgs; +use crate::parse::packages::readsyspkgs; +use crate::parse::packages::Package; +use crate::ui::categories::PkgCategory; +use rand::prelude::SliceRandom; +use rand::thread_rng; +use relm4::adw::prelude::*; +use relm4::*; +use std::{collections::HashMap, env}; +use strum::IntoEnumIterator; + +pub struct WindowAsyncHandler; + +#[derive(Debug)] +pub enum WindowAsyncHandlerMsg { + CheckCache(CacheReturn), +} + +#[derive(Debug, PartialEq)] +pub enum CacheReturn { + Init, + Update, +} + +impl Worker for WindowAsyncHandler { + type InitParams = (); + type Input = WindowAsyncHandlerMsg; + type Output = AppMsg; + + fn init(_params: Self::InitParams, _sender: relm4::ComponentSender) -> Self { + Self + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + match msg { + WindowAsyncHandlerMsg::CheckCache(cr) => { + println!("CHECK CACHE"); + relm4::spawn(async move { + match checkcache() { + Ok(_) => {} + Err(_) => { + println!("FAILED TO CHECK CACHE"); + sender.output(AppMsg::LoadError( + String::from("Could not load cache"), + String::from( + "Try connecting to the internet or launching the application again", + ), + )); + return; + } + } + let pkgs = match readpkgs().await { + Ok(pkgs) => pkgs, + Err(_) => { + println!("FAILED TO LOAD PKGS"); + sender.output(AppMsg::LoadError( + String::from("Could not load packages"), + String::from( + "Try connecting to the internet or launching the application again", + ), + )); + return; + } + }; + + let newpkgs = match readsyspkgs() { + Ok(newpkgs) => newpkgs, + Err(_) => { + println!("FAILED TO LOAD NEW PKGS"); + sender.output(AppMsg::LoadError( + String::from("Could not load new packages"), + String::from( + "Try connecting to the internet or launching the application again", + ), + )); + return; + } + }; + + println!("GOT PKGS"); + + let mut recpicks = vec![]; + let mut catpicks: HashMap> = HashMap::new(); + let mut catpkgs: HashMap> = HashMap::new(); + + + if cr == CacheReturn::Init { + println!("INIT"); + let desktopenv = env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); + let appdatapkgs = pkgs + .iter() + .filter(|(x, _)| { + if let Some(p) = pkgs.get(*x) { + if let Some(data) = &p.appdata { + (if let Some(i) = &data.icon { + i.cached.is_some() + } else { + false + }) && data.description.is_some() + && data.name.is_some() + && data.launchable.is_some() + && data.screenshots.is_some() + && (!x.starts_with("gnome.") || desktopenv == "GNOME") + && (!x.starts_with("xfce.") || desktopenv == "XFCE") + && (!x.starts_with("mate.") || desktopenv == "MATE") + && (!x.starts_with("cinnamon.") + || desktopenv == "X-Cinnamon") + && (!x.starts_with("libsForQt5") || desktopenv == "KDE") + && (!x.starts_with("pantheon.") + || desktopenv == "Pantheon") + } else { + false + } + } else { + false + } + }) + .collect::>(); + + let mut recommendedpkgs = appdatapkgs + .keys() + .map(|x| x.to_string()) + .collect::>(); + let mut rng = thread_rng(); + recommendedpkgs.shuffle(&mut rng); + + let mut desktoppicks = recommendedpkgs + .iter() + .filter(|x| { + if desktopenv == "GNOME" { + x.starts_with("gnome.") || x.starts_with("gnome-") + } else if desktopenv == "XFCE" { + x.starts_with("xfce.") + } else if desktopenv == "MATE" { + x.starts_with("mate.") + } else if desktopenv == "X-Cinnamon" { + x.starts_with("cinnamon.") + } else if desktopenv == "KDE" { + x.starts_with("libsForQt5") + } else if desktopenv == "Pantheon" { + x.starts_with("pantheon.") + } else { + false + } + }) + .collect::>(); + + for p in desktoppicks.iter().take(3) { + recpicks.push(p.to_string()); + } + for category in PkgCategory::iter() { + println!("CATEGORY: {}", category); + desktoppicks.shuffle(&mut rng); + let mut cvec = vec![]; + let mut allvec = vec![]; + let mut rpkgs = recommendedpkgs.clone(); + fn checkpkgs( + pkg: String, + pkgs: &HashMap<&String, &Package>, + category: PkgCategory, + ) -> bool { + match category { + PkgCategory::Audio => { + // Audio: + // - pkgs/applications/audio + if let Some(p) = pkgs.get(&pkg) { + if let Some(pos) = &p.meta.position { + if pos.starts_with("pkgs/applications/audio") { + return true; + } + } + if let Some(data) = &p.appdata { + if let Some(categories) = &data.categories { + if categories.contains(&String::from("Audio")) { + return true; + } + } + } + } + false + } + PkgCategory::Development => { + // Development: + // - pkgs/development + // - pkgs/applications/terminal-emulators + // - xdg: Development + if let Some(p) = pkgs.get(&pkg) { + if let Some(pos) = &p.meta.position { + if pos.starts_with("pkgs/development") + || pos.starts_with( + "pkgs/applications/terminal-emulators", + ) + { + return true; + } + } + if let Some(data) = &p.appdata { + if let Some(categories) = &data.categories { + if categories + .contains(&String::from("Development")) + { + return true; + } + } + } + } + false + } + PkgCategory::Games => { + // Games: + // - pkgs/games + // - pkgs/applications/emulators + // - pkgs/tools/games + // - xdg::Games + if let Some(p) = pkgs.get(&pkg) { + if let Some(pos) = &p.meta.position { + if pos.starts_with("pkgs/games") + || pos.starts_with( + "pkgs/applications/emulators", + ) + || pos.starts_with("pkgs/tools/games") + { + return true; + } + } + if let Some(data) = &p.appdata { + if let Some(categories) = &data.categories { + if categories.contains(&String::from("Games")) { + return true; + } + } + } + } + false + } + PkgCategory::Graphics => { + // Graphics: + // - pkgs/applications/graphics + // - xdg: Graphics + if let Some(p) = pkgs.get(&pkg) { + if let Some(pos) = &p.meta.position { + if pos.starts_with("pkgs/applications/graphics") + || pos.starts_with("xdg:Graphics") + { + return true; + } + } + if let Some(data) = &p.appdata { + if let Some(categories) = &data.categories { + if categories.contains(&String::from("Graphics")) { + return true; + } + } + } + } + false + } + PkgCategory::Network => { + // Network: + // - pkgs/applications/networking + // - xdg: Network + if let Some(p) = pkgs.get(&pkg) { + if let Some(pos) = &p.meta.position { + if pos.starts_with("pkgs/applications/networking") + || pos.starts_with("xdg:Network") + { + return true; + } + } + if let Some(data) = &p.appdata { + if let Some(categories) = &data.categories { + if categories.contains(&String::from("Network")) { + return true; + } + } + } + } + false + } + PkgCategory::Video => { + // Video: + // - pkgs/applications/video + // - xdg: Video + if let Some(p) = pkgs.get(&pkg) { + if let Some(pos) = &p.meta.position { + if pos.starts_with("pkgs/applications/video") + || pos.starts_with("xdg:Video") + { + return true; + } + } + if let Some(data) = &p.appdata { + if let Some(categories) = &data.categories { + if categories.contains(&String::from("Video")) { + return true; + } + } + } + } + false + } + } + } + + for pkg in desktoppicks.iter().take(3) { + if checkpkgs(pkg.to_string(), &appdatapkgs, category.clone()) { + cvec.push(pkg.to_string()); + } + } + + while cvec.len() < 12 { + if let Some(pkg) = rpkgs.pop() { + if !cvec.contains(&pkg.to_string()) + && checkpkgs( + pkg.to_string(), + &appdatapkgs, + category.clone(), + ) + { + cvec.push(pkg.to_string()); + } + } else { + break; + } + } + + println!("{} PICKS {:#?}", category, cvec); + + let catagortypkgs = pkgs + .iter() + .filter(|(x, _)| { + if let Some(p) = appdatapkgs.get(*x) { + if let Some(position) = &p.meta.position { + (position.starts_with("pkgs/applications/audio") && category == PkgCategory::Audio) + || (position.starts_with("pkgs/applications/terminal-emulators") && category == PkgCategory::Development) + || (position.starts_with("pkgs/applications/emulators") && category == PkgCategory::Games) + || (position.starts_with("pkgs/applications/graphics") && category == PkgCategory::Graphics) + || (position.starts_with("pkgs/applications/networking") && category == PkgCategory::Network) + || (position.starts_with("pkgs/applications/video") && category == PkgCategory::Video) + || (position.starts_with("pkgs/tools/games") && category == PkgCategory::Games) + || (position.starts_with("pkgs/games") && category == PkgCategory::Games) + || (position.starts_with("pkgs/development") && category == PkgCategory::Development) + || appdatapkgs.contains_key(x) + } else { + false + } + } else { + false + } + }) + .collect::>(); + + for pkg in catagortypkgs.keys() { + if checkpkgs(pkg.to_string(), &catagortypkgs, category.clone()) { + allvec.push(pkg.to_string()); + } + } + + println!("{} ALL {:#?}", category, allvec); + cvec.shuffle(&mut rng); + allvec.sort_by_key(|x| x.to_lowercase()); + catpicks.insert(category.clone(), cvec); + catpkgs.insert(category.clone(), allvec); + } + + while recpicks.len() < 12 { + if let Some(p) = recommendedpkgs.pop() { + if !recpicks.contains(&p.to_string()) { + recpicks.push(p); + } + } else { + break; + } + } + recpicks.shuffle(&mut rng); + } + + println!("SEND INIT"); + match cr { + CacheReturn::Init => { + sender.output(AppMsg::Initialize(pkgs, recpicks, newpkgs, catpicks, catpkgs)); + } + CacheReturn::Update => { + sender.output(AppMsg::ReloadUpdateItems(pkgs, newpkgs)); + } + } + }); + } + } + } +} + +pub struct LoadErrorModel { + hidden: bool, + msg: String, + msg2: String, +} + +#[derive(Debug)] +pub enum LoadErrorMsg { + Show(String, String), + Retry, + Close, + // Preferences, +} + +#[relm4::component(pub)] +impl SimpleComponent for LoadErrorModel { + type InitParams = gtk::Window; + type Input = LoadErrorMsg; + type Output = AppMsg; + type Widgets = LoadErrorWidgets; + + view! { + dialog = gtk::MessageDialog { + set_transient_for: Some(&parent_window), + set_modal: true, + #[watch] + set_visible: !model.hidden, + #[watch] + set_text: Some(&model.msg), + #[watch] + set_secondary_text: Some(&model.msg2), + set_use_markup: true, + set_secondary_use_markup: true, + add_button: ("Retry", gtk::ResponseType::Accept), + // add_button: ("Preferences", gtk::ResponseType::Help), + add_button: ("Quit", gtk::ResponseType::Close), + connect_response[sender] => move |_, resp| { + sender.input(match resp { + gtk::ResponseType::Accept => LoadErrorMsg::Retry, + gtk::ResponseType::Close => LoadErrorMsg::Close, + // gtk::ResponseType::Help => LoadErrorMsg::Preferences, + _ => unreachable!(), + }); + }, + } + } + + fn init( + parent_window: Self::InitParams, + root: &Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let model = LoadErrorModel { + hidden: true, + msg: String::default(), + msg2: String::default(), + }; + let widgets = view_output!(); + let accept_widget = widgets + .dialog + .widget_for_response(gtk::ResponseType::Accept) + .expect("No button for accept response set"); + accept_widget.add_css_class("warning"); + // let pref_widget = widgets + // .dialog + // .widget_for_response(gtk::ResponseType::Help) + // .expect("No button for help response set"); + // pref_widget.add_css_class("suggested-action"); + ComponentParts { model, widgets } + } + + fn update(&mut self, msg: Self::Input, sender: ComponentSender) { + match msg { + LoadErrorMsg::Show(s, s2) => { + self.hidden = false; + self.msg = s; + self.msg2 = s2; + } + LoadErrorMsg::Retry => { + self.hidden = true; + sender.output(AppMsg::TryLoad) + } + LoadErrorMsg::Close => sender.output(AppMsg::Close), + // LoadErrorMsg::Preferences => sender.output(AppMsg::ShowPrefMenu), + } + } +}