Implement CLI and silent install / uninstall

This commit is contained in:
Disyer
2026-03-31 22:47:13 +03:00
parent 9547be7f16
commit 4683064637
20 changed files with 2953 additions and 69 deletions
@@ -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]);
}