From 4683064637d04e844de8198f25273de70cc96219 Mon Sep 17 00:00:00 2001 From: Disyer Date: Sat, 28 Mar 2026 18:21:48 +0200 Subject: [PATCH] Implement CLI and silent install / uninstall --- .gitignore | 7 +- rust/Cargo.lock | 1320 ++++++++++++++++- rust/Cargo.toml | 3 + .../crates/clean_flash_common/src/registry.rs | 252 +++- .../src/uninstaller_linux.rs | 1 - rust/crates/clean_flash_installer/Cargo.toml | 3 + rust/crates/clean_flash_installer/build.rs | 14 + .../clean_flash_installer/src/cli_args.rs | 70 + .../src/installer_windows.rs | 181 ++- rust/crates/clean_flash_installer/src/main.rs | 36 +- .../src/silent_installer.rs | 81 + .../src/tui_installer.rs | 639 ++++++++ .../crates/clean_flash_uninstaller/Cargo.toml | 3 + rust/crates/clean_flash_uninstaller/build.rs | 14 + .../clean_flash_uninstaller/src/cli_args.rs | 38 + .../clean_flash_uninstaller/src/main.rs | 35 +- .../src/silent_uninstaller.rs | 47 + .../src/tui_uninstaller.rs | 306 ++++ rust/resources/macos/installer-Info.plist | 24 + rust/resources/macos/uninstaller-Info.plist | 24 + 20 files changed, 2991 insertions(+), 107 deletions(-) create mode 100644 rust/crates/clean_flash_installer/src/cli_args.rs create mode 100644 rust/crates/clean_flash_installer/src/silent_installer.rs create mode 100644 rust/crates/clean_flash_installer/src/tui_installer.rs create mode 100644 rust/crates/clean_flash_uninstaller/src/cli_args.rs create mode 100644 rust/crates/clean_flash_uninstaller/src/silent_uninstaller.rs create mode 100644 rust/crates/clean_flash_uninstaller/src/tui_uninstaller.rs create mode 100644 rust/resources/macos/installer-Info.plist create mode 100644 rust/resources/macos/uninstaller-Info.plist diff --git a/.gitignore b/.gitignore index ba68924..bee2d56 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,9 @@ obj *.zip *.7z -target \ No newline at end of file +target + +macinstaller +trueinstaller +build*.sh +bundle_macos.sh diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5b07380..520a360 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -18,18 +18,94 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "argh" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "211818e820cda9ca6f167a64a5c808837366a6dfd807157c64c1304c486cd033" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c442a9d18cef5dde467405d27d461d080d68972d6d0dfd0408265b6749ec427d" +dependencies = [ + "argh_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "argh_shared" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ade012bac4db278517a0132c8c10c6427025868dca16c801087c28d5a411f1" +dependencies = [ + "serde", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -42,12 +118,36 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.57" @@ -64,6 +164,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clean_flash_common" version = "34.0.0" @@ -75,9 +181,12 @@ dependencies = [ name = "clean_flash_installer" version = "34.0.0" dependencies = [ + "argh", "clean_flash_common", "clean_flash_ui", + "crossterm", "minifb", + "ratatui", "sevenz-rust2", "windows-sys", "winresource", @@ -96,13 +205,30 @@ dependencies = [ name = "clean_flash_uninstaller" version = "34.0.0" dependencies = [ + "argh", "clean_flash_common", "clean_flash_ui", + "crossterm", "minifb", + "ratatui", "windows-sys", "winresource", ] +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -113,6 +239,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -122,6 +266,134 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dlib" version = "0.5.3" @@ -131,12 +403,27 @@ dependencies = [ "libloading", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -153,24 +440,78 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures" version = "0.3.32" @@ -227,7 +568,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -259,6 +600,28 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -267,7 +630,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -278,7 +641,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -286,6 +649,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -293,12 +661,24 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.13.0" @@ -311,6 +691,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "instant" version = "0.1.13" @@ -323,6 +725,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -339,6 +750,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + [[package]] name = "lazy_static" version = "1.5.0" @@ -376,7 +804,16 @@ dependencies = [ "bitflags 2.11.0", "libc", "plain", - "redox_syscall", + "redox_syscall 0.7.3", +] + +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.11.0", ] [[package]] @@ -385,24 +822,64 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "lzma-rust2" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix 0.29.0", + "winapi", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + [[package]] name = "memoffset" version = "0.6.5" @@ -412,6 +889,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "minifb" version = "0.28.0" @@ -441,6 +927,24 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + [[package]] name = "nix" version = "0.24.3" @@ -450,7 +954,65 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", ] [[package]] @@ -471,79 +1033,371 @@ dependencies = [ ] [[package]] -name = "owned_ttf_parser" -version = "0.25.1" +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "ratatui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.11.0", + "compact_str", + "hashbrown 0.16.1", + "indoc", + "itertools", + "kasuari", + "lru", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" dependencies = [ - "ttf-parser", + "ratatui-core", + "ratatui-widgets", ] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "ratatui-termwiz" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] [[package]] -name = "pkg-config" -version = "0.3.32" +name = "ratatui-widgets" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] [[package]] -name = "plain" -version = "0.2.3" +name = "raw-window-handle" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] -name = "prettyplease" -version = "0.2.37" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "proc-macro2", - "syn", + "bitflags 2.11.0", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "redox_syscall" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "unicode-ident", + "bitflags 2.11.0", ] [[package]] -name = "quote" -version = "1.0.45" +name = "regex" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ - "proc-macro2", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] -name = "r-efi" -version = "6.0.0" +name = "regex-automata" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] -name = "raw-window-handle" -version = "0.6.2" +name = "regex-syntax" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "redox_syscall" -version = "0.7.3" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags 2.11.0", + "semver", ] [[package]] @@ -565,12 +1419,24 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "scoped-tls" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sdl2" version = "0.38.0" @@ -627,7 +1493,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -664,12 +1530,60 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -682,6 +1596,50 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -700,12 +1658,136 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys", ] +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.11.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "toml" version = "1.0.6+spec-1.1.0" @@ -751,18 +1833,71 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "atomic", + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version-compare" version = "0.1.1" @@ -775,6 +1910,21 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -841,7 +1991,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -897,7 +2047,7 @@ dependencies = [ "bitflags 1.3.2", "downcast-rs", "libc", - "nix", + "nix 0.24.3", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -910,7 +2060,7 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" dependencies = [ - "nix", + "nix 0.24.3", "once_cell", "smallvec", "wayland-sys", @@ -922,7 +2072,7 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" dependencies = [ - "nix", + "nix 0.24.3", "wayland-client", "xcursor", ] @@ -971,6 +2121,78 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1054,7 +2276,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -1070,7 +2292,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 7f7abbe..1611546 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -11,6 +11,9 @@ members = [ ab_glyph = "0.2" sevenz-rust2 = { version = "0.20", default-features = false, features = ["compress", "util"] } minifb = "0.28" +argh = "0.1.19" +ratatui = "0.30" +crossterm = "0.29" windows-sys = { version = "0.61", features = [ "Win32_Foundation", "Win32_Networking_WinHttp", diff --git a/rust/crates/clean_flash_common/src/registry.rs b/rust/crates/clean_flash_common/src/registry.rs index d737948..0988194 100644 --- a/rust/crates/clean_flash_common/src/registry.rs +++ b/rust/crates/clean_flash_common/src/registry.rs @@ -1,50 +1,238 @@ -use crate::{process_utils, system_info, InstallError}; -use std::fs; -use std::io::Write; +use crate::{system_info, InstallError}; -/// Apply registry contents by writing a .reg file and importing with reg.exe. +/// Apply registry entries using native Windows registry APIs. +/// +/// The entries are in .reg file format (without the header line). +/// Supported syntax: +/// - `[HKEY_xxx\path]` — create/open key +/// - `[-HKEY_xxx\path]` — delete entire key tree +/// - `"name"="string_value"` — set REG_SZ value +/// - `"name"=dword:XXXXXXXX` — set REG_DWORD value +/// - `"name"=-` — delete a value +/// - `@="string_value"` — set the default (unnamed) value #[cfg(windows)] pub fn apply_registry(entries: &[&str]) -> Result<(), InstallError> { let combined = entries.join("\n\n"); let filled = system_info::fill_string(&combined); + apply_reg_content(&filled) +} + +#[cfg(windows)] +fn apply_reg_content(content: &str) -> Result<(), InstallError> { + use windows_sys::Win32::System::Registry::{ + RegCloseKey, RegCreateKeyExW, RegDeleteValueW, RegSetValueExW, + KEY_WOW64_64KEY, KEY_WRITE, REG_DWORD, REG_OPTION_NON_VOLATILE, REG_SZ, + }; - let content = format!("Windows Registry Editor Version 5.00\n\n{}", filled); - - let temp_dir = std::env::temp_dir(); - // Include the process ID to avoid collisions when two instances run concurrently, - // matching the unique-file guarantee of C#'s Path.GetTempFileName(). - let reg_file = temp_dir.join(format!("cleanflash_reg_{}.tmp", std::process::id())); - - // Write as UTF-16LE with BOM (Windows .reg format). - { - let mut f = fs::File::create(®_file) - .map_err(|e| InstallError::new(format!("Failed to create temp reg file: {}", e)))?; - let utf16: Vec = content.encode_utf16().collect(); - // BOM - f.write_all(&[0xFF, 0xFE]) - .map_err(|e| InstallError::new(format!("Failed to write BOM: {}", e)))?; - for word in &utf16 { - f.write_all(&word.to_le_bytes()) - .map_err(|e| InstallError::new(format!("Failed to write reg data: {}", e)))?; + let mut current_hkey: Option = None; + + for line in content.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + + // Delete key: [-HKEY_xxx\path] + if line.starts_with("[-") && line.ends_with(']') { + if let Some(hk) = current_hkey.take() { + unsafe { RegCloseKey(hk) }; + } + + let key_path = &line[2..line.len() - 1]; + if let Some((root, subkey)) = parse_root_and_subkey(key_path) { + delete_key_tree(root, subkey); + } + continue; + } + + // Open/create key: [HKEY_xxx\path] + if line.starts_with('[') && line.ends_with(']') { + if let Some(hk) = current_hkey.take() { + unsafe { RegCloseKey(hk) }; + } + + let key_path = &line[1..line.len() - 1]; + if let Some((root, subkey)) = parse_root_and_subkey(key_path) { + let subkey_wide: Vec = + subkey.encode_utf16().chain(std::iter::once(0)).collect(); + unsafe { + let mut hkey = std::ptr::null_mut(); + let mut disposition: u32 = 0; + let result = RegCreateKeyExW( + root, + subkey_wide.as_ptr(), + 0, + std::ptr::null(), + REG_OPTION_NON_VOLATILE, + KEY_WRITE | KEY_WOW64_64KEY, + std::ptr::null(), + &mut hkey, + &mut disposition, + ); + if result == 0 { + current_hkey = Some(hkey); + } + } + } + continue; } - } - let reg_filename = reg_file.to_string_lossy().to_string(); - let result = process_utils::run_process("reg.exe", &["import", ®_filename]); + // Value operations require an open key. + let Some(hkey) = current_hkey else { + continue; + }; + + // Default value: @="value" + if let Some(rest) = line.strip_prefix("@=") { + if let Some(val) = parse_reg_string(rest) { + let val_wide: Vec = + val.encode_utf16().chain(std::iter::once(0)).collect(); + unsafe { + RegSetValueExW( + hkey, + std::ptr::null(), + 0, + REG_SZ as u32, + val_wide.as_ptr() as *const u8, + (val_wide.len() * 2) as u32, + ); + } + } + continue; + } + + // Named value: "name"=... + if !line.starts_with('"') { + continue; + } + + let Some((name, value_part)) = parse_name_and_value(line) else { + continue; + }; + + let name_wide: Vec = name.encode_utf16().chain(std::iter::once(0)).collect(); + + // Delete value: "name"=- + if value_part == "-" { + unsafe { + RegDeleteValueW(hkey, name_wide.as_ptr()); + } + continue; + } + + // DWORD value: "name"=dword:XXXXXXXX + if let Some(hex_str) = value_part.strip_prefix("dword:") { + if let Ok(dword_val) = u32::from_str_radix(hex_str, 16) { + unsafe { + RegSetValueExW( + hkey, + name_wide.as_ptr(), + 0, + REG_DWORD as u32, + &dword_val as *const u32 as *const u8, + 4, + ); + } + } + continue; + } - let _ = fs::remove_file(®_file); + // String value: "name"="value" + if let Some(val) = parse_reg_string(value_part) { + let val_wide: Vec = val.encode_utf16().chain(std::iter::once(0)).collect(); + unsafe { + RegSetValueExW( + hkey, + name_wide.as_ptr(), + 0, + REG_SZ as u32, + val_wide.as_ptr() as *const u8, + (val_wide.len() * 2) as u32, + ); + } + continue; + } + } - if !result.is_successful() { - return Err(InstallError::new(format!( - "Failed to apply changes to registry: error code {}\n\n{}", - result.exit_code, result.output - ))); + if let Some(hk) = current_hkey { + unsafe { RegCloseKey(hk) }; } Ok(()) } +/// Parse a root key name and subkey path from a combined string like +/// `HKEY_LOCAL_MACHINE\Software\Something`. +#[cfg(windows)] +fn parse_root_and_subkey( + path: &str, +) -> Option<(windows_sys::Win32::System::Registry::HKEY, &str)> { + use windows_sys::Win32::System::Registry::{ + HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS, + }; + + let (root_name, subkey) = path.split_once('\\')?; + let root = match root_name { + "HKEY_LOCAL_MACHINE" => HKEY_LOCAL_MACHINE, + "HKEY_CURRENT_USER" => HKEY_CURRENT_USER, + "HKEY_CLASSES_ROOT" => HKEY_CLASSES_ROOT, + "HKEY_USERS" => HKEY_USERS, + _ => return None, + }; + Some((root, subkey)) +} + +/// Delete an entire registry key tree. +#[cfg(windows)] +fn delete_key_tree(root: windows_sys::Win32::System::Registry::HKEY, subkey: &str) { + use windows_sys::Win32::System::Registry::{ + RegCloseKey, RegDeleteTreeW, RegOpenKeyExW, KEY_WOW64_64KEY, KEY_WRITE, + }; + + if let Some((parent, leaf)) = subkey.rsplit_once('\\') { + let parent_wide: Vec = parent.encode_utf16().chain(std::iter::once(0)).collect(); + let leaf_wide: Vec = leaf.encode_utf16().chain(std::iter::once(0)).collect(); + unsafe { + let mut hkey = std::ptr::null_mut(); + let result = + RegOpenKeyExW(root, parent_wide.as_ptr(), 0, KEY_WRITE | KEY_WOW64_64KEY, &mut hkey); + if result == 0 { + RegDeleteTreeW(hkey, leaf_wide.as_ptr()); + RegCloseKey(hkey); + } + } + } else { + let subkey_wide: Vec = subkey.encode_utf16().chain(std::iter::once(0)).collect(); + unsafe { + RegDeleteTreeW(root, subkey_wide.as_ptr()); + } + } +} + +/// Parse a quoted string value: `"some value"` → `some value`. +/// Handles escaped backslashes (`\\` → `\`) inside the string. +#[cfg(windows)] +fn parse_reg_string(s: &str) -> Option { + let s = s.trim(); + if !s.starts_with('"') || !s.ends_with('"') || s.len() < 2 { + return None; + } + let inner = &s[1..s.len() - 1]; + Some(inner.replace("\\\\", "\\")) +} + +/// Parse `"name"=value` into `(name, value)`. +#[cfg(windows)] +fn parse_name_and_value(line: &str) -> Option<(String, &str)> { + let rest = &line[1..]; // skip opening " + let end_quote = rest.find('"')?; + let name = &rest[..end_quote]; + let after_name = &rest[end_quote + 1..]; // skip closing " + let value_part = after_name.strip_prefix('=')?; + Some((name.to_string(), value_part)) +} + #[cfg(not(windows))] pub fn apply_registry(_entries: &[&str]) -> Result<(), InstallError> { Ok(()) -} \ No newline at end of file +} diff --git a/rust/crates/clean_flash_common/src/uninstaller_linux.rs b/rust/crates/clean_flash_common/src/uninstaller_linux.rs index 5b8464d..07f8cad 100644 --- a/rust/crates/clean_flash_common/src/uninstaller_linux.rs +++ b/rust/crates/clean_flash_common/src/uninstaller_linux.rs @@ -6,7 +6,6 @@ use std::fs; /// This removes the native messaging host binary, the Pepper (pp64) files /// installed alongside it, and the browser manifests. pub fn uninstall(form: &dyn ProgressCallback) -> Result<(), InstallError> { - form.update_progress_label("Removing native messaging host...", true); native_host::uninstall_native_host(form); // Remove the entire install directory (host binary + pp64 files). diff --git a/rust/crates/clean_flash_installer/Cargo.toml b/rust/crates/clean_flash_installer/Cargo.toml index 17da2e6..b84178a 100644 --- a/rust/crates/clean_flash_installer/Cargo.toml +++ b/rust/crates/clean_flash_installer/Cargo.toml @@ -10,6 +10,9 @@ clean_flash_ui = { path = "../clean_flash_ui" } minifb = { workspace = true } windows-sys = { workspace = true } sevenz-rust2 = { workspace = true } +argh = { workspace = true } +ratatui = { workspace = true } +crossterm = { workspace = true } [build-dependencies] winresource = "0.1" diff --git a/rust/crates/clean_flash_installer/build.rs b/rust/crates/clean_flash_installer/build.rs index 274c4e0..6fa3921 100644 --- a/rust/crates/clean_flash_installer/build.rs +++ b/rust/crates/clean_flash_installer/build.rs @@ -1,4 +1,18 @@ fn main() { + // On macOS, embed Info.plist into the binary so the OS recognises it as a GUI app. + if cfg!(target_os = "macos") { + let plist = std::path::Path::new("../../resources/macos/installer-Info.plist"); + if plist.exists() { + println!("cargo:rustc-link-arg=-sectcreate"); + println!("cargo:rustc-link-arg=__TEXT"); + println!("cargo:rustc-link-arg=__info_plist"); + println!( + "cargo:rustc-link-arg={}", + plist.canonicalize().unwrap().display() + ); + } + } + // Set application icon from .ico resource (if present). if cfg!(target_os = "windows") { let mut res = winresource::WindowsResource::new(); diff --git a/rust/crates/clean_flash_installer/src/cli_args.rs b/rust/crates/clean_flash_installer/src/cli_args.rs new file mode 100644 index 0000000..e639844 --- /dev/null +++ b/rust/crates/clean_flash_installer/src/cli_args.rs @@ -0,0 +1,70 @@ +use argh::FromArgs; + +/// Clean Flash Player Installer +/// +/// Install Clean Flash Player with GUI, TUI, or silent mode. +#[derive(FromArgs)] +pub struct InstallerArgs { + /// use the terminal UI (ratatui) instead of the graphical window + #[argh(switch)] + pub cli: bool, + + /// run the installation non-interactively (no GUI or TUI) + #[argh(switch)] + pub install: bool, + + /// suppress all output (only valid with --install) + #[argh(switch)] + pub silent: bool, + + /// install the Modern Browsers (MV3) native host + #[argh(switch)] + pub native_host: bool, + + /// install the Pepper API (PPAPI) plugin + #[argh(switch)] + pub pepper: bool, + + /// install the Netscape API (NPAPI) plugin + #[argh(switch)] + pub netscape: bool, + + /// install the ActiveX (OCX) plugin + #[argh(switch)] + pub activex: bool, + + /// install the standalone Flash Player + #[argh(switch)] + pub player: bool, + + /// create desktop shortcuts for standalone player (requires --player) + #[argh(switch)] + pub player_desktop: bool, + + /// create start menu shortcuts for standalone player (requires --player) + #[argh(switch)] + pub player_start_menu: bool, + + /// install the debug version + #[argh(switch)] + pub debug: bool, +} + +/// The mode in which the application should run. +pub enum RunMode { + Gui, + Tui, + Silent, +} + +impl InstallerArgs { + pub fn determine_mode(&self) -> RunMode { + if self.install { + RunMode::Silent + } else if self.cli { + RunMode::Tui + } else { + RunMode::Gui + } + } +} diff --git a/rust/crates/clean_flash_installer/src/installer_windows.rs b/rust/crates/clean_flash_installer/src/installer_windows.rs index 9da33bc..e45b187 100644 --- a/rust/crates/clean_flash_installer/src/installer_windows.rs +++ b/rust/crates/clean_flash_installer/src/installer_windows.rs @@ -5,6 +5,8 @@ use clean_flash_common::{ use std::env; use std::fs; use std::io::{self, Cursor}; +#[cfg(windows)] +use std::os::windows::ffi::OsStrExt; use std::path::{Path, PathBuf}; /// Metadata for a single installable component. @@ -46,35 +48,170 @@ pub fn register_activex(filename: &str) -> Result<(), InstallError> { Ok(()) } -/// Create a Windows shortcut (.lnk) using a PowerShell one-liner. +/// Create a Windows shortcut (.lnk) using native COM APIs (IShellLinkW + IPersistFile). +#[cfg(windows)] fn create_shortcut( folder: &Path, executable: &Path, name: &str, description: &str, ) -> Result<(), InstallError> { - let lnk_path = folder.join(format!("{}.lnk", name)); - let exe_str = executable.to_string_lossy(); - let lnk_str = lnk_path.to_string_lossy(); - - // Use PowerShell to create the shortcut via WScript.Shell COM. - let script = format!( - "$ws = New-Object -ComObject WScript.Shell; \ - $s = $ws.CreateShortcut('{}'); \ - $s.TargetPath = '{}'; \ - $s.Description = '{}'; \ - $s.IconLocation = '{}'; \ - $s.Save()", - lnk_str, exe_str, description, exe_str - ); - - let result = process_utils::run_process("powershell.exe", &["-NoProfile", "-Command", &script]); - if !result.is_successful() { - return Err(InstallError::new(format!( - "Failed to create shortcut: {}", - result.output - ))); + use std::ffi::c_void; + + // COM GUIDs. + const CLSID_SHELL_LINK: windows_sys::core::GUID = + windows_sys::core::GUID::from_u128(0x00021401_0000_0000_c000_000000000046); + const IID_ISHELL_LINK_W: windows_sys::core::GUID = + windows_sys::core::GUID::from_u128(0x000214F9_0000_0000_C000_000000000046); + const IID_IPERSIST_FILE: windows_sys::core::GUID = + windows_sys::core::GUID::from_u128(0x0000010b_0000_0000_C000_000000000046); + + // IShellLinkW vtable layout — we only need SetPath, SetDescription, SetIconLocation. + // Vtable indices (0-based from IUnknown): + // 0: QueryInterface, 1: AddRef, 2: Release, + // 3: GetPath, 4: GetIDList, 5: SetIDList, 6: GetDescription, + // 7: SetDescription, 8: GetWorkingDirectory, 9: SetWorkingDirectory, + // 10: GetArguments, 11: SetArguments, 12: GetHotkey, 13: SetHotkey, + // 14: GetShowCmd, 15: SetShowCmd, 16: GetIconLocation, 17: SetIconLocation, + // 18: SetRelativePath, 19: Resolve, 20: SetPath + const VTBL_SET_DESCRIPTION: usize = 7; + const VTBL_SET_ICON_LOCATION: usize = 17; + const VTBL_SET_PATH: usize = 20; + + // IPersistFile vtable layout: + // 0: QueryInterface, 1: AddRef, 2: Release, + // 3: GetClassID (IPersist), + // 4: IsDirty, 5: Load, 6: Save, 7: SaveCompleted, 8: GetCurFile + const VTBL_SAVE: usize = 6; + + type ComMethodFn = unsafe extern "system" fn() -> i32; + + unsafe { + // Initialize COM (apartment-threaded). + let hr = CoInitializeEx(std::ptr::null(), 0x2 /* COINIT_APARTMENTTHREADED */); + if hr < 0 { + return Err(InstallError::new(format!( + "CoInitializeEx failed: 0x{:08X}", + hr + ))); + } + + let mut shell_link: *mut c_void = std::ptr::null_mut(); + let hr = CoCreateInstance( + &CLSID_SHELL_LINK, + std::ptr::null(), + 1, // CLSCTX_INPROC_SERVER + &IID_ISHELL_LINK_W, + &mut shell_link, + ); + if hr < 0 { + CoUninitialize(); + return Err(InstallError::new(format!( + "CoCreateInstance for IShellLinkW failed: 0x{:08X}", + hr + ))); + } + + // Get the vtable. + let vtable = *(shell_link as *const *const *const ComMethodFn); + + // SetPath + let exe_wide: Vec = executable + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + let set_path: unsafe extern "system" fn(*mut c_void, *const u16) -> i32 = + std::mem::transmute(*vtable.add(VTBL_SET_PATH)); + set_path(shell_link, exe_wide.as_ptr()); + + // SetDescription + let desc_wide: Vec = description.encode_utf16().chain(std::iter::once(0)).collect(); + let set_description: unsafe extern "system" fn(*mut c_void, *const u16) -> i32 = + std::mem::transmute(*vtable.add(VTBL_SET_DESCRIPTION)); + set_description(shell_link, desc_wide.as_ptr()); + + // SetIconLocation + let set_icon_location: unsafe extern "system" fn(*mut c_void, *const u16, i32) -> i32 = + std::mem::transmute(*vtable.add(VTBL_SET_ICON_LOCATION)); + set_icon_location(shell_link, exe_wide.as_ptr(), 0); + + // QueryInterface for IPersistFile. + let query_interface: unsafe extern "system" fn( + *mut c_void, + *const windows_sys::core::GUID, + *mut *mut c_void, + ) -> i32 = std::mem::transmute(*vtable.add(0)); + let mut persist_file: *mut c_void = std::ptr::null_mut(); + let hr = query_interface(shell_link, &IID_IPERSIST_FILE, &mut persist_file); + if hr < 0 { + // Release IShellLinkW. + let release: unsafe extern "system" fn(*mut c_void) -> u32 = + std::mem::transmute(*vtable.add(2)); + release(shell_link); + CoUninitialize(); + return Err(InstallError::new(format!( + "QueryInterface for IPersistFile failed: 0x{:08X}", + hr + ))); + } + + // Save the .lnk file. + let lnk_path = folder.join(format!("{}.lnk", name)); + let lnk_wide: Vec = lnk_path + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + let pf_vtable = *(persist_file as *const *const *const ComMethodFn); + let save: unsafe extern "system" fn(*mut c_void, *const u16, i32) -> i32 = + std::mem::transmute(*pf_vtable.add(VTBL_SAVE)); + let hr = save(persist_file, lnk_wide.as_ptr(), 1 /* TRUE */); + + // Release IPersistFile. + let pf_release: unsafe extern "system" fn(*mut c_void) -> u32 = + std::mem::transmute(*pf_vtable.add(2)); + pf_release(persist_file); + + // Release IShellLinkW. + let release: unsafe extern "system" fn(*mut c_void) -> u32 = + std::mem::transmute(*vtable.add(2)); + release(shell_link); + + CoUninitialize(); + + if hr < 0 { + return Err(InstallError::new(format!( + "IPersistFile::Save failed: 0x{:08X}", + hr + ))); + } } + + Ok(()) +} + +#[cfg(windows)] +#[link(name = "ole32")] +extern "system" { + fn CoInitializeEx(pvreserved: *const std::ffi::c_void, dwcoinit: u32) -> i32; + fn CoCreateInstance( + rclsid: *const windows_sys::core::GUID, + punkouter: *const std::ffi::c_void, + dwclscontext: u32, + riid: *const windows_sys::core::GUID, + ppv: *mut *mut std::ffi::c_void, + ) -> i32; + fn CoUninitialize(); +} + +#[cfg(not(windows))] +fn create_shortcut( + _folder: &Path, + _executable: &Path, + _name: &str, + _description: &str, +) -> Result<(), InstallError> { Ok(()) } diff --git a/rust/crates/clean_flash_installer/src/main.rs b/rust/crates/clean_flash_installer/src/main.rs index 73caf38..29ad1dc 100644 --- a/rust/crates/clean_flash_installer/src/main.rs +++ b/rust/crates/clean_flash_installer/src/main.rs @@ -1,7 +1,10 @@ #![windows_subsystem = "windows"] +mod cli_args; mod install_flags; mod install_form; +mod silent_installer; +mod tui_installer; #[cfg(windows)] #[path = "installer_windows.rs"] @@ -11,11 +14,12 @@ mod installer; #[path = "installer_linux.rs"] mod installer; +use cli_args::{InstallerArgs, RunMode}; use install_form::{InstallForm, HEIGHT, WIDTH}; use clean_flash_ui::renderer::Renderer; use minifb::{Key, MouseButton, MouseMode, Window, WindowOptions}; -fn main() { +fn run_gui() { let title = format!( "Clean Flash Player {} Installer", clean_flash_common::update_checker::FLASH_VERSION @@ -64,3 +68,33 @@ fn main() { .expect("Failed to update window buffer"); } } + +fn main() { + let args: InstallerArgs = argh::from_env(); + + match args.determine_mode() { + RunMode::Silent => { + if let Err(e) = silent_installer::run_silent_install(&args) { + eprintln!("[!] Fatal error: {}", e); + std::process::exit(1); + } + } + RunMode::Tui => { + if let Err(e) = tui_installer::run_tui_installer() { + eprintln!("[!] TUI error: {}", e); + std::process::exit(1); + } + } + RunMode::Gui => { + // Try GUI; fall back to TUI if window creation fails. + let gui_result = std::panic::catch_unwind(run_gui); + if gui_result.is_err() { + eprintln!("GUI failed to initialize, falling back to terminal UI..."); + if let Err(e) = tui_installer::run_tui_installer() { + eprintln!("[!] TUI error: {}", e); + std::process::exit(1); + } + } + } + } +} diff --git a/rust/crates/clean_flash_installer/src/silent_installer.rs b/rust/crates/clean_flash_installer/src/silent_installer.rs new file mode 100644 index 0000000..824a470 --- /dev/null +++ b/rust/crates/clean_flash_installer/src/silent_installer.rs @@ -0,0 +1,81 @@ +use crate::cli_args::InstallerArgs; +use crate::install_flags::{self, InstallFlags}; +use crate::installer; +use clean_flash_common::{uninstaller, redirection, update_checker, ProgressCallback, InstallError}; + +struct SilentProgressCallback { + silent: bool, +} + +impl ProgressCallback for SilentProgressCallback { + fn update_progress_label(&self, text: &str, _tick: bool) { + if !self.silent { + eprintln!("[*] {}", text); + } + } + + fn tick_progress(&self) {} +} + +pub fn run_silent_install(args: &InstallerArgs) -> Result<(), Box> { + let version = update_checker::FLASH_VERSION; + + let mut flags = InstallFlags::new(); + flags.set_conditionally(args.native_host, install_flags::NATIVE_HOST); + flags.set_conditionally(args.pepper, install_flags::PEPPER); + flags.set_conditionally(args.netscape, install_flags::NETSCAPE); + flags.set_conditionally(args.activex, install_flags::ACTIVEX); + flags.set_conditionally(args.player, install_flags::PLAYER); + flags.set_conditionally(args.player_desktop, install_flags::PLAYER_DESKTOP); + flags.set_conditionally(args.player_start_menu, install_flags::PLAYER_START_MENU); + flags.set_conditionally(args.debug, install_flags::DEBUG); + + if flags.is_none_set() { + if !args.silent { + eprintln!("No components selected. Use flags to select what to install:"); + eprintln!(" --native-host Modern Browsers (MV3)"); + eprintln!(" --pepper Pepper API (PPAPI)"); + eprintln!(" --netscape Netscape API (NPAPI)"); + eprintln!(" --activex ActiveX (OCX)"); + eprintln!(" --player Standalone Flash Player"); + eprintln!(" --player-desktop Desktop shortcuts (requires --player)"); + eprintln!(" --player-start-menu Start Menu shortcuts (requires --player)"); + eprintln!(" --debug Debug version"); + eprintln!("\nExample: clean_flash_installer --install --native-host --pepper"); + } + std::process::exit(1); + } + + if !args.silent { + eprintln!("Clean Flash Player {} - Installation", version); + } + + let callback = SilentProgressCallback { + silent: args.silent, + }; + + let redir = redirection::disable_redirection(); + + let result = (|| -> Result<(), InstallError> { + uninstaller::uninstall(&callback)?; + installer::install(&callback, &mut flags)?; + Ok(()) + })(); + + redirection::enable_redirection(redir); + + match result { + Ok(()) => { + if !args.silent { + eprintln!("[+] Installation completed successfully."); + } + Ok(()) + } + Err(e) => { + if !args.silent { + eprintln!("[!] Installation failed: {}", e); + } + std::process::exit(1); + } + } +} diff --git a/rust/crates/clean_flash_installer/src/tui_installer.rs b/rust/crates/clean_flash_installer/src/tui_installer.rs new file mode 100644 index 0000000..cde5659 --- /dev/null +++ b/rust/crates/clean_flash_installer/src/tui_installer.rs @@ -0,0 +1,639 @@ +use crate::install_flags::{self, InstallFlags}; +use crate::installer; +use clean_flash_common::{uninstaller, redirection, update_checker, ProgressCallback, InstallError}; +use crossterm::{ + event::{self, Event, KeyCode, KeyEventKind}, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + execute, +}; +use ratatui::{ + prelude::*, + widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, Wrap}, +}; +use std::io::{self, stdout}; +use std::sync::{Arc, Mutex}; + +const FLASH_VERSION: &str = update_checker::FLASH_VERSION; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Panel { + Disclaimer, + Choice, + PlayerChoice, + DebugChoice, + BeforeInstall, + Install, + Complete, + Failure, +} + +struct ProgressState { + label: String, + value: u32, + maximum: u32, + done: bool, + error: Option, +} + +struct TuiInstallState { + panel: Panel, + // Disclaimer + disclaimer_accepted: bool, + // Choice + native_host: bool, + pepper: bool, + netscape: bool, + activex: bool, + choice_cursor: usize, + // Player choice + player: bool, + player_desktop: bool, + player_start_menu: bool, + player_cursor: usize, + // Debug + debug_chosen: bool, + // Progress + progress: Arc>, + // Failure + failure_detail: String, +} + +impl TuiInstallState { + fn new() -> Self { + Self { + panel: Panel::Disclaimer, + disclaimer_accepted: false, + native_host: true, + pepper: false, + netscape: false, + activex: false, + choice_cursor: 0, + player: false, + player_desktop: false, + player_start_menu: false, + player_cursor: 0, + debug_chosen: false, + progress: Arc::new(Mutex::new(ProgressState { + label: "Preparing...".into(), + value: 0, + maximum: 10, + done: false, + error: None, + })), + failure_detail: String::new(), + } + } + + fn choice_items(&self) -> Vec<(&str, bool, bool)> { + let is_windows = cfg!(target_os = "windows"); + vec![ + ("Modern Browsers (MV3) - Chrome/Firefox/Edge", self.native_host, true), + ("Pepper API (PPAPI) - Chrome/Opera/Brave", self.pepper, is_windows), + ("Netscape API (NPAPI) - Firefox/ESR/Waterfox", self.netscape, is_windows), + ("ActiveX (OCX) - IE/Embedded/Desktop", self.activex, is_windows), + ] + } + + fn player_items(&self) -> Vec<(&str, bool, bool)> { + vec![ + ("Install Standalone Flash Player", self.player, true), + ("Create Desktop Shortcuts", self.player_desktop, self.player), + ("Create Start Menu Shortcuts", self.player_start_menu, self.player), + ] + } + + fn toggle_choice(&mut self) { + let is_windows = cfg!(target_os = "windows"); + match self.choice_cursor { + 0 => self.native_host = !self.native_host, + 1 if is_windows => self.pepper = !self.pepper, + 2 if is_windows => self.netscape = !self.netscape, + 3 if is_windows => self.activex = !self.activex, + _ => {} + } + } + + fn toggle_player(&mut self) { + match self.player_cursor { + 0 => { + self.player = !self.player; + if !self.player { + self.player_desktop = false; + self.player_start_menu = false; + } + } + 1 if self.player => self.player_desktop = !self.player_desktop, + 2 if self.player => self.player_start_menu = !self.player_start_menu, + _ => {} + } + } + + fn build_flags(&self) -> InstallFlags { + let mut flags = InstallFlags::new(); + flags.set_conditionally(self.native_host, install_flags::NATIVE_HOST); + flags.set_conditionally(self.pepper, install_flags::PEPPER); + flags.set_conditionally(self.netscape, install_flags::NETSCAPE); + flags.set_conditionally(self.activex, install_flags::ACTIVEX); + flags.set_conditionally(self.player, install_flags::PLAYER); + flags.set_conditionally(self.player_desktop, install_flags::PLAYER_DESKTOP); + flags.set_conditionally(self.player_start_menu, install_flags::PLAYER_START_MENU); + flags.set_conditionally(self.debug_chosen, install_flags::DEBUG); + flags + } + + fn start_install(&mut self) { + self.panel = Panel::Install; + let mut flags = self.build_flags(); + let max_ticks = flags.get_ticks(); + + { + let mut state = self.progress.lock().unwrap(); + state.label = "Preparing...".into(); + state.value = 0; + state.maximum = max_ticks; + state.done = false; + state.error = None; + } + + let progress = Arc::clone(&self.progress); + std::thread::spawn(move || { + let callback = ThreadProgressCallback { + state: Arc::clone(&progress), + }; + + let redir = redirection::disable_redirection(); + let result = (|| -> Result<(), InstallError> { + uninstaller::uninstall(&callback)?; + installer::install(&callback, &mut flags)?; + Ok(()) + })(); + redirection::enable_redirection(redir); + + let mut state = progress.lock().unwrap(); + if let Err(e) = result { + state.error = Some(e.to_string()); + } + state.done = true; + }); + } + + fn summary_text(&self) -> String { + let mut parts = Vec::new(); + if self.native_host { parts.push("Modern Browsers (via extension)"); } + if self.pepper { parts.push("Google Chrome"); } + if self.netscape { parts.push("Mozilla Firefox"); } + if self.activex { parts.push("Internet Explorer"); } + if self.player { parts.push("Standalone Player"); } + + if parts.is_empty() { + "You are about to uninstall Clean Flash Player.\n\ +The installer will completely remove all versions of Flash Player." + .to_string() + } else { + format!( + "You are about to install Clean Flash Player for:\n {}\n\n\ +The installer will close browsers running Flash, uninstall previous versions, and install.", + parts.join(", ") + ) + } + } +} + +struct ThreadProgressCallback { + state: Arc>, +} + +impl ProgressCallback for ThreadProgressCallback { + fn update_progress_label(&self, text: &str, tick: bool) { + let mut state = self.state.lock().unwrap(); + state.label = text.to_string(); + if tick { + state.value += 1; + } + } + + fn tick_progress(&self) { + let mut state = self.state.lock().unwrap(); + state.value += 1; + } +} + +pub fn run_tui_installer() -> Result<(), Box> { + enable_raw_mode()?; + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let result = run_app(&mut terminal); + + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + + result +} + +fn run_app(terminal: &mut Terminal>) -> Result<(), Box> { + let mut state = TuiInstallState::new(); + + #[cfg(not(target_os = "windows"))] + { + state.pepper = false; + state.netscape = false; + state.activex = false; + } + + loop { + terminal.draw(|f| draw_ui(f, &state))?; + + // While installing, poll both events (with timeout) and progress + if state.panel == Panel::Install { + // Poll progress + { + let ps = state.progress.lock().unwrap(); + if ps.done { + if let Some(ref err) = ps.error { + state.failure_detail = err.clone(); + drop(ps); + state.panel = Panel::Failure; + } else { + drop(ps); + state.panel = Panel::Complete; + } + continue; + } + } + + if event::poll(std::time::Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press && (key.code == KeyCode::Char('q') || key.code == KeyCode::Esc) { + break; + } + } + } + continue; + } + + if let Event::Key(key) = event::read()? { + if key.kind != KeyEventKind::Press { + continue; + } + + match key.code { + KeyCode::Char('q') | KeyCode::Esc => { + match state.panel { + Panel::Install => {} // can't quit during install + _ => break, + } + } + KeyCode::Enter => { + match state.panel { + Panel::Disclaimer => { + if state.disclaimer_accepted { + state.panel = Panel::Choice; + } + } + Panel::Choice => { + if cfg!(target_os = "windows") { + state.panel = Panel::PlayerChoice; + } else { + state.panel = Panel::BeforeInstall; + } + } + Panel::PlayerChoice => state.panel = Panel::DebugChoice, + Panel::DebugChoice => state.panel = Panel::BeforeInstall, + Panel::BeforeInstall => state.start_install(), + Panel::Complete => break, + Panel::Failure => state.start_install(), // retry + Panel::Install => {} // handled by progress polling above + } + } + KeyCode::Backspace => { + match state.panel { + Panel::Choice => state.panel = Panel::Disclaimer, + Panel::PlayerChoice => state.panel = Panel::Choice, + Panel::DebugChoice => state.panel = Panel::PlayerChoice, + Panel::BeforeInstall => { + if cfg!(target_os = "windows") { + state.panel = Panel::DebugChoice; + } else { + state.panel = Panel::Choice; + } + } + _ => {} + } + } + KeyCode::Char(' ') => { + match state.panel { + Panel::Disclaimer => { + state.disclaimer_accepted = !state.disclaimer_accepted; + } + Panel::Choice => state.toggle_choice(), + Panel::PlayerChoice => state.toggle_player(), + Panel::DebugChoice => { + state.debug_chosen = !state.debug_chosen; + } + _ => {} + } + } + KeyCode::Up => { + match state.panel { + Panel::Choice => { + if state.choice_cursor > 0 { + state.choice_cursor -= 1; + } + } + Panel::PlayerChoice => { + if state.player_cursor > 0 { + state.player_cursor -= 1; + } + } + _ => {} + } + } + KeyCode::Down => { + match state.panel { + Panel::Choice => { + let max = state.choice_items().len().saturating_sub(1); + if state.choice_cursor < max { + state.choice_cursor += 1; + } + } + Panel::PlayerChoice => { + let max = state.player_items().len().saturating_sub(1); + if state.player_cursor < max { + state.player_cursor += 1; + } + } + _ => {} + } + } + _ => {} + } + } + } + + Ok(()) +} + +fn draw_ui(f: &mut Frame, state: &TuiInstallState) { + let area = f.area(); + + // Layout: title area, main panel, footer + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // title + Constraint::Min(5), // content + Constraint::Length(3), // footer + ]) + .split(area); + + // Title bar + let title = format!( + " Clean Flash Player {} Installer ", + FLASH_VERSION + ); + let title_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Cyan)) + .title(title) + .title_alignment(Alignment::Center); + f.render_widget(title_block, chunks[0]); + + // Content + let content_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)); + + let inner = content_block.inner(chunks[1]); + f.render_widget(content_block, chunks[1]); + + match state.panel { + Panel::Disclaimer => draw_disclaimer(f, inner, state), + Panel::Choice => draw_choice(f, inner, state), + Panel::PlayerChoice => draw_player_choice(f, inner, state), + Panel::DebugChoice => draw_debug_choice(f, inner, state), + Panel::BeforeInstall => draw_before_install(f, inner, state), + Panel::Install => draw_install(f, inner, state), + Panel::Complete => draw_complete(f, inner, state), + Panel::Failure => draw_failure(f, inner, state), + } + + // Footer + let footer_text = match state.panel { + Panel::Disclaimer => "[Space] Toggle accept [Enter] Next [q/Esc] Quit", + Panel::Choice => "[↑↓] Move [Space] Toggle [Enter] Next [Backspace] Back [q] Quit", + Panel::PlayerChoice => "[↑↓] Move [Space] Toggle [Enter] Next [Backspace] Back [q] Quit", + Panel::DebugChoice => "[Space] Toggle debug [Enter] Next [Backspace] Back [q] Quit", + Panel::BeforeInstall => "[Enter] Install [Backspace] Back [q] Quit", + Panel::Install => "Installing...", + Panel::Complete => "[Enter/q] Quit", + Panel::Failure => "[Enter] Retry [q] Quit", + }; + let footer = Paragraph::new(footer_text) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::DarkGray))); + f.render_widget(footer, chunks[2]); +} + +fn draw_disclaimer(f: &mut Frame, area: Rect, state: &TuiInstallState) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(3), Constraint::Length(1)]) + .split(area); + + let text = "I am aware that Adobe Flash Player is no longer supported, nor provided by Adobe Inc.\n\ +Clean Flash Player is a third-party version of Flash Player built from the latest Flash Player \ +version with adware removed.\n\n\ +Adobe is not required by any means to provide support for this version of Flash Player."; + + let paragraph = Paragraph::new(text) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(Color::White)); + f.render_widget(paragraph, chunks[0]); + + let checkbox = if state.disclaimer_accepted { + "[x] I accept the above disclaimer" + } else { + "[ ] I accept the above disclaimer" + }; + let cb_style = if state.disclaimer_accepted { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::Yellow) + }; + let cb = Paragraph::new(checkbox).style(cb_style); + f.render_widget(cb, chunks[1]); +} + +fn draw_choice(f: &mut Frame, area: Rect, state: &TuiInstallState) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(2), Constraint::Min(3)]) + .split(area); + + let header = Paragraph::new("Which browser plugins would you like to install?") + .style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD)); + f.render_widget(header, chunks[0]); + + let items: Vec = state + .choice_items() + .iter() + .enumerate() + .map(|(i, (label, checked, enabled))| { + let marker = if *checked { "[x]" } else { "[ ]" }; + let text = format!("{} {}", marker, label); + let style = if !enabled { + Style::default().fg(Color::DarkGray) + } else if i == state.choice_cursor { + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + }; + ListItem::new(text).style(style) + }) + .collect(); + + let list = List::new(items); + f.render_widget(list, chunks[1]); +} + +fn draw_player_choice(f: &mut Frame, area: Rect, state: &TuiInstallState) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(2), Constraint::Min(3)]) + .split(area); + + let header = Paragraph::new("Would you like to install the standalone Flash Player?") + .style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD)); + f.render_widget(header, chunks[0]); + + let items: Vec = state + .player_items() + .iter() + .enumerate() + .map(|(i, (label, checked, enabled))| { + let marker = if *checked { "[x]" } else { "[ ]" }; + let text = format!("{} {}", marker, label); + let style = if !enabled { + Style::default().fg(Color::DarkGray) + } else if i == state.player_cursor { + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + }; + ListItem::new(text).style(style) + }) + .collect(); + + let list = List::new(items); + f.render_widget(list, chunks[1]); +} + +fn draw_debug_choice(f: &mut Frame, area: Rect, state: &TuiInstallState) { + let text = "Would you like to install the debug version of Clean Flash Player?\n\ +You should only choose the debug version if you are planning to create Flash applications.\n\ +If you are not sure, simply press Enter to continue."; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(3), Constraint::Length(1)]) + .split(area); + + let paragraph = Paragraph::new(text) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(Color::White)); + f.render_widget(paragraph, chunks[0]); + + let checkbox = if state.debug_chosen { + "[x] Install debug version" + } else { + "[ ] Install debug version" + }; + let cb_style = if state.debug_chosen { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::White) + }; + let cb = Paragraph::new(checkbox).style(cb_style); + f.render_widget(cb, chunks[1]); +} + +fn draw_before_install(f: &mut Frame, area: Rect, state: &TuiInstallState) { + let text = state.summary_text(); + let paragraph = Paragraph::new(text) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(Color::White)); + f.render_widget(paragraph, area); +} + +fn draw_install(f: &mut Frame, area: Rect, state: &TuiInstallState) { + let ps = state.progress.lock().unwrap(); + let label = ps.label.clone(); + let ratio = if ps.maximum > 0 { + (ps.value as f64 / ps.maximum as f64).min(1.0) + } else { + 0.0 + }; + drop(ps); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(2), + Constraint::Length(1), + Constraint::Length(3), + Constraint::Min(0), + ]) + .split(area); + + let header = Paragraph::new("Installation in progress...") + .style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD)); + f.render_widget(header, chunks[0]); + + let status = Paragraph::new(label).style(Style::default().fg(Color::White)); + f.render_widget(status, chunks[1]); + + let gauge = Gauge::default() + .block(Block::default().borders(Borders::ALL)) + .gauge_style(Style::default().fg(Color::Cyan).bg(Color::DarkGray)) + .ratio(ratio); + f.render_widget(gauge, chunks[2]); +} + +fn draw_complete(f: &mut Frame, area: Rect, _state: &TuiInstallState) { + let text = "Clean Flash Player has been successfully installed!\n\n\ +Don't forget, Flash Player is no longer compatible with new browsers.\n\ +For browser recommendations and Flash Player updates, check out Clean Flash Player's website!"; + + let paragraph = Paragraph::new(text) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(Color::Green)); + f.render_widget(paragraph, area); +} + +fn draw_failure(f: &mut Frame, area: Rect, state: &TuiInstallState) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(2)]) + .split(area); + + let header = Paragraph::new( + "Oops! The installation process has encountered an unexpected problem.\nPress Enter to retry or q to quit.", + ) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)); + f.render_widget(header, chunks[0]); + + let detail_text = if state.failure_detail.len() > 500 { + &state.failure_detail[..500] + } else { + &state.failure_detail + }; + let detail = Paragraph::new(detail_text.to_string()) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(Color::White)); + f.render_widget(detail, chunks[1]); +} diff --git a/rust/crates/clean_flash_uninstaller/Cargo.toml b/rust/crates/clean_flash_uninstaller/Cargo.toml index 7d17cca..e7d9f61 100644 --- a/rust/crates/clean_flash_uninstaller/Cargo.toml +++ b/rust/crates/clean_flash_uninstaller/Cargo.toml @@ -9,6 +9,9 @@ clean_flash_common = { path = "../clean_flash_common" } clean_flash_ui = { path = "../clean_flash_ui" } minifb = { workspace = true } windows-sys = { workspace = true } +argh = { workspace = true } +ratatui = { workspace = true } +crossterm = { workspace = true } [build-dependencies] winresource = "0.1" diff --git a/rust/crates/clean_flash_uninstaller/build.rs b/rust/crates/clean_flash_uninstaller/build.rs index 075d8a4..2438334 100644 --- a/rust/crates/clean_flash_uninstaller/build.rs +++ b/rust/crates/clean_flash_uninstaller/build.rs @@ -1,4 +1,18 @@ fn main() { + // On macOS, embed Info.plist into the binary so the OS recognises it as a GUI app. + if cfg!(target_os = "macos") { + let plist = std::path::Path::new("../../resources/macos/uninstaller-Info.plist"); + if plist.exists() { + println!("cargo:rustc-link-arg=-sectcreate"); + println!("cargo:rustc-link-arg=__TEXT"); + println!("cargo:rustc-link-arg=__info_plist"); + println!( + "cargo:rustc-link-arg={}", + plist.canonicalize().unwrap().display() + ); + } + } + if cfg!(target_os = "windows") { let mut res = winresource::WindowsResource::new(); if std::path::Path::new("../../resources/icon.ico").exists() { diff --git a/rust/crates/clean_flash_uninstaller/src/cli_args.rs b/rust/crates/clean_flash_uninstaller/src/cli_args.rs new file mode 100644 index 0000000..d422b21 --- /dev/null +++ b/rust/crates/clean_flash_uninstaller/src/cli_args.rs @@ -0,0 +1,38 @@ +use argh::FromArgs; + +/// Clean Flash Player Uninstaller +/// +/// Uninstall Clean Flash Player with GUI, TUI, or silent mode. +#[derive(FromArgs)] +pub struct UninstallerArgs { + /// use the terminal UI (ratatui) instead of the graphical window + #[argh(switch)] + pub cli: bool, + + /// run the uninstallation non-interactively (no GUI or TUI) + #[argh(switch)] + pub uninstall: bool, + + /// suppress all output (only valid with --uninstall) + #[argh(switch)] + pub silent: bool, +} + +/// The mode in which the application should run. +pub enum RunMode { + Gui, + Tui, + Silent, +} + +impl UninstallerArgs { + pub fn determine_mode(&self) -> RunMode { + if self.uninstall { + RunMode::Silent + } else if self.cli { + RunMode::Tui + } else { + RunMode::Gui + } + } +} diff --git a/rust/crates/clean_flash_uninstaller/src/main.rs b/rust/crates/clean_flash_uninstaller/src/main.rs index 9f1be3a..f38f074 100644 --- a/rust/crates/clean_flash_uninstaller/src/main.rs +++ b/rust/crates/clean_flash_uninstaller/src/main.rs @@ -1,12 +1,16 @@ #![windows_subsystem = "windows"] +mod cli_args; +mod silent_uninstaller; +mod tui_uninstaller; mod uninstall_form; +use cli_args::{UninstallerArgs, RunMode}; use clean_flash_ui::renderer::Renderer; use minifb::{Key, MouseButton, MouseMode, Window, WindowOptions}; use uninstall_form::{UninstallForm, HEIGHT, WIDTH}; -fn main() { +fn run_gui() { let title = format!( "Clean Flash Player {} Uninstaller", clean_flash_common::update_checker::FLASH_VERSION @@ -51,3 +55,32 @@ fn main() { .expect("Failed to update window buffer"); } } + +fn main() { + let args: UninstallerArgs = argh::from_env(); + + match args.determine_mode() { + RunMode::Silent => { + if let Err(e) = silent_uninstaller::run_silent_uninstall(&args) { + eprintln!("[!] Fatal error: {}", e); + std::process::exit(1); + } + } + RunMode::Tui => { + if let Err(e) = tui_uninstaller::run_tui_uninstaller() { + eprintln!("[!] TUI error: {}", e); + std::process::exit(1); + } + } + RunMode::Gui => { + let gui_result = std::panic::catch_unwind(run_gui); + if gui_result.is_err() { + eprintln!("GUI failed to initialize, falling back to terminal UI..."); + if let Err(e) = tui_uninstaller::run_tui_uninstaller() { + eprintln!("[!] TUI error: {}", e); + std::process::exit(1); + } + } + } + } +} diff --git a/rust/crates/clean_flash_uninstaller/src/silent_uninstaller.rs b/rust/crates/clean_flash_uninstaller/src/silent_uninstaller.rs new file mode 100644 index 0000000..bf7ceae --- /dev/null +++ b/rust/crates/clean_flash_uninstaller/src/silent_uninstaller.rs @@ -0,0 +1,47 @@ +use crate::cli_args::UninstallerArgs; +use clean_flash_common::{uninstaller, redirection, update_checker, ProgressCallback, InstallError}; + +struct SilentProgressCallback { + silent: bool, +} + +impl ProgressCallback for SilentProgressCallback { + fn update_progress_label(&self, text: &str, _tick: bool) { + if !self.silent { + eprintln!("[*] {}", text); + } + } + + fn tick_progress(&self) {} +} + +pub fn run_silent_uninstall(args: &UninstallerArgs) -> Result<(), Box> { + let version = update_checker::FLASH_VERSION; + + if !args.silent { + eprintln!("Clean Flash Player {} - Silent Uninstall", version); + } + + let callback = SilentProgressCallback { + silent: args.silent, + }; + + let redir = redirection::disable_redirection(); + let result: Result<(), InstallError> = uninstaller::uninstall(&callback); + redirection::enable_redirection(redir); + + match result { + Ok(()) => { + if !args.silent { + eprintln!("[+] Uninstallation completed successfully."); + } + Ok(()) + } + Err(e) => { + if !args.silent { + eprintln!("[!] Uninstallation failed: {}", e); + } + std::process::exit(1); + } + } +} diff --git a/rust/crates/clean_flash_uninstaller/src/tui_uninstaller.rs b/rust/crates/clean_flash_uninstaller/src/tui_uninstaller.rs new file mode 100644 index 0000000..7696600 --- /dev/null +++ b/rust/crates/clean_flash_uninstaller/src/tui_uninstaller.rs @@ -0,0 +1,306 @@ +use clean_flash_common::{uninstaller, redirection, update_checker, ProgressCallback, InstallError}; +use crossterm::{ + event::{self, Event, KeyCode, KeyEventKind}, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + execute, +}; +use ratatui::{ + prelude::*, + widgets::{Block, Borders, Gauge, Paragraph, Wrap}, +}; +use std::io::{self, stdout}; +use std::sync::{Arc, Mutex}; + +const FLASH_VERSION: &str = update_checker::FLASH_VERSION; +const UNINSTALL_TICKS: u32 = 10; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Panel { + Confirm, + Uninstall, + Complete, + Failure, +} + +struct ProgressState { + label: String, + value: u32, + maximum: u32, + done: bool, + error: Option, +} + +struct TuiUninstallState { + panel: Panel, + progress: Arc>, + failure_detail: String, +} + +impl TuiUninstallState { + fn new() -> Self { + Self { + panel: Panel::Confirm, + progress: Arc::new(Mutex::new(ProgressState { + label: "Preparing...".into(), + value: 0, + maximum: UNINSTALL_TICKS, + done: false, + error: None, + })), + failure_detail: String::new(), + } + } + + fn start_uninstall(&mut self) { + self.panel = Panel::Uninstall; + + { + let mut state = self.progress.lock().unwrap(); + state.label = "Preparing...".into(); + state.value = 0; + state.maximum = UNINSTALL_TICKS; + state.done = false; + state.error = None; + } + + let progress = Arc::clone(&self.progress); + std::thread::spawn(move || { + let callback = ThreadProgressCallback { + state: Arc::clone(&progress), + }; + + let redir = redirection::disable_redirection(); + let result: Result<(), InstallError> = uninstaller::uninstall(&callback); + redirection::enable_redirection(redir); + + let mut state = progress.lock().unwrap(); + if let Err(e) = result { + state.error = Some(e.to_string()); + } + state.done = true; + }); + } +} + +struct ThreadProgressCallback { + state: Arc>, +} + +impl ProgressCallback for ThreadProgressCallback { + fn update_progress_label(&self, text: &str, tick: bool) { + let mut state = self.state.lock().unwrap(); + state.label = text.to_string(); + if tick { + state.value += 1; + } + } + + fn tick_progress(&self) { + let mut state = self.state.lock().unwrap(); + state.value += 1; + } +} + +pub fn run_tui_uninstaller() -> Result<(), Box> { + enable_raw_mode()?; + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let result = run_app(&mut terminal); + + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + + result +} + +fn run_app(terminal: &mut Terminal>) -> Result<(), Box> { + let mut state = TuiUninstallState::new(); + + loop { + terminal.draw(|f| draw_ui(f, &state))?; + + if state.panel == Panel::Uninstall { + // Poll progress + { + let ps = state.progress.lock().unwrap(); + if ps.done { + if let Some(ref err) = ps.error { + state.failure_detail = err.clone(); + drop(ps); + state.panel = Panel::Failure; + } else { + drop(ps); + state.panel = Panel::Complete; + } + continue; + } + } + + if event::poll(std::time::Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press && (key.code == KeyCode::Char('q') || key.code == KeyCode::Esc) { + break; + } + } + } + continue; + } + + if let Event::Key(key) = event::read()? { + if key.kind != KeyEventKind::Press { + continue; + } + + match key.code { + KeyCode::Char('q') | KeyCode::Esc => break, + KeyCode::Enter => { + match state.panel { + Panel::Confirm => state.start_uninstall(), + Panel::Complete => break, + Panel::Failure => state.start_uninstall(), // retry + _ => {} + } + } + _ => {} + } + } + } + + Ok(()) +} + +fn draw_ui(f: &mut Frame, state: &TuiUninstallState) { + let area = f.area(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(5), + Constraint::Length(3), + ]) + .split(area); + + // Title + let title = format!( + " Clean Flash Player {} Uninstaller ", + FLASH_VERSION + ); + let title_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Cyan)) + .title(title) + .title_alignment(Alignment::Center); + f.render_widget(title_block, chunks[0]); + + // Content + let content_block = Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)); + let inner = content_block.inner(chunks[1]); + f.render_widget(content_block, chunks[1]); + + match state.panel { + Panel::Confirm => draw_confirm(f, inner), + Panel::Uninstall => draw_uninstall(f, inner, state), + Panel::Complete => draw_complete(f, inner), + Panel::Failure => draw_failure(f, inner, state), + } + + // Footer + let footer_text = match state.panel { + Panel::Confirm => "[Enter] Uninstall [q/Esc] Quit", + Panel::Uninstall => "Uninstalling...", + Panel::Complete => "[Enter/q] Quit", + Panel::Failure => "[Enter] Retry [q] Quit", + }; + let footer = Paragraph::new(footer_text) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::DarkGray))); + f.render_widget(footer, chunks[2]); +} + +fn draw_confirm(f: &mut Frame, area: Rect) { + let text = "You are about to uninstall Clean Flash Player.\n\ +Please close all browsers, including Google Chrome, Mozilla Firefox and Internet Explorer.\n\n\ +The installer will completely remove all versions of Flash Player from this computer,\n\ +including Clean Flash Player and older versions of Adobe Flash Player.\n\n\ +Press Enter to proceed."; + + let paragraph = Paragraph::new(text) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(Color::White)); + f.render_widget(paragraph, area); +} + +fn draw_uninstall(f: &mut Frame, area: Rect, state: &TuiUninstallState) { + let ps = state.progress.lock().unwrap(); + let label = ps.label.clone(); + let ratio = if ps.maximum > 0 { + (ps.value as f64 / ps.maximum as f64).min(1.0) + } else { + 0.0 + }; + drop(ps); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(2), + Constraint::Length(1), + Constraint::Length(3), + Constraint::Min(0), + ]) + .split(area); + + let header = Paragraph::new("Uninstallation in progress...") + .style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD)); + f.render_widget(header, chunks[0]); + + let status = Paragraph::new(label).style(Style::default().fg(Color::White)); + f.render_widget(status, chunks[1]); + + let gauge = Gauge::default() + .block(Block::default().borders(Borders::ALL)) + .gauge_style(Style::default().fg(Color::Cyan).bg(Color::DarkGray)) + .ratio(ratio); + f.render_widget(gauge, chunks[2]); +} + +fn draw_complete(f: &mut Frame, area: Rect) { + let text = "All versions of Flash Player have been successfully uninstalled.\n\n\ +If you ever change your mind, check out Clean Flash Player's website!"; + + let paragraph = Paragraph::new(text) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(Color::Green)); + f.render_widget(paragraph, area); +} + +fn draw_failure(f: &mut Frame, area: Rect, state: &TuiUninstallState) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(2)]) + .split(area); + + let header = Paragraph::new( + "Oops! The uninstallation process has encountered an unexpected problem.\nPress Enter to retry or q to quit.", + ) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)); + f.render_widget(header, chunks[0]); + + let detail_text = if state.failure_detail.len() > 500 { + &state.failure_detail[..500] + } else { + &state.failure_detail + }; + let detail = Paragraph::new(detail_text.to_string()) + .wrap(Wrap { trim: false }) + .style(Style::default().fg(Color::White)); + f.render_widget(detail, chunks[1]); +} diff --git a/rust/resources/macos/installer-Info.plist b/rust/resources/macos/installer-Info.plist new file mode 100644 index 0000000..b6a8cc5 --- /dev/null +++ b/rust/resources/macos/installer-Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleName + Clean Flash Installer + CFBundleDisplayName + Clean Flash Player Installer + CFBundleIdentifier + com.flashpatch.clean-flash-installer + CFBundleVersion + 34.0.0.330 + CFBundleShortVersionString + 34.0.0.330 + CFBundlePackageType + APPL + CFBundleExecutable + clean_flash_installer + NSHighResolutionCapable + + LSMinimumSystemVersion + 10.13 + + diff --git a/rust/resources/macos/uninstaller-Info.plist b/rust/resources/macos/uninstaller-Info.plist new file mode 100644 index 0000000..5d15202 --- /dev/null +++ b/rust/resources/macos/uninstaller-Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleName + Clean Flash Uninstaller + CFBundleDisplayName + Clean Flash Player Uninstaller + CFBundleIdentifier + com.flashpatch.clean-flash-uninstaller + CFBundleVersion + 34.0.0.330 + CFBundleShortVersionString + 34.0.0.330 + CFBundlePackageType + APPL + CFBundleExecutable + clean_flash_uninstaller + NSHighResolutionCapable + + LSMinimumSystemVersion + 10.13 + +