Implement CLI and silent install / uninstall

master
Disyer 3 weeks ago
parent 9547be7f16
commit 4683064637

5
.gitignore vendored

@ -16,3 +16,8 @@ obj
*.7z
target
macinstaller
trueinstaller
build*.sh
bundle_macos.sh

1320
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -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",

@ -1,49 +1,237 @@
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 mut current_hkey: Option<windows_sys::Win32::System::Registry::HKEY> = 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<u16> =
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;
}
// 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<u16> =
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 content = format!("Windows Registry Editor Version 5.00\n\n{}", filled);
let Some((name, value_part)) = parse_name_and_value(line) else {
continue;
};
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()));
let name_wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
// Write as UTF-16LE with BOM (Windows .reg format).
{
let mut f = fs::File::create(&reg_file)
.map_err(|e| InstallError::new(format!("Failed to create temp reg file: {}", e)))?;
let utf16: Vec<u16> = 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)))?;
// Delete value: "name"=-
if value_part == "-" {
unsafe {
RegDeleteValueW(hkey, name_wide.as_ptr());
}
continue;
}
let reg_filename = reg_file.to_string_lossy().to_string();
let result = process_utils::run_process("reg.exe", &["import", &reg_filename]);
// 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(&reg_file);
// String value: "name"="value"
if let Some(val) = parse_reg_string(value_part) {
let val_wide: Vec<u16> = 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<u16> = parent.encode_utf16().chain(std::iter::once(0)).collect();
let leaf_wide: Vec<u16> = 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<u16> = 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<String> {
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(())

@ -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).

@ -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"

@ -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();

@ -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
}
}
}

@ -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
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<u16> = 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<u16> = 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
)));
}
let result = process_utils::run_process("powershell.exe", &["-NoProfile", "-Command", &script]);
if !result.is_successful() {
// Save the .lnk file.
let lnk_path = folder.join(format!("{}.lnk", name));
let lnk_wide: Vec<u16> = 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!(
"Failed to create shortcut: {}",
result.output
"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(())
}

@ -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);
}
}
}
}
}

@ -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<dyn std::error::Error>> {
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);
}
}
}

@ -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<String>,
}
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<Mutex<ProgressState>>,
// 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<Mutex<ProgressState>>,
}
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<dyn std::error::Error>> {
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<CrosstermBackend<io::Stdout>>) -> Result<(), Box<dyn std::error::Error>> {
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<ListItem> = 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<ListItem> = 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]);
}

@ -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"

@ -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() {

@ -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
}
}
}

@ -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);
}
}
}
}
}

@ -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<dyn std::error::Error>> {
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);
}
}
}

@ -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<String>,
}
struct TuiUninstallState {
panel: Panel,
progress: Arc<Mutex<ProgressState>>,
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<Mutex<ProgressState>>,
}
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<dyn std::error::Error>> {
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<CrosstermBackend<io::Stdout>>) -> Result<(), Box<dyn std::error::Error>> {
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]);
}

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>Clean Flash Installer</string>
<key>CFBundleDisplayName</key>
<string>Clean Flash Player Installer</string>
<key>CFBundleIdentifier</key>
<string>com.flashpatch.clean-flash-installer</string>
<key>CFBundleVersion</key>
<string>34.0.0.330</string>
<key>CFBundleShortVersionString</key>
<string>34.0.0.330</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>clean_flash_installer</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
</dict>
</plist>

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>Clean Flash Uninstaller</string>
<key>CFBundleDisplayName</key>
<string>Clean Flash Player Uninstaller</string>
<key>CFBundleIdentifier</key>
<string>com.flashpatch.clean-flash-uninstaller</string>
<key>CFBundleVersion</key>
<string>34.0.0.330</string>
<key>CFBundleShortVersionString</key>
<string>34.0.0.330</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>clean_flash_uninstaller</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
</dict>
</plist>
Loading…
Cancel
Save